Directive reference

Each built-in directive: what it does, its attributes, and its rules; and project widgets.

For AI agents: the documentation index is at llms.txt, and this page is available as Markdown.

Each built-in directive, in the order of the Ascribe specification, the normative definition, whose part each section links to. How a directive is written, and what it applies to, is in directive syntax.

Directive Forms Primary Purpose
@id line an id Give a heading a stable id
@include line a path Transclude a file or a region
@variant container, grouped none Mark alternative content
@available line an availability spec or feature key Declare where content applies
@note line, container text, optional Callout
@steps line none Mark an ordered list as a procedure
@details line, container none Collapsible content
@snippet line an address Take a code example from a file
@intended line a reason Acknowledge a check’s problem in the next block, or at the section’s heading
@memo line, container text Text for whoever edits the page next, never published
@term line none Make a glossary page’s heading a term
@summary line text, optional Give a section a summary: this text, or the paragraph below it
@operation line a link to an operation Put an operation of a declared API in the page: its method, path, and summary, linked to its reference

@id

(SPEC §4.1)

Gives a heading an explicit, stable id, used for its anchor and by links and includes.

## Configuration
@id: config-setup
  • It goes directly under its heading.
  • An id uses letters, digits, hyphens, underscores, and periods, so an existing anchor such as ece_setup or v1.2 can be kept.
  • Ids must be unique on a page, including ids that come from included fragments.

Without @id, a heading’s id is the slug of its text (heading ids).

{permalink=true} makes the id a permalink as well: a name for the section that something outside the docs can link to, at an address of its own, go/<id> under the site’s base path or permalink-path, which forwards to the section wherever it is. Permalinks says how a product links to one, how its CI checks those links, and what to do when a section moves or goes away.

## `ascribe check`
@id {permalink=true}: cli-check
  • The id is still the heading’s anchor, and links inside the docs name it as they name any id: checks/check.md#cli-check. A permalink is for links from outside the docs; inside, link to the file.
  • A build publishes a permalink in one place only (permalink-duplicate), and names that differ only in case, such as Setup and setup, are one name. So a marked heading belongs in a fragment only one page of each build includes. Two pages that no build publishes together may share a permalink.
  • A permalink that’s lost is a warning (permalink-lost) once the project commits its list; restore the name, or move or retire it. Moving the heading to another page isn’t removing it.
  • A page has a permalink in its frontmatter, from the same set of names: a page’s permalink.

@include

(SPEC §4.2)

Includes another file, or one heading’s section of it, in place.

@include: _snippets/prerequisites.md
@include {heading=false}: guides/setup.md#install
  • Paths are relative to the file they’re written in, or to the content root when they start with /. Only source files (.md files under the content root) can be included. A symbolic link is followed only to another source file, so no other file on disk ends up in a page. When the file isn’t there, ascribe check offers the source files with its name, then those with names close to it, each as the path to write from the including file.
  • #id includes only the section of the heading with that id. {heading=false} leaves out that section’s own heading; it has no effect without #id, and Ascribe warns.
  • Included content becomes part of the page: phrases and builds apply to it, and its ids must not collide with the page’s.
  • Relative paths inside a fragment resolve from the fragment, not the page including it, so a fragment’s links and images work wherever it’s included.
  • Included headings keep the levels they’re written with.
  • An include cycle is an error.

A page’s includes can expand it only so far, far beyond any real page: the include in the page’s own file that takes it past the limit is an error, include-limit, whose message gives the limit, and it and every include after it on the page are left out.

A link names the page that includes a fragment, never the fragment, and can name a heading the fragment brings in (links).

@variant

(SPEC §4.3)

Marks alternatives: content that differs by a dimension declared in ascribe.toml, such as a package manager or a deployment.

Dimensional arms carry dimension values as attributes:

@variant {pm=npm}:
```shell
npm install -g @quill/agent
```
@variant {pm=pnpm}:
```shell
pnpm add -g @quill/agent
```
@end

Labeled arms carry a title instead, for one-off alternatives that aren’t a declared dimension:

.Using Docker Hardened Images
@variant:
…

.Using other images
@variant:
…
@end
  • @variant is container-only and grouped: every arm’s opener ends in a colon, and one @end closes the group.
  • Each arm has attributes or a title, never both and never neither, and a group’s arms are all one kind.
  • A value set ({platform=cloud|on-prem}) matches any of its values. Several keys on one arm ({deployment=cloud, pm=npm}) must all match.
  • The arms of a group share at least one dimension.
  • A dimension is declared before it’s used. In a project that declares none, ascribe check gives the table to add to ascribe.toml, [dimensions.<name>] with the values the pages give it.
  • @variant varies whole blocks. For a single term that differs, use a phrase.

Each build decides what happens to a group: switch keeps every arm, for readers to switch between (the site shows tabs), and a selection such as { deployment = "cloud" } keeps only the matching arms. See builds. A whole page that differs by dimension is a page for each, with variant in its frontmatter.

@available

(SPEC §4.4)

Says where content applies and its lifecycle state. Unlike @variant, it doesn’t select content: every reader sees it, marked with its availability.

@available: cloud, self-managed preview 3.4
  • At the top of a section, it applies to the section, and anywhere else to the block it touches (binding).
  • For a whole page, use the frontmatter key available, with the same spec.
  • Several @available lines on one heading or block all apply: the content is available only where all of them allow it.

Availability specs

A spec is a comma-separated list of targets, each with an optional lifecycle:

Spec Meaning
cloud Generally available on cloud
self-managed 3.3 Generally available on self-managed since version 3.3
self-managed preview 3.4 In preview on self-managed since version 3.4
self-managed (preview 3.3, ga 3.5, deprecated 4.0) A history: each state lasts until the next begins
cloud, self-managed preview 3.3 Two targets
self-managed preview planned In preview on self-managed from its planned line
  • A target is a dimension value, or a dimension’s name, which stands for all its values. A dimension name takes a state but no version: deployment beta.
  • A state is a lifecycle state: preview, beta, ga, deprecated, removed, or one the project declares. No state means ga.
  • A history is in chronological order. A versionless target (declared in ascribe.toml) takes one state and no version.
  • There are no ranges, alternatives, or negation. Commas separate targets; | belongs to @variant.

Scope. A section or block’s spec can’t go beyond the page or section around it: it can’t add a target the enclosing scope doesn’t list, or name an earlier version. Content with no spec inherits its enclosing scope’s availability.

Feature keys. A spec that is a single word matching a key in [features] stands for that feature’s declared spec, so a feature going GA takes one edit in ascribe.toml:

@available: streaming-sync

Each build decides what happens to availability: badge keeps everything and marks it, and a filter such as { filter = "self-managed 3.3" } removes what isn’t available there.

The planned line

Where a version goes, planned means the target’s planned line in [releases], so content for a release that hasn’t shipped is marked without its number:

@available: self-managed planned
  • It reads as the line’s version does, in a filter, a badge, and the checks: with 3.6 planned, self-managed preview planned is self-managed preview 3.6. Each target says its own, as self-managed planned, edge beta planned, and a history can end in one, self-managed (preview 3.5, ga planned).
  • The target needs exactly one planned line. With none, or with two, planned names no version, and it’s an error at the word, available-planned-line: write the version, or declare the line. A versionless target or a dimension name takes no version, planned included.
  • ascribe versions release rewrites each planned for the target to the released line’s version, so the marks say which release the content came in.
  • It’s a word, not a feature key: a feature’s spec names its versions, and no state, dimension, value, or feature can be named planned.
  • The editor offers planned after a target or a state when the target has one planned line, and its hover on a spec names the line.

Table rows

A table row takes a spec from an attribute block at the end of its first cell, with the key available. Quote a spec that has spaces or commas:

| Option | Meaning |
|---|---|
| `timeout` | Seconds to wait for the agent. |
| `stream` {available="self-managed preview 3.4"} | Streams results as they arrive. |
| `sync` {available=streaming-sync} | Keeps the local copy in sync. |
  • The block goes last in the row’s first cell, after a space, and isn’t part of the cell’s text. The header row can’t have one.
  • available is the only key a row takes. Its spec is checked like an @available line’s, and can’t go beyond the table’s own availability.
  • A badge build marks the row at the end of its first cell, and a filter build removes the row where it isn’t available.

There’s no form for part of a sentence: a filter would break the sentence, and a badge in the middle of one gets in the way. Write a value that differs by release as prose, such as “glibc 2.28 or later (2.39 for 0.1.1 and earlier)”.

@note

(SPEC §4.5)

A callout. Its type is note (the default), tip, important, warning, caution, or a type the project declares in [notes].

@note {type=caution}: Back up your database first.

.Try it without installing
@note {type=tip}
You can run Quill in the browser with no local setup.

@note {type=warning}:
Several blocks,

- including lists.
@end
  • With a primary, the primary is the note. It can continue onto the next lines, like a paragraph.
  • Without a colon, the note is the next block.
  • With a trailing colon, the note is a container.
  • A note may have a title.

A page brought over from GitHub may write a callout as GitHub’s alert, a block quote whose first line is [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], or [!CAUTION]. Ascribe shows that as a plain quote, so ascribe check warns about it (github-alert), and its fix, which ascribe check --fix applies, writes the @note of the same type, keeping the quote’s content:

> [!TIP]
> Quill runs in the browser too.

becomes @note {type=tip}: Quill runs in the browser too. A quote of several blocks becomes the container, @note {type=tip}: through @end.

@steps

(SPEC §4.6)

Marks an ordered list as a procedure. It goes directly above the list.

@steps
1. Install the agent package.
2. Verify the install.
3. Create `quill.yaml`.

Directives inside the list’s items are indented to the item’s content (lists).

@details

(SPEC §4.7)

Content readers can expand or collapse. The title is required: it’s what readers see while the content is collapsed.

.Show the full configuration reference
@details:
…
@end

Without a colon, @details applies to the next block; with one, it’s a container.

@snippet

(SPEC §4.8)

Takes a code example from a file outside the content, and puts it in the page as a fenced code block. There’s no copy: Ascribe reads the file whenever it checks, builds, or diffs, so changing the file changes every page that uses it.

@snippet: code:examples/quill/ascribe.toml#dimensions
@snippet {lang=shell, title="Install"}: code:scripts/install.sh
  • The address is <source>:<path>, optionally followed by #<region>. <source> is a source declared in ascribe.toml ([sources.<name>]), and <path> is the file’s path relative to the source’s folder (for a source in another repository, its path in that repository), with / on every platform. There’s no relative form: an address doesn’t depend on where the page is.
  • The file must exist with exactly that name, the source’s include and ignore must take it in, and it must be text. A symbolic link is followed, but only to a file in the source’s folder that include and ignore take in.
  • When the file isn’t found, the message says why, and when ascribe check can tell which file was meant, it offers that address:
    • The path is relative to the source’s folder, so it doesn’t repeat it: with path = "src", src/config.js is code:config.js.
    • A file the source’s patterns leave out needs a pattern in include that matches it, or to come out of ignore.
    • A symbolic link that leads out of the source is named by the file it leads to, through a source that includes it.
    • A file of a source in another repository is copied by ascribe sources fetch. When it isn’t in the repository at the pinned commit, because it was moved or deleted there, name it where it is now, or change the code and move the pin with ascribe sources update.
  • Without #<region>, the snippet is the whole file.
  • Attributes: lang sets the code block’s language, which is otherwise the file’s extension (toml for ascribe.toml), or for a file known by its name, the language of its name (dockerfile for Dockerfile, makefile for Makefile); title gives the code block a title; phrases=true substitutes phrases in the code, as in a fence that opts in (phrases).
  • It’s a block, allowed wherever a code block is: in a list item, a note, or a variant’s arm. A @note, @details, or widget directly above it applies to it.

A region is marked in the code with Bluehawk’s tags in comments, :snippet-start: <name> and :snippet-end:: mark a region in the code has the tags and the comments they’re read in.

Every problem is reported at the @snippet line (source files, in the diagnostics reference).

In VS Code, a code file says which pages show it: see code files a page shows.

@intended

(SPEC §4.9)

Says that a problem a check reports is intended: you’ve looked at it and are keeping it. Write the check’s name and why.

@intended {check=link-external-broken}: help.example.com refuses link checkers; checked by hand.
Read the [vendor's deployment notes](https://help.example.com/deploy) first.
  • Only review checks, whose next step is review, since what they report may be fine as it is, such as a page nothing links to. A problem that’s always wrong is fixed instead. A review check the project raises to an error in [checks] can still be acknowledged. The diagnostics reference says where each review check is acknowledged, and Quieting a problem compares @intended with the other ways to quiet one.
  • The names are Ascribe’s. A check is named as the diagnostics reference names it. The specification names no checks, so another processor may name its checks differently, and reports a name it doesn’t know as an error, as Ascribe does (intended-check).
  • Where it’s written depends on the check, which reports its problems at one kind of place:
    • A block: @intended directly above it, as above. It covers the check’s problems anywhere in the block, including inside a container. Above a block of a fragment, it covers that block in every page that includes it.

    • A heading: @intended at the top of its section, among the directives under the heading. It covers the check’s problems at the heading, and, when no blank line comes between, in the block directly under the @intended line, so a section’s first block is acknowledged as any other block is. It covers nothing else in the section. An unused glossary term is acknowledged this way:

      ## Workspace
      @term
      @intended {check=glossary-term-unused}: Linked from the product's own screens.
      
      The space a team's projects are in.
    • A page or a fragment: the intended frontmatter key, a list of entries with a check and a reason:

      intended:
        - check: page-orphan
          reason: Linked from the site's sidebar, which Ascribe can't see.
    • A content model entry or an image: an [[intended]] table in ascribe.toml.

  • The reason is required, and is the rest of the line: ascribe check lists it with the problem it covers.
  • It changes no output. Nothing renders for @intended, and the intended key isn’t in any output’s frontmatter.

An acknowledged problem isn’t reported, but ascribe check and ascribe build count it in their summary (1 acknowledged), and list it, with its reason, in their JSON report. When an acknowledgement covers nothing in any build, because the problem was fixed or the check changed, ascribe check reports it as advice, intended-unused, with a fix that removes it. That’s reported only when every build is checked: not with --build or --editor-build, and not in the editor.

In the editor, a review check’s problem has a quick fix, Mark as intended, and write why, that writes the acknowledgement in the right place for you to fill in the reason. Its placeholder reason is reported as an error until you replace it, so the fix alone silences nothing.

@memo

(SPEC §4.10)

Keeps a note for the people and agents who maintain the page, such as why a section comes first, beside what it’s about. It’s never published. Leave a memo for whoever edits next says when to write one, when a memo file is better, and when a pull request comment is.

## Rotate a key
@memo:
Rotation comes before revocation: support found that people who revoke
first lock out their own agents. See [the incident](notes/2026-08.md).
@end

Generate a new key, then restart the agent.

@memo: Kept to one sentence; the support macro quotes it.
Revoke the old key once every agent uses the new one.
  • What it’s about is decided as for @available (binding). A container memo touches the block that starts on the line after its @end. Several memos stack, in either form, and ascribe fmt removes a blank line between a memo and its block.
  • A line memo is the rest of its line, and needs some text. A container memo is every line up to a line that’s only @end at the memo’s own indent, as written. It takes no attributes. A container memo with nothing in it, or only blank lines, is almost always a slip: ascribe check reports it as advice, memo-empty, whose fix removes it.
  • Nothing resolves or publishes a memo, nor a memo file’s text. Ascribe reads only the words that say a page is generated (below), and a memo file’s quote, which places it. A link in one isn’t checked and isn’t a use of the page it names, a {key} isn’t a phrase, an @include isn’t followed, a # line isn’t a heading, and Vale isn’t given the text. A code fence that doesn’t close, or a line such as @note:, doesn’t change the page around it. The one line a container memo can’t hold is the @end that closes it.
  • A memo can say its page is generated. A memo among the page’s top-level blocks whose text begins Generated from or Generated by says another program writes the file, and its text says what to change instead and the command that writes it again: @memo: Generated from options.toml by scripts/options.ts; edit the options, then run `node scripts/options.ts` to rewrite it. Each problem ascribe check and the editor report in the file names the memo as a related place, the agent prompt says to change what the file is generated from rather than the file, and no quick fix or ascribe check --fix edits it. Nothing else reads the memo’s text.
  • It changes no output. No build, preview, or report shows a memo, in a page, a fragment, or a memo file, and a change to one alone isn’t a change to ascribe diff or to review.
  • An HTML comment isn’t a memo: html-comment says why, and how to rewrite one as a memo.

@term

(SPEC §4.11)

Makes a heading of a glossary page a glossary term. The heading’s text is the term, and the paragraph under it is its definition, which a built page shows where it links the term, and the editor on hover.

## Rollout
@id: rollout
@term {aliases=rollouts}

A plan for turning a flag on for a growing share of users: by percentage, by segment, or on a schedule.
  • It goes directly under the heading, with the heading’s @id if it has one. It takes no primary.
  • Attributes: aliases, other forms that count as the term, separated by | (aliases="API keys|keys"); match, which occurrences are linked, first, every, or marked; and case-sensitive, true or false. The last two default to [glossary]’s settings.
  • A term is only a term on a page [glossary] pages lists, or in a fragment such a page includes. Anywhere else, @term is an error, term-off-glossary-page, whose fix lists the page in pages. See Glossary terms.
  • The paragraph under the heading must be the first thing in the section, after the heading’s directives: a section that opens with anything else has no definition, which is an error, glossary-definition-missing. Keep it to a sentence or two, since it’s shown where there’s no room for more: one over 300 characters is reported as advice, glossary-definition-long.
  • It renders nothing: the heading and its section are published as they would be without it.

To set up a glossary from the start, see Set up a glossary.

@summary

(SPEC §4.12)

Gives a section a summary: a sentence or two that says what the section is about, for a reader who sees it apart from the page, as a product’s embedded help does. Any section may have one, with a permalink or without.

Give the summary its own text, which the page doesn’t show, as a page’s description isn’t in its body:

## ASC149 `page-size`
@id {permalink=true}: ASC149
@summary: A page's Markdown is as long as the limit or longer.

Or mark a paragraph the page already shows, which stays where it is, so nothing is written twice:

## `ascribe versions release`
@id {permalink=true}: cli-versions-release

@summary
Releases a line.

It rewrites each mark's `planned` for its target to the line's version.
  • With text (@summary: text), it goes at the top of its section, directly under the heading and its @id; anywhere else it’s an error, binding-not-section-top. The text is inline content, as a note’s: code spans, links, emphasis, and phrases. Use it when nothing on the page reads well alone, such as a lead-in that ends in a colon.
  • With no text (@summary), it marks the paragraph directly below it, anywhere in the section. It marks a paragraph only, summary-not-paragraph: to use a first sentence, make it a paragraph of its own.
  • One per section, summary-repeated. A subsection’s summary is its own. A @summary with no text before a page’s first heading is the summary of the page’s opening.
  • In a fragment, a summary is the summary of the section it lands in, on each page that includes it.
  • Not in a variant arm, whether it’s written there or an include brings it in: summary-in-variant, since a summary can’t differ by arm. Write one at the top of the section that holds for every arm.
  • In the outputs, the text form writes nothing in the site and plain outputs, and the paragraph of the other form is written as it would be without it. The JSON output gives the section’s heading its summary. A summary’s text isn’t searched for glossary terms.
  • Nothing is guessed. A section without @summary has no summary, whatever its first paragraph says.

@operation

(SPEC §4.13)

Puts an operation of an API that ascribe.toml declares in [apis] in the page: its method, its path, and its summary, as one line linked to the operation in the published reference.

@operation: ../api/openapi.yaml#getUser
  • The primary is a link to an operation, as a link’s destination names one: the spec’s path from the page (or from the content root, starting with /), #, and the operation’s operationId, or, for an operation with no id, its method in lowercase and its path, #delete/users/{id} (links to operations).
  • It’s checked as that link is: an operation the spec doesn’t have is link-operation-missing, and one named by its method and path that has an id is advice, link-operation-by-path. Anything that isn’t a link to an operation of a declared API, a page, another file, the spec with no #, or a URL, is an error, operation-link-not-operation.
  • What it shows: GET /users/{id}: Get a user, with (deprecated) after it when the spec marks the operation deprecated. Never the operation’s parameters, request, or responses.
  • It’s a block, so a @note, @available, or widget directly above it applies to it.
  • In the outputs, the site output writes an <ascribe-operation> element holding the link, the plain output the line, and the JSON output a block of type operation.

Show an operation in a page has the rest.

Project widgets

(SPEC §6)

A project widget is a directive a project declares in ascribe.toml, for needs the built-in directives don’t cover. Its name contains a hyphen, so it’s easy to tell from a built-in:

@quill-labspace {lab=first-sync}

A declared widget works like a built-in directive, with the forms, primary, attributes, binding, title, and grouping its declaration sets. In the site output it becomes a custom element with its name; in plain Markdown, its declared fallback text. See [widgets.<name>].