Workflows
Failures that answer 200
How to notice and avoid the workflow API failures that never return an error: sanitised content, dropped placement keys, frozen theme overrides, and silently substituted fonts.
Most of this API refuses what it cannot do. A handful of writes instead
accept the request, quietly do less than you asked, and answer 200 or
201 as if nothing happened. None of these are bugs (each is a
deliberate, documented choice), but every one of them can only be caught by
reading a specific field back, never by reading the status code.
Workflows are a private beta, available on request at [email protected]; see Workflows.
Scan for your symptom
| Symptom | Cause | Where it shows up |
|---|---|---|
| Content you wrote is missing after a reload | Sanitiser strips it on save | validate's sanitisation/stored, removed_tags/removed_attributes |
| A placement's label never appears on the page | Placement key misspelled | Nothing (the key is gone from the stored section) |
| A token prints the variable's own name instead of its value | Inert <variable> token | sanitisation/stored's inert_tokens |
| A picture prints as an empty frame | Image cited by external URL | sanitisation/stored's unreachable_images |
| One paragraph becomes two or three in the output | Block element ejected from a <p> | sanitisation's ejected_from_paragraph |
| A CSS rule you set has no visible effect | Declaration dropped from style | sanitisation/stored's removed_css_declarations |
| A header or footer is missing its first lines | Band clipped at the margin | stored's clipped_bands, validate only |
| Editing the theme stops changing a section | Inline value frozen against the theme | Nothing (read the section's own content) |
| PATCH | Unparseable value dropped | Re-read the theme after writing it |
| Text renders at an unexpected weight | No provisioned family carries that weight | workflow_selfcheck only (MCP), not a REST field |
1. Content is sanitised on save
You write: a rich-content or running-title fragment containing a
<script> tag, an onclick attribute, or anything else outside the
sanitiser's allow-list.
What happens: POST/PATCH on the section answers 200/201 with
the fragment already stripped. The disallowed tag or attribute is gone from
what gets stored; nothing in the response says so.
How you notice: dry-run the exact fragment through
POST .../validate with content: ["<fragment>"] before saving it.
See Validate. The section
create/update endpoints themselves give no signal either way.
Avoid it: treat validate's sanitisation.clean as the gate before any
write that carries new HTML, not the section endpoint's status code.
2. A misspelled placement key is dropped
You write: a placement object on an image_with_variable section with
a key that is not exactly one of id, kind, variable, value, x, y, width, height, align, font_size, color, font_family, font_weight.
What happens: the unknown key is silently dropped. The write still
answers 200/201; the label simply never renders at the position it
should have occupied.
How you notice: nothing reports this after the fact. The key is gone,
not flagged. Compare what you sent against
capabilities.placement.keys_by_kind/.keys before you send it.
Avoid it: spell placement keys exactly as the live capabilities
endpoint lists them; never hardcode a remembered list.
3. An inert token prints its own name
You write: a <variable> element with the right tag but missing the
editor-parsed class (or any class capabilities.content.tokens requires).
What happens: it survives sanitising and passes the integrity check
(it is not a broken_reference), but at render time it prints the
variable's own name instead of its value.
How you notice: validate's sanitisation/stored report it as
inert_tokens. Nothing else in the API flags it.
Avoid it: always write the exact token HTML capabilities.content.tokens
gives for the field type, e.g. for text:
<variable class="editor-text-variable non-editable-content editor-parsed">field_name</variable>.
4. An external image prints an empty frame
You write: an <img> citing an absolute or external URL instead of one
returned by POST .../images or .../background.
What happens: the rendering engine issues no network request while producing the PDF, so the picture prints as nothing: an empty frame, not an error.
How you notice: validate's sanitisation/stored report it as
unreachable_images.
Avoid it: upload every picture through the images or background endpoints first, and cite only the token/URL they return.
5. A block element is ejected from a paragraph
You write: a div, table, or another <p> nested inside a <p>.
What happens: the HTML5 parser moves it out, splitting the paragraph into up to three siblings. Nothing is removed, so even a before/after tag count reads clean.
How you notice: validate's sanitisation reports it as
ejected_from_paragraph.
Avoid it: never nest a block-level element inside a paragraph.
6. A CSS declaration is dropped from style
You write: a style="..." attribute where one declaration is not on
the sanitiser's allowed property list.
What happens: the attribute itself survives (so removed_attributes
stays empty), but that one declaration inside it is dropped. The fragment
still "reads clean" by the attribute count alone.
How you notice: validate's sanitisation/stored report it
separately, as removed_css_declarations.
Avoid it: check removed_css_declarations specifically; do not infer
safety from removed_attributes being empty.
7. A running-title band is clipped
You write: a header or footer band whose rendered content is longer than the margin reserved for that side.
What happens: the renderer clips it with no error anywhere. It clips from the opening lines, not the end.
How you notice: only validate's stored.clipped_bands, with an
estimated line count. The dry-run sanitisation preview does not check
length at all; this one only shows up once the band is actually saved.
Avoid it: keep bands short enough for the page's margins; check
stored.clipped_bands after any change to a header/footer or to the page
margins themselves.
8. An inline value freezes against the theme
You write: a theme property directly on a section, with a value that already matches what the template's theme currently declares for that block.
What happens: it renders identically today. It stops following the theme from that point on. A later edit to the theme (a new font, a new size) will not reach that section anymore, because it now carries its own, frozen value.
How you notice: nothing reports this: no anomaly, no stored field. It
is a modelling discipline, not a checked rule.
Avoid it: set the document's theme first, before writing section content, and only declare a property inline when you deliberately mean to override the theme for that one section.
9. An unparseable theme write is dropped
You write: PATCH .../theme with a value that does not parse under
that property's rules: a font family the renderer does not provision, a
line_height carrying a unit (e.g. "1.45em"), a font_size in a unit
other than pt, a size or line-height outside its bounds, a non-hex
color, an unknown block or property, or any font_weight at all
(deliberately never a writable property).
What happens: the offending value is dropped (per property, not per
request), and the write still answers 200. A known family name that
resolves to another is instead silently rewritten (e.g. "Arial" is stored
as its metric substitute, "Liberation Sans").
How you notice: the response is always the stored, re-normalised theme read back after the write, never an echo of what you sent. Reading that response is the only way to see a drop happen.
Avoid it: after every PATCH .../theme, compare the response against
what you sent, property by property, rather than trusting the 200.
| Value | Bounds/unit |
|---|---|
| font_size | 6-96, unit pt |
| line_height | 1.0-2.5, no unit |
| color | 3- or 6-digit hex |
| font_weight | never a writable property, always dropped |
A fixed instance, for context
A payload whose conditions excluded every section, or left only sections
that rendered no visible content, used to be delivered as a blank
document with a successful status. This is closed: both cases are refused
today with a 422 (excluded_sections/printing_nothing) at
POST /api/v1/document_requests. See
Workflow templates.
It is listed here only because older integration code, or older
documentation, may still assume the blank-page behaviour; nothing about it
is a live trap.
10. A font weight no family carries
You write: a weight in content or in the theme that no provisioned font family actually carries.
What happens: no write refuses it. The rendering engine rounds or synthesises the closest weight it has and prints that instead: a thickness nobody asked for.
How you notice: validate does not check this. Only the MCP layer's
workflow_selfcheck surfaces it, as weights_no_family_carries, computed
from stored.typography against capabilities.authoring.fonts.
Avoid it: pick weights from capabilities.authoring.fonts's declared
families, not from what an editor menu happens to offer.