Meridian handbook
For agents

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. check prints them (_shared/base.html:3:1: warning: ...) and then a summary such as 0 errors, 0 warnings; --strict does not fail on them and --json leaves 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 as templates/_shared/page/entry.html:3:5: ... with the source line. It is one problem.
  • Problems in endpoints.yaml are 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 --json the 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

MessageCodeMeaning and fix
environment variable DATABASE_URL is not setMRD-1005The 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 setMRD-1005engine.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 charactersMRD-1006The value you exported is too short. Use 32 characters or more
<path> is a secret and must be an environment interpolation, not a literalMRD-1004Write ${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-1006A 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 YAMLMRD-1002A 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 idMRD-1006An element added by hand. Run engine-dmo check --fix; never type an id
id fld_... is written on two elementsMRD-1006A copied block kept its id. Delete the copy’s id: line and run check --fix
"id" is an engine-managed field and cannot be declaredMRD-1006A 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 underscoresMRD-1006Use tour_operator
type "walk" has an index route and no walk/index.html on site main, no template found for type "walk" on site mainMRD-1006engine-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 priorityMRD-1006Two 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 hasMRD-1006Write the template the endpoint names (Enquiry form)
endpoint "contact" mails enquiry-receipt, which is not under templates/emails/MRD-1006Write templates/_shared/emails/enquiry-receipt.html
home type "page" has no route on site mainMRD-1006The 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.yamlOn a core type, --after can name only a field of your own extends: file. Leave --after out: added fields follow the core ones

Templates

MessageFix
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 functionCheck 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 |jsonPut 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 |urlhref="{{ 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 allowedMove the value to a data-* attribute or a class
an attribute value with a variable must be quotedclass="{{ 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 pageOnly 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 contractMatch the call to the component’s component.yaml
x belongs to the y module, which this bundle does not compileA DMO feature used on engine-cms. Remove it or use engine-dmo

The dev verbs

MessageMeaning and fix
the change would leave the configuration invalid; nothing was writtenThe 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 3The 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 definedThe 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 filedev config set does not take list indexes. Edit the file by hand
error: engine-dmo dev up has not run in this projectFrom dev status. Start dev up, or use --apply on the verbs instead
Exit 2 after a verb with --applyThe 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

SymptomMeaning and fix
schema plan exits 2Changes 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 YETThis engine version cannot apply that change. Undo it in the files to apply the rest
schema apply refuses to leave nobody with the owner roleKeep a grant of owner in config/access/grants.yaml
Applying the schema plan failedMRD-2005. Run schema plan for the step and its reason

Seeding and content

MessageFix
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 tagAdd the tag to config/tags/vocabulary.yaml, by its full dotted key, or name one that is there
tags: invalid: the vocabulary has 1 problemsFrom 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 firstIn 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 optionsThe 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 thereThe 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

SymptomMeaning and fix
Exit 1 and status 404, kind: not_foundNothing 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 errorsA 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 bPass --host with one of the site’s domains, or leave --site out
fragment x did not render on this pageThe fragment name is wrong, or its region sits inside a false condition
A page renders but something is emptyengine-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.500000Pass them through |number before t()
A select prints its raw valueIts 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 errorA 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 emptyExpected 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 0The 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 429No 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 errorsAdd form.problems to the strings, with one and other
A sign-in link answers Open this link in the browser where you requested itA 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:

RuleFix
statusThe page did not answer 200: render it to see why
image-altAn <img> with no alt. Use img()/figure() and give the asset alt text, or alt="" for a decorative use
control-nameA 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-nameA control named only by its placeholder. Add a <label for>
missing-referencefor, aria-labelledby or aria-describedby names an id not on the page
duplicate-idTwo elements share an id. Derive ids from the item (f-{{ fld.name }})
positive-tabindexRemove tabindex values above 0
one-h1A page has exactly one h1
html-lang with page-title on a --pathUsually 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)

MessageCodeFix
The database has migrations the binary has not appliedMRD-2001engine-dmo migrate up
The schema in the database differs from the configurationMRD-2004engine-dmo schema plan, then schema apply
The database is newer than this binaryMRD-2003Use the engine version that migrated it; migrations never go back
The engine cannot reach the databaseMRD-3001Check DATABASE_URL and that PostgreSQL 14 or later answers
The engine cannot listen on its addressMRD-3003Another 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 bootMRD-3004Run 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
  1. Run engine-dmo check and read every line.
  2. Print what the engine sees: describe route <path> --json, describe type <key>, config render.
  3. Search the handbook for a few words of the message: https://docs.cherwell.studio/llms-full.txt.
  4. Make the smallest change that could fix it and run the loop again.
  5. 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.txt writes a support bundle they can send on.