Corpus Seeding
Editing authored corpus source files, compiling seed fixtures, and reseeding Convex
Library documents are authored as Markdown/YAML source files and compiled into Convex seed fixtures. Use this workflow when changing module definitions, block content, interview questions, data fields, or authored flow rules.
The Corpora
Two documents are seedable. Both compile through the same script.
| Corpus | Modules | Purpose |
|---|---|---|
services_agreement | 3 | Start here. Small demo fixture for fast iteration. |
legal_zebra_will | 8 | Full production-shaped corpus. |
Prefer the services agreement while iterating. It compiles to roughly 400 fixture lines against the Will's ~2,600, so a broken block is findable and a reseed is cheap. Reach for the Will when you need the scale or the specific content.
Each corpus owns a deterministic key prefix so the two cannot collide:
legal_zebra_will doc_01krq700000000000000000001 docv_01krq700000000000000000002
services_agreement doc_01ksa100000000000000000001 docv_01ksa100000000000000000002Never change a corpus keyBase
A corpus's keyBase in scripts/compile-legal-zebra-seed.mjs determines
every key it emits. Those keys are referenced by working documents already in
the database, so changing one silently orphans them. Adding a corpus means
adding a new prefix.
Source Files
tests/skills/demo_skill_v1/documents/services_agreement/document_input/document.md
tests/skills/legal_zebra_will_skill_v1/documents/legal_zebra_will/document_input/Two Layouts
The compiler detects which layout a corpus uses:
- One file per document (settled). No
modules/directory; modules are amodules:sequence insidedocument.md, and sequence position is the order. The services agreement is authored this way. See One File Per Document. - File per module (legacy). The compiler retains compatibility for old
sources, but both seedable corpora use the settled one-file layout. The
dormant
goldiblocks_simple_will_skill_v1fixture remains deliberately parked outside the seed compiler; do not half-migrate it.
Author new corpora in the one-file layout.
Authoring Rules
- A module need not have a heading, and its data blocks may precede its prose.
When its prose opens a section, write that heading as Markdown (
# Heading) in a content block and set itsdisplay.start_level. Headings are transparent and render wherever their span has visible content. See Blocks → Headings as Structure. - Put conditional data/detail blocks after the heading block.
- Never author placement. There is one venue and every block lands on the
document, so
display.surfacesanddisplay.targetsare both retired (2026-08-25) anddisplayholdsstart_levelandnumberingonly. See Permissions and Visibility → One venue. - Do not write interview recap prose as a block. A summary such as "you
have told us you have no children" now renders into the instrument, because
nothing keeps it out. Such text belongs in the data block's
guide:, which is Builder- and agent-facing and never renders as document content. - Do not author
condition: uiMode = .... It is retired: a condition tests data, never placement. See DSL Spec → Placement is never a condition. - For collection summaries, use
joinForwith newline separators. - Author every reference with
@:@data.fieldfor local values,@module.data.fieldfor cross-module values,@block_handlefor local block citations, and explicit@item.fieldinside collection bodies. - Use only the closed modifier set (
:label,:number,:handle,:upper,:lower). Do not author{{ }},[[ ]], fallback filters, modifier arguments, or chained modifiers. - Use the existing field names in the same module unless the change intentionally adds or migrates fields.
`yes` / `no` are coerced at compile time
The yaml package parses on the YAML 1.2 core schema, where only
true/false are booleans — bare yes and no arrive as strings. The
compiler coerces them for the known boolean flags (the numbering flags),
so the authored forms above are safe.
This matters because the write path tests === true: an uncoerced "yes"
would read as false and silently turn a flag off. Anywhere outside those
known flags, write true/false explicitly.
Unrelated to YesNo field values, which are the string literals "Yes"
and "No" by design.
Compile and Reseed
bun run reseed:demobun run reseedreseed:demo handles the services agreement; reseed handles the Will. Each
does three things:
- Compiles the corpus into its fixture under
convex-dev/. - Touches
convex/folio/seed.tsso the runningbunx convex devwatcher redeploys. (convex-dev/is outside theconvex/watch root, so changes there do not trigger a redeploy on their own.) - Runs the corpus's unified seed mutation (
folio/seed:seedServicesAgreementorfolio/seed:seedLegalZebraWill) withreset:true.
The unified seed writes the Folio tables and nothing else (GB-152 Stage A):
every Document, Module and Block becomes a folios row with one sealed
version 1 and one live head cloned from it, every container edition gets
its schedule of folioPins, and the Document gets a route (services-agreement,
legal-zebra-will) and an authorised session (session_seed_<handle>). It
never writes libraries, libraryPins or tags.
In plain terms: each sample document is filed into the new cabinet as a sealed first edition plus an open working copy, with the web address and the permission slip needed to open it.
reset:true deletes the seed's own rows by key — both editions of every card,
their pins and the seeded session — and inserts afresh, so a reseed replaces
rather than duplicates and discards every sample edit made since (owner
ruling, 2026-09-02: the data is disposable). Without reset, a corpus that is
already seeded is refused rather than patched, because a sealed edition is
never rewritten in place.
Who owns a seeded Document (owner ruling 2026-09-25, GB-10). With no
createdBy and no browserClientId, the seed belongs to the one signed-in
account on the deployment and is filed in that account's Workspace, in a
"Seeded examples" Project, so it opens signed in from any browser. On a fresh
deployment no one has signed in to, it belongs to the seed user
(user_dev_seed), whom no account answers to; sign in once and reseed to make
it yours. With several accounts the seed refuses rather than guess: pass
createdBy with the owning user's key.
In plain terms: a sample file goes in the one client's cabinet if there is exactly one client; with several, the clerk asks whose it is.
To give a signed-out browser its own copy instead, pass the same client id the
canvas presents (getBrowserClientId() in the browser console):
bunx convex run folio/seed:seedServicesAgreement '{"reset":true,"browserClientId":"<your id>"}'To compile without seeding — every corpus, or named ones:
bun scripts/compile-legal-zebra-seed.mjsbun scripts/compile-legal-zebra-seed.mjs services_agreementDo not hand-edit the generated files under convex-dev/. Change the Markdown
source, then recompile.
A type error anywhere in convex/ blocks seeding
Convex typechecks the whole convex/ directory before pushing, so a type
error in any file there — including a test file unrelated to seeding —
stops the new functions from registering. The symptom is misleading:
bunx convex run reports Could not find function, as if the mutation did
not exist.
Run bunx convex dev --once to see the real error.
Verify
Run the reconciliation report against the deployment:
bunx convex run folio/seed:reconcileIt returns ok: true with the row counts, or a list of breaches. It fails on
any folio without exactly one live head cloned from a sealed baseline, any
version or pin naming a missing row, a child pinned under the wrong parent or
twice, a sealed schedule pinning a live edition, a payload disagreeing with its
kind, any difference in structure or wording from the authored corpus, and any
row in libraries, libraryPins or tags. Name the corpora to check when
only one is seeded:
bunx convex run folio/seed:reconcile '{"corpora":["services_agreement"]}'The same report runs in convex/folio/seed.convex.test.ts on a test backend,
beside the golden fixtures under convex/folio/golden/ — the canvas
snapshot, version list and export input of each seeded Document, pinned as
data. A change that alters what a seeded Document reads as fails those tests
and is re-pinned deliberately (bunx vitest run -u on that file).
Optional local fixture checks:
rg '\{\{|\[\[' tests/skills convex-dev/seedLegalZebraWill.ts convex-dev/seedServicesAgreement.tsThis should return no matches. Authored source contains readable @
references; generated fixtures contain their stable compiled form.
Adding a Corpus
- Create
document_input/document.mdunder a newtests/skills/{skill}/documents/{handle}/directory, in the one-file layout. - Add an entry to
CORPORAinscripts/compile-legal-zebra-seed.mjswith a freshkeyBase. - Add the corpus to
SEED_CORPORAinconvex/folio/seed.tsand export a seed mutation wrappingseedUnifiedCorpus. - Add a
reseed:*script topackage.json, and the corpus's golden fixtures by runningconvex/folio/seed.convex.test.ts.
Working Documents
There is no library at MVP: the seeded Document is the working document, opened by its route. Reseeding replaces its rows outright, so any sample edit made since is gone — start again from the reseeded editions.