Goldie Blocks

YAML and JSON Mapping

Shared conversion rules between authored YAML source and parsed JSON/runtime schema storage

Phase scope: this page is shared by [skill] and [app]. YAML is the editable authoring source format. Parsed JSON is the validation, tool, import, storage, test, and runtime contract.

The local skill and app Build mode share the same authored YAML shape. The local skill reads and writes YAML/Markdown files directly. App Build mode may expose the same raw YAML to a user or AI editor during template authoring.

Runtime code, tests, importer previews, and Convex storage consume the parsed JSON shape. They should not depend on YAML-specific syntax or parse handwritten YAML on hot paths.

Purpose

  • Defines the boundary between human/AI-editable authoring files and machine-readable runtime records.
  • Keeps document, module, block, and data-field conversion rules in one place.
  • Explains how snake_case YAML source maps to camelCase JSON/TypeScript/Convex fields.
  • Shows the concrete YAML-to-JSON transformations used by import, validation, runtime, and export.
  • Applies the same conversion pattern uniformly to documents, modules, and first-class blocks.

Boundary Model

YAML is source. JSON is contract.

authored YAML
-> parse and validate
-> parsed JSON contract
-> import / store / execute / test
-> export authored YAML when needed

Prop

Type

Naming Rules

Authoring-side keys use snake_case. Parsed JSON, TypeScript, and Convex fields use camelCase.

YAML sourceParsed JSON / Convex
reuse_typereuseType
derived_from_document_keyderivedFromDocumentKey
derived_from_module_keyderivedFromModuleKey
document_version_keydocumentVersionKey
repeat_for when exposed as a source keyrepeatFor
join_for when exposed as a source keyjoinFor

The mapping is mechanical and one-to-one. Do not create alternate names for the same concept between local-skill files and app Build mode.

Document Mapping

A document folder maps to one documents row plus its version chain when imported into Convex. Export performs the reverse conversion from the stored JSON snapshot back to the authored folder shape.

YAML keeps a nested document: namespace because nesting is how authors group related fields. Convex storage is flat — each authored sub-key becomes its own column on documentVersions. The mapping is mechanical: importers split the namespace; exporters re-nest it.

Prop

Type

Example document YAML

handle: simple_will
label: Simple Will
reuse_type: working
derived_from_document_key: doc_01HV8XQZK4J9R9W3M6T4M2RT01

document:
  description: A straightforward will with executor and beneficiary provisions.
  requires_payment: true
  defaults:
    block:
      display:
        numbering: decimal
  modes:
    supports: [fill, edit]
    default: fill
  output:
    generation:
      supported: [preview, final]
      default: preview

Parsed document JSON

{
  "documents": {
    "key": "doc_01HV8XQZK4J9R9W3M6T4M2R8E1",
    "handle": "simple_will",
    "label": "Simple Will",
    "reuseType": "working",
    "derivedFromDocumentKey": "doc_01HV8XQZK4J9R9W3M6T4M2RT01",
    "headVersionKey": "docv_01HV8XQZK4J9R9W3M6T4M2RH09"
  },
  "documentVersions": {
    "key": "docv_01HV8XQZK4J9R9W3M6T4M2RH09",
    "documentKey": "doc_01HV8XQZK4J9R9W3M6T4M2R8E1",
    "versionNumber": 1,
    "reuseTypeSnapshot": "working",
    "creationMethod": "imported",
    "description": "A straightforward will with executor and beneficiary provisions.",
    "requiresPayment": true,
    "defaults": {
      "block": { "display": { "numbering": "decimal" } }
    },
    "modes": { "supports": ["fill", "edit"], "default": "fill" },
    "output": { "generation": { "supported": ["preview", "final"], "default": "preview" } }
  }
}

See Folios Schema for canonical table fields.

Module Mapping

A module file maps to one modules row plus its version chain when imported into Convex. The authored module: namespace fans out into flat columns on moduleVersions. Each entry in the authored blocks: array maps to a blocks row plus a blockVersions row pinned to the module version (blockVersions.moduleVersionKey). Authors write nested YAML; importers split it; exporters re-nest it and flatten the pinned blocks back into the file's blocks: array.

There is no authored data: namespace: field declarations live on kind: data blocks, and the module's dataSchema / data surfaces are derived from them — see Modules Schema → Derived Data Surfaces.

Prop

Type

Example module YAML

One module, shown on its own for readability. In authored source it is an entry in the document file's modules: sequence, not its own file — there is no document_input/modules/ folder. Its index in that sequence is its order; see Local Skills → One File Per Document.

module:
  handle: executors
  label: Executors
  reuse_type: working
  flow:
    condition: data_pool.data.has_executor = true

blocks:
  - handle!: executor_details
    label: Executor details
    kind: data
    guide: |
      Ask who administers the estate. Explain the executor role briefly if asked.
    fields:
      primary_executor|Person!:
        question: Who is the primary executor?
      wants_backup|YesNo:
        question: Would you like to appoint a backup executor?
      backup_executor|Person:
        question: Who is the backup executor?

  - handle!: appointment_clause
    label: Appointment of executor
    kind: content
    flow:
      condition:
    display:
      type: paragraph
      level: 1
      numbering: decimal
    content: |
      I appoint {{ data.primary_executor.name }} of {{ data.primary_executor.address }} as my executor.

Parsed module JSON

{
  "modules": {
    "key": "mod_01HV8XQZK4J9R9W3M6T4M2RMD1",
    "handle": "executors",
    "label": "Executors",
    "reuseType": "working",
    "documentKey": "doc_01HV8XQZK4J9R9W3M6T4M2R8E1",
    "headVersionKey": "modv_01HV8XQZK4J9R9W3M6T4M2RMV4"
  },
  "moduleVersions": {
    "key": "modv_01HV8XQZK4J9R9W3M6T4M2RMV4",
    "moduleKey": "mod_01HV8XQZK4J9R9W3M6T4M2RMD1",
    "documentVersionKey": "docv_01HV8XQZK4J9R9W3M6T4M2RH09",
    "versionNumber": 1,
    "reuseTypeSnapshot": "working",
    "creationMethod": "imported",
    "flow": {
      "condition": "data_pool.data.has_executor = true"
    },
    "order": { "position": 2000 }
  },
  "blocks": [
    { "key": "blk_01HV8XQZK4J9R9W3M6T4M2RB01", "handle": "executor_details", "label": "Executor details", "reuseType": "working", "moduleKey": "mod_01HV8XQZK4J9R9W3M6T4M2RMD1" },
    { "key": "blk_01HV8XQZK4J9R9W3M6T4M2RB02", "handle": "appointment_clause", "label": "Appointment of executor", "reuseType": "working", "moduleKey": "mod_01HV8XQZK4J9R9W3M6T4M2RMD1" }
  ],
  "blockVersions": [
    {
      "key": "blkv_01HV8XQZK4J9R9W3M6T4M2RBV1",
      "blockKey": "blk_01HV8XQZK4J9R9W3M6T4M2RB01",
      "moduleVersionKey": "modv_01HV8XQZK4J9R9W3M6T4M2RMV4",
      "versionNumber": 1,
      "reuseTypeSnapshot": "working",
      "creationMethod": "imported",
      "kind": "data",
      "order": { "position": 1000 },
      "guide": "Ask who administers the estate. Explain the executor role briefly if asked.",
      "fields": [
        { "key": "primary_executor", "type": "Person", "required": true, "question": "Who is the primary executor?" },
        { "key": "wants_backup", "type": "YesNo", "required": false, "question": "Would you like to appoint a backup executor?" },
        { "key": "backup_executor", "type": "Person", "required": false, "question": "Who is the backup executor?" }
      ]
    },
    {
      "key": "blkv_01HV8XQZK4J9R9W3M6T4M2RBV2",
      "blockKey": "blk_01HV8XQZK4J9R9W3M6T4M2RB02",
      "moduleVersionKey": "modv_01HV8XQZK4J9R9W3M6T4M2RMV4",
      "versionNumber": 1,
      "reuseTypeSnapshot": "working",
      "creationMethod": "imported",
      "kind": "content",
      "flow": { "condition": null },
      "order": { "position": 2000 },
      "display": { "type": "paragraph", "level": 1, "numbering": "decimal" },
      "content": "I appoint {{ data.primary_executor.name }} of {{ data.primary_executor.address }} as my executor."
    }
  ]
}

The module's dataSchema and data surfaces are derived from the data blocks' fields at read time (or maintained as a cache) — they are not authored or imported columns.

See Folios Schema for canonical table fields.

Field Declaration Mapping

Field declaration markers are authoring syntax only. Field declarations live in a data block's fields: list. The stable field key is the part before the pipe. The pipe type, required marker, collection bounds, defaults, questions, and computed expressions become structured JSON metadata on the blockVersions.fields[] entry. A field-level value stores collected or pre-filled data, co-located on the same entry. Clean reusable sources (library blocks) must omit value; working document folders usually contain it.

Authoring YAMLParsed JSON
notice_period|Duration!:"notice_period": { "type": "Duration", "required": true }
children|Person[1..5]:"children": { "type": "Person", "collection": true, "minItems": 1, "maxItems": 5 }
notice_deadline|date: = completion_date - notice_period"notice_deadline": { "type": "date", "computed": { "expression": "completion_date - notice_period" } }
your_name|PersonLegalName!: with label: Your Name"your_name": { "type": "PersonLegalName", "required": true, "label": "Your Name" }

A field's label is display text and carries no reference weight: the left of the pipe stays the handle every @ binds to. An unlabelled field omits the key entirely rather than storing an empty string.

Example field YAML

Inside a data block:

blocks:
  - handle!: notice_details
    label: Notice details
    kind: data
    fields:
      notice_period|Duration!:
        question: What notice period applies?
        value: "30 days"
      children|Person[1..5]:
        question: List the willmaker's children.
        value:
          - handle: child_001
            name: Sam Smith
          - handle: child_002
            name: Riley Smith
      notice_deadline|date: = completion_date - notice_period

Parsed field entries JSON

On the block version:

{
  "kind": "data",
  "fields": [
    {
      "key": "notice_period",
      "type": "Duration",
      "required": true,
      "question": "What notice period applies?",
      "value": "30 days"
    },
    {
      "key": "children",
      "type": "Person",
      "collection": true,
      "minItems": 1,
      "maxItems": 5,
      "question": "List the willmaker's children.",
      "value": [
        { "handle": "child_001", "name": "Sam Smith" },
        { "handle": "child_002", "name": "Riley Smith" }
      ]
    },
    {
      "key": "notice_deadline",
      "type": "date",
      "computed": { "expression": "completion_date - notice_period" }
    }
  ]
}

The derived module surfaces fold these entries: declarations (everything except value) fold into dataSchema; values fold into the data namespace. Collected runtime values use the same stable field keys as their declarations. Computed values store expression text only and are evaluated lazily by runtime logic — they never store value.

Block Mapping

Blocks are authored under a module's blocks: namespace. Each entry maps to a blocks row (stable identity) plus a blockVersions row (immutable snapshot) pinned to the containing module version. Authored YAML remains source; parsed JSON remains the storage/runtime contract.

Prop

Type

Example block YAML

blocks:
  - handle!: executor_appointment_list
    label: Executor appointment list
    join_for:
      items: data.executors
      separator: "; "
      last_separator: "; and "
    display:
      type: clause
    content: |
      {{ name }} of {{ address }}

Parsed block JSON

{
  "blocks": {
    "key": "blk_01HV8XQZK4J9R9W3M6T4M2RB03",
    "handle": "executor_appointment_list",
    "label": "Executor appointment list",
    "reuseType": "working",
    "moduleKey": "mod_01HV8XQZK4J9R9W3M6T4M2RMD1"
  },
  "blockVersions": {
    "key": "blkv_01HV8XQZK4J9R9W3M6T4M2RBV3",
    "blockKey": "blk_01HV8XQZK4J9R9W3M6T4M2RB03",
    "moduleVersionKey": "modv_01HV8XQZK4J9R9W3M6T4M2RMV4",
    "versionNumber": 1,
    "reuseTypeSnapshot": "working",
    "creationMethod": "imported",
    "kind": "content",
    "order": { "position": 3000 },
    "joinFor": {
      "items": "data.executors",
      "separator": "; ",
      "lastSeparator": "; and "
    },
    "display": { "type": "clause" },
    "content": "{{ name }} of {{ address }}"
  }
}

See Folios Schema for canonical table fields.

Import and Export Contract

Importers must validate before write, produce JSON diagnostics/import operations, and write object/version rows only after the parsed JSON contract is valid enough for the target operation.

Exporters must read a specific immutable version snapshot and emit the authored folder/file shape from that snapshot. Do not export by dereferencing mutable heads of source library rows unless the requested export target explicitly is that current head.

For blocks, export shape stays nested even though the app stores blocks in first-class blocks / blockVersions tables. Skill-side export writes blocks back into the parent module file under blocks:; block rows are a storage/query optimization, not a one-file-per-block package format. See Blocks Schema → Export Shape Is Unchanged.

Ordering across the boundary

Order changes representation at the boundary, in both directions. Authored YAML carries it structurally as sequence position; storage carries it as a scalar.

  • Import. Assign order.position as (index + 1) * 1000 per scope — modules against their document version, blocks against their module version. Never write flow.after.
  • Export. Sort the scope by order.position ascending (ties on the row's key), emit in that order, and drop order from the emitted YAML. Re-importing the exported file reproduces the same relative order, though not necessarily the same absolute numbers — a document whose modules sit at 1000/1500/2000 exports and re-imports as 1000/2000/3000. Relative order is what round-trips; absolute values are not part of the authored contract.
  • flow.after on stored rows. Historical rows may still carry it, but import/export ignores it. No boundary operation may reconstruct ordering from an anchor.

The same rule holds one tier down: a data block's fields: is an ordered array whose index is interview order (see Field Declaration Mapping). Order must never be transported as an unordered map at any tier.

Example import preview JSON

{
  "status": "valid",
  "source": {
    "path": "documents/simple_will/modules/executors.md",
    "format": "yaml"
  },
  "operations": [
    {
      "op": "upsert",
      "table": "modules",
      "key": "mod_01HV8XQZK4J9R9W3M6T4M2RMD1",
      "handle": "executors"
    },
    {
      "op": "insert",
      "table": "moduleVersions",
      "key": "modv_01HV8XQZK4J9R9W3M6T4M2RMV4",
      "moduleKey": "mod_01HV8XQZK4J9R9W3M6T4M2RMD1"
    },
    {
      "op": "upsert",
      "table": "blocks",
      "key": "blk_01HV8XQZK4J9R9W3M6T4M2RB01",
      "handle": "executor_details"
    },
    {
      "op": "insert",
      "table": "blockVersions",
      "key": "blkv_01HV8XQZK4J9R9W3M6T4M2RBV1",
      "blockKey": "blk_01HV8XQZK4J9R9W3M6T4M2RB01",
      "moduleVersionKey": "modv_01HV8XQZK4J9R9W3M6T4M2RMV4"
    }
  ],
  "diagnostics": []
}

Round-trip behavior may normalize formatting, ordering, and implicit defaults, but it must preserve semantic content:

  • stable handles and field keys
  • lifecycle and lineage metadata
  • data declarations and collected values
  • block ordering, conditions, display metadata, and content
  • expressions as expression text
  • references and interpolation paths

Comments are authoring aids only. Exporters may emit comments, but importers must not depend on comments for persisted meaning.

On this page