Workflows

Workflows

API v115 August 2026·8 min read

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 Template row itself. It carries the same title, description, orientation, and four margin_* fields as any other template, plus published. 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.kinds from GET .../capabilities at 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 collection variable additionally declares the fields of one row; its rows are addressed as collection.field in the tree, and collection.i.field once expanded inside a repeated section.
  • Datasets: named, flat name → value maps 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 at POST /api/v1/document_requests without 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_variable section 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:

ValueBacked byDefault
limits.sectionsthe organization's own sections ceiling200
limits.image_bytesthe organization's own image size ceiling10 MB
limits.document_lengththe organization's own rendered-size ceiling800 ,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.

  1. 1Read GET .../capabilities, once per session. Nothing enforces this; everything below should be checked against this live response, never against a hardcoded assumption.
  2. 2Create the template: POST .../templates with title and description; orientation and the four margins can follow with PATCH. You need the id from this step before anything else can exist, so it is first by necessity.
  3. 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 validate or publication.
  4. 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.
  5. 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 a parent_id you name yourself (an unknown parent is refused), but a whole subtree created in one call sidesteps the question entirely.
  6. 6Validate (POST .../templates/:id/validate) before publishing, and again after any large edit. Not enforced as a precondition: anomalies always answer 200 and never block a save on their own; only publication blocks on the six blocking types. Running this first only saves you a guess at which one tripped.
  7. 7Publish: POST .../templates/:id/publication. Enforced: refused with 422 while any blocking anomaly stands, and the body lists every one of them.
  8. 8Generate: POST /api/v1/document_requests naming this workflow as template_id. Enforced: only a published workflow can be generated from; see Document requests API.
  9. 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