How Keystone checks your book
Keystone is a compiler for books, and like any compiler it checks your source as it builds. When something is wrong it tells you — on the terminal, naming the offending element or metadata key — instead of silently letting it through.
This page is the map. Hit a message on the terminal? Find its row below and follow it to a page with the exact message and how to fix it.
Two outcomes: warnings and errors
Every Keystone diagnostic is one of two severities.
WARN:— something is off, but the build has a safe fallback. The element is dropped or its bad input ignored, and you still get an artifact — unless you've enabled strict mode, where any warning fails the build.ERROR:— the build cannot proceed correctly, so it stops and writes no completed artifact.
A line behind a │ bar is not Keystone's. It belongs to whatever tool Keystone
is reporting for — Pandoc's math converter, the Lua loader, your operating
system — and is reproduced exactly as that tool wrote it, so any line numbers or
carets it carries still point where they should:
ERROR: 1 equation(s) cannot be converted for epub output
│ Could not convert TeX math \begin{array}[b]{r} a \\ b \end{array}, rendering as TeX:
│ \begin{array}[b]{r} a \\ b \end{array}
│ ^
│ unexpected "["
Some checks are Pandoc's rather than Keystone's — reading your Markdown,
fetching an image, resolving a citation. Their warnings carry the same WARN:
prefix as any other, so there is one shape to recognize. The rows below are
marked (Pandoc) to tell you which tool to consult if you need to dig, and
strict mode fails on them too. When Pandoc stops the build outright it reports
in its own words, and Keystone closes with an ERROR: line naming the pass that
failed — the ✗ line above it names the stage.
Where checks happen
Your book passes through stages, and each can fail in its own way. They run in the order below — the same order and the same names a build reports as it works (see Watching a build run). The first rows fall before any stage runs, where a build has none to name yet, and the last after every stage has run. Find the row whose message matches yours:
| Build stage | What can go wrong | A message you'll see | Severity |
|---|---|---|---|
| Starting the build | No pandoc.yaml to read |
user-metadata.yaml is missing or empty |
ERROR |
| Starting the build | A target name that doesn't match | target metadata file not found: … |
ERROR |
| Starting the build | A format Keystone doesn't write | unsupported format '…' |
ERROR |
| Starting the build | The project name is still the default | the project name is still the default "keystone" |
WARN |
| Reading your Markdown (Pandoc) | Unclosed div | Div at … unclosed … |
WARN |
| Reading your Markdown | A malformed fence or unclosed span | (nothing — silent) | — |
| Rendering hooks | A hook that fails | hook '…' did not answer … · … returned no body |
ERROR |
| Expanding shortcuts | Unrecognized class | unrecognized class '…' |
WARN |
| Expanding shortcuts | Unrecognized attribute | ignores unrecognized attributes |
WARN |
| Expanding shortcuts | Missing required field | shortcut '…' requires … |
ERROR |
| Resolving configuration | Missing or invalid metadata | the target requires metadata this project does not set · draft-scale must be… |
ERROR |
| Resolving configuration | Overriding a reserved key | overrides Keystone-owned metadata |
ERROR |
| Resolving configuration | Unknown page size | papersize '…' is not a page size Keystone names |
ERROR |
| Resolving configuration | Page sized twice | … and geometry both set the page |
ERROR |
| Resolving configuration | Geometry auto combined | geometry 'auto' cannot be combined with other options |
ERROR |
| Resolving configuration | Class option belongs to another key | classoption '…' sets the page size · … sets the base font size · classoption 'landscape' applies only with geometry: auto |
ERROR |
| Resolving configuration | Unsupported font size | fontsize '…' is not a size this set allows · … is not a font size |
WARN |
| Resolving configuration | Unrecognized font family | fontfamily '…' is not a registry key |
WARN |
| Resolving configuration | Missing font license | font '…' declares no license · font '…' names two licenses that cannot be told apart · font '…' has no license file named … · font '…' license … |
WARN |
| Resolving configuration | Duplicate font file | fonts '…' and '…' both declare '…' · font '…' declares '…' twice · … which differ only in case |
ERROR |
| Running handlers | Invalid attribute value | unrecognized size · unknown type · invalid cols |
WARN |
| Running handlers | Undeclared mark | mark '…' is not declared |
ERROR |
| Composing headers & footers | Unknown placeholder | unrecognized placeholder '{…}' |
WARN |
| Composing headers & footers | Header/footer text suppressed | …-text suppressed |
WARN |
| Composing headers & footers | An invalid or colliding mark name | marks: '…' is not a valid mark name · … collides with the built-in {…} placeholder |
ERROR |
| Converting math | Math that can't convert | equation(s) cannot be converted |
ERROR |
| Embedding PDF metadata | An unaccepted xmp-metadata value |
xmp-metadata must be 'auto' or 'disabled' |
ERROR |
| Resolving citations (Pandoc) | Undefined citation | Citeproc: citation … not found |
WARN |
| Fetching images (Pandoc) | Image not found | Could not fetch resource … |
WARN |
| Promoting the artifact | An artifact that can't be written | publishing failed while promoting the artifact to … |
ERROR |
The message column is a representative fragment, not the exact text. Handler
warnings in particular share the shape <handler>: <problem> '<value>' — so
font: unknown family 'garmond' is the Running handlers row even though its
words aren't listed. Match on the stage and the shape, not a literal string.
Finding the offender
A compiler usually points at a file and line number. Keystone can't: it builds on Pandoc, whose Markdown reader doesn't record where each element came from in your source, so there is no line for Keystone to report.
Instead, most messages name the offending element with a compact selector you can search for — its classes, its identifier, and a snippet of its leading text:
WARN: aside: unknown type 'todo'
(in .aside "A callout whose type is not one of the d…")
See https://keystone.knight-owl.dev/errors/invalid-value/
That (in …) is your search key. Search your source for the quoted class, the
identifier, or the leading-text snippet — in the manuscript for a content
mistake, or in shortcuts.yaml and your metadata for a configuration one.
A metadata check is the exception: it has no element to point at, so it names the
offending pandoc.yaml key in the message itself (fontsize '13pt' is not a
size this set allows) — search for the key.
Strict mode
While you're drafting, a warning shouldn't stop you — you want to see the page even with a placeholder that isn't quite right. So by default warnings are lenient: they print and the build continues.
Before you publish, flip that around. Set KEYSTONE_WARNINGS_AS_ERRORS=true in
project.conf and every warning becomes
fatal:
ERROR: warnings-as-errors is enabled and the build produced warnings:
vspace: unrecognized size 'banana'
No new artifact was written.
Fix the warnings above, or unset KEYSTONE_WARNINGS_AS_ERRORS for a lenient
build.
See https://keystone.knight-owl.dev/errors/#strict-mode
For a one-off check without editing config, make publish strict=true builds
once in strict mode — see Publishing your book.
Strict mode fails the build on any warning and promotes no artifact. Where the warning came from decides how you hear about it. The filters collect theirs and report them together after the build, as above, so you fix them in one pass. A warning about your configuration is raised before any filter runs, and stops the build where it is raised, reporting only itself:
WARN: the project name is still the default "keystone"
Set KEYSTONE_PROJECT, in project.conf or your environment, to name your own
output.
See https://keystone.knight-owl.dev/errors/default-project-name/
ERROR: warnings-as-errors is enabled
The warning above stops the build.
No new artifact was written.
Fix the warning above, or unset KEYSTONE_WARNINGS_AS_ERRORS for a lenient
build.
See https://keystone.knight-owl.dev/errors/#strict-mode
Pandoc's own warnings turn fatal too, and there the build stops with the pass
that failed rather than with a warnings-as-errors message. Draft lenient,
publish strict.
Silent by design
Not everything absent is a mistake. A few things are meant to be optional, so their absence is never reported:
- Optional attribution. An epigraph or pull-quote with no
sourceis a quote without an attribution line — a normal choice, not an error. - Undefined conditional symbols.
ifdef/ifndefgate on whether a symbol is defined; an undefined symbol is simply false, so the content is omitted. That's the whole mechanism, not a failure. See Conditional content.
Separately, there is a small set of mistakes Keystone cannot see at all — malformed markup that never becomes an element. Know the limits — the mistakes Keystone can't catch.
Under the hood
How the checks are implemented — the closed vocabulary, the shared diagnostic
path, and how a core template forker adds their own — is in
Validation & diagnostics.