Documents, Modules, and Blocks
Author's-eye view of the YAML/Markdown shape — module file layout, ingestion guidance, and pointers to the schema reference
This page is the authoring view of the content model: what a module file looks like on disk, how ordering and references work in source, and when to split content into a new module. For storage shape, lifecycle, lineage, and version mechanics, see the schema reference:
- Modules — module identity, lifecycle, data-pool rules, version pinning
- Blocks — block kinds, field entries, rendering model, numbering resolution
- Documents — document object/version split, template defaults
- Derived Selection Facets —
performs/consumes/flow.adapt
A document is assembled from modules; modules contain ordered blocks; every block is content or data:
Document → Module[] → Block[] # block.kind: content | dataThe module is the sequencing and reusable legal unit. The block is the universal leaf unit: data blocks capture a group of fields (one block per interview step) and hold their collected values; content blocks express the data as composed prose. There is no separate section / block-group level — module-level structure is the only grouping.
Workspace and Project
A Project is a Folio that contains Documents. In the UI a Project Stack shows those Documents as its Substacks, by the same recursive containment model that makes a Module Stack show its Blocks.
Workspace and Project are stored Folio kinds in folios, with
keys, handles, owners and live heads. Workspace contains Projects; Project
contains Documents through the ordinary parent and pin mechanism. The Workspace
canvas is Home, showing its Projects. Cards expose Label and About in place;
only their bottom Open action navigates.
One Workspace is provisioned per browser through the interim door. Container reads take an explicit Folio key and check the Workspace's browser binding; Document reads and edits keep their existing session authority. Work Bar crumbs follow Document → Project → Workspace, with no parent above Workspace. Organisation is not a Folio. Sign-in and team membership remain future work.
Workspace and Project operation-target entries remain undefined, with refusal
coverage. The interim creation and metadata door does not invent insertion,
arrangement or lifecycle operations for those kinds. See
The Workspace Is Home; the Project Is the Sharing Grain.
Folio Insertion
Insertion belongs to the parent containment relationship, not to
the child in isolation: a Document adds a Module, a Module adds a Block, and a
Project adds a Document. The parent operation decides whether and where
the new Folio enters its arrangement; the child-kind adapter mints the stable
identity and initial version that make that Folio. UI labels name the child
(Add Module, Add Block, Add Document), never the Stack furniture.
This rule does not collapse creation into generic CRUD. Placement, parent invariants, and the child's initial version payload remain distinct concerns.
The operation contract and current menus apply this parent-owned rule. Complete persisted Builder authoring for Module/Block insertion remains in the Builder-first MVP; an available local menu action does not claim reload-safe persistence.
Folio Lifecycle and Removal
Delete and removal are deliberately different operation families, and each applies on its own kind of card.
Delete is the reversible lifecycle operation, on any Folio. Delete /
Put back (the on-screen labels; delete and restore in code) take a
Document, a Module or a Block off the active list and put it back on — list
visibility: deleting a container also deletes its owned descendants after confirmation. Saved editions remain stored; restoring the container restores the same deletion cohort.
Permanent, unrecoverable destruction is a separate decision this product does
not build; there is no purge, no cron, and no "Deleted" page today.
Remove is arrangement, not lifecycle. Remove Module and Remove Block
change the parent Folio's arrangement: the assembled child no longer belongs
in that Document or Module, which is why the row names what it removes the
folio from ("Remove from this document", "Remove from this module") rather
than the folio itself. A removed folio is not deleted — search still finds
it — and a deleted folio is not removed from its parent's arrangement; the
two facts are independent.
Delete/Put back and Remove both use the existing unified executors. Since 2026-09-09 the card menus offer only Delete; the Remove executor is kept for the Library and is not reachable from a Module or Block menu.
Folio Duplication
Duplicate is source-owned copy-write branching, not ordinary
insertion. The source Folio determines the snapshot and lineage the branch
inherits; its parent arrangement determines where the new branch goes—normally
immediately after its source in the same sibling scope. The UI may simply say
Duplicate, but the operation contract must preserve both halves.
Document duplication is persisted. Module and Block duplication currently uses the local arrangement transition and therefore does not yet survive reload.
Folio Library Journey
Consolidated 2026-09-25 into Libraries: what a Library is, its tables, saving, search, insertion, access, the Library menu (formerly Library menu readiness here) and the open questions.
Folio Reviewed Edits
A reviewed edit is a Folio operation family realised through the canonical Proposed Change lifecycle: propose → diff → revise → accept or reject → apply. The shared contract owns authority, pending and stale-base state, acceptance/rejection semantics, and the rule that an agent describes its edit as proposed and awaiting acceptance.
A kind-and-facet adapter owns the editable representation, complete-source requirement, compiler/validator, and applied patch. Direct collected-value writes remain immediate writes under their own guards; they are not Proposed Changes.
One resolved operation contract supplies the Block agent's instruction, allowed output schema, and outcome fold. Proposed Change acceptance separately reconstructs trusted current facts and requires the matching available descriptor before applying the content, About, or Field-structure validator.
Folio Operation Menus
A Folio action menu is a projection of the available operations, not a second operation authority. The operation vocabulary owns canonical action names, grouping and order, required capability, and destructive posture. Its UI adapter supplies a callback and any local disabled reason, then projects the available operations into the menu.
Facet navigation (for example, About this module) is not a Folio operation:
it changes the viewed face rather than the Folio or its arrangement. Icons,
triggers, focus handling, confirmations, and notifications remain UI concerns.
Availability is a pure projection of static operation metadata and current Folio/relationship facts, not capability alone. Capability makes an operation eligible; lifecycle, reuse stage, classification, lock state, parent, sibling position, and other kind rules decide whether it is available. The UI binds the handler and may explain a local disabled reason, but must not present an unexecutable action.
Canonical Module File
A module file uses two top-level namespaces: module and blocks. Field declarations live on kind: data blocks — there is no standalone data: namespace, no computed:, no ui:, and no top-level flow:. Required fields use the DSL ! marker.
module:
handle!: termination
label!: Termination
description: Termination rights and consequences
flow:
condition: '@data.has_termination_rights'
blocks:
- handle!: termination_details
label: Termination details
kind: data
guide: |
Establish how termination should work before drafting the clause.
fields:
allows_termination_for_convenience|YesNo!:
label: Allows Termination For Convenience
notice_period|Duration!:
label: Notice Period
notice_deadline|date: = completion_date - notice_period
- handle!: termination_for_convenience_clause
label: Termination for convenience clause
kind: content
flow:
condition: '@data.allows_termination_for_convenience = "Yes"'
classification:
taxonomy: termination
detail: concise
protects: both
display:
start_level: 3
content: |
Either party may terminate this agreement...
- handle!: termination_notice_summary
label: Termination notice summary
kind: content
display:
numbering: {paragraphs: no}
content: |
Notice period: @data.notice_period.Notes for authors:
- A block's
handle!is required authored identity. A UI may derive a proposed handle from a person-entered label, but an agent writing a block always emits the handle. Human-readable@references use that handle and compile to the stableblk_identity underneath. Blocklabel:is optional display text and never reference authority. - A data block's
fields:holds collected fields and computed values. A field declaredkey|type: = expressionis computed; without=it is collected from the user. Per-fieldlabelis the human-facing prompt; block-levelguidedescribes the block and tells Goldie how to run the step. - Leave a collected field without
defaultunless the author deliberately intends to pre-answer it. See DSL Syntax → Literal Values. - One data block per interview step: a cohesive group of fields asked, shown, conditioned, or repeated together. Field paths are unique across all data blocks of one module.
- Content blocks read the module data namespace (
@data.notice_period) — never a block handle. Moving a field between data blocks never breaks content. blocksis an ordered list, and its order is sequence position — nothing declares it. See Ordering Is Sequence Position.flow.conditionon a block gates rendering (or, for data blocks, whether the step is asked); on the module it gates whether the module enters the document at all.flow.conditioncarries data state only — it never encodes where a block renders. Nothing does: every folio item lands on the document, and the placement keys that once said otherwise (display.targets, thendisplay.surfaces) are retired — see Permissions and Visibility → One venue and Placement is never a condition. On blocks a condition may usehasValues— true once every reference in the block resolves — for hide-until-complete behavior without hand-listing fields (see DSL Spec → hasValues).display.*is optional — a block'sstart_levelandnumberingflags resolve through the module and then the document when omitted (see Blocks → The Display Facet and Numbering).- For the full field catalog, see Modules → Authored Snapshot and Blocks → Version Snapshot.
Three Independent Axes
These are commonly conflated. Keep them separate:
block.kind— what the block is:contentordata. Modules have no type; a data pool is just a module whose blocks are all data blocks. See Modules → Data Pools.- Derived selection facets (
performs,consumes) — what behaviors and reads the compiler infers from source. Not authored. See Derived Selection Facets. - Placement — no longer an axis at all.
display.targets, and thedisplay.surfaceskey that replaced it, are both retired (2026-08-25): there is one venue and a block does not declare where it renders. What a block is and what it does were always separate from where it went, and that separation now needs no key to express. See Permissions and Visibility → One venue.
Formal block attributes such as editorial detail level and the position the
drafting protects belong in the About card's classification and signal tags;
do not recreate them as free-form collected fields. flow.condition branches on
actual document data, such as a factual answer or an explicit drafting choice.
See Folios Schema → tags.
Data Pool Modules
The canonical handle for a document's primary data pool is literally data_pool. Other modules read its facts through cross-module references:
module:
handle!: guardians
label: Guardianship Provisions
flow:
condition: '@data_pool.data.has_minor_children = "Yes"'
blocks:
- handle!: guardian_details
label: Guardian details
kind: data
fields:
primary_guardian_name|PersonLegalName!:
question: Who should be appointed as guardian?
primary_guardian_address|StreetAddressAU:
- handle!: guardian_appointment
label: Guardian appointment
kind: content
content: |
I appoint @data.primary_guardian_name as guardian of my children.For pool sharing, write authority, and the multi-pool naming convention, see Modules → Data Pools.
References in Source
One @ syntax addresses field values and block citations. Authored references
use module and block handles or field paths, never labels:
@data.notice_period
@termination.data.notice_period
@termination_for_convenience_clause
@termination_for_convenience_clause:numberThe compiler binds readable handles to stable fld_ and blk_ identities
before persistence. Renaming a handle therefore updates the projected source
without breaking the reference. Legacy {{ ... }} interpolation and
[[module#^block]] wiki links are not supported.
The full reference grammar, modifiers, termination rules, and typed failures
are in DSL Syntax → References.
Identifier scoping (handle vs key vs label) is in
Modules → Identifier Model and
Blocks → Identifier Model. Labels are
safe to edit because they are never reference authority.
Ordering Across Source and Storage
In an offline authored Markdown file, order is position in the YAML
sequence. Modules are sequenced within a document and blocks within their
module. The authored entry carries no order property: being third in the
list means being third.
modules:
- handle!: lead_capture # first
- handle!: guardians # second
- handle!: executors # thirdTo reorder offline source, move the entry. To delete it, remove it and let the remaining entries close up. There is no authored pointer to repair, first-item marker, or absolute position number.
The database uses a different representation. Each moduleVersions or
blockVersions row owns a gapped integer at order.position, scoped to its
document or module. Database reads sort sibling rows by that number; the
parent does not store an ordered child array. App moves update the row's
position rather than rewriting a YAML sequence.
At the import/export boundary, relative order is translated in both directions:
- Import: sequence index becomes
order.position, initially spaced as1000,2000,3000, and so on within each sibling scope. - Export: rows are sorted by
order.positionand emitted as a YAML sequence; the numeric position is omitted from authored source.
Only relative order round-trips. The database may use 1500 for an insertion
between 1000 and 2000; exporting and re-importing can normalize those
numbers while preserving the same sequence. See
YAML ↔ JSON Mapping → Ordering.
flow.after is retired from authored source. It stored a relative anchor — "render me after the sibling named X" — which conflated ordering with inclusion and broke as soon as conditions entered: if X is conditioned out, the anchored item has nowhere to go, and every recovery rule is a guess. It also made cycles representable, cost three writes per move, and required a topological sort on every read.
Order and inclusion are separate concerns: stored position is absolute
(order.position, unaffected by any runtime answer) and inclusion is
conditional (flow.condition). Authored sequence position is the offline
notation for that stored scalar. A block that only applies on one branch
carries that branch's condition and sits in its natural position; if the
branch does not fire it renders nothing and the blocks after it close up.
See the archived plan 2026-07-30-block-ordering-model. Stored form is now the absolute order.position scalar.
Conditional Runs
Both a per-block condition and a module condition exist, deliberately:
- Condition the module when the run is the module's whole reason to exist — guardians, business assets, a testamentary trust. Written once; every block inside inherits it. The module is also the unit that moves as a group.
- Condition the block when the run is a detail inside a module that stands on its own — one or two blocks that only apply on a branch.
A condition names a value, never the existence of another block. "Appears if the previous block appeared" cannot express branching: the block a branch depends on is usually the unconditional question that creates the branch, so existence cannot distinguish two branches sitting behind it.
Lists and Variants
List rendering belongs on block metadata via joinFor or repeatFor (with optional sibling intro/outro blocks), not inline in content:. For full grammar, see DSL Syntax → Block Iteration.
Conditional selection is data-driven: a flow.condition such as
@data.allows_termination_for_convenience = "Yes" determines whether a block
renders from an actual document answer. Formal descriptive facets such as
detail level remain in classification; metadata does not directly drive
rendering.
Optional App/Library Metadata
Local skills do not need access, classification, or signal to interview or render. Those fields are app-tier metadata for sharing, search, review, ranking, and reuse:
signal:
rating: gold # curated tier: gold | silver | bronze
readability: 82 # computed score
popularity: 0.74 # adoption signalSignals never control rendering. They inform recommendations, drafting feedback, and ranking only.
UI Behavior (Editor)
Default editor rules:
- modules are collapsible; blocks are not
- module labels sit as subtle left-gutter anchors
- block labels appear above blocks on edit/hover/focus, and are UI chrome — not document text
- block actions (change display type, change level, edit label, move, remove) open from the label/control surface
Ingestion Rules
Create a new module when:
- there is a part heading
- there is a major section or topic shift
- clauses move into a distinct legal concept
- the unit should be reusable, swappable, or versioned independently
Do not combine unrelated concepts into one module — termination and indemnity are separate modules even when they sit next to each other in the source.
Common module boundaries: Definitions, Interpretation, Conditions precedent, Payment, Confidentiality, Warranties, Indemnity, Limitation of liability, Termination, Boilerplate, Execution.