Document Models
Document models let you define structured schemas for your content types. Instead of relying on ad-hoc frontmatter fields, you declare which fields a page type should have, what types they are, whether they’re required, and what constraints apply.
Quick Start
Create a models/ directory in your project root and add a YAML file for each content type:
my-site/
config.yaml
models/
blog.yaml
event.yaml
content/
...
Example: Blog Post Model
# models/blog.yaml description: Blog post with author and category fields: author: type: string required: true label: Author Name category: type: enum values: [tutorial, news, opinion, release] required: true featured_image: type: media reading_time: type: integer constraints: min: 1 defaults: category: news
Pages using the blog template will automatically be validated against this model.
The Core Fields
Every page carries a set of fields Accent itself understands: title, date,
author, lead, tags, and the routing and workflow keys behind them. These
are declared by a built-in model named core, and every model inherits it
automatically. You never need to write extends: core – a model with no
extends: is already a child of it. Writing it out says exactly the same
thing, so a model that spells the inheritance out is accepted too.
That has one practical consequence: you can constrain a core field from your own model, exactly like any field you invented.
# models/news.yaml -- a dated post is not publishable without a date fields: date: type: date required: true label: Publication date lead: type: text required: true constraints: max_length: 300
Your declaration replaces the core one for that key, so the label, the requirement, and the constraints are yours. Everything you do not mention keeps the core definition.
Two core fields are filled in for you when you leave them out: title falls
back to the first heading or the filename, and lead to the opening paragraph.
required: true asks for a value you wrote, so a filled-in one does not
satisfy it – title: { required: true } reports every page that carries no
title: of its own, which is the whole point of asking.
A date: is checked against the same formats Accent’s loader accepts, not just
YYYY-MM-DD: 03/15/2026, 15.03.2026, March 15, 2026 and an ISO 8601
timestamp such as 2026-03-15T09:00:00Z all validate, because they all load.
A page’s date sorts by its day, and a timestamp then by its moment, so two
posts on one day keep the order they were written in.
What core declares
| Field | Type | In the editor |
|---|---|---|
title | string | Yes |
date | date | Yes |
author | string | Yes |
lead | text | Yes |
tags | list<string> | Yes |
publish_date, unpublish_date | date | Yes |
status, published | enum / boolean | No – owned by the workflow buttons |
template, model, slug, url, redirect, rewrite | routing keys | No |
menu, content, relations, process, media, pdf_thumbnails | structured | No |
anchors, noindex | boolean | No |
Run accent model show <name> to see the merged result for any model,
core fields included.
The one thing you cannot do
You may constrain a core field. You may not retype one, because the value has a fixed shape inside Accent:
fields: title: type: number # rejected: `title` holds a string tags: type: string # rejected: `tags` holds a list
A model that does this is refused at load time with a message naming both
shapes, and the models around it keep working. Types that describe the same
shape are all fine – date, string, url, and enum all store a string,
so any of them is a legal description of title.
A module’s frontmatter is a page frontmatter too, so the same shape rule
applies inside module_models: – where media, content, and menu are
names you might reach for without meaning the page-level field at all. There
the cost is kept to the field: it is dropped with a message, the module’s other
fields keep validating, and the model keeps governing its pages.
core is a reserved model name: a models/core.yaml is refused rather than
silently ignored.
Marking a field non-editable
Any field can be hidden from the admin editor’s form pane with
editable: false. Use it for values a plugin or a build step writes and only
templates read:
fields: search_index_id: type: string editable: false
The field is still validated, still available to templates, and still written by whatever produces it – it just is not offered as an author input.
Field Types
Document models support 15 field types:
| Type | Description | YAML Example |
|---|---|---|
string | Short text | "Hello World" |
text | Multi-line text | "Long description..." |
integer | Whole number | 42 |
number | Decimal number | 3.14 |
boolean | True/false | true |
date | ISO date | "2026-06-15" |
datetime | ISO datetime | "2026-06-15T10:30:00" |
enum | Fixed set of values | "published" |
url | URL (absolute or site-relative) | "https://example.com" |
email | Email address | "user@example.com" |
list | Array of typed items | [a, b, c] |
map | Key-value pairs with typed values | {key: value} |
reference | Path to another page | "/blog/my-post" |
media | Path to a media file | "hero.jpg" |
object | Nested fields | {width: 100, height: 200} |
any | Any shape; presence is checked, shape is not | /moved or {url: /moved} |
Complex Types
Enum fields require a values list:
status: type: enum values: [draft, review, published, archived]
List fields require an items definition:
tags: type: list items: type: string
Map fields use items for the value type:
metadata: type: map items: type: string
Object fields define nested fields:
dimensions: type: object fields: width: type: number required: true height: type: number required: true
Constraints
Add validation constraints to fields:
fields: price: type: number constraints: min: 0 max: 99999 title: type: string constraints: min_length: 1 max_length: 200 pattern: "^[A-Z]" tags: type: list items: { type: string } constraints: min_items: 1 max_items: 10 event_date: type: date constraints: future_only: true
Available Constraints
| Constraint | Applies to | Description |
|---|---|---|
min | integer, number | Minimum numeric value |
max | integer, number | Maximum numeric value |
min_length | string, text | Minimum length, in Unicode code points |
max_length | string, text | Maximum length, in Unicode code points |
pattern | string, text | Regex pattern to match |
min_items | list | Minimum item count |
max_items | list | Maximum item count |
future_only | date, datetime | Date must be in the future |
past_only | date, datetime | Date must be in the past |
Model Resolution
Accent CMS matches pages to models using this priority:
- Explicit model field – Set
model: blogin your page’s frontmatter - Template name match – A page using
template: eventmatchesmodels/event.yaml - Directory model – A
_model.yamlfile in the content directory applies to all pages in that directory
Explicit Model
--- title: My Event model: event start_date: 2026-08-01 ---
Directory-Level Model
Place a _model.yaml file in a content directory to apply a model to all pages within it:
# content/events/_model.yaml model: event
All markdown pages under content/events/ will be validated against the event model.
Editing models during development
When you run accent serve with hot reload on, saving a model file (in your project’s models/, in your theme’s models/, or in a plugin’s models/) or a _model.yaml under content/ rebuilds the content index and reloads the browser, the same as saving a page. Validation, the admin schema browser, and the editor’s field views pick up the new schema on the next request. You don’t need to touch a content file or restart the server.
Defaults
Models can provide default values for fields. These are applied when a field is absent from the page’s frontmatter.
Per-field defaults (set on individual fields):
fields: capacity: type: integer default: 100
Top-level defaults:
defaults: featured: false
Per-field defaults take precedence over top-level defaults for the same field name. Existing frontmatter values are never overwritten.
A default on a core field that always has a value – status, published,
anchors, process – never fires, because such a field is never absent.
status: published in a defaults: block does nothing; publishing status is
owned by the workflow, not by a model file. Defaults on date, author,
lead, and tags do apply when the page leaves them empty.
Renaming a Field
To rename a field without editing every page at once, give the field its new
name and list the old one under renamed_from:
fields: summary: type: text renamed_from: [teaser]
A page that still says teaser: loads with summary, so templates, queries,
and validation all see the new name. The file changes only when the page is
next saved – from the admin editor, with accent content update-frontmatter, or by an agent – and the save renames the key where it
stands, keeping its value and any comment beside it.
accent validate reports each page that still uses an old name as info, and
ends with a count for each rename:
12 pages still use `teaser`, renamed to `summary`
The count falls as pages are saved. When it reaches zero, you can remove
renamed_from.
A few rules keep a rename unambiguous:
- A page that sets both
teaserandsummarykeeps thesummaryvalue, andaccent validatewarns about it. Saving that field in the editor settles it: the old key goes, and the value you were looking at is the one that stays. Until then both keys remain in the file, and removing the old one by name removes that key, not the value Accent kept. renamed_fromworks on top-level fields only.- A core field –
title,lead,tags, and the rest listed under The Core Fields – can’t be renamed, and an old name can’t be a core key. Templates, listings, and feeds read core fields by their own names. - An old name can’t be another field the model declares, and two fields can’t claim the same old name.
Accent ignores an old name that breaks one of these rules and logs a warning naming the model and the field; the rest of the model works as usual.
Model Inheritance
Models can extend a parent model using extends. The child inherits all parent fields and can add new fields or override parent definitions:
# models/default.yaml name: Default Page fields: subtitle: type: string required: false hero_image: type: media required: false # models/blog.yaml name: Blog Post extends: default fields: category: type: enum required: true values: [tech, design, business, personal] reading_level: type: enum default: intermediate values: [beginner, intermediate, advanced]
The blog model inherits subtitle and hero_image from default, plus adds its own fields.
Inheritance rules:
- Every chain ends at the built-in
coremodel, so the core fields are always present - Child inherits all parent fields, defaults, and validation rules
- Child can override parent field definitions (e.g., make an optional field required)
- Child can add new fields
- Child cannot remove parent fields
- Single inheritance only (no multiple parents)
- Circular inheritance is detected and rejected at load time
- Deep chains work: grandparent -> parent -> child
Sharing a Social / SEO meta Schema via extends
Social sharing tags (Open Graph, Twitter Cards, and any other platform tags)
ride an open meta object on every page. The object always works as a raw map
with no model bound – the share card still renders. Binding a model adds
validation: an over-long description or a typo’d og:type / twitter:card
value is caught at content load instead of surfacing only when a human inspects
the unfurl.
Because object schemas are inline (there is no named $ref), the way to reuse
one meta schema across content types is to put it on a base model and
extends that base. The default theme ships exactly such a base model:
# themes/default/models/base.yaml name: Base Document fields: description: type: string constraints: max_length: 200 # platforms silently truncate longer values meta: type: object label: Social / SEO tags fields: og:type: # colon keys are supported as-is type: enum values: [website, article, profile] default: website twitter:card: type: enum values: [summary, summary_large_image] default: summary og:description: type: string constraints: max_length: 200
A content model inherits the whole schema – the description cap and every
declared meta sub-field – with a one-line extends:
# themes/default/models/docs.yaml name: Documentation Page extends: base
Two things make this safe to apply broadly:
- The object is an open carrier. Only the sub-fields you declare are
validated. Undeclared keys (
fediverse:creator,fb:app_id, the next network) pass through untouched – adding a platform is a content edit, not a schema change. - No field is required and the model is not strict. A page that omits
descriptionormetavalidates cleanly; the schema only catches mistakes in metadata that is actually present.
To validate your own pages, bind a model that extends: base the same way you
bind any model (by template name, an _model.yaml directory file, or an
explicit model: field – see Model Resolution). See the
Social Sharing and SEO Metadata
guide for how the resolved tags are emitted.
Cross-Field Validation Rules
Single-field validation catches type errors and constraint violations, but many content contracts involve relationships between fields. Use rules to define cross-field validation expressions:
# models/event.yaml fields: start_date: { type: date, required: true } end_date: { type: date, required: true } capacity: { type: integer, default: 0 } registration_url: { type: url } is_virtual: { type: boolean, default: false } location: { type: string } rules: - name: end_after_start description: End date must be on or after start date expression: "end_date >= start_date" - name: capacity_with_registration description: If capacity > 0, registration_url should be set expression: "capacity == 0 or registration_url is defined" severity: warning - name: virtual_or_location expression: "is_virtual or location != ''" severity: warning
Rules use MiniJinja expression syntax. Each rule’s expression is evaluated against the page’s frontmatter fields. If the expression evaluates to false, the rule fails.
An expression can read every field in the page’s frontmatter: its custom fields, and the core fields in the table above, such as title, date, tags, status, publish_date and url, the permalink override. A core field reads as the field constraints read it, so a title Accent derived from the page’s heading is the page’s title. A field with no value is undefined, so field is defined and field is undefined test for it; tags is an empty list on an untagged page, and status reads as its default when the page sets none. A rule can relate a custom field to a core one:
rules: - name: title_starts_with_id expression: "title is startingwith(id ~ ': ')"
Rule properties:
| Property | Required | Description |
|---|---|---|
name | yes | Unique identifier for the rule |
description | no | Human-readable explanation |
expression | yes | MiniJinja expression to evaluate |
severity | no | error (default) or warning |
Available operators: ==, !=, >, >=, <, <=, and, or, not, is defined, is undefined
Rules are evaluated only after all referenced fields pass individual validation. If start_date has a type error, the end_date >= start_date rule is skipped to avoid confusing cascading errors.
Module Constraints
For modular pages (pages with _module subdirectories), models can restrict which modules are allowed:
# models/landing.yaml modules: allowed: - hero - features - pricing - testimonials - cta required: - hero max: 8
Validation behavior:
- A module not in
allowedproduces aModelDisallowedModuleissue - A module in
requiredthat is missing produces aModelMissingModuleissue - Exceeding
maxmodules produces aModelTooManyModulesissue - If no
modulesblock is defined, all modules are allowed (backward compatible)
Module Field Validation
Models can also validate frontmatter within individual modules using module_models:
module_models: hero: fields: heading: type: string required: true background: type: media required: false features: fields: columns: type: integer default: 3 constraints: min: 1 max: 4
Section Constraints
For pages with named sections (---section: name--- markers), models can restrict which section names are valid:
# models/wiki.yaml sections: allowed: - overview - details - references - changelog
A section not in the allowed list produces a ModelDisallowedSection issue. If no sections block is defined, all section names are allowed.
Strict Mode
Enable strict_fields: true to warn about frontmatter fields not declared in the model:
strict_fields: true fields: title_field: { type: string } category: { type: enum, values: [a, b] }
The core fields are always allowed in strict mode: your model inherits them, so
they are declared fields like any other. Strict mode flags only keys nothing has
declared. To allow a key without describing its shape, declare it as
type: any.
Theme Models
Themes can provide models in their models/ directory. Project models override theme models with the same name:
themes/default/models/blog.yaml # Theme-provided model
models/blog.yaml # Project override (wins)
Template Access
The resolved model name is available in templates as page.model:
{% if page.model == "event" %}
<div class="event-badge">Event</div>
{% endif %}
Validation
Model violations appear in the debug panel (dev mode) and in accent validate output:
- ModelMissingRequired – A required field is not present
- ModelTypeMismatch – A field value doesn’t match the expected type
- ModelConstraintViolation – A value violates a constraint rule
- ModelUnknownField – An undeclared field is present (strict mode only)
- ModelRuleViolation – A cross-field rule expression evaluated to false
- ModelDisallowedModule – A module not in the allowed list
- ModelMissingModule – A required module is absent
- ModelTooManyModules – Page exceeds the maximum module count
- ModelDisallowedSection – A section name not in the allowed list
- ModelFilenamePatternMismatch – A filename or directory name does not match the model’s pattern
- ModelLoadFailed – A model file was found but could not be loaded
When a model file fails to load
A model file that cannot be parsed is skipped, and every other model still loads. One typo in one file never switches off validation for the rest of your models.
Skipping is never silent, because a model that did not load means content it governs is not being checked at all:
accent validatereports it as an error and exits non-zero, alongside any other issues. It is shown even under--models/--model.accent buildrefuses to build, naming each file that failed. A site is not published against models that did not load.accent servekeeps running on the models that did load – so a half-written model file during a hot-reload edit does not take the dev server down – and prints the failures, reprinting only when they change.accent model listlists what loaded and reports what did not.accent new <model>tells you the model failed to load, rather than reporting it as missing.
Refusing to build costs you nothing: nothing is written and nothing is deleted,
so a previous build in the output directory survives even with --clean.
A model whose extends: parent failed to load, or whose inheritance chain is
circular, is dropped the same way and reported the same way. Unrelated models
are unaffected.
When a failed model does not stop the build
The tiers are plugin > project > theme, and only a failure that leaves
content genuinely unchecked is fatal:
- If your theme ships a
blog.yamlthat no longer parses and your ownmodels/blog.yamloverrides it, nothing is unvalidated – your model is in force, and the build proceeds. You are not asked to fix a file inside a theme you do not maintain. - If your own
blog.yamlis the one with the typo, the theme’sblogis not used as a fallback. Silently reverting to a definition you overrode would apply defaults you never wrote to your pages. The model is dropped and the build stops.
Setting content.validation.mode: off disables document-model validation
entirely, including these checks – a model that fails to load then costs you no
coverage, so neither validate nor build fails on it.
Filename Patterns
Models can declare naming conventions for both the directory name and the file name of content pages. Patterns are composed of typed segments that are validated against the actual filesystem names.
Defining Filename Patterns
# models/event.yaml filename: directory: pattern: "{order}.{date}-{slug}" description: Events are ordered, date-prefixed directories file: pattern: "event.md" description: All event pages use the event template filename
Both directory and file are optional. Models without a filename block impose no naming constraints.
Pattern Segment Types
Each {placeholder} in a pattern maps to a segment type:
| Segment | Syntax | Matches | Examples |
|---|---|---|---|
| literal | Raw text | Exact string | event, .md |
order | {order} or {order:3} | N-digit integer | 01, 003 |
slug | {slug} | Kebab-case lowercase ASCII | my-first-post |
date | {date} | ISO 8601 date (YYYY-MM-DD) | 2026-03-03 |
year | {year} | 4-digit year | 2026 |
month | {month} | 2-digit month (01-12) | 03 |
day | {day} | 2-digit day (01-31) | 15 |
number | {number} or {number:4} | Digits | 0001, 42 |
enum | {enum:a,b,c} | One of listed values | bug, feature |
field | {field:name} | Slugified frontmatter value | ab-1234 |
semver | {semver} | Semantic version | 1.2.3 |
lang | {lang} | ISO 639-1 language code | en, de |
any | {any} | Any non-separator string | Wildcard |
Cross-Validation with Frontmatter
Date and field segments can reference frontmatter fields. The filename value must match the frontmatter value:
filename: directory: pattern: "{order}.{date}-{slug}"
If the directory is 03.2026-06-15-rust-conf but the page’s date frontmatter is 2026-07-01, a mismatch warning is produced.
Multi-Level Directory Patterns
Use / in a directory pattern to validate multiple directory levels:
filename: directory: pattern: "{year}/{month}/{slug}"
This validates the 3 innermost directory levels above the page file.
Inheritance
Filename patterns are inherited from parent models. A child model can override the parent’s patterns:
# models/content.yaml filename: directory: pattern: "{order}.{slug}" # models/event.yaml extends: content filename: directory: pattern: "{order}.{date}-{slug}"
If a child defines only a directory pattern, the parent’s file pattern (if any) is still inherited, and vice versa.
Validation Mode
Control how strictly model violations are enforced with the content.validation.mode setting:
# config.yaml content: validation: mode: warn # "warn" (default), "strict", or "off"
| Mode | Behavior |
|---|---|
warn | Log validation issues but serve pages normally. This is the default. |
strict | Pages with model errors return HTTP 500. Builds halt on errors. |
off | Skip model validation entirely. Useful during migration periods. |
In strict mode, accent build will stop and report errors if any page fails validation. The dev server will return a 500 error page for invalid pages, making issues immediately visible during development.
Individual rules can override the global mode with severity: warning to produce warnings even in strict mode.
accent content update-frontmatter follows the same mode. It checks the page against its model as the edit leaves it and reports the findings the edit introduces in the result’s model_findings; in strict mode it refuses a write that introduces a model error. An edit that would leave frontmatter that does not parse is refused in every mode.
CLI Tooling
Inspecting Models
Use accent model to inspect loaded models without reading YAML files directly.
List all models:
$ accent model list Models loaded: blog (A blog article with author, date, and tags) event (A calendar event with date, location, and capacity) 2 model(s) loaded
Show a model’s full schema:
$ accent model show event Model: Event Description: A calendar event with date, location, and capacity Fields: title string required Title start_date date required Start Date end_date date required End Date location string required Location capacity integer optional Maximum Capacity (default: 0) category enum [...] required Event Category Rules: end_after_start End date must be on or after start date (error) Filename: directory: {order}.{date:start_date}-{slug} file: event.md
Both subcommands support --json for programmatic output and --config to specify a config file.
Show a model as a JSON Schema:
$ accent model show event --schema
Two fields of the result, and the model-level keys (the real output lists every field the model declares, core fields included):
{ "$id": "accent:model/event", "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "A calendar event with date, location, and capacity", "properties": { "capacity": { "default": 0, "minimum": 0.0, "title": "Maximum Capacity", "type": "integer", "x-accent-empty": "skips-type-check", "x-accent-widget": "integer" }, "start_date": { "format": "accent-date", "title": "Start Date", "type": "string", "x-accent-empty": "skips-type-check", "x-accent-widget": "date" } }, "required": ["title", "start_date", "end_date", "location", "category"], "title": "Event", "type": "object" }
The real output also carries x-accent-source, and an x-accent-order that
lists all 30 fields in the order they are declared: the 23 core fields every
model inherits first, then the seven fields event adds. Its title
override keeps core’s place.
This is the same model in the form a tool, an editor, or an agent can read. It is JSON Schema 2020-12, so anything that speaks JSON Schema can use it directly to check a draft before you save it.
Date, URL, and email fields use Accent’s own formats (accent-date,
url-or-path, accent-email) rather than the standard ones, so a value
that reaches those checks is judged the way accent validate judges it –
every date spelling the loader parses, a site-relative path as well as an
absolute URL.
Reading the x-accent-* keys
They carry what JSON Schema cannot say. On a field: x-accent-widget is the
kind of value it holds, x-accent-kind marks a page reference or a media
path, x-accent-temporal marks a date that must fall in the future or the
past, x-accent-editable: false marks a key Accent owns rather than you,
and
x-accent-empty marks a field whose checks Accent skips when you leave it
empty. A list’s items and a map’s additionalProperties carry
x-accent-widget too, because a list of string and a list of text
otherwise read the same. On the model: x-accent-source says which tier
defined it, x-accent-strict mirrors strict_fields, and
x-accent-requires-review mirrors the review workflow.
x-accent-order lists the fields in the order the model declares them, on
the model and on every object field that declares fields. properties is
a JSON object, and the
printed schema sorts its keys, so read the order from x-accent-order when
it matters – the editor lays out its form from it.
Where Accent and an outside validator disagree
Hand the printed schema to any tool that speaks JSON Schema and it will check most of your frontmatter correctly. Three rules are Accent’s own, though, and they run in both directions.
| Rule | An outside validator says | Accent says |
|---|---|---|
required | the key is present | present and non-empty and not filled in for you – a title taken from your first heading does not satisfy title: { required: true } |
An empty value ("", null, [], {}) in a typed field | it fails the type or format check | the check is skipped, because an empty value says nothing about its type. accent new scaffolds "" into every text field, so this is the common case, and x-accent-empty marks it |
A whole number written 3.0 on an integer field | it passes – JSON Schema’s integer is about a number’s value | it fails: YAML resolved 3.0 as a float, and that is what Accent reads |
So Accent is stricter on required and on the shape of a number, and more
forgiving on an empty value. accent validate is always the authority.
One thing the two agree on, and which surprises people: capacity: "20" is
not a number. YAML says a quoted value is a string, so it fails an
integer field on both sides. Write capacity: 20.
minLength and maxLength need no such note: both count Unicode code
points, so the schema and accent validate agree.
Scaffolding Content
Use accent new to create content pages from a model:
# Create an event with field overrides accent new event "Rust Conf 2026" \ --path content/events/ \ --field start_date=2026-06-15 \ --field end_date=2026-06-17 \ --field location="Portland, OR" \ --field category=conference # Created: content/events/01.2026-06-15-rust-conf-2026/event.md
The generated file includes all model fields with defaults:
--- title: Rust Conf 2026 template: event start_date: 2026-06-15 end_date: 2026-06-17 location: Portland, OR capacity: 0 category: conference speakers: [] price: 0.0 status: published tags: [] --- # Rust Conf 2026
Key behaviors:
- Directory and file names follow the model’s
filenamepattern when defined {order}auto-increments from existing siblings in the target directory{date}and{slug}are derived from field values and the title- Without a filename pattern, defaults to
{slug}/{model}.md --dry-runpreviews the output without creating files--no-defaultsskips optional fields without defaults- Refuses to overwrite existing files
Validate with Model Filters
Filter validation output to model-specific issues:
# Show only model validation issues accent validate --models # Validate only pages matching a specific model accent validate --model=event
Collection Model Filter
Filter content collections by model type in frontmatter:
# Only include pages matching the "event" model content: items: "@self.children" model: event order: by: custom.start_date dir: asc
Query by Model
Filter and display model information in query results:
# Filter pages by model accent query list --model=event # Include model name in output accent query list --show-model
Plugin-Provided Models
Plugins can ship document model definitions alongside their WASM modules and templates. This allows plugins to enforce content contracts without requiring users to manually create matching model files.
Static Model Files
Plugins can include a models/ directory in their distribution:
plugins/
ecommerce/
plugin.toml
plugin.wasm
models/
product.yaml
cart.yaml
templates/
product.html.jinja
Model YAML files in the plugin’s models/ directory are loaded automatically during startup.
Dynamic Model Registration
Plugins can also register models programmatically via the register_models WASM hook. This enables config-driven schemas (e.g., available currencies, custom fields based on plugin settings).
Enable the hook in plugin.toml:
[hooks]
on_register_models = true
The plugin’s register_models function receives the plugin configuration as JSON and returns a JSON array of model definitions.
Override Priority
Models are loaded in priority order (highest priority wins when names conflict):
- Plugin models (highest) – from
plugins/<name>/models/andregister_modelshook - Project models – from
models/directory - Theme models (lowest) – from
themes/<name>/models/
When a conflict occurs, the higher-priority source wins completely (no field-level merging across sources).
Model Source Tracking
The accent model list command shows where each model originates:
$ accent model list Models loaded: product [plugin:ecommerce] (E-commerce product) blog [project] (Blog post) default [theme:default] (Default page)
Error Handling
Invalid plugin models produce startup warnings but do not prevent the server from starting. This ensures a broken plugin model does not take down the entire site.
Configuration
The models directory defaults to ./models relative to your project root. Override it in config.yaml:
content: models_directory: ./schemas validation: mode: warn