ascribe.toml format contract
The normative contract for the content model file, with the rules a loader enforces.
For AI agents: the documentation index is at llms.txt, and this page is available as Markdown.
This is the normative contract for ascribe.toml, the 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, 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/, listed in decision #689a.
Every file in 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:
- Conventions
- Top level:
spec, and[from] [project][types.<name>]and[fragments]- Field and attribute types
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:[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). The[dimensions]tables keep theirs too: it’s the canonical order of@variantattributes, and decides which dimension a tab group syncs on (the element contract, §3). Order is kept elsewhere for stable output (for example, the order of dimension values in a tab switcher is the order ofvalues). 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, §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).
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), 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’sincludeandignore(§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 spec” are strings with the grammars in §5 and SPEC §4.4.
- 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
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]
[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.
[project]
content-root = "docs"
output-dir = ".ascribe/build"| Key | Type | Default | Description |
|---|---|---|---|
content-root |
string (path) | "docs" |
The content root (SPEC §2.2): 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). |
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.
[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.
---
type: reference
title: Commands
api-version: "3"
---
- A project that declares no types has one, the implicit
page, whose fields are a required stringtitleand[frontmatter]’s. Its pages need notype, 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 addstype:to the page’s frontmatter. A page whosetypenames 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 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:
[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 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 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:
[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, 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 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, 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 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
[fragments]
patterns = ["includes/**", "**/*.partial.md"]
[fragments.frontmatter]
owner = "string?"| Key | Type | Default | Description |
|---|---|---|---|
patterns |
array of patterns | [] |
Additional fragment patterns (SPEC §2.2). 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): 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 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’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.
[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). |
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), 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):
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) 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.