> For the complete documentation index, see [llms.txt](https://ascribed-dev.com/docs/llms.txt). This documentation describes Ascribe 0.4.4. Anything marked in main, not yet released isn't in that release yet.

# `ascribe.toml` format contract

> Reference · The normative contract for the content model file, with the rules a loader enforces.

This is the normative contract for `ascribe.toml`, the [content model](https://ascribed-dev.com/docs/glossary.md#content-model) file (SPEC §7): its tables and keys, the short syntax for field and attribute types, and every rule a loader enforces. The loader in `ascribe-model` implements it. What each key is for, with examples, is in the [`ascribe.toml` reference](https://ascribed-dev.com/docs/content-model.md), which shares this page's key tables and section numbers.

SPEC.md is normative for the language, and this contract for the file format. The decisions that settle what the spec leaves open are files in [`project-docs/decisions/`](https://github.com/ascribed-dev/ascribe/tree/main/project-docs/decisions), listed in [decision #689a](https://github.com/ascribed-dev/ascribe/blob/main/project-docs/decisions/689a-what-a-contract-page-holds.md).

Every file in [`examples/content-models/`](https://github.com/ascribed-dev/ascribe/tree/main/examples/content-models) is valid under this contract: `minimal.toml` is the smallest valid model, `quill.toml` the model SPEC Appendix B assumes, and `full.toml` has every section and key.

## Contents

Each page holds the sections for one job.

On this page, what pages are:

1. [Conventions](https://ascribed-dev.com/docs/contracts/content-model.md#1-conventions)
2. [Top level: `spec`](https://ascribed-dev.com/docs/contracts/content-model.md#2-top-level-spec), and [`[from]`](https://ascribed-dev.com/docs/contracts/content-model.md#21-from)
3. [`[project]`](https://ascribed-dev.com/docs/contracts/content-model.md#3-project)
4. [`[types.<name>]` and `[fragments]`](https://ascribed-dev.com/docs/contracts/content-model.md#4-typesname-and-fragments)
5. [Field and attribute types](https://ascribed-dev.com/docs/contracts/content-model.md#5-field-and-attribute-types)

[What pages hold](https://ascribed-dev.com/docs/contracts/content-model/content.md):

6. [`[notes.<type>]`](https://ascribed-dev.com/docs/contracts/content-model/content.md#6-notestype)
7. [`[phrases]`](https://ascribed-dev.com/docs/contracts/content-model/content.md#7-phrases)
8. [`[glossary]`](https://ascribed-dev.com/docs/contracts/content-model/content.md#8-glossary)
9. [`[images]`](https://ascribed-dev.com/docs/contracts/content-model/content.md#9-images)
10. [`[widgets.<name>]`](https://ascribed-dev.com/docs/contracts/content-model/content.md#10-widgetsname)

[How content varies](https://ascribed-dev.com/docs/contracts/content-model/variation.md):

11. [`[dimensions.<name>]`](https://ascribed-dev.com/docs/contracts/content-model/variation.md#11-dimensionsname)
12. [`[versions]`](https://ascribed-dev.com/docs/contracts/content-model/variation.md#12-versions), and [`[releases.<target>]`](https://ascribed-dev.com/docs/contracts/content-model/variation.md#121-releasestarget)
13. [`[lifecycle.<state>]`](https://ascribed-dev.com/docs/contracts/content-model/variation.md#13-lifecyclestate)
14. [`[features.<key>]`](https://ascribed-dev.com/docs/contracts/content-model/variation.md#14-featureskey)

[Where pages are published](https://ascribed-dev.com/docs/contracts/content-model/output.md):

15. [`[consumer]`](https://ascribed-dev.com/docs/contracts/content-model/output.md#15-consumer)
16. [`[builds.<name>]`](https://ascribed-dev.com/docs/contracts/content-model/output.md#16-buildsname)
17. [`[listing]`](https://ascribed-dev.com/docs/contracts/content-model/output.md#17-listing)
18. [`[crawlers]`](https://ascribed-dev.com/docs/contracts/content-model/output.md#18-crawlers)
19. [`[permalinks]`](https://ascribed-dev.com/docs/contracts/content-model/output.md#19-permalinks)

[What pages draw from](https://ascribed-dev.com/docs/contracts/content-model/sources.md):

20. [`[sources.<name>]`](https://ascribed-dev.com/docs/contracts/content-model/sources.md#20-sourcesname)
21. [`[apis.<name>]`](https://ascribed-dev.com/docs/contracts/content-model/sources.md#21-apisname)

[Checks and the editor](https://ascribed-dev.com/docs/contracts/content-model/checks.md):

22. [`[checks]`](https://ascribed-dev.com/docs/contracts/content-model/checks.md#22-checks)
23. [`[editor]`](https://ascribed-dev.com/docs/contracts/content-model/checks.md#23-editor)
24. [Defaults: what an absent section means](https://ascribed-dev.com/docs/contracts/content-model/checks.md#24-defaults-what-an-absent-section-means)

[Validation rules](https://ascribed-dev.com/docs/contracts/content-model/validation.md):

25. [Validation rules](https://ascribed-dev.com/docs/contracts/content-model/validation.md#25-validation-rules)
26. [Map of SPEC §7.2 to this file](https://ascribed-dev.com/docs/contracts/content-model/validation.md#26-map-of-spec-72-to-this-file)

---

## 1\. Conventions

### 1\.1 The file

- The content model is a TOML 1.0 file named `ascribe.toml`. The directory that contains it is the **project root**. Every path in the file is relative to the project root unless a key says otherwise.

- The file is UTF-8.

- TOML's equivalent spellings are all accepted: a table can be written as a `[header]` section, as an inline table (`key = { … }`), or with dotted keys. This reference shows the most readable form for each section. For example, these are the same:

  ```toml
  [dimensions.pm]
  values = ["npm", "pnpm", "yarn"]

  # same as
  dimensions.pm.values = ["npm", "pnpm", "yarn"]
  ```

- **Keys are kebab-case** (`content-root`, `trailing-slash`), matching Ascribe's attribute keys. TOML allows hyphens in bare keys, so they need no quotes.

- **Unknown keys are errors**, everywhere, with a did-you-mean suggestion. A misspelled key is otherwise a silently ignored setting. The exceptions are the tables whose keys are names the project chooses (`[phrases]`, `[types]`, `[dimensions]`, and so on); their keys are validated as names instead.

- **Declaration order is kept where it matters.** Attribute keys keep the order they're declared in (`[images.attributes]`, `[widgets.<name>.attributes]`), because canonical form writes attributes in declared order ([SPEC §8.3](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#83-canonical-form)). The `[dimensions]` tables keep theirs too: it's the canonical order of `@variant` attributes, and decides which [dimension](https://ascribed-dev.com/docs/glossary.md#dimension) a tab group syncs on (the [element contract](https://github.com/ascribed-dev/ascribe/blob/main/packages/elements/CONTRACT.md), §3). Order is kept elsewhere for stable output (for example, the order of dimension values in a tab switcher is the order of `values`). Nothing else in the model depends on order.

### 1\.2 Names

Several kinds of name appear in the file. Each has a grammar from SPEC Appendix A or from this reference:

| Name | Grammar | Rule | Used for |
| --- | --- | --- | --- |
| name-word | SPEC A `name-word` | A letter, then letters, digits, `_`, or `-` | Dimension values, lifecycle states, feature keys, frontmatter field names |
| key | SPEC A `key` | A lowercase letter, then lowercase letters, digits, or `-` | Dimension names, phrase keys, note types, attribute keys, content type names, source names |
| widget name | SPEC A `widget-name` | Lowercase words of letters and digits joined by single hyphens, with at least one hyphen; starting with a letter | Widget names |
| build name | This reference | A letter, then letters, digits, `_`, `-`, or `.` | Build names |

Dimension names follow the stricter `key` rule, not just `name-word`, because they're also written as attribute keys in `@variant {deployment=cloud}` ([SPEC §3.3](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#33-attributes), §4.3).

Build names allow `.` so that names like `self-managed-3.3` work. A name containing `.` must be quoted in a TOML header: `[builds."self-managed-3.3"]`.

Names are case-sensitive and spelled exactly as declared ([SPEC §4.4](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#44-available)).

### 1\.3 Paths and patterns

- Paths use `/` as the separator on every platform.

- Filesystem paths in `[project]` and `[sources]` are relative to the project root. They can't be absolute, and they can use `..`.

- A path in `[glossary] pages` (§8) is written like a link destination in a document ([SPEC §5.2](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#52-links)), relative to the content root.

- **Patterns** (globs) are matched against a source file's path relative to the content root, including its extension, with `/` separators. They are case-sensitive. The syntax:

  | Syntax | Matches |
  | --- | --- |
  | `*` | Any run of characters within one path segment, not including `/` |
  | `**` | Any number of whole segments, including none. It's only ever a whole segment (`a/**/b`, `a/**`, `**/b`) |
  | `?` | Any one character except `/` |
  | `{a,b}` | Either alternative. Alternatives don't nest |
  | `\` | Escapes the next character |

  Every other character matches itself. A pattern doesn't start with `/`, and has no `..` segment. Patterns only ever apply to Markdown source files (`*.md`); other files under the content root are assets. The exception is a source's `include` and `ignore` (§20), which are matched against any file's path relative to the source's folder.

### 1\.4 How this reference describes keys

Each section has a table of keys with these columns:

- **Type:** the TOML type. "Field type", "attribute type", and "[availability](https://ascribed-dev.com/docs/glossary.md#availability) spec" are strings with the grammars in §5 and [SPEC §4.4](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#44-available).
- **Default:** the value when the key is absent, or **required**.
- **Description.**

**Rules.** A loader MUST preserve the order in which attribute keys (`[images.attributes]`, `[widgets.<name>.attributes]`) and the `[dimensions]` tables are declared, and SHOULD preserve the order of everything else. A filesystem path in `[project]` or `[sources]` MUST NOT be absolute, and MAY contain `..` segments (`model-path-absolute`). A pattern's `**` MUST be a whole segment, and a pattern MUST NOT start with `/` or contain a `..` segment (`model-pattern-syntax`).

---

## 2\. Top level: `spec`

```toml
spec = "0.1"
```

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `spec` | string | **required** | The version of the Ascribe specification this project targets (SPEC §11). It MUST be quoted: `spec = 0.1` is a TOML float and is an error. A processor accepts only the spec versions it implements, compared as exact strings; this reference defines `"0.1"`. |

The only other top-level keys are `[from]` (§2.1) and the tables in §3–§23, `[releases]` (§12.1) and `[[intended]]` (§22.1) among them. Anything else is an unknown key.

### 2\.1 `[from]`

```toml
[from."../../docs/ascribe.toml"]
take = ["releases", "dimensions.pm", "phrases.version", "consumer.site"]
```

The key after `from` is the other file's path, relative to the folder `ascribe.toml` is in. A project can take from more than one file, each a table of its own.

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `take` | array of strings | **required** | What's taken from the file, each a table or a key as a dotted path: `releases` takes the whole table, `consumer.site` one key of `[consumer]`. It's read as if written here, and can't be written here too. |

What's taken is read from the other file as if written in this one, and every rule of this reference holds it there. A source's `path` is the one exception: it's relative to the folder of the file it's written in, so a taken source is the same folder from either project. The other file's own `[from]` isn't followed: what's taken is always written in the file named.

**Rules** (SPEC §7.9). The path MUST NOT be absolute (`model-path-absolute`). A loader MUST read the file as a file outside the content root, with its exact name and through the symbolic links on its way, and MUST report it when it isn't there or isn't TOML, and when the project is in a git repository and the file, its links resolved, isn't inside that repository (`model-from-file`). Each name in `take` MUST be a dotted path of keys the file has, MUST NOT start with `spec`, `project`, or `from`, and MUST NOT be named twice, from one file or two, or with a key it holds (`model-from-take`). A key this file writes MUST NOT be taken, nor a table this file writes a key in (`model-from-written`). A loader MUST NOT follow the other file's `[from]`. A problem with a taken value is reported at its name in `take`. A processor that edits `ascribe.toml` in place MUST make an edit of a taken key in the file that writes it, at the same path, and MUST rename a key `take` names in `take` too; a rename of a name the other file declares, such as a phrase, MUST also be made where the other file's project uses it; it MUST say which other file it changed, and MUST refuse, naming that file, an edit it can't make there.

---

## 3\. `[project]`

Where the source lives and where builds write.

```toml
[project]
content-root = "docs"
output-dir = ".ascribe/build"
```

Tested with ascribe 0.4.4 on Linux.

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `content-root` | string (path) | `"docs"` | The content root ([SPEC §2.2](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#22-pages-and-fragments)): the directory holding every source file. Paths in links and includes that begin with `/` are relative to it. It must exist and be a directory. |
| `output-dir` | string (path) | `".ascribe/build"` | Where `ascribe build` writes output. Each build and emitter writes under `<output-dir>/<build>/<emitter>/`. It need not exist. It may be outside the project's folder, with `..`, and a build then writes there. A build never writes or removes through a symbolic link on the way to it, or in it ([output-layout contract](https://ascribed-dev.com/docs/contracts/output-layout.md#2-what-ascribe-owns)). |

**Rules** (§25.2): `content-root` and `output-dir` MUST be relative (`model-path-absolute`). The content root MUST exist and be a directory (`model-content-root-missing`); the output directory need not exist. The output directory MUST NOT be inside the content root, the content root MUST NOT be inside the output directory, and the two MUST differ (`model-output-overlaps-content`), each compared after its `.` and `..` segments are normalized and, when both exist, its symbolic links resolved.

The defaults: `".ascribe/build"` keeps generated output out of the way of both the source and a consumer's own `dist/`.

---

## 4\. `[types.<name>]` and `[fragments]`

### 4\.1 Page types

A **content type** is a frontmatter schema for a kind of page. A page names its type in its frontmatter, with `type`.

```toml
[frontmatter]
description = "string?"

[types.guide]

[types.reference.frontmatter]
api-version = "string"
```

`<name>` is the type's name (`key` rule). It appears in diagnostics, in generated code (the Zod schema's name), and on each page of the type, as the value of `type`.

**Frontmatter every type shares.** `[frontmatter]` declares the fields every page type has, written as a type's own are, so a field every page has is written once. A type's own `frontmatter` adds fields to them. A type that declares a field `[frontmatter]` has replaces it whole, for that type: an index page's type can make a shared `description` optional, or give it a default. The two aren't merged key by key, so a field reads as one declaration, wherever it's written. The implicit `page` type of a project that declares no types has them too.

**Where a field is declared.** In VS Code, hover a field's key in `ascribe.toml`, or a frontmatter key in a page, to see whether the field is `[frontmatter]`'s, the type's own, or the `title` every type has; on a field of `[frontmatter]`, the hover names the types that replace it. `ascribe model --section types` says the same after each field's type, and its JSON in each field's `declared`: `frontmatter`, `type`, or `implicit`. A type's field that's the same as `[frontmatter]`'s, key for key, changes nothing, and is reported as advice at the type's field (`frontmatter-field-repeated`), with a fix that deletes it; written in another form, as `{ type = "string" }` for `"string"`, it's still the same field.

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `frontmatter` | table of fields | `{}` (no fields) | The fields every page type has, as a type's own `frontmatter` declares them (below), so a field every page has is written once. A type's own fields add to them, and one of the same name replaces the shared field whole, for that type. The implicit `page` type of a project with no types has them too. |

Every page type has a required string `title`, whether it's declared or not (below), so a type with nothing else of its own needs no keys: `[types.guide]` above is a type a page can name, with `title` and the shared fields.

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `frontmatter` | table of fields | `{}` (no fields of its own) | The type's own frontmatter schema: one entry per field, keyed by field name (`name-word`). Values are field types (§5). Every type has a required string `title`, declared or not (below). A type can't declare a field `type`, which is reserved (`model-field-reserved`). |
| `purpose` | string | none | What a page of the type is for, in a few words. The rules `ascribe agents sync` writes say it on the type's line, and `ascribe model` shows it. A type copied from the starter set says its summary there without one (below). |
| `template` | string (path) | none | The page a page of the type starts from: a file from the folder `ascribe.toml` is in, outside the content root (below). A type with no `template` and no `guide` takes both from `templates/templates.lock`, when the lock `ascribe templates eject` wrote records the type. |
| `guide` | array of paths | `[]` | What to read before writing a page of the type, in the order to read it, such as a guide and a project's own notes on it: files from the folder `ascribe.toml` is in, outside the content root (below). Without it or `template`, from `templates/templates.lock`, as `template` is. |

**Which type a page is.** A page says what type it is with `type` in its frontmatter, the first key by convention, wherever its file is: a folder can hold pages of every type.

```yaml
---
type: reference
title: Commands
api-version: "3"
---
```

- **A project that declares no types** has one, the implicit `page`, whose fields are a required string `title` and `[frontmatter]`'s. Its pages need no `type`, and a page that has one is an error (`content-type-unknown`), with a fix that removes it.
- **A project that declares types** has no implicit one. A page that names no type is an error on the page (`content-type-missing`), with a fix for each declared type that adds `type:` to the page's frontmatter. A page whose `type` names a type that isn't declared is an error at the value (`content-type-unknown`), with a fix to the closest name when one is close.
- **[Fragments](https://ascribed-dev.com/docs/glossary.md#fragment)** have no type (§4.3), and can't use `type` (`frontmatter-reserved-in-fragment`).

A type's table has no keys that say which pages it applies to: `files` and `default` are unknown keys (`model-unknown-key`), whose message says to name the type on its pages instead.

**A type's template and guide.** Both are paths from the folder `ascribe.toml` is in, and both name files that aren't pages:

```toml
[types.how-to]
template = "templates/how-to.md"
guide = ["templates/how-to.guide.md", "templates/how-to.ascribe.md"]
```

Each file must be there, with its name's case as it is on disk, inside the folder `ascribe.toml` is in, and outside the content root, where it would be read as a page, and the output directory, which a build writes and removes. A symbolic link is followed to where it leads (`model-type-file`). `guide` is always a list, even with one file in it (`model-wrong-type`). The template is read when the content model loads, and must read as a page: valid YAML between its `---` lines, and no error in its Ascribe syntax (`model-type-template`). Either is a problem with the content model, reported at the path, once, rather than on every page made from the template.

**A template from the lock.** A type with neither key takes its template and guide from `templates/templates.lock`, the file [`ascribe templates eject`](https://ascribed-dev.com/docs/templates/command.md#ascribe-templates-eject) writes, when the lock records a template under the type's name: its template, then their guide and Ascribe's notes on the type, in the order to read them. That's how a type `eject` added or kept has its template, with nothing about it in `ascribe.toml`. A type that names either key keeps what it names and takes nothing from the lock, so a project without one, or a template written by hand, names its files as above.

The files the lock gives are held to the same rules, and a problem with one is the type's (`model-type-template`), reported at the type's `[types.<name>]`, since `ascribe.toml` has no path to point at. A lock that isn't there, or isn't one `eject` wrote, gives nothing; [`ascribe templates status`](https://ascribed-dev.com/docs/templates/command.md#ascribe-templates-status) says what's wrong with it. In VS Code, the hover on a type's name in `ascribe.toml` says where its template comes from.

**What a type is for.** `purpose` is one string, for a project's own types, and for a team that wants to say what one of the starter set's types means to it:

```toml
[types.how-to]
purpose = "One task, done step by step, for someone who knows why they're doing it."
```

The rules for agents say it at the end of the type's line. A type `ascribe templates eject` added and that has no `purpose` says their index's summary instead, from the `templates.lock` beside its template. When the rules don't fit in their 4,000 characters, a type's line keeps its name and what it's for after the rest of it is cut, and the page types are [the last thing cut](https://ascribed-dev.com/docs/agents/instructions.md#when-the-rules-are-long), with `ascribe model --section types` named for the rest.

**A page that still has its template's text.** On a page of a type with a template, `ascribe check` reports each of the template's placeholders the page still has word for word, and a frontmatter field with the template's value: as advice in a draft (`template-placeholder-left`), and as an error in a page that publishes (`template-placeholder-published`). The [guide to page templates](https://ascribed-dev.com/docs/templates.md#whats-left-to-fill-in) says what it finds and what it can't.

**A section the template leaves to the page.** A section the template gives nothing under its heading but [memos](https://ascribed-dev.com/docs/glossary.md#memo), and that a page of the type still leaves empty, builds as a heading with nothing under it; `ascribe check` reports it as advice at its heading (`template-section-empty`). The [guide to page templates](https://ascribed-dev.com/docs/templates.md#whats-left-to-fill-in) says what counts as empty.

**The page title.** Every page type has `title`, a required `string` field, whether `[frontmatter]` or the type declares it or not, and a page's frontmatter `title` is its title wherever SPEC needs one (SPEC §5.2). A declaration of it MAY set `phrases = true` (§7) and `inline = "code"` (§5.3), and MUST NOT make it optional, give it a default, or give it another type (`model-field-reserved`).

**Frontmatter every type shares.** A page type's fields are `[frontmatter]`'s, in their order, each replaced whole by the type's own field of its name, then the type's other fields in theirs, with `title` first when neither declares it. Fields are never merged key by key.

**Reserved keys.** `type`, `available`, `variant`, `permalink`, `listing`, `replaces`, `nav-label` (SPEC §2.1, §4.3, §4.4, §7.2), and `intended` (SPEC §4.9) are reserved frontmatter keys. Every page MUST accept them, with the meaning SPEC gives them, whatever its type declares, and neither a type nor `[frontmatter]` MAY declare one (`model-field-reserved`). A generated consumer schema MUST include `type`, as the literal of the type's name on a type the project declares, `available`, `variant`, and `permalink`, and MUST NOT include `listing` (the page's record has it, output-layout contract §1.3), `replaces` (the versions file has it, §1.5), `nav-label` (the navigation has it, §1.6), or `intended`, which no output has. A page type MUST NOT declare `formatted` (§5.3) or `source` (SPEC §9.6), which the site output writes, nor, under the `astro` profile, `slug`, which Astro's content loader reads as an entry id (`model-field-reserved`); a page's frontmatter can't use `formatted` or `source`, since no type declares them.

**Unknown frontmatter keys.** A page whose frontmatter has a key its type doesn't declare (and that isn't reserved) is an error on the page.

### 4\.2 Frontmatter values

Frontmatter is YAML. For type checking, processors parse it with the YAML 1.2 **core schema**: `true` and `false` are booleans (`yes`, `no`, `on`, and `off` are strings), and `3.10` is the number 3.1. So a `string` field whose value is `3.10` unquoted is a type error, and the message suggests quoting it.

### 4\.3 Fragments

```toml
[fragments]
patterns = ["includes/**", "**/*.partial.md"]

[fragments.frontmatter]
owner = "string?"
```

Tested with ascribe 0.4.4 on Linux.

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `patterns` | array of patterns | `[]` | Additional fragment patterns ([SPEC §2.2](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#22-pages-and-fragments)). A file is a fragment if any segment of its path begins with `_`, or its path matches one of these patterns. |
| `frontmatter` | table of fields | `{}` (no fields) | The fragment schema ([SPEC §2.2](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#22-pages-and-fragments)): the frontmatter every fragment is validated against. Fields as in §5. `title` is not required. |

Content types never apply to fragments: a fragment names no type, and `type` in a fragment's frontmatter is an error (`frontmatter-reserved-in-fragment`).

**Reserved keys on fragments.** A fragment's frontmatter MUST NOT use `type`, `available`, `variant`, `permalink`, `listing`, `replaces`, or `nav-label`, which SPEC defines for pages only (`frontmatter-reserved-in-fragment`, an error on the fragment), and MAY use `intended`. `[fragments.frontmatter]` MUST NOT declare any of the eight (`model-field-reserved`).

---

## 5\. Field and attribute types

Two related type languages:

- **Field types** describe frontmatter fields (`[types.<name>.frontmatter]`, `[fragments.frontmatter]`). They cover the [SPEC §7.2](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#72-declarations) set: string, number, boolean, date, enumeration, list, and object, each optional or with a default.
- **Attribute types** describe attributes on images and widgets (`[images.attributes]`, `[widgets.<name>.attributes]`). They cover [SPEC §3.3](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#33-attributes)'s set: string, enumeration, boolean, and number, plus set-valued keys.

Each entry is written either in **short form**, a string, or in **table form**, when it needs a default, a description, phrases, enumeration values that aren't simple words, or nested fields.

```toml
[types.guide.frontmatter]
title = "string"                                   # required string
description = "string?"                            # optional string
level = "enum(beginner, intermediate, advanced)"   # required, one of three
tags = "list(string)?"                             # optional list of strings
updated = "date?"                                  # optional date
status = { type = "enum(draft, published)", default = "published" }
author = { type = "object?", fields = { name = "string", url = "string?" } }
```

### 5\.1 Short form

A field is **required** unless its type ends in `?` or it has a default. The same holds for attributes: `lab = "string"` means every use of the widget must give `lab`.

| Short form | Field types | Attribute types | Accepts |
| --- | --- | --- | --- |
| `string` | yes | yes | Frontmatter: a YAML string. Attribute: a token or quoted string. |
| `number` | yes | yes | Frontmatter: a YAML integer or float. Attribute: a token matching `["-"] 1*DIGIT ["." 1*DIGIT]` (`600`, `-2`, `1.5`; not `600px`). |
| `boolean` | yes | yes | Frontmatter: YAML `true` or `false`. Attribute: the token `true` or `false` ([SPEC §3.3](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#33-attributes)). |
| `date` | yes | no | A calendar date written `YYYY-MM-DD` that exists (`2026-02-30` is an error), quoted or not. Times aren't accepted. |
| `enum(a, b, …)` | yes | yes | One of the listed values, compared exactly. Frontmatter: a YAML string. Attribute: a token or quoted string. |
| `list(T)` | yes | no | A YAML sequence whose items are all of type `T`, where `T` is `string`, `number`, `boolean`, `date`, `enum(…)`, or (table form only) `object`. An empty sequence is allowed. |
| `set(T)` | no | yes | A value set ([SPEC §3.3](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#33-attributes)), such as `platform=cloud\|on-prem`, or a single token. `T` is `string` or `enum(…)`. Members are tokens. Only keys typed `set(…)` accept `\|`. |
| `object` | table form only | no | A YAML mapping whose keys are the object's `fields` (nested field types). Unknown keys are errors. |
| any of the above + `?` | yes | yes | The same type, optional. |

Grammar (ABNF, with the rules of SPEC Appendix A):

```abnf
field-type      = field-base [ "?" ]
field-base      = scalar / enum / list-type / "object"    ; "object" in table form only
scalar          = "string" / "number" / "boolean" / "date"
list-type       = "list" "(" OWS ( scalar / enum / "object" ) OWS ")"
                                        ; "list(object)" in table form only

attribute-type  = attribute-base [ "?" ]
attribute-base  = "string" / "number" / "boolean" / enum / set-type
set-type        = "set" "(" OWS ( "string" / enum ) OWS ")"

enum            = "enum" [ "(" OWS enum-value *( OWS "," OWS enum-value ) OWS ")" ]
                                        ; bare "enum" in table form only, with "values"
enum-value      = 1*( ALPHA / DIGIT / "-" / "_" / "." )
```

Spaces are allowed only where `OWS` appears. The canonical spelling has no spaces except one after each comma: `enum(a, b)`.

### 5\.2 Table form

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | string | **required** | A field type or attribute type (§5.1). In table form it may also be `object`, `list(object)`, or bare `enum`, with the keys below. |
| `fields` | table of fields | required when `type` is `object`, `object?`, `list(object)`, or `list(object)?`; not allowed otherwise | The nested fields of an object, in the same syntax (short or table form). Field types only. |
| `values` | array of strings | required when `type` uses bare `enum`; not allowed otherwise | The enumeration's values, for values that aren't simple words, such as `["Getting started", "How-to"]`. Values must be distinct. Values of a `set(enum)` attribute must be tokens. |
| `default` | any TOML value | none | The value used when the field or attribute is absent. It must have the declared type (a TOML string for `string` and `enum`, integer or float for `number`, boolean for `boolean`, local date for `date`, array for `list`, inline table for `object`, a string or array of strings for `set`). A field with a default is optional; adding `?` as well is allowed and changes nothing. |
| `phrases` | boolean | `false` | Whether phrases ([SPEC §5.1](https://github.com/ascribed-dev/ascribe/blob/main/SPEC.md#51-phrases)) are substituted in this field's value. Field types only; allowed only on `string` and `list(string)` fields (and their optional forms), including fields nested in objects. See §11. |
| `inline` | string | none | `"code"` reads the value's code spans. Allowed only on a page type's top-level `string` fields (and `string?`), its own or `[frontmatter]`'s. See §5.3. |
| `description` | string | none | Help text shown by the editor (hover and completion) and emitted into generated schemas as documentation. |
| `role` | string | none | What the field is for, which a check reads: `"description"`, `"recheck-date"` (or `"review-date"`, its old name, for a release), or `"owner"`. Allowed only on a page type's top-level fields, its own or `[frontmatter]`'s. A field named for a role has it without `role`, unless the type gives it to another field. See §5.4. |

**Rules** (§25.3): `fields` MUST be given exactly when the type is `object` or `list(object)`, optional or not (`model-type-fields`). `values` MUST appear only with bare `enum`, with at least one value and no two alike (`model-enum-values`), and a `set(enum)` attribute's values MUST be tokens (`model-set-token`). A `default` MUST have the declared type, and an enumeration's MUST be one of its values (`model-default-type`). `phrases = true` MUST be set only on `string` and `list(string)` fields, optional or not, including fields in objects (`model-phrases-field-type`). `inline` and `role` MUST be set only on a page type's top-level fields, its own or `[frontmatter]`'s, not in an object or the fragment schema (`model-inline-field`, `model-field-role`).

Nesting beyond one level of `fields` is allowed but discouraged; keep frontmatter flat.

### 5\.3 Inline code in a field

A field that sets `inline = "code"` MUST be read as inline Markdown that knows only code spans; any other `string` field's value is its text, backticks and all.

- **Code spans** are CommonMark's (CommonMark §6.1): a run of backticks opens one, the next run of the same length closes it, and a run with no closer is literal. Their content is normalized as CommonMark does: line endings become spaces, and one space is stripped from each end when both ends have one.
- **Escapes.** Outside code spans, a backslash before ASCII punctuation escapes it, as in CommonMark, so `` \` `` is a literal backtick. Inside them, a backslash is literal.
- **Nothing else is markup.** Emphasis, links, and HTML stay literal text.
- **Phrases** (`phrases = true`) are substituted in the text, never inside code spans, as in prose (SPEC §5.1).

Each output carries two forms of the value. The **plain text** is the value without markup: the code spans' content, with no backticks (`ascribe.toml reference`). The **formatted form** keeps the code spans. Where each goes:

| Output | Plain text | Formatted form |
| --- | --- | --- |
| Site output (its frontmatter) | The field's frontmatter value, for a layout's `<title>`, search, and sorting | `formatted.<field>`, as HTML (`<code>ascribe.toml</code> reference`), for a layout's heading and navigation |
| Empty link text (SPEC §5.2) | Not used | The link text, with the title's code spans |
| Plain-markdown output | Not used | The page's level-1 heading |
| JSON output | `title` and `frontmatter.<field>` | `formatted.<field>`, a list of `{ "type": "text" \| "code", "value": … }` |
| Generated Zod schema | The field, as `z.string()` | `formatted`, with a `z.string()` for each field |

A field the page leaves out gets its default's formatted form, so `formatted.<field>` is there whenever the field has a value.

**Rules** (§25.3): `inline` MUST be `"code"`, on a page type's top-level `string` or `string?` field, its own or `[frontmatter]`'s (`model-inline-field`).

---

### 5\.4 Field roles

A page type's top-level field MAY have a `role`, which says what the field is for, so a check or the report reads it whatever the field's name (SPEC §7.2). A field named `description`, `recheck-date`, or `owner` has that role without `role`, unless another field of the type has it:

| Role | The field's type | Read by |
| --- | --- | --- |
| `description` | `string` or `string?` | `page-description-missing`, which reports a page of the type whose field is absent, null, or blank. A field that's required or has a default is never absent, so the check skips it. |
| `recheck-date` | `date` or `date?` | `recheck-overdue`, which reports a page whose date is before the day the check runs. `review-date`, its old name, names it too, for a release. |
| `owner` | `string`, `string?`, or an `enum`, optional or not | `ascribe report inventory`, which counts pages by the field's value; a page without one counts as having no owner. |

A type without a field with the role has none of that check's diagnostics. A role changes no output: not the generated schemas, the JSON, or the pages.

**Rules** (§25.3): the role is one of the three, on a field of its type, in a page type's top-level fields, its own or `[frontmatter]`'s, and at most one field of a table has each role (`model-field-role`). A second field with a role keeps its type, and loses the role. A type's own field given a role with `role` has it, and a `[frontmatter]` field given the same role loses it, for that type. A role no field is given with `role` is the field's of the role's name, when its type is one the role takes; otherwise no field has it.
