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 field | TINS truth | What to do |
|---|---|---|
stats (six D&D scores) | Single Matrix stat 0–6 + Skills rated 3–6 | Replace or repurpose. Don't keep both systems — pick one. |
level | Seniority / operator rating, not power | Keep if you run progression, or delete if Matrix is your only ladder |
ancestry / characterClass | Origin (Zion-born, Matrix-born, Program) / Role | Rename in place or replace with origin + role strings |
inventory (Equipment refs) | Operator-coded loadout | Keep. This is already correct — it's a reference array |
carryingCapacity / totalInventoryWeight | Encumbrance | Keep only if you run meat-world missions; otherwise delete — they're computed and cost nothing to leave, but they mislead builders |
background / personality / description | Deal, Fate, residual self-image | Keep for prose, but add dedicated deal and fate text fields so you can search them |
value / weight on Equipment | Rarity / concealment, not gp/lbs | Keep 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
titlewithout changing the key, so existing files don't break - Make it computed correctly: add an
expressionto a new field, never to a required field, and reference only$selfandload()on a properly annotated reference field
Three hard rules from the platform:
-
Computed fields are derived, never stored. If you write
matrixModifieras 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. -
Reference fields must be annotated. An
inventorythat 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. -
Never write a partial schema back as a whole. If you
readwithselectand thenwritethat projection, you delete every field you didn't select — for every file of that type. Alwaysupdatewith dot-paths or re-read withoutselectbefore a fullwrite.
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:
- Add the new fields with sensible defaults (
matrix: 3,bodyChips: 3,matrixChips: 1) so old files stay valid. - Move data out of
notes/backgroundinto the new fields. Keepnotesfor cribbed Double Success moves and edge cases — don't try to formalize everything. - Only then make a new field
requiredif you truly need it. Markingdealrequired 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
levelorcharacterClassto derive different Matrix behavior is fighting the game. Onematrixfield 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.
fatecan 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.mdif you added any reference or computed field - Every new field has a
title; every reference field hasreferencedFileTypeSlugorreference.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
updatewith dot-paths, not a partialwrite - 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.