Builder's Manual · Chapter 8

Hacking the System

You have a working game and a working project. Sooner or later you'll want to close the gap between them — adding a real Matrix stat, real Deals and Fates, real chips. This chapter tells you how to do that without painting yourself into a corner.

The rule of staging

Every idea starts in notes and graduates to a field only when you write the same structured line ten times. That's not laziness — it's how you discover the right shape before you lock it in:

Notes: Matrix: 3 — Skills: Kung Fu 4, Gun Fu 3 — Chips: 3/1 — Deal: ...

becomes

{ "matrix": 3, "skills": { "kungFu": 4, "gunFu": 3 }, "bodyChips": 3, "matrixChips": 1, "deal": "..." }

only after you're sure you want exactly those keys everywhere. If you promote too early you migrate files twice. If you never promote, you can't filter or sort.

Before you touch a schema, read these two docs in full:

  • /platform-instructions/creating-file-types.md — how reference and computed fields actually work
  • /platform-instructions/refactoring-file-types.md — the anti-patterns that make schemas painful (numbered slots, per-level branches, duplicated derived values)

Skimming them costs more than reading them.

What to change — and what to leave alone

The starter schemas are intentionally generic. Here's the honest mapping to TINS:

Starter fieldTINS truthWhat to do
stats (six D&D scores)Single Matrix stat 0–6 + Skills rated 3–6Replace or repurpose. Don't keep both systems — pick one.
levelSeniority / operator rating, not powerKeep if you run progression, or delete if Matrix is your only ladder
ancestry / characterClassOrigin (Zion-born, Matrix-born, Program) / RoleRename in place or replace with origin + role strings
inventory (Equipment refs)Operator-coded loadoutKeep. This is already correct — it's a reference array
carryingCapacity / totalInventoryWeightEncumbranceKeep only if you run meat-world missions; otherwise delete — they're computed and cost nothing to leave, but they mislead builders
background / personality / descriptionDeal, Fate, residual self-imageKeep for prose, but add dedicated deal and fate text fields so you can search them
value / weight on EquipmentRarity / concealment, not gp/lbsKeep weight for encumbrance if you use it; treat value as rarity tier or remove it

The smallest honest TINS character schema is:

{
  "matrix": { "type": "number", "title": "Matrix", "minimum": 0, "maximum": 6 },
  "skills": {
    "type": "object",
    "properties": {
      "kungFu": { "type": "number", "minimum": 3, "maximum": 6 },
      "gunFu": { "type": "number", "minimum": 3, "maximum": 6 },
      "intrusion": { "type": "number", "minimum": 3, "maximum": 6 }
    }
  },
  "bodyChips": { "type": "number", "title": "Body Chips", "minimum": 0 },
  "matrixChips": { "type": "number", "title": "Matrix Chips", "minimum": 0 },
  "deal": { "type": "string", "title": "Deal", "format": "textarea" },
  "fate": { "type": "string", "title": "Fate (GM only)", "format": "textarea" }
}

Add skills as named keys, not skill1/skill2/skill3. Numbered slots are the classic anti-pattern — they force every character through the same three slots and make queries useless.

How to edit a file type safely

All schema work happens at /file-types/<slug>/schema.json, not by rewriting the whole config. Use targeted updates:

  • Add a field: update /file-types/character/schema.json updates={"properties.matrix": {"type":"number","title":"Matrix","minimum":0,"maximum":6}}
  • Rename in place: update the title without changing the key, so existing files don't break
  • Make it computed correctly: add an expression to a new field, never to a required field, and reference only $self and load() on a properly annotated reference field

Three hard rules from the platform:

  1. Computed fields are derived, never stored. If you write matrixModifier as editable and also compute it, you've duplicated a derived value. Make it {"type":"number","expression":"floor((($self.matrix ?? 0) - 2) / 1)"} or whatever your table needs, and stop writing it into files. Writes that include computed fields are rejected.

  2. Reference fields must be annotated. An inventory that should load Equipment must be {"type":"string","referencedFileTypeSlug":"equipment"} or {"reference":{"targets":[{"fileTypeSlug":"equipment"}]}}. Without that, load($self.inventory) returns nothing and your weight sums silently compute to zero.

  3. Never write a partial schema back as a whole. If you read with select and then write that projection, you delete every field you didn't select — for every file of that type. Always update with dot-paths or re-read without select before a full write.

For layout, prefer to leave it generated until your schema is stable. A custom layout that hard-codes stats.strength will break the moment you replace stats with matrix.

Migrating existing files

You already have an Example Character and four Equipment files. Migration is just editing them to match the new shape:

  1. Add the new fields with sensible defaults (matrix: 3, bodyChips: 3, matrixChips: 1) so old files stay valid.
  2. Move data out of notes/background into the new fields. Keep notes for cribbed Double Success moves and edge cases — don't try to formalize everything.
  3. Only then make a new field required if you truly need it. Marking deal required on day one blocks every bulk import.

For Locations and Equipment the same applies. If you find yourself writing this in every Location's notes:

Hard line: Yes — lobby payphone
Threat: 2 Advantage Dice
Tell: Clock stuck at 12:00

promote them to:

{ "hardLine": {"type":"string","enum":["yes","no","compromised"]},
  "advantageDice": {"type":"number","minimum":0},
  "tell": {"type":"string"} }

But don't promote flavour. atmosphere and description should stay freeform — they're what the Operator and camera say, not queryable data.

What not to build

  • Per-level or per-class branches. TINS has no classes or levels in the D&D sense. A schema that switches on level or characterClass to derive different Matrix behavior is fighting the game. One matrix field plus skills is the whole system.
  • Presentation strings built by expressions. Don't compute "Matrix 3 — Kung Fu 4" as a field. That's layout's job. Store numbers as numbers.
  • Placeholder computed fields that just copy another field. If the expression is $self.matrix, delete it — you already have the value.
  • A Deal/Fate mechanic that enforces secrecy in schema. fate can be a plain text field with a naming convention ("GM only"). Don't build visibility logic into the type — GM Instructions and table trust handle that.

Checklist before you publish a schema change

  • Read /platform-instructions/reference-and-computed-fields.md if you added any reference or computed field
  • Every new field has a title; every reference field has referencedFileTypeSlug or reference.targets
  • No computed field is in required; no editable field duplicates a computed value
  • Existing Example files still validate (open them — if they show errors, add defaults or loosen constraints)
  • You used update with dot-paths, not a partial write
  • Layout still renders (or you left it generated until the schema settles)

Get this right once and the next fifty characters and constructs are just content — fast, consistent, and true to the film.

Manual updated Aug 24, 2026.