Workflows
Workflows
What a Doclift workflow is, its object model, and the exact order to build one: from capabilities to a generated document.
What a workflow is
A workflow is a category: "workflow" template built from a tree of
sections instead of one HTML body. A custom template is a single blob of
markup you write by hand; a fillable form derives its variables from an
uploaded PDF's form fields. A workflow has no content column of its own at
all: what it renders is the concatenation of whichever sections your
generation payload's conditions leave visible. It has its own authoring
surface, entirely under /api/v1/workflows/*; the classic
/api/v1/templates write endpoints never touch it, and read it only after
it is published (see Template types).
Private beta
Workflows are a private beta: not enabled by default, granted on request. Write to [email protected] to ask for access. As with any beta, expect this surface to keep changing.
Structure is what a workflow buys you over the other two families: nested groups, each with its own condition, holding content sections that print or don't depending on what the caller sends. That is also the entire reason it has thirty-odd endpoints where a custom template has four: every piece of that structure (a group, a condition, a placement on an image, a theme override) is its own addressable thing.
The object model
- Workflow template: the
Templaterow itself. It carries the sametitle,description,orientation, and fourmargin_*fields as any other template, pluspublished. It owns everything below: sections, variables, datasets, images, and a theme. - Sections: the tree. A node is either a group (a title, an
optional condition, and children; never content of its own) or a content
section that carries content/placements but never children. The tree is
three levels deep at most: "group, group, section." The list of section
kinds is not fixed in this documentation: read
section.kindsfromGET .../capabilitiesat runtime; see Sections for the shape each kind carries. - Variables: declared once, cited by name from conditions, tokens, and
placements, with no foreign key: deleting or renaming one never
cascades into the sections that cite it. The reference survives, broken,
and is reported the next time you validate or publish, not swept away
silently. A
collectionvariable additionally declares the fields of one row; its rows are addressed ascollection.fieldin the tree, andcollection.i.fieldonce expanded inside a repeated section. - Datasets: named, flat
name → valuemaps you save for previewing the workflow. A dataset's shape is identical to a real generation payload (a set you can preview with is a payload you can replay atPOST /api/v1/document_requestswithout reshaping it). - Images: pictures uploaded through the workflow API and cited from
section content by the URL the upload returns, versus backgrounds,
which are attached directly to an
image_with_variablesection rather than living as their own resource. Both matter for the same reason: the rendering engine issues no network request while producing a PDF, so a picture cited by any other URL prints as an empty frame, silently, with the generation still answering success. Every picture has to come from this API first. - Theme, the document's own typographic defaults: four block types
(
paragraph,h1,h2,h3) crossed with four writable properties (font_family,font_size,color,line_height). It travels inside each published snapshot, so a document already published keeps rendering the way it looked the day it was published even if the theme is edited afterwards.
Capabilities are authoritative, not a copy
GET /api/v1/workflows/capabilities is the one place every value below is
declared, and it has to be read at request time: never transcribed into a
client or a script as a fixed list. Three groups of values it returns are
per-organization settings, not product constants:
| Value | Backed by | Default |
|---|---|---|
| limits.sections | the organization's own sections ceiling | 200 |
| limits.image_bytes | the organization's own image size ceiling | 10 MB |
| limits.document_length | the organization's own rendered-size ceiling | 800 ,000 characters |
An organization's back office can raise or lower any of the three
independently of the others; a document that prints "200 sections" as a
product fact is describing one customer's current setting, not a limit of
the API. Two more groups are run-dependent rather than
organization-dependent, and can change with a deploy: the sanitiser
allow-lists (content.allowed_tags, content.allowed_attributes) and the
font catalogue (authoring.fonts, theme.font_size, theme.fonts) are both
read from the running application at every call rather than frozen at boot,
because what they answer moves independently of the code that reads them.
Everything else capabilities returns (section kinds, condition operators,
placement keys, page geometry, anomaly types) is a genuine, stable product
constant. Read it live anyway: the endpoint carries a few enrichments that
only exist at call time, and a client that assumes a static shape is the
exact class of bug this endpoint exists to prevent. See
Get capabilities for the
full response.
The costliest mistake: freezing a value out of the theme
Nothing in the API's shape reveals this, and nothing reports it after the
fact: writing content that declares inline exactly what the theme already
says looks identical to leaving it inherited, today. A paragraph whose
theme says 12pt Arial, written with an inline font-size: 12pt on the same
element, renders the same PDF right now. The difference only shows up the
next time someone changes the theme: the plain paragraph follows it, the one
carrying the matching inline value does not, forever, because nothing in
PATCH .../theme or in section content ever looks at what the other one
declared. There is no anomaly type for this, no field in validate's
response: it is a modelling discipline, not a checked rule. Set the theme
before you write section content, and let sections inherit it rather than
restating it.
Building a workflow end to end
This is the order that gets you to a document without a detour through a broken reference discovered late. For each step, the note says whether the API stops you from skipping it, or whether skipping it merely saves cleanly and fails later.
- 1Read
GET .../capabilities, once per session. Nothing enforces this; everything below should be checked against this live response, never against a hardcoded assumption. - 2Create the template:
POST .../templateswithtitleanddescription;orientationand the four margins can follow withPATCH. You need the id from this step before anything else can exist, so it is first by necessity. - 3Create every variable, one call per variable, before any content
cites one. Not enforced: there is no foreign key from a condition or
a token to a variable, so writing content first succeeds and leaves you a
broken reference to find later, at
validateorpublication. - 4Set the theme (
PATCH .../theme) before writing section content. Not enforced by any write, but see the freezing mistake above: doing this after content already exists is how a section ends up with an inline value that silently stops tracking the theme. - 5Create sections, top-down: one group (with its whole subtree passed
as
children) or one leaf at a time, populating backgrounds (PATCH .../sections/:id/background) and rich-content images (POST .../images, then cite the returned URL) as you go. Parent-before- child is enforced for aparent_idyou name yourself (an unknown parent is refused), but a whole subtree created in one call sidesteps the question entirely. - 6Validate (
POST .../templates/:id/validate) before publishing, and again after any large edit. Not enforced as a precondition: anomalies always answer200and never block a save on their own; onlypublicationblocks on the six blocking types. Running this first only saves you a guess at which one tripped. - 7Publish:
POST .../templates/:id/publication. Enforced: refused with422while any blocking anomaly stands, and the body lists every one of them. - 8Generate:
POST /api/v1/document_requestsnaming this workflow astemplate_id. Enforced: only a published workflow can be generated from; see Document requests API. - 9Read the actual document. Nothing in this API compares the rendered PDF to the intention behind it. This is the one step that catches everything the others cannot.
A caller may create sections, variables, and datasets in any order the HTTP layer allows: none of steps 3 through 5 are enforced as preconditions by any validation. The ordering above is advice against one specific, otherwise-silent failure mode, not a state machine the API itself enforces.
Next
Templates
Capabilities, the template resource, validate, the payload contract, publish/withdraw, and whole-document export/import.
Sections & theme
The section tree, backgrounds, and the document's typographic theme.
Variables & data
Variables, preview datasets, and content images.
Publishing a workflow
What validate and publication check, and what blocks a release.
Pitfalls
The complete list of failures that answer 200, and how to catch them
before they ship.