Frontmatter: the cover sheet on every file

This page is for the attorney who is about to write or edit a file in Neon Law Navigator — a notation template, an event page, a blog post, or board minutes — and wants to know what the little block at the top is for. You do not need to be a programmer to read it. You need to know which label goes on which document, and what each line means.

You cannot quietly ship a broken document

Start here, because it is the part that protects you. Every file is checked as you type, in your editor, against the same rules the project enforces everywhere else. If you leave out something a document needs — a title, the attorney review step, the second half of a pair — the editor underlines it in red before the file ever leaves your screen. You are caught at your desk, not in production and not in front of a client. The rest of this page is just what the checker is looking for.

What frontmatter is

Most files in Neon Law Navigator are plain text, and many of them begin with a small block fenced top and bottom by a line of three dashes (---). The block holds a few key: value lines, like this:

title: Retainer Agreement
code: onboarding__retainer

That block is the frontmatter (the real file has a --- line above and below it). Think of it as the caption on a pleading: a short, structured label that tells the system what kind of document this is and the handful of facts it needs to file it correctly. Everything below the block is the document itself — the prose you write and, in the end, sign.

The format is called YAML, but it is nothing more than key: value, one per line. There is no programming. Spell the key correctly on the left, put a valid value on the right, and keep the indentation the examples show. When something is wrong, the editor underlines it — the same way a word processor underlines a misspelling.

The kinds of file, and what each one declares

You say what a file is by declaring it: a kind: key names the file's kind outright, and that declaration is the only classifier — the system never guesses the kind from a file's structure or its path. Its value is one of a small, fixed vocabulary. Most values name notation-template kinds — retainer, letter, filing, will, trust, directive, agreement, onboarding, memo — and the rest name content pages — event, post, minutes, workshop. Anything else is a blocking error (S103). The vocabulary grows as the firm's practice areas do.

A file that declares no kind: is ordinary prose, held only to general writing rules. Because classification is declaration-only, a file that carries notation or event structure — a questionnaire:/workflow: block, or a starts_at: time — but forgets its kind: is a blocking error (S104): it would otherwise lint silently as prose and skip its whole rule family. Each kind and the keys it must carry:

Workshop and presentation bodies use one shared outline contract. A top-level # names the material, each ## starts a chapter, and each ### starts one section/slide inside that chapter. Prose between a chapter heading and its first section is the chapter preamble, rendered on the public overview without becoming a numbered slide. Put the slide face above a --- thematic break and its presenter notes below. Every deck begins with an ## Intro chapter and ends with an ## Wrap Up chapter; the public overview and light table group sections by those chapter headings while playback keeps one stable section order.

A notation template is the document a client eventually signs, plus the questions that fill it in and the path it walks to get there. Here is the real frontmatter from the shared retainer, templates/neon_law/shared/retainer.md (shown without its surrounding --- fences):

title: Retainer Agreement
respondent_type: person_and_entity
code: onboarding__retainer
jurisdiction: NV
confidential: true
questionnaire:
  BEGIN:               { _: person__client }
  person__client:      { _: project__engagement }
  project__engagement: { _: END }
  END: {}
workflow:
  BEGIN:                       { intake_submitted: intake_persisted__client }
  intake_persisted__client:    { retainer_rendered: staff_review }
  staff_review:                { approved: generate_pdf__retainer_pdf, rejected: END }
  generate_pdf__retainer_pdf: { pdf_persisted: sent_for_signature__pending }
  sent_for_signature__pending: { signature_received: END, signature_declined: END }
  END: {}

Each key, in plain English:

Where a question's wording comes from

Most questionnaire states are bank-backed — a state like person__client or entity__company reuses a question the firm has already worded once, in the shared question bank. You write the state and nothing else; the bank supplies the prompt (your editor shows it when you hover the state). Rewording that prompt for one template is the exception, not the rule: add a prompts: entry keyed by the state's role (e.g. client) only when this template genuinely needs different wording. Improving the bank prompt itself is usually the better fix, because every template inherits it at once.

A one-off question — something no bank type covers — uses a custom_* type and is defined in custom_questions:, keyed by the part of the state after the first __ (so custom_single_choice__management_structure is keyed management_structure). That block is the single home for a custom question's wording, and, for custom_single_choice / custom_multiple_choice, its options:

questionnaire:
  BEGIN:                                       { _: custom_single_choice__management_structure }
  custom_single_choice__management_structure:  { _: custom_datetime__formation_date }
  custom_datetime__formation_date:             { _: END }
  END: {}
custom_questions:
  management_structure:
    prompt: How will the company be managed?
    choices:
      members: Managed by its members — the owners
      managers: Managed by appointed managers
  formation_date:
    prompt: When was the formation date?

N104 enforces the split: every custom_* state needs a matching custom_questions: entry with a non-empty prompt; a choice type needs choices and every other custom type must not carry them. Options live inside custom_questions, so there is no top-level choices: key.

One rule worth saying twice: questionnaire and workflow travel together

A notation template has both questionnaire: and workflow:, or neither. If you write one and forget the other, the checker stops you. A blueprint with questions but no path — or a path but no questions — is half a document, and a half-built document should never reach a client. This is a guardrail, not a nicety.

The body below the frontmatter is the legal prose, in English, carrying {{placeholder}} slots that the questionnaire answers fill in ({{person__client.name}}, {{project__engagement.name}}, and so on). Authoring that body, and the full list of structural checks, is covered in <notation-authoring.md>.

How the finished document looks: output

A notation template may carry an optional output key. It is the one place a template declares its render profile — what comes out and how it is dressed:

letter and form are the values the checker accepts today (N109); leaving the key off gives you the plain page. As we add court-specific layouts (pleading paper), each becomes one more named value here — so output stays the one place a template says what it should look like.

Government form templates carry two extra keys

A template backed by an official government form (under templates/forms/) declares output: form and adds form: (the form's identity) and origin_url: (the official .gov page the blank form came from), as in templates/forms/united_states/nevada/state/nv__llc_formation.md:

title: Neon Law Nest — Nevada Entity Formation
respondent_type: person_and_entity
code: nv__llc_formation
jurisdiction: NV
origin_url: https://www.nvsos.gov/businesses/commercial-recordings/forms-fees/all-business-forms
confidential: false
output: form
form: nv__llc_formation

The three travel together: N109 requires form: and origin_url: whenever output: form is declared, and rejects a form: key on any other profile. So form: present and output: form always imply each other.

Event pages

An event page (a public show-and-tell) is dated, so it carries a start time on top of a title and description. From web/content/events/:

title: "Salt Lake City Nebula Show and Tell"
description: >
  A Salt Lake City session for practical legal AI workflows, demos, peer review, and responsible adoption habits.
draft: true
starts_at: "2026-07-20T11:00:00"
ends_at: "2026-07-20T15:00:00"
timezone: America/Denver
location_address: Salt Lake City, Utah

The description: > you see is just a way to wrap one long sentence across several lines; it still reads as a single sentence.

Blog posts and board minutes

These two are the simplest: a title and a description, and a filename that follows a fixed shape.

A blog post (web/content/blog/) takes its publish date from the filename, so the name must be YYYYMMDD_slug.md (e.g. 20260625_going_all_in_on_rust.md). A name whose date does not parse is silently dropped — the post never shows up and never errors — so the checker flags a bad name for you.

title: Going All-In on Rust
description: Why Neon Law Foundation chose one language for fast, safe, local-first access-to-justice software.

Board minutes (web/content/foundation/minutes/) are one file per quarter, named YYYY-qN.md (e.g. 2026-q1.md):

title: "Board Meeting Minutes — Q2 2023"
description: "Minutes of the Neon Law Foundation board of directors for the Q2 2023 regular meeting."

Every frontmatter key at a glance

The narrative above covers the keys you reach for daily. This table is the complete set the system knows, grouped by document kind, so nothing is hidden:

Notation template

KeyRequiredValuesChecked by
kindyesa notation kind (retainermemo, the nine listed above)S103, S104
titleyesany non-empty textN101
codeyesunique snake_caseN108
respondent_typeyesperson, entity, person_and_entityN102
jurisdictionyesNV, CA, USN110
confidentialyestrue or falseN105
questionnaireyes (paired)a BEGINEND ladderN104
workflowyes (paired)a BEGINEND path that includes staff_reviewN104, N106
custom_questionswith any custom_* statewording (and options) for one-off questionsN104
promptsnooverride the bank's wording for a bank-backed stateN104
outputnoletter or form (omit for a plain page)N109
formwith output: formthe bundled form's codeN109
origin_urlforms onlythe .gov page the blank form came fromN109, N110

Event page

KeyRequiredValuesChecked by
kindyeseventS103, S104
titleyesany non-empty textC001
descriptionyesany non-empty textC002
starts_atyesan ISO-8601 timeE001
timezoneyesan IANA zone, e.g. America/DenverE001
luma_urlyesthe Luma event URL — Luma hosts the event and its RSVPsE004
ends_atnoan ISO-8601 timeweb build
image_url, image_altnothe event picture (same as on Luma) and its alt textweb build
public_slugnoa custom URL slugweb build

Blog post, board minutes, and workshop page

KeyRequiredValuesChecked by
kindyespost (blog), minutes (board minutes), workshop (workshop page)S103
titleyesany non-empty textC001
descriptionyesany non-empty textC002

Three footnotes. form rides along on government-form templates and is bound to output: form — N109 requires the two together and rejects a form: key on any other profile, so a stray or orphaned form: is now a loud error rather than a silent one. The event keys marked "web build" are read when the page is rendered rather than by the command-line checker, so they will not underline in your editor. Finally, on how the required kind: is enforced: a template or event that carries structure but omits kind: trips S104 right in your editor; a content page (blog, minutes, workshop) has no such structural tell, so a repo-wide corpus test — not a per-file squiggle — fails CI if one ships without its kind:.

The squiggly underline: red versus yellow

Open any of these files in a supported editor and the checker runs as you type:

Hover over an underline and it tells you the rule and what to fix. Nothing you type leaves your machine: the checker reads only the buffer in front of you and sends nothing anywhere, which is the same confidentiality the confidential flag is there to protect. Editor setup is in <lsp/README.md>.

Checking it yourself from the command line

The editor checks continuously, but you can run the same checker by hand over a file or folder:

cargo run -p cli --quiet -- validate <path>

It classifies each file automatically — a template is held to the template rules, a blog post to the blog rules, prose to the writing rules — and prints any problem with its file, line, and rule code.

Where to go next