Quill.yaml Reference¶
Complete reference for authoring Quill.yaml configuration files. For a hands-on introduction, see Creating Quills.
File Structure¶
A Quill.yaml has these top-level sections:
quill: # Required: format metadata
...
main: # Optional, main entry-point card: field schemas and optional ui/body
fields:
...
ui: # optional UI hints (e.g. title)
body: # optional body-region config (e.g. enabled, example)
card_kinds: # Optional: additional composable card kinds
...
typst: # Optional: backend-specific configuration
...
Root-level fields: is not supported; define the main document's field schemas under main.fields.
Quill.yaml is parsed strictly. Unknown keys in the quill: section, unknown top-level sections, malformed ui: blocks, and field schemas that can't be parsed all produce errors: they are never silently dropped. Every error is collected in a single pass, so authors see all problems at once. Run quillmark validate <quill_dir> to surface them.
quill Section¶
Every Quill.yaml must have a quill section with format metadata.
quill.name must be snake_case (^[a-z][a-z0-9_]*$).
| Key | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Unique identifier for the Quill |
backend |
string | yes | Rendering backend (e.g. typst) |
description |
string | yes | Human-readable description of the quill itself (non-empty). Independent of main.description, which is the optional schema description authored under main:. |
version |
string | yes | Semantic version (MAJOR.MINOR or MAJOR.MINOR.PATCH) |
author |
string | no | Creator of the Quill (defaults to "Unknown") |
ui |
object | no | Document-level UI metadata |
A backend's own settings live under its backend-named section, not in quill:.
The Typst template, for example, is declared as typst.plate_file (see the
typst Section below).
quill:
name: usaf_memo
version: "0.1"
backend: typst
description: Typesetted USAF Official Memorandum
author: TongueToQuill
typst:
plate_file: plate.typ
main Section¶
The main document card holds root-block field schemas under main.fields. Optional main.description describes the schema itself (independent of quill.description, which describes the quill package). Optional main.ui sets container-level UI for that card. quill.ui is a fallback for main.ui, not a merge: any main.ui (even an empty ui: {}) wins wholesale, and quill.ui applies only when main.ui is absent.
Field order under main.fields is display order in UIs: the declaration order of the keys is carried structurally through parsing and schema emission, so consumers walk the fields in key order. There is no ui.order knob: to reorder fields, reorder them in Quill.yaml.
Field keys must be snake_case (^[a-z][a-z0-9_]*$). Capitalized field keys are reserved.
main:
fields:
subject: # Field name (used as the card-yaml payload key)
type: string
description: Be brief and clear.
Field Properties¶
| Property | Type | Required | Description |
|---|---|---|---|
type |
string | yes | Data type (see Field Types) |
description |
string | no | Detailed help text |
default |
matches type |
no | The value the majority of authors want. When the field is omitted, the default is filled in. Declaring default makes the field Endorsed: the blueprint renders the concrete default value with a type-only annotation (no marker), shippable as-is. Omitting default makes the field Unendorsed: the blueprint stamps the !must_fill marker (carrying the field's example as a suggested value when present, else bare). A surviving marker raises the non-fatal validation::must_fill warning: it never gates render, since an absent or present-null field zero-fills. |
example |
matches type |
no | A value matching the type and shape of what the author wants, but not the value desired most of the time. Documents shape only: surfaced in the blueprint's # e.g. line for documentation and LLM authoring, never rendered as the value. |
values |
array of strings | for enum |
The closed set of allowed string values. Required on every enum field. |
enum |
array of strings | no | Deprecated alias of values, accepted on type: string for one release. Prefer type: enum with values:. |
ui |
object | no | UI rendering hints (see UI Properties) |
items |
object | for array |
Element schema for an array field (a nested field schema). Required on every array. |
properties |
object | for object |
Nested field schemas for an object typed dictionary (or an array's object-typed items). Required on every object field. |
inline |
boolean | no | For richtext and plaintext only: constrain the content to a single paragraph/line (a one-line editor surface). |
Field Types¶
| Type | Notes |
|---|---|
string |
Open scalar UTF-8 text: a value the template computes with (a URL, path, identifier, or reference key), not prose it lays out |
enum |
A closed set of string values; requires a values: list. Projects to JSON-Schema {type: string, enum: […]} |
plaintext |
Navigable, unformatted prose over the canonical content: the same nav/regions as richtext, but a literal codec (delimiters stay literal, no markup). Add inline: true for the single-line variant |
number |
Numeric scalar (integers and decimals) |
integer |
Integer-only numeric scalar |
boolean |
true or false |
array |
Ordered list; requires an items: element schema |
date |
A strict calendar date YYYY-MM-DD; rejects any time component |
datetime |
A strict offset-less wall-clock datetime YYYY-MM-DDThh:mm[:ss]; rejects offsets, the space separator, fractional seconds, and bare dates |
richtext |
Rich, formatted prose over a canonical content; backends lower it to the target format. Markdown is its import/export projection. Add inline: true for the single-paragraph variant |
object |
Structured map; requires a properties: map |
The four text-ish types form a 2×2 of data vs content and open/plain vs
closed/formatted: enum (closed data), string (open data), plaintext
(plain content), richtext (formatted content). Reach for string when the
value is computed with, plaintext/richtext when it is prose the author
navigates.
Enum Constraints¶
Declare a closed string domain with type: enum and a required values: list:
main:
fields:
format:
type: enum
values:
- standard
- informal
- separate_page
default: standard
description: "Format style for the endorsement."
The enum: modifier on type: string is a deprecated alias, accepted for one
release. enum:/values: on any other type is a load error.
Primitive Arrays, Typed Tables, and Typed Dictionaries¶
Every array declares its element type under items:. For a primitive list, give items a scalar type, coercion and validation then apply element-wise (e.g. each element of an integer[] is coerced to an integer, and a bad element fails at its indexed path like counts[1]):
main:
fields:
tags:
type: array
items:
type: string
counts:
type: array
items:
type: integer
sections:
type: array
items:
type: richtext # each element's content is lowered to backend markup
For a typed table (a list of structured rows) give items an object type with its own properties:. Coercion recurses into each element and converts property values to their declared types:
main:
fields:
cells:
type: array
items:
type: object
properties:
category:
type: string
score:
type: number
Use type: object with properties: for a single structured mapping:
Nesting beyond one level is not supported: an array element may not itself be an array.
UI Properties¶
The ui property on fields controls how form builders and wizards render the field. These are UI hints, not validation constraints.
title¶
Overrides the display label shown next to the input. Form builders derive a label automatically from the snake_case field key (memo_for → "Memo For"), so ui.title is only needed when that automatic label is wrong or misleading:
main:
fields:
memo_for:
type: array
items:
type: string
ui:
title: To # "Memo For" would confuse users unfamiliar with memo conventions
group and the group registry¶
Groups organize fields into visual sections. A group has two parts: a registry declared once on the card (ui.groups), and a per-field reference (ui.group) into that registry.
main:
ui:
groups: [addressing, letterhead] # declaration order = display order
fields:
memo_for:
type: array
items:
type: string
ui:
group: addressing # a reference, validated against the registry
memo_from:
type: array
items:
type: string
ui:
group: addressing
letterhead_title:
type: string
ui:
group: letterhead
The registry is the card's table of contents. Its keys are snake_case ids (same discipline as field keys), and their declaration order fixes the group display order: the contract every consumer follows, exactly as field declaration order fixes field display order. A field's ui.group names one of those ids; a value with no matching registry key is a load error (quill::unknown_group), so a one-character typo cannot silently split a section.
Identity is the id, not the label. Consumers derive a group's display label from its id (addressing → "Addressing"), just as a field label is derived from its key. Override the derived label with title:, which requires the mapping form of the registry:
main:
ui:
groups:
addressing: {} # label derived: "Addressing"
letterhead: { title: "Letterhead & Seal" } # label overridden
The two registry forms are interchangeable: a bare sequence of ids ([addressing, letterhead]) when no labels need overriding, or a mapping of id → attributes when they do. Renaming a label touches one line and never breaks a ui.group reference or persisted per-group editor state.
group applies only to card-level fields (those directly under a card's fields:). Grouping never descends into an object's properties or an array's items, so a group on a nested property is a hard error (quill::nested_group_not_supported) rather than a silently inert knob.
Implicit groups (deprecated). A ui.group with no ui.groups registry on the card works: each distinct value is an implicit group whose label is the value, ordered by first appearance. It emits a quill::implicit_group deprecation warning and will become an error in a future release. Declare a registry to silence it.
field order¶
Field display order is declaration order: the order the keys appear in Quill.yaml. This holds at every level: card-level fields, and the properties of a typed dictionary or typed-table row. The order is carried structurally (the schema's field maps preserve key order, and schema() re-emits that order), so no per-field knob is involved.
There is no ui.order key: an authored ui: { order: N } is a load error (quill::field_parse_error) directing you to reorder the fields instead. To move a field, move its block in Quill.yaml.
compact¶
When true, the UI renders this field in a compact style (smaller vertical footprint).
multiline¶
Controls the initial size of the text input for string and richtext fields. When true, the UI starts with a larger text box instead of a single-line input:
main:
fields:
summary:
type: richtext
description: Executive summary
ui:
multiline: true # start as a larger text box
notes:
type: string
description: Free-form notes
ui:
multiline: true
tagline:
type: richtext
description: One-sentence tagline
# no multiline: single-line input that expands on demand
Meaningful on string and richtext fields; ignored on other types.
card_kinds Section¶
card_kinds define composable, repeatable content blocks (the kinds: a document can then carry zero or more instances of each kind, interleaved with body content). Each entry is shaped exactly like main: (fields, optional description, ui, body); think of main: as the single mandatory card-kind for the document body, and card_kinds: as the library of additional kinds that may attach to it.
Card-kind names (the keys under card_kinds) must match [a-z_][a-z0-9_]* (leading underscore is allowed).
card_kinds:
indorsement: # Card-kind name
description: Chain of routing endorsements.
fields:
from:
type: string
ui:
group: Addressing
format:
type: string
enum: [standard, informal, separate_page]
default: standard
Invalid card-kind names include:
BadCard(uppercase letters)my-card(hyphen)2nd_card(starts with a digit)
Card Properties¶
| Property | Type | Required | Description |
|---|---|---|---|
description |
string | no | Help text describing the card's purpose |
fields |
object | no | Field schemas (same structure as top-level fields) |
ui |
object | no | Container-level UI hints (see Card-level ui) |
body |
object | no | Body-region config (see Card-level body) |
Card-level ui¶
| Property | Type | Description |
|---|---|---|
title |
string | Display label for the card kind. Literal string or {field} template |
Card-level body¶
| Property | Type | Description |
|---|---|---|
enabled |
bool | Whether the body editor is enabled (default: true). When false, consumers must not accept or store body content for this card kind. |
example |
string | Default body text used when seeding a card of this kind and shown in the blueprint body region; falls back to Write <kind> body here. when absent. |
title¶
A human-readable display label for the card kind. UI consumers should prefer it over the snake_case map key when rendering section headers, chips, picker entries, or per-instance titles in a list.
The label is decoupled from the map key (e.g. indorsement), which is the on-the-wire $kind discriminator. Authors can rename the label freely without invalidating stored documents.
Two flavors:
A literal string serves as a static type label:
A template containing {field_name} tokens lets UI consumers produce a per-instance title by interpolating live field values:
With the template form, a UI rendering a list of cards can title each instance (e.g. "ORG1/SYM → ORG2/SYM") instead of falling back to a generic "Card (2)".
Interpolation rules (for UI consumers):
- {field_name} is replaced with the current value of that field.
- A title with no {} tokens is rendered verbatim: it's just a literal label.
- If a referenced field is absent or empty, the token resolves to an empty string.
- UI consumers are responsible for trimming degenerate separators (e.g. ": " with one empty side).
When omitted, UI consumers fall back to the prettified map key.
body.enabled¶
When false, the card kind has no body/content area. Consumers must not accept or store body content for instances of this card kind. The validator enforces this: a document instance that provides body content for a body.enabled: false card kind is rejected with a BodyDisabled error.
card_kinds:
metadata_block:
body:
enabled: false # Card has fields only, no body/content area
fields:
category:
type: string
body.example¶
Default body text seeded into a card of this kind and shown verbatim in the blueprint body region (it falls back to Write <kind> body here. when absent). Has no effect when body.enabled is false.
card_kinds:
experience:
body:
example: Describe your role, responsibilities, and key achievements.
fields:
company:
type: string
Using Cards in Markdown¶
Card kinds defined here are authored as ~~~ blocks (with a $kind: <kind> line) in the document body. See card-yaml Blocks for the markdown syntax.
typst Section¶
Backend-specific configuration for the Typst renderer.
| Key | Type | Required | Description |
|---|---|---|---|
plate_file |
string | no | Path (relative to the quill root) to the Typst template the backend compiles |
packages |
array | no | Typst packages the template depends on |
See the Typst Backend Guide for details.
Reading the schema programmatically¶
Quillmark emits a public schema contract derived from Quill.yaml. Accessors:
- Rust:
QuillConfig::schema()(JSON) /QuillConfig::schema_yaml()(YAML) - Python:
quill.schema(structured dict) - WASM:
quill.schema(JSON) - CLI:
quillmark schema <path>
ui: hints are preserved verbatim in the output. See SCHEMAS.md for the emitted shape.
Complete Example¶
quill:
name: project_report
version: "1.0"
backend: typst
description: Monthly project status report
author: Engineering Team
typst:
plate_file: plate.typ
main:
fields:
project_name:
type: string
ui:
group: Header
status:
type: string
enum: [on_track, at_risk, blocked]
ui:
group: Header
risk_description:
type: string
default: ""
ui:
group: Header
description: Describe the risk or blocker. Only needed when status is not on_track.
date:
type: date
ui:
group: Header
team_members:
type: array
items:
type: string
default: []
ui:
group: Team
budget:
type: number
default: 0
ui:
group: Financials
card_kinds:
milestone:
description: A project milestone with target date and status.
fields:
name:
type: string
target_date:
type: date
completed:
type: boolean
default: false
Next Steps¶
- Creating Quills: hands-on tutorial
- Markdown Syntax: document authoring syntax
- CLI Reference: validating quills with the
validatecommand