card-yaml Blocks¶
Quillmark documents carry structured metadata in card-yaml blocks: explicitly delimited blocks that isolate YAML data from the surrounding Markdown prose. The first such block (the root block) names the format used to render the document; later blocks are composable cards.
~~~
$quill: my_format
$kind: main
title: My Document
author: Jane Doe
date: 2025-01-15
tags: ["important", "draft"]
~~~
# Document content starts here
Block Structure¶
A card-yaml block has three parts, in order:
- Opening fence: a bare
~~~(three tildes, no info string). No leading indentation. The~~~card-yamlinfo string is also accepted on input as a non-canonical alias; it parses identically and re-emits as a bare~~~. - YAML payload: a standard YAML mapping. The reserved keys
$quill,$kind,$ext, and$seedcarry system metadata (see below); every other key is a user-defined data field. - Closing fence: a tilde run at least as long as the opener. The canonical opener and closer are both
~~~; a longer opener (e.g.~~~~) requires an equally long closer.
The unstructured Markdown body begins immediately after the closing ~~~
fence and runs to the next opening fence or the end of the document.
A blank line is required immediately above every ~~~ opener,
except when the opener is the very first line of the document. A
~~~ line without a blank line above it is not an opener: it is
treated as an ordinary code block.
Because every column-zero ~~~ block is a card-yaml block, writing a literal
fenced code block in prose requires the escape hatch: use a backtick fence
(or a ~~~ fence carrying a language info string, e.g. ~~~rust). Adding more
tildes does not escape: a ~~~~ block is still a card (its closer must just
be at least as long). A ~~~ fence whose info string is anything other than
card-yaml stays an ordinary code block.
System Metadata ($)¶
The block's YAML payload may contain up to five reserved $-prefixed keys.
After parsing, these keys are extracted from the user field set and exposed
on the block's typed metadata.
$quill: <name>@<version>names the format used to render the document. The root block (the first block, identified by position) must declare$quill: it is the only required$entry. If the root block is missing$quill, parsing fails.$kind: <kind>identifies a card's kind. The root block's kind ismainby position;$kind: mainmay be omitted or declared explicitly: any other value is a parse error. Every composable card must declare a kind matching[a-z_][a-z0-9_]*other thanmain.$ext: <mapping>is an opaque YAML mapping reserved for out-of-band extension data: UI editor state, agent annotations, anything bespoke to a consumer that should not reach the rendered output. Round-trips through Markdown and the storage DTO; never appears in the plate JSON consumed by backends. The value must be a mapping (scalars and sequences are parse errors); an empty$ext: {}is preserved as a distinct, explicit declaration. Consumers namespace inside the map ($ext.editor,$ext.agent, …) to avoid collisions;$ext.editor.titleis the canonical slot for a per-card display name (an editor-side rename).$seed: <mapping>is a root-only mapping of per-kind seed overlays, keyed by card-kind. Like$extit round-trips through Markdown and storage but never appears in the plate JSON; the seeding layer interprets it (see CARDS.md "Per-kind Seed Overlays"). A composable card carrying$seedis rejected.
$ metadata entries may appear anywhere in the block's payload (the
canonical emission puts them first, in the order $quill, $kind,
$ext, $seed). Any other $-prefixed key is a parse error: the set
is closed. To carry a per-card key of your own, put it in $ext under a
namespace you own.
Version Selectors¶
Pin a specific version with @version syntax on the $quill line:
A bare name selects the latest version; @latest, @2, @2.1, and @2.1.0
pin progressively tighter. The Quill Versioning
page owns the full selector semantics.
Quill names must match [a-z_][a-z0-9_]* (lowercase letters, digits, and
underscores; must start with a lowercase letter or underscore).
Payload Data Types¶
The data payload (everything in the YAML mapping except the $-prefixed
metadata keys) is standard YAML.
Strings:
title: Simple String
quoted: "String with special chars: $%^"
multiline: |
This is a
multiline string
Numbers:
Booleans:
Arrays:
Objects:
Object-valued fields must be schematized in Quill.yaml with type: object
and a properties: map; array-valued fields with type: array and an
items: element schema (e.g. items: { type: string }, or items: { type:
object, properties: … } for a list of objects). Nesting beyond one level is
not supported. See
Quill.yaml Reference: Field Types.
Field names must match [A-Za-z_][A-Za-z0-9_]*. Lowercase is the canonical,
recommended convention, but uppercase is accepted and preserved verbatim (case
is significant). Only $-prefixed keys are reserved for system metadata.
Comments¶
YAML comments are supported in the payload and round-trip through
toMarkdown, both own-line comments and inline comments:
Comments adjacent to $ metadata keys (own-line or inline) round-trip
identically to comments on data fields.
Placeholder Fields (!must_fill)¶
A field tagged !must_fill marks the value as a placeholder awaiting input.
The tag round-trips through parsing and emit, so editors and bindings can
detect and update placeholders without losing them.
recipient: !must_fill
department: !must_fill Department Here
tags: !must_fill []
addr:
street: !must_fill # nested leaf, inside an object
city: Anytown
recipients:
- name: !must_fill # nested leaf, inside an array element
role: lead
!must_fill is valid on scalars (string, number, bool, null) and sequences,
both at the top level and on leaves nested inside objects and array elements.
It is rejected on mappings (tag the leaves, not the container). !must_fill
is the only placeholder tag; every other custom YAML tag (!include, !env,
!fill) is dropped with a warning and the value kept.
Use block style for placeholders. A marker written inside a flow
collection (addr: {street: !must_fill}), on a bare sequence element
(- !must_fill), or under a YAML anchor/merge key is not preserved: the
flow and bare-element cases emit a parse::fill_marker_unsupported_position
warning so the loss is never silent.
Card Blocks¶
Every block after the root is a card: a composable, repeatable record. A card
declares $kind: <kind> (matching [a-z_][a-z0-9_]*, never main) alongside its
data fields; the Markdown after its closing ~~~ fence is the card's body.
~~~
$quill: my_quill@1.0
$kind: main
title: Main Document
~~~
# Introduction
Some content here.
~~~
$kind: products
name: Widget
price: 19.99
~~~
Widget description.
~~~
$kind: products
name: Gadget
price: 29.99
~~~
Gadget description.
Each card is collected into the plate JSON's $cards array. Its body Markdown:
everything between that card's closing ~~~ fence and the next block's opener
(or document end): is carried as the card's $body value.
Card kinds and their field schemas are declared in Quill.yaml under
card_kinds; see the
Quill.yaml Reference.
Emission¶
toMarkdown always emits the canonical block form: a bare ~~~
opener, the $ metadata lines in the canonical order $quill, $kind,
$ext, $seed, the remaining data fields, and a ~~~ closer. The root
block emits $quill and $kind: main plus any $ext / $seed it
declared ($seed is root-only); composable cards emit $kind: <kind> plus
any $ext they declared. Fence markers,
key ordering, and YAML quoting are normalised; !must_fill tags and YAML
comments (own-line and inline trailing, including those adjacent to $
lines) survive the round-trip.
The payload is coerced and validated against the schema declared in the
Quill's Quill.yaml (main.fields). See the
Quill.yaml Reference for field
types and constraints.