Troubleshooting
The errors a coding agent meets in a Meridian project (from check, the dev verbs, schema plan and apply, seed, render, the accessibility audit and server start), what each message means and the exact fix, with the MRD error codes.
Read the whole output before fixing anything. check reports every problem in one run, each at file:line:col with the source line, a caret, one or more = hints and an MRD-nnnn code. Fix the first error in each file first: a file that fails to load can cause later errors that are only consequences (an unknown key in routes.yaml also produces “home type page has no route”). Then run the command again. Error codes has every code’s cause and fix.
How to read a diagnostic
config/sites/main/routes.yaml:5:76: unknown field "pagniate"
5 | - { type: walk, id: rte_34agwcmxkh, uri: "/walks/{slug}", index: /walks, pagniate: 12 }
| ^
= did you mean "paginate"?
= error code MRD-1006 The path, line and column; the source line; the caret under the problem; hints (=), the first usually the fix; the code. A line with warning: does not fail check (except under --strict), but most warnings are real defects on a public site: fix them.
Quirks of the output to expect:
- Template warnings are not counted.
checkprints them (_shared/base.html:3:1: warning: ...) and then a summary such as0 errors, 0 warnings;--strictdoes not fail on them and--jsonleaves them out. Read every line, not the summary. - A template error is printed twice, once as
_shared/page/entry.html:3:5: error: ...and once astemplates/_shared/page/entry.html:3:5: ...with the source line. It is one problem. - Problems in
endpoints.yamlare reported at the endpoint’s line with column 0 (endpoints.yaml:2:0), not at the key that is wrong. Read the message for the key. - Commands that start the engine (
render,describe route,seed apply,mail) print log lines (level=INFO ... database connected) on standard error before their result. With--jsonthe result is on standard output alone; a command that fails prints its error after the log lines. - Hints sometimes cite design documents by file name (such as
27-frontend-toolkit.md). They are not in the handbook: search https://docs.cherwell.studio/llms-full.txt for the topic.
Configuration
| Message | Code | Meaning and fix |
|---|---|---|
environment variable DATABASE_URL is not set | MRD-1005 | The configuration names ${DATABASE_URL} and it is unset. Export it; commands that need no database (seed verify, new, config render) accept a placeholder: DATABASE_URL=postgres://placeholder@localhost/placeholder. check --no-db, describe and the dev verbs run with it unset. A following database.url is required is a consequence. The engine never reads .env: export the variable in the shell |
environment variable SIGNING_KEY is not set | MRD-1005 | engine.yaml names ${SIGNING_KEY} (added for production), and every command that loads the configuration now needs it, in development too. export SIGNING_KEY="$(openssl rand -hex 32)". The same goes for every other secret, such as ${SMTP_PASSWORD}: export a placeholder |
server.security.signing_key must be at least 32 characters | MRD-1006 | The value you exported is too short. Use 32 characters or more |
<path> is a secret and must be an environment interpolation, not a literal | MRD-1004 | Write ${VAR} and set the variable. If the literal was a real secret, tell the person to rotate it. A default (${SIGNING_KEY:-}) counts as a literal: a secret takes a bare ${VAR} |
unknown field "x" with did you mean "y"? | MRD-1006 | A misspelt or invented key. Use the suggestion, or look the file up with engine-dmo config schema --file <kind> |
A configuration file is not valid YAML | MRD-1002 | A tab used for indentation, an unclosed quote or bracket, or a value starting with @, % or a backtick. Quote the value |
route "page" has no id, nav slot "main" has no id, field "x" has no id | MRD-1006 | An element added by hand. Run engine-dmo check --fix; never type an id |
id fld_... is written on two elements | MRD-1006 | A copied block kept its id. Delete the copy’s id: line and run check --fix |
"id" is an engine-managed field and cannot be declared | MRD-1006 | A reserved field name (id, type, uri, url, status, locale, tags, seo, noindex, author, organisation, createdAt, updatedAt, publishedAt, redirectFrom, translations). Choose another |
type key "tourOperator" must be lowercase letters, digits and underscores | MRD-1006 | Use tour_operator |
type "walk" has an index route and no walk/index.html on site main, no template found for type "walk" on site main | MRD-1006 | engine-dmo new template walk/index.html and engine-dmo new template walk/entry.html. schema plan and schema apply work until you have written them |
routes "/{slug}" (page@en) and "/{slug}" (area@en) are ambiguous: same shape and priority; give one a priority | MRD-1006 | Two types at one address shape. Keep both and add priority: 10 to the page route, or route the other under its own word (/towns/{slug}). Lookups go by each document’s stored address, so both keep answering, whichever has the higher number (DMO directory) |
endpoint "contact" responds with partials/enquiry-sent.html, which no template root has | MRD-1006 | Write the template the endpoint names (Enquiry form) |
endpoint "contact" mails enquiry-receipt, which is not under templates/emails/ | MRD-1006 | Write templates/_shared/emails/enquiry-receipt.html |
home type "page" has no route on site main | MRD-1006 | The home page’s type is not routed, or routes.yaml failed to load: fix the error above it first |
the label of field X has no translation for cy (warning) | Add the locale to the label map: label: { en: Photo, cy: Llun }. Saying for en on a one-locale site means the labels were pasted under another code (en-GB): use your source locale’s code. --strict fails on it | |
slug for locale en-GB, which site main does not enable, landing by-place has no slug for stay in en, ...; the value itself is used (warning) | The landing’s maps name the wrong locale, so the page is served at the raw value (/monmouth/stay). Use your source locale’s code. --strict fails on it | |
the structured_data mapping of listing has no priceRange, which the local business panel recommends (warning) | The type is a LocalBusiness, and search engines’ guidance recommends a price range. check --strict fails on it. If the businesses charge, add a text field and map it: priceRange: priceRange under structured_data.map, with a value such as ££ on each document. If a whole type has no price, such as a directory of walks or viewpoints, make it type: TouristAttraction, which has no such recommendation. Do not map one fixed { value: "££" }: it silences the warning with a claim that is false for most businesses | |
type listing maps structured data, and its page on site main calls no jsonld(), so the mapping is never emitted (warning) | Print {{ jsonld() }} in base.html after {{ head() }}. --strict does not fail on it | |
cy is missing 14 of 18 strings: ... (warning) | Add the listed keys to config/strings/cy.yaml | |
site main routes search and no backend is configured (warning) | The search page runs in browse mode. Expected in development; tell the person | |
the image transforms name avif and webp, which this build cannot encode (note) | Harmless. A transform with no formats: asks for AVIF and WebP; this binary serves JPEG and PNG, and the -vips image serves all of them. formats: [jpeg] on every transform silences the note, at the cost of the modern formats on the -vips image too, so leave it unless the person asks | |
--after names "slug", which is not a field in schema/page.yaml | On a core type, --after can name only a field of your own extends: file. Leave --after out: added fields follow the core ones |
Templates
| Message | Fix |
|---|---|
entry has no field "subtitle" in this project's schema with did you mean entry.title? | Use a field the type has (engine-dmo describe type <key>), or add the field |
"x" is not in the template context (contract 1) | A variable that does not exist here. Print the real context with engine-dmo describe route <path> --json |
x() is not a template function | Check the name in engine-dmo template api |
|safe is not allowed in this site's templates and Usage of filter 'safe' is not allowed (sandbox restriction active) | Print engine markup as it is ({{ entry.body|ptext }}, {{ head() }}); never mark your own strings safe |
a variable inside <script> must go through |json | Put the value in a data-* attribute that a script file under public/ reads. Inside a <script type="application/ld+json"> block, print it as {{ value|json }} |
a variable inside href must go through |url | href="{{ walk.uri|url }}". The same for src, action and other URL attributes, and for helpers: href="{{ track(entry, 'website', entry.website.url)|url }}" |
a link to listing.website, a member's own destination, does not go through track() (warning), with the hint href="{{ track(listing, 'website', listing.website.url) }}" | The hint leaves out |url, and following it as written fails with the error above. Write {{ track(entry, 'website', entry.website.url)|url }} |
a variable inside an inline event handler is not allowed, a variable inside a style attribute is not allowed | Move the value to a data-* attribute or a class |
an attribute value with a variable must be quoted | class="{{ x }}", never class={{ x }} |
x.y is the field's old handle; it is now x.z (warning) | A field was renamed. Use the new handle, then engine-dmo schema alias retire x.y |
t("x") is not in the string catalogue and renders as its id (warning) | Add x to config/strings/<locale>.yaml |
this head has no head() and its own tags miss a meta description (warning) | Print {{ head() }} in <head> |
x is rendered with picture() and its credit is printed nowhere in this template (warning) | Use figure(), or print {{ x.credit }} beside the image |
this template writes more than one h1 (warning) | Make later headings h2 and below |
this control is named only by its placeholder (warning) | Add <label for="..."> matching the control’s id |
x.y(...): the site has no transform "z" | Declare the transform in config/images.yaml |
a second priority=true on the page | Only the page’s main image takes priority=true |
required prop x is missing from the call to y, slot "x" is not declared in this component's contract | Match the call to the component’s component.yaml |
x belongs to the y module, which this bundle does not compile | A DMO feature used on engine-cms. Remove it or use engine-dmo |
The dev verbs
| Message | Meaning and fix |
|---|---|
the change would leave the configuration invalid; nothing was written | The diagnostics printed above it say why. Common cause: a select with no --options |
dev route add --scaffold printed wrote config/sites/main/routes.yaml, then environment variable ... is not set, exit 3 | The route is written; the template is not. Export the variable, then engine-dmo new template <type>/entry.html. Running the verb again only says nothing to change |
there is no type "walk"; did you mean "page"? | Add the type first (dev type add) |
the edit would not parse: mapping key "id" already defined | The verb cannot edit a field written in block style (one key per line), as dev field remove does on such files. Make the change by hand in the YAML, then engine-dmo check |
0.timezone does not exist under locales.available; set it from the top or edit the file | dev config set does not take list indexes. Edit the file by hand |
error: engine-dmo dev up has not run in this project | From dev status. Start dev up, or use --apply on the verbs instead |
Exit 2 after a verb with --apply | The plan is destructive and was not applied. The file is written. Ask the person to review engine-dmo schema plan and run engine-dmo schema apply --allow-destructive |
Some verbs write a valid change that leaves follow-up work: dev endpoint add writes an endpoint whose templates you must then write, dev route add without --scaffold a route whose templates are missing, and dev route add --index with --scaffold still leaves index.html to engine-dmo new template <type>/index.html. Run engine-dmo check after every verb.
Schema plan and apply
| Symptom | Meaning and fix |
|---|---|
schema plan exits 2 | Changes are pending. Normal: read them, then schema apply |
A line marked ! | An element’s handle has a new id: a copied or hand-edited id. Put the stored id back (engine-dmo describe type on the old configuration, or git diff). Never --replace unless the person asked for a new element |
[DESTRUCTIVE: ...] | Data or access would be lost. Stop and ask the person |
A line under NOT YET | This engine version cannot apply that change. Undo it in the files to apply the rest |
schema apply refuses to leave nobody with the owner role | Keep a grant of owner in config/access/grants.yaml |
Applying the schema plan failed | MRD-2005. Run schema plan for the step and its reason |
Seeding and content
| Message | Fix |
|---|---|
duplicate key value violates unique constraint "document_site_uri_path_idx" | Two documents want one address, usually a page and an area at /{slug} with the same slug. Change one slug, or route one type under its own word, then seed reset --confirm and seed apply |
publish en refused: phone: Enter the number with its country code, starting with + | A bare national number with no region. Write { raw: "01600 712345", region: GB }, or region: GB on the field. A seed does not read site.country |
primaryTag: nope is not a vocabulary tag | Add the tag to config/tags/vocabulary.yaml, by its full dotted key, or name one that is there |
tags: invalid: the vocabulary has 1 problems | From seed apply, which does not say which. Usually a child key not under its parent (hotel instead of stay.hotel) or a tag with no label. engine-dmo tags import lists them |
organisation "x" is not in organisations/ | From seed verify. The value is the organisation’s file name with no extension, under seed/organisations/. List the folder to see the keys |
the database already holds N documents and M assets; run engine-dmo seed reset first | In development: engine-dmo seed reset --confirm, then seed apply. Never on a database with real content without asking |
no en content (the source locale) | Every seed document needs the source locale’s values |
publish en refused: distanceKm: Must be 0 or more; difficulty: "trivial" is not one of the options | The value breaks the field’s rules. seed verify does not check values, so this shows only at seed apply. The documents before this one were written: run engine-dmo seed reset --confirm, fix the value, and seed apply again |
init says the seed imports photographs from a media library that is not there | The copied seed’s library.dir points outside the project. Fetch the library as init says, or replace those photographs with files under seed/assets/ |
nonsense: type page has no field "nonsense" ... (unknown_field) | From content create or update: name fields by handle (engine-dmo describe type page) |
Render
| Symptom | Meaning and fix |
|---|---|
Exit 1 and status 404, kind: not_found | Nothing published answers that path in that locale: a wrong slug, an unpublished document, a locale prefix missing (/cy/...), or a landing page not yet counted. engine-dmo content list --type <key> shows what is published |
Exit 1 and status 500 with errors | A template failed. Each error has file, line, column and message. Common: query().where("summary"): summary is not declared index: true, so it cannot be queried: add --index to the field (dev field set <type> <field> --param index=true) or filter on another field |
site main does not enable locale "fr" | Use a locale the site enables, or add it (Multilingual site) |
host x resolves to site a, not b | Pass --host with one of the site’s domains, or leave --site out |
fragment x did not render on this page | The fragment name is wrong, or its region sits inside a false condition |
| A page renders but something is empty | engine-dmo describe route <path> --json shows the context: is the value there? Unrelated rows from a database error in a query are logged and return an empty list |
Numbers print as 6.500000 | Pass them through |number before t() |
| A select prints its raw value | Its options have no labels: add label: to each option. check does not warn about an option with no label at all, only about a label missing a locale, so the first option of a select made with --options passes silently |
| A field prints nothing, with no error | A misspelt field on a loop variable or a component prop ({{ w.phto }}), which check does not hold to the schema. Compare with engine-dmo describe type <key> |
A form’s _csrf value is empty | Expected for an anonymous visitor, under render and on a running server, with no cookie set. Only a signed-in member or staff user has a token; an anonymous post is checked by its origin (Enquiry form) |
status: 301, exit 0, and a11y lists a page has exactly one h1, this one has 0 | The address redirects, and its small redirect body was checked as a page. Render the address it redirects to, shown in the html |
| A form post from a script answers 422 with valid input, or 429 | No challenge token, or a token too new (422), or over the endpoint’s rate_limit (429). See Post it from a script |
form.problems prints as its key above a form’s errors | Add form.problems to the strings, with one and other |
A sign-in link answers Open this link in the browser where you requested it | A sign-in link works only with the cookie of the browser that asked. In a script use one cookie jar for both requests (Partner portal) |
Accessibility audit
engine-dmo a11y audit exits 1 on any failure. Each finding names its rule:
| Rule | Fix |
|---|---|
status | The page did not answer 200: render it to see why |
image-alt | An <img> with no alt. Use img()/figure() and give the asset alt text, or alt="" for a decorative use |
control-name | A link or button with no text: an empty link (a search result with no title, an icon link). Add text or aria-label from t() |
placeholder-name | A control named only by its placeholder. Add a <label for> |
missing-reference | for, aria-labelledby or aria-describedby names an id not on the page |
duplicate-id | Two elements share an id. Derive ids from the item (f-{{ fld.name }}) |
positive-tabindex | Remove tabindex values above 0 |
one-h1 | A page has exactly one h1 |
html-lang with page-title on a --path | Usually an address that redirects: its redirect body has neither. render <path> --json shows status: 301. Name the address it redirects to |
Server start (dev up and serve)
| Message | Code | Fix |
|---|---|---|
| The database has migrations the binary has not applied | MRD-2001 | engine-dmo migrate up |
| The schema in the database differs from the configuration | MRD-2004 | engine-dmo schema plan, then schema apply |
| The database is newer than this binary | MRD-2003 | Use the engine version that migrated it; migrations never go back |
| The engine cannot reach the database | MRD-3001 | Check DATABASE_URL and that PostgreSQL 14 or later answers |
| The engine cannot listen on its address | MRD-3003 | Another process holds the port: dev up --listen 127.0.0.1:8081. If the message says metrics listen on 127.0.0.1:9090, it is another Meridian server’s metrics port: set telemetry.metrics.bind in this project’s engine.yaml to another port |
| The templates failed the linter at boot | MRD-3004 | Run engine-dmo check and fix each finding |
Production check
ENVIRONMENT=production engine-dmo check --no-db reports in groups, so fix and run it again until it passes. Typical findings: server.security.signing_key is required in production (add security: { signing_key: ${SIGNING_KEY} } under server:), a real IP header is configured but server.trusted_proxies is empty (list the proxy’s addresses, or real_ip_header: "" with nothing in front), the log driver is development only (configure smtp, postmark, ses or resend with mail.from), and require_mfa: false refused. See Configuration and secrets.
Extensions
check reports each loader problem at its line in engine.yaml. The common ones: this engine is a pure Go build, which cannot load extensions (run a cgo build such as the -vips image), and does not match this engine (...) followed by each difference (Go version, platform, -trimpath, meridian-extend version). The full list is in Extensions.
When you are stuck
flowchart TD
start["Something failed"] --> read["Read every line of<br/>engine-dmo check"]
read --> see["Print what the engine sees:<br/>describe route, describe type,<br/>config render"]
see --> search["Search the handbook for<br/>words from the message"]
search --> fix["Smallest change<br/>that could fix it"]
fix --> loop{"Loop passes?"}
loop -- yes --> done["Carry on"]
loop -- "no, and the engine<br/>cannot do it" --> tell["Tell the person;<br/>doctor --out doctor.txt"]
loop -- "no, another idea" --> fix- Run
engine-dmo checkand read every line. - Print what the engine sees:
describe route <path> --json,describe type <key>,config render. - Search the handbook for a few words of the message: https://docs.cherwell.studio/llms-full.txt.
- Make the smallest change that could fix it and run the loop again.
- If the engine cannot do what is needed, say so to the person with the message and what you tried.
engine-dmo doctor --out doctor.txtwrites a support bundle they can send on.
Extension
When an agent should write a Meridian extension (a Go plugin adding template filters, globals, routes or commands), the minimal filter extension, how it must be built to load, how to name it in engine.yaml and how to verify it.
Checklists
What done means for a coding agent handing a Meridian site back, as checklists with the command that proves each item, covering the project check, every route rendering, accessibility, content, SEO, performance, deploy configuration and secrets.