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
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_setuporv1.2can 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).
Permalinks
{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 asSetupandsetup, 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
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 (.mdfiles 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 checkoffers the source files with its name, then those with names close to it, each as the path to write from the including file. #idincludes 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
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
@variantis container-only and grouped: every arm’s opener ends in a colon, and one@endcloses 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 checkgives the table to add toascribe.toml,[dimensions.<name>]with thevaluesthe pages give it. @variantvaries 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
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
@availablelines 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 meansga. - 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 plannedisself-managed preview 3.6. Each target says its own, asself-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,
plannednames 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,plannedincluded. ascribe versions releaserewrites eachplannedfor 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
plannedafter 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.
availableis the only key a row takes. Its spec is checked like an@availableline’s, and can’t go beyond the table’s own availability.- A
badgebuild 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
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
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
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
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 inascribe.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
includeandignoremust take it in, and it must be text. A symbolic link is followed, but only to a file in the source’s folder thatincludeandignoretake in. - When the file isn’t found, the message says why, and when
ascribe checkcan 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.jsiscode:config.js. - A file the source’s patterns leave out needs a pattern in
includethat matches it, or to come out ofignore. - 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 withascribe sources update.
- The path is relative to the source’s folder, so it doesn’t repeat it: with
- Without
#<region>, the snippet is the whole file. - Attributes:
langsets the code block’s language, which is otherwise the file’s extension (tomlforascribe.toml), or for a file known by its name, the language of its name (dockerfileforDockerfile,makefileforMakefile);titlegives the code block a title;phrases=truesubstitutes 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
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@intendedwith 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:
@intendeddirectly 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:
@intendedat 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@intendedline, 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
intendedfrontmatter key, a list of entries with acheckand areason: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 inascribe.toml.
-
- The reason is required, and is the rest of the line:
ascribe checklists it with the problem it covers. - It changes no output. Nothing renders for
@intended, and theintendedkey 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
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, andascribe fmtremoves 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
@endat 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 checkreports 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@includeisn’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@endthat closes it. - A memo can say its page is generated. A memo among the page’s top-level blocks whose text begins
Generated fromorGenerated bysays 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 problemascribe checkand 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 orascribe check --fixedits 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 diffor to review. - An HTML comment isn’t a memo:
html-commentsays why, and how to rewrite one as a memo.
@term
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
@idif 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, ormarked; andcase-sensitive,trueorfalse. The last two default to[glossary]’s settings. - A term is only a term on a page
[glossary] pageslists, or in a fragment such a page includes. Anywhere else,@termis an error,term-off-glossary-page, whose fix lists the page inpages. 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
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@summarywith 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
@summaryhas no summary, whatever its first paragraph says.
@operation
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’soperationId, 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 typeoperation.
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>].