The development loop
The commands for day-to-day work on a Meridian project (dev up, serve --dev, the dev authoring verbs, check, schema plan and apply, migrate, seed, describe, render, template tests, guide and docs serve), what each needs and what it exits with.
Working on a project is a short loop: change a file, let the engine check and apply it, look at the page, repeat. This page lists the commands in that loop, what each needs, and how to read what it prints.
Run the site while you work
engine-dmo dev up dev up runs the server in development mode and watches config/. On every change it:
- writes an id for every element that has none (as
engine-dmo check --fixdoes); - loads and checks the whole project;
- computes the schema plan against the database;
- applies the plan if it is not destructive;
- reloads the engine in the same process.
flowchart TD
save["You save a file in config/"] --> ids["Write missing ids"]
ids --> check{"Project checks?"}
check -- no --> keep["Keep serving the last good configuration<br/>and print the errors"]
check -- yes --> plan{"Schema plan destructive?"}
plan -- yes --> hold["Keep serving; print the plan<br/>for you to approve and apply"]
plan -- no --> apply["Apply the plan"]
apply --> reload["Reload the engine"]A failed check or a destructive plan leaves the engine running on the last good configuration and prints why. A destructive change (removing a field that holds values, for example) is yours to apply by hand with engine-dmo schema apply; see Destructive changes.
engine-dmo dev status prints what dev up last found: the check, the plan, live field aliases, the address it serves on, and images the sizing check found oversized. --json gives the same as JSON, which is how an agent reads it. It reads .meridian/dev-status.json, and says so when dev up has not run in the project.
| Flag | Effect |
|---|---|
--listen <addr> | Serve on this address instead of server.listen |
Development mode, in both:
- reparses a template on every request, so a template change shows on reload;
- shows the development error page, with the template, line and context of a failure;
- starts each extension’s development server as a child process (the
viteextension runs Vite); - watches the configuration;
- serves Go’s profiler at
/_debug/pprof/.
The admin is at /studio, the public sites at their configured domains (localhost answers for the default site).
Change configuration with the dev verbs
You can edit the YAML by hand, but the engine-dmo dev verbs are safer: each edits the files keeping their comments, writes the new element’s id, and checks the configuration it changed. A change that would make the configuration invalid (an unknown field type, an unknown option) is refused with nothing was written and exit 3. With dev up running, the change is applied as soon as the verb writes it.
A verb does not count templates, so it can exit 0 and leave check failing:
dev route addwrites the route even though the type has noentry.html(or noindex.htmlfor--index) yet.dev endpoint addwrites the endpoint; the recipe’s partials and mail templates are yours to write, and it says so.
Run engine-dmo check --no-db after every verb.
engine-dmo dev type add walk --label Walk --plural Walks
engine-dmo dev field add walk distance --type number --label Distance
engine-dmo dev route add walk "/walks/{slug}" --index /walks --scaffold
engine-dmo new template walk/index.html
engine-dmo dev field rename walk distance lengthKm --scaffold writes the type’s entry.html from its fields. It does not write index.html: a route with an index also needs <type>/index.html, which engine-dmo new template walk/index.html starts for you, and check refuses the route until it exists. The scaffolded index is an empty body block with no heading, so fill it in before you check a page’s accessibility.
--scaffold needs the configuration to load
--scaffold and new template load the whole configuration, so every ${VAR} without a default must be set (a placeholder DATABASE_URL is enough). When one is not, dev route add --scaffold has already written routes.yaml before it fails with exit 3, and running it again prints nothing to change without writing the template. Set the variable and run engine-dmo new template <type>/entry.html to finish.
Every verb takes --dry-run, which prints the diff of every file it would change and writes nothing, and --apply, which applies the schema plan afterwards when it is not destructive (use it when dev up is not running).
| Verb | Does |
|---|---|
dev type add <key> | Add a custom type with a title, a slug and a summary (--label, --plural, --site for one site’s own type, --sites to limit a shared type) |
dev field add <type> <name> | Add a field (--type, --label, --required, --index, --translate, --options, --param key=value, --group, --after, --site) |
dev field set <type> <name> | Change a field’s label, help text or a parameter |
dev field rename <type> <old> <new> | Rename a field: the id and every stored value stay, and the old handle keeps answering as an alias (--no-alias to drop it) |
dev field remove <type> <name> | Remove a field; its values stay stored under its id |
dev group add <type> <key> | Add a group to a type’s editor layout |
dev option add <type> <field> <value> | Add an option to a select field, or a style to a link field |
dev route add <type> <uri> | Route a type on a site (--index, --site, --scaffold to write its template) |
dev site add <key> | Add a site with its site.yaml and routes.yaml (--name, --domain, --locales, --network) |
dev site rename <old> <new> | Rename a site: its directory moves, its id and data stay |
dev locale add <code> | Declare a locale and enable it on a site |
dev config set <file> <path> <value> | Set one value in a configuration file, such as dev config set engine.yaml search.driver meilisearch |
dev config unset <file> <path> | Remove one key |
dev endpoint add <key> | Add a declared endpoint from a recipe (recipe:enquiry, recipe:newsletter) |
dev network add <key> | Declare a network of sites |
dev source rename, dev importer rename, dev webhook rename | Rename an outbound declaration, keeping what is stored under its id |
engine-dmo adds dev tier, dev role, dev portal and dev slot for the DMO modules. engine-dmo help --json lists every verb with its arguments and flags. When a project has AGENTS.md or the agent skills, the verbs regenerate them after each change.
Check the project
engine-dmo check check is the gate to run before every commit and in CI. It validates every configuration file, the schema, every template and the deployment settings, and prints every problem at once (seed verify checks the seed):
config/sites/main/routes.yaml:2:11: route "page" has no id
2 | - type: page
| ^^^^
= run engine-dmo check --fix to write one; ids are written by the tooling, never by hand
= error code MRD-1006
1 error, 0 warnings. Configuration is not valid. check prints template findings first, one line each (_shared/base.html:3:1: warning: ...), then every diagnostic in full with its source line. An error can therefore appear twice, once in each form.
Read every line, not the summary
The summary counts configuration errors and warnings only. Template warnings, such as a layout with no head() or a t() key missing from the string catalogue, are printed above it and not counted: a project with template warnings still ends 0 errors, 0 warnings and exits 0, even with --strict.
| Flag | Effect |
|---|---|
--no-db | Skip the checks that need a database. Secrets need not be set, so a CI job or an image build can run it; a secret that is set is still checked (a SIGNING_KEY shorter than 32 characters fails) |
--fix | Write an id for every element without one, keeping comments, then check |
--strict | Treat the counted warnings as errors. Template warnings are not counted and do not fail it |
--contract-report | List template usage the next template contract version removes, with the replacement for each |
| Exit code | Meaning |
|---|---|
0 | Valid; warnings may have been printed |
3 | The configuration is not valid |
1 | The check could not run (for example, the database could not be reached), or the command line was wrong: an unknown command or flag exits 1, although help --json lists exit 3 for a bad invocation |
Every error carries an error code (MRD-1006 above); Error codes explains each.
engine-dmo config validate checks only the configuration files, and engine-dmo config render prints the configuration as the engine resolved it, every default filled in and secrets redacted.
Plan and apply schema changes
The database holds a copy of your schema: types, fields, locales, sites, routes, roles and grants, each by its id. schema plan compares the files with it; schema apply makes the database match.
engine-dmo schema plan
engine-dmo schema apply schema plan is read-only and exits 2 when changes are pending, 0 when there are none. Each line is one change:
| Mark | Change |
|---|---|
+ | Add |
~ | Change |
- | Remove |
> | Rename: same id, new handle. Every stored value is kept |
= | Bind: a stored element made before ids takes the id its file gives it |
! | Replace: the file gives an existing handle a new id, usually a copied block |
A change marked [DESTRUCTIVE: ...] loses data or access: a removed field that documents hold values in, or a grant whose removal takes a permission from active users. schema apply runs the whole plan in one transaction and refuses a destructive plan unless it is approved.
Destructive changes
Approve a destructive plan in one of two ways:
- An approval file. The plan prints its digest; write it to
config/schema/.destructive-approved(or the pathschema.destructive_approvalnames) and runengine-dmo schema apply. A different plan has a different digest, so the approval covers exactly the plan you reviewed. - Interactively.
engine-dmo schema apply --allow-destructiveshows the plan and asks you to confirm; add--yeswhere no terminal can ask.
A removed field’s values are not deleted: they stay in each document’s revisions, kept under _orphaned with the field’s id, and nothing is dropped silently.
Replacements
A ! line means an element in the files has a new id but an old handle. That is almost always a copied block whose id came with it. Put the stored id back in the file to keep the stored element, or, when you really mean a new element, apply with --replace <type>.<field> for each one named.
Refusals
schema apply refuses a plan that would leave nobody able to manage users or holding the owner role, even with --allow-destructive. A plan line under NOT YET is a change this engine version cannot apply; undo it in the files to apply the rest.
Migrations
engine-dmo migrate status # exits 2 if any are pending
engine-dmo migrate up Migrations create and change the engine’s own tables. They only move forward, and they are separate from your schema: a new engine version brings migrations, your configuration brings schema changes. The server refuses to start while a migration is pending. See Upgrading the engine.
Seed and reset content
| Command | Does | Needs |
|---|---|---|
seed verify | Check the seed loads and matches the schema: known types, fields and locales | Configuration |
seed list | List the seed sets and what each contains | Nothing |
seed apply | Load the seed through the same validation and history as an editor | An applied schema and an empty database |
seed reset --confirm | Remove all content, media and analytics; keep users and configuration. Never runs in production | A database |
seed generate --type <key> --count 20 | Create documents whose values pass every field’s validation, from word lists and placeholder images (--publish, --locale, --site, --near lat,lng, --seed) | A database |
seed write | Write the seed’s generated records from seed/generate.yaml; --check reports how the files differ | Configuration |
seed verify does not validate values: a select value that is not an option, or a number below its min, passes it and fails only in seed apply. Its errors name the file but not the line, and a problem in a field is reported once per locale.
seed apply refuses a database that already holds documents or media, since seeding would duplicate them: run seed reset --confirm first. Its flags are --set <dir> (another seed set under seed/), --profile <name> (a profile from seed.yaml; the help text’s mention of make check refers to the engine’s own test suite, not your project), --scale small|medium|large (the volume of generated analytics) and --timeout. In production it refuses unless the database is empty and you pass --i-know-what-i-am-doing, and it never creates seed users there.
You can also write content from the command line: engine-dmo content create, content update, content publish and content get take and print JSON. The CLI reference lists them.
Look at what the engine sees
| Command | Shows | Needs |
|---|---|---|
describe project | Networks, sites, locales, types, routes, endpoints, reports, tiers and roles, with ids | The project files |
describe type <key> | A type’s fields at every depth, with ids, types, parameters and aliases | The project files |
describe fieldtypes | Every field type, what it stores and its parameters | Nothing |
describe template <name> | Which file a template resolves to on a site’s chain, what it extends and includes, and the contract it is checked against | Configuration |
describe route <path> | Which route answers a path, the template it resolved, and the whole template context with real values | A database |
template api | Every global, function, filter and type a site’s templates can use (--site, --json) | The project files |
render <path> | Render a public page through the real pipeline, in process. --json adds errors, queries, assets, accessibility findings and the image report (--site, --locale, --host, --fragment) | A database |
template test | Render every page tests/templates/*.test.yaml names and check each (--filter, --snapshots, --update) | A seeded database |
Every command takes --json. “The project files” means the files are read and no variable need be set; “Configuration” means the whole configuration must load, which needs every ${VAR} without a default to be set (a placeholder value is enough); “a database” means one that is migrated and has the schema applied. describe project and describe type show the core fields of a core type (page has only title and slug) and the fields you added.
render and describe route write the engine’s start-up log lines to standard error before their result, and before failing when the database cannot be reached; with --json, standard output holds only the JSON. A path the site does not answer renders with status 404: the home page answers /, and its slug path (/home) is not found.
The usual way to write a template: describe route <path> --json for the context it receives, write it, then render <path> --json to see the result and any errors.
Scaffold files
engine-dmo new writes starting files you then edit:
| Command | Writes |
|---|---|
new type <key> | A schema file for a custom type, and its template |
new template <name> | A starter template, such as walk/entry.html or walk/index.html; --from-shared copies the shared template into a site’s own root and records where it came from, so check reports when the original changes |
new component | A component: its contract, markup and co-located CSS |
new block | A block template and the schema stub that declares the block |
--site <key> writes into that site’s template root; --force overwrites.
Editor support
- Configuration:
initmaps each configuration file to its JSON Schema for VS Code and Zed.engine-dmo config schema --writerewrites them after an engine upgrade, andengine-dmo config schema --file siteprints one kind of file’s schema.--filetakes a kind (engine,locales,site,routes,schema,endpointsand the rest) or a real path such asconfig/sites/main/site.yaml; a bare file name such assite.yamlis refused with the list of kinds. - Templates:
engine-dmo lspis a language server on standard input and output. It givescheck’s diagnostics as you type, completion, hover and go to definition. Configure your editor to start it in the project directory. It refuses a production project.
Read the guide and the handbook
engine-dmo guide # this project: rules, sites, types, fields, template roots and filters
engine-dmo guide --out AGENTS.md # the same, to a file
engine-dmo docs serve # this project's template API at http://127.0.0.1:6061 guide is generated from the project’s schema, routes and the engine’s template contract, so it is always current; regenerate it after any change rather than editing it. --skill writes it with skill front matter. docs serve takes --listen and --site (one site’s template API only).
Build the frontend
engine-dmo build Runs the project’s package.json build script (or --command), then engine-dmo check (--no-check to skip it, --no-db and --strict passed to check).
A typical session
Start engine-dmo dev up in one terminal.
Change the model with the dev verbs, or edit YAML and save. Watch dev up’s output, or run engine-dmo dev status --json.
Generate content to work against: engine-dmo seed generate --type walk --count 20 --publish.
Write the template against engine-dmo describe route /walks/some-walk --json, and check it with engine-dmo render /walks/some-walk --json.
Finish with engine-dmo check, then commit.
Project layout
Every directory and file a Meridian project can hold, what each is for, which bundle reads it, and which files you write and which the tooling writes.
Upgrading the engine
How to move a project to a new engine version, covering engine.version, the template contract and check --contract-report, migrations as an explicit deploy step, and the order to run things in.