Skip to content

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 quill that renders 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

The rules a writer meets first:

  • Open and close the block with a line of exactly ~~~, at column zero. Not --- front matter, and not a backtick fence.
  • $quill: names the quill that renders the document. The $ is part of the key.
  • Every other line is field: value. Quote a value that contains :, as in subject: "Re: budget".
  • A code block in the prose takes a backtick fence: a ~~~ at column zero after a blank line opens a card-yaml block.

Prettier breaks cards

Prettier reprints every ~~~ fence as a backtick fence, which opens no card, so a formatted document fails to parse. Keep Quillmark documents out of prettier with a .prettierignore line naming their directory, such as quills/ in a quiver.

Block Structure

A card-yaml block has three parts, in order:

  1. Opening fence: a bare ~~~ (three tildes). No leading indentation. Any info string is accepted on input — ~~~card-yaml and ~~~yaml among them — and re-emits as a bare ~~~.
  2. YAML payload: a standard YAML mapping. The reserved keys $quill, $kind, $ext, and $seed carry system metadata (see below); every other key is a user-defined data field. The payload is one YAML value: text after a {...} mapping written as the whole payload, or after a ... line, fails the parse as parse::yaml_error_with_location at that text. Comments may follow it. A mapping names each key once: two keys of one text, quoted or not (1 and "1"), fail the parse at the second.
  3. 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. Tildes offer no escape. Adding more does not help — a ~~~~ block is still a card (its closer must just be at least as long) — and neither does a language info string: ~~~rust opens a card whose payload is your Rust. Code that reads as a YAML string or list fails at that fence's line and names the backtick fence to write instead.

System Metadata ($)

The block's YAML payload may contain up to four 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 quill that renders 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 is main by position; $kind: main may be omitted or declared explicitly: any other value is a parse error. A composable card names its kind, matching [a-z_][a-z0-9_]* other than main, from the quill's card_kinds. A block after the root with no $kind fails the parse (parse::missing_kind) at its fence. A card with a kind the quill does not declare still renders, but nothing checks its fields, and a plate shows only the kinds it knows. quill.validate(doc) warns on it (validation::unknown_card) and lists the declared kinds.
  • $id: <card-id> names a composable card across edits, so a rewrite or a comment finds it wherever it moved. Leave it out of a new card and one is minted (c3kq9f); keep the line a card already has when you edit it. You may write your own: 1 to 64 letters, digits, _ or -, opening with a letter or digit, unique in the document. One outside that, or one an earlier card holds, is replaced with a minted id and a parse::card_id_reminted warning. The root block takes none, and $id never reaches the plate JSON.
  • $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.title is the canonical key 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 $ext it 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 $seed is rejected.

$ metadata entries may appear anywhere in the block's payload (the canonical emission puts them first, in the order $quill, $kind, $id, $ext, $seed). Any other $-prefixed key is a parse error: the set is closed.

Version Selectors

Pin a specific version with @version syntax on the $quill line:

~~~
$quill: my_format@2.1
$kind: main
title: Document Title
~~~

A bare name matches any version, @2.1 any 2.1.x, and @2.1.0 that version alone. A lone major, @2, fails to parse, and there is no @latest: write the bare name. The Quill Versioning page owns the full selector semantics.

Quill names are [a-z][a-z0-9_]*, at most 64 characters. A $quill name may carry the collection the quill comes from, $quill: borb.usaf_memo@1.2, under the same rule; the host resolves the collection, and Quillmark pairs the document with its quill by the name after the ..

Payload Data Types

The data payload (everything in the YAML mapping except the $-prefixed metadata keys) is standard YAML.

YAML reads an unquoted value by its shape before the schema sees it, so a text field takes what YAML read, not what you wrote. Quote these:

Written Reaches the page Write instead
zip: 02134, a leading zero 2134.0 zip: "02134"
ver: 1.10, a version-like number 1.1 ver: "1.10"
batch: 2024_07, an _ between digits 202407 batch: "2024_07"
code: 0x1F, a 0x, 0o or 0b prefix 31 code: "0x1F"
room: 1E3, an exponent 1000.0 room: "1E3"
phone: +15551234567, a leading + 15551234567 phone: "+15551234567"
id: 18446744073709551616, an integer past 18446744073709551615 1.8446744073709552e+19 id: "18446744073709551616"
ratio: .5 or 5., a . with no digit on one side 0.5, 5.0 ratio: ".5"
ext: .inf or .nan, in any letter case nothing: the parse fails ext: ".inf"
id: 1e999, a number too large to hold nothing: the parse fails id: "1e999"
text: Approve Item #12, a space or tab then # Approve Item: the rest is a comment text: "Approve Item #12"

quillmark check reports only the rows that fail the parse; the rest are well-formed YAML, read as a number or cut at a comment. A word needs no quotes unless it is true, false or null, in any letter case, or ~: yes, no, on, off, y and n stay text.

Strings:

title: Simple String
quoted: "String with special chars: $%^"
multiline: |
  This is a
  multiline string

Numbers:

count: 42
price: 19.99

Booleans:

published: true
draft: false

Arrays:

tags: ["tech", "tutorial"]
# or
authors:
  - Alice
  - Bob

Objects:

author:
  name: John Doe
  email: john@example.com

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: text }, or items: { type: object, properties: … } for a list of objects). See Quill.yaml Reference: Field Types.

A Quill.yaml declares field names as [a-z][a-z0-9_]*, so every schema field is lowercase. The document parser is wider: it accepts any [A-Za-z_][A-Za-z0-9_]* key and preserves case, so an uppercase or underscore-led key parses but is always undeclared. An undeclared key stays in the document, and quill.validate(doc) warns on it (validation::unknown_field), naming the declared field it most likely meant where one is close. 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:

# An own-line comment.
title: My Document  # an inline comment

Comments adjacent to $ metadata keys (own-line or inline) round-trip identically to comments on data fields.

YAML Tags

Custom YAML tags (!include, !env, …) are not supported. A tag is dropped with a parse::unsupported_yaml_tag warning at the tagged node's path and its value kept, wherever it sits: on a key's value, a - element, a key, or a node inside a flow collection.

A core tag (!!str 5) takes effect as the value is read, so k: !!str 5 keeps "5", a string; the tag itself drops and warns the same way.

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. A card of a kind declaring body.enabled: false renders without its body, and quill.validate(doc) warns (validation::body_disabled).

~~~
$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; 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.