Workflows
Variables, datasets & images
Workflow variables, preview datasets, and content images: the three smallest, most uniform CRUD resources in the workflow API.
Every endpoint below is under /api/v1/workflows. Read
Workflows first for the object model; read
Templates, publication & document for
capabilities, the template resource, and publication, and
Sections & theme for the section tree. This
page covers variables, preview datasets, and content images.
Workflows are a private beta, available on request at [email protected]; see Workflows.
Variables
This API always reads and writes this field as name, and so does the
dashboard.
List variables
curl https://app.doclift.io/api/v1/workflows/templates/100050/variables \
--header "X-Api-Key: <your-api-key>"
[
{ "id": 200100, "name": "investor_name", "description": "Full legal name", "field_type": "text", "allowed_values": [], "seed_value": "Jane Doe", "required": true, "fields": null }
]
| Status | Body | When |
|---|---|---|
| 200 | array | success |
| 404 | {"error": "..."} | not a workflow, archived, or another organization's |
| 403 | {"error": "..."} | see Shared behaviour |
Show a variable
{ "id": 200100, "name": "investor_name", "description": "Full legal name", "field_type": "text", "allowed_values": [], "seed_value": "Jane Doe", "required": true, "fields": null }
| Status | Body | When |
|---|---|---|
| 200 | variable | success |
| 404 | {"error": "..."} | variable belongs to another workflow, or the workflow itself is out of scope |
| 403 | {"error": "..."} | see Shared behaviour |
Create a variable
curl https://app.doclift.io/api/v1/workflows/templates/100050/variables \
--request POST \
--header "X-Api-Key: <your-api-key>" \
--header "Content-Type: application/json" \
--data '{
"variable": {
"name": "investments",
"description": "One row per investment",
"field_type": "collection",
"required": false,
"fields": [
{ "name": "product", "description": "Product name", "required": true },
{ "name": "amount", "description": "Amount invested", "required": true }
]
}
}'
{
"id": 200101,
"name": "investments",
"description": "One row per investment",
"field_type": "collection",
"allowed_values": [],
"seed_value": null,
"required": false,
"fields": [
{ "name": "product", "description": "Product name", "required": true, "seed_value": null },
{ "name": "amount", "description": "Amount invested", "required": true, "seed_value": null }
]
}
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | [a-z0-9_]+ only (refused, not rewritten, on any other character); unique per workflow |
| description | string | yes | |
| field_type | string | no | one of text, checkbox, radio, select, collection; default text |
| seed_value | string | no | must be one of allowed_values if both are set; refused on a collection |
| required | boolean | no | meaningful only for workflows (see required variables) |
| allowed_values | array | no | refused on a collection |
| fields | array | collection only | non-empty, each {name, description, required, seed_value}; unique, pattern-valid names |
Names are refused, not downcased
Unlike a custom template's variables, an invalid workflow variable name is
rejected outright: surrounding blanks are stripped silently, but a space,
a capital letter, or a dash all answer 422. There is no lowercasing pass
to rely on.
| Status | Body | When |
|---|---|---|
| 201 | variable | success |
| 422 | {"errors": ["..."]} | invalid/duplicate name, unknown field_type, a collection with no fields (or with allowed_values/seed_value), a non-collection carrying fields, seed_value outside allowed_values |
| 400 | {"error": "..."} | body missing the variable root key |
| 404 | {"error": "..."} | not a workflow, archived, or another organization's |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Update a variable
Same fields as create.
Renaming does not rewrite anything that cites the old name
A condition or a token citing this variable keeps the old, now-broken name.
Renaming here never cascades. Expect a broken_reference anomaly at the
next validate, not an automatic rewrite.
| Status | Body | When |
|---|---|---|
| 200 | variable | success |
| 422 | {"errors": ["..."]} | same validations as create |
| 404 | {"error": "..."} | variable or workflow out of scope |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Delete a variable
curl https://app.doclift.io/api/v1/workflows/templates/100050/variables/200101 \
--request DELETE \
--header "X-Api-Key: <your-api-key>"
204 No Content. Succeeds even while a condition or a token still cites
the deleted variable: the citation is left standing; it becomes a
broken_reference blocking anomaly the next time the tree is validated or
published, rather than being cascaded away or refused.
| Status | Body | When |
|---|---|---|
| 204 | none | success |
| 404 | {"error": "..."} | variable or workflow out of scope |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Datasets
A dataset is a named, flat name → value map you save for previewing the
workflow: a scalar per variable, or an array of flat row objects for a
collection variable. The route is deliberately datasets, not the
model's own preview_datasets: what you read here is a sample payload you
can replay at POST /api/v1/document_requests, not a preview screen.
List datasets
[
{ "id": 400100, "name": "natural_person", "values": { "investor_type": "natural" } }
]
Alphabetical by name.
| Status | Body | When |
|---|---|---|
| 200 | array | success |
| 404 | {"error": "..."} | not a workflow, archived, or another organization's |
| 403 | {"error": "..."} | see Shared behaviour |
Show a dataset
{ "id": 400100, "name": "natural_person", "values": { "investor_type": "natural" } }
| Status | Body | When |
|---|---|---|
| 200 | dataset | success |
| 404 | {"error": "..."} | dataset or workflow out of scope |
| 403 | {"error": "..."} | see Shared behaviour |
Create a dataset
curl https://app.doclift.io/api/v1/workflows/templates/100050/datasets \
--request POST \
--header "X-Api-Key: <your-api-key>" \
--header "Content-Type: application/json" \
--data '{
"dataset": {
"name": "natural_person",
"values": {
"investor_type": "natural",
"investments": [{ "product": "SCPI", "amount": "10 000 €" }]
}
}
}'
{
"id": 400100,
"name": "natural_person",
"values": { "investor_type": "natural", "investments": [{ "product": "SCPI", "amount": "10 000 €" }] }
}
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | yes | ≤ 60 characters, unique per workflow (case-insensitive) |
| values | object | yes | flat: a scalar, or an array of flat row objects, per key; nothing nested deeper |
| Status | Body | When |
|---|---|---|
| 201 | dataset | success |
| 422 | {"errors": ["..."]} | blank/too-long/duplicate name, or a value nested deeper than one level |
| 400 | {"error": "..."} | body missing the dataset root key |
| 404 | {"error": "..."} | not a workflow, archived, or another organization's |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Update a dataset
Same fields as create; rewrites name/values in full.
| Status | Body | When |
|---|---|---|
| 200 | dataset | success |
| 422 | {"errors": ["..."]} | same validations as create |
| 404 | {"error": "..."} | dataset or workflow out of scope |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Delete a dataset
204 No Content.
| Status | Body | When |
|---|---|---|
| 204 | none | success |
| 404 | {"error": "..."} | dataset or workflow out of scope |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Images
Pictures cited from rich-content, distinct from section backgrounds (see Set a section's background). Producing a PDF fetches nothing over the network, so a picture cited by an external URL prints as an empty frame, silently. This endpoint exists so every picture you cite is one it already has the bytes for.
List images
[
{ "id": 500100, "token": "9f2a1c7e...", "created_at": "2026-01-15 10:00:00 +0100", "url": "/workflows/images/9f2a1c7e...", "content_type": "image/webp", "byte_size": 84213, "width": 1200, "height": 800 }
]
url is path-only (no host), so a workflow copied to another environment
keeps its pictures resolving correctly.
| Status | Body | When |
|---|---|---|
| 200 | array | success |
| 404 | {"error": "..."} | not a workflow, archived, or another organization's |
| 403 | {"error": "..."} | see Shared behaviour |
Show an image
:id here is the token, not the numeric id
This route looks the image up by its token, not its numeric id; a
numeric id in this position answers 404 even if the image exists under a
different, valid token. Reading the raw bytes is a separate, unauthenticated
route (GET /workflows/images/:token); the 32-character token itself is the
only thing standing between the bytes and the world.
{ "id": 500100, "token": "9f2a1c7e...", "created_at": "2026-01-15 10:00:00 +0100", "url": "/workflows/images/9f2a1c7e...", "content_type": "image/webp", "byte_size": 84213, "width": 1200, "height": 800 }
| Status | Body | When |
|---|---|---|
| 200 | image | success |
| 404 | {"error": "..."} | wrong token, a numeric id, or another workflow's image |
| 403 | {"error": "..."} | see Shared behaviour |
Upload an image
Same two upload shapes as a background: multipart image[file], or JSON
{filename, content_type, data}. No minimum width, since a content image
is placed at whatever size you give it.
curl https://app.doclift.io/api/v1/workflows/templates/100050/images \
--request POST \
--header "X-Api-Key: <your-api-key>" \
--header "Content-Type: application/json" \
--data '{
"image": {
"filename": "logo.png",
"content_type": "image/png",
"data": "data:image/png;base64,<...>"
}
}'
{ "id": 500101, "token": "b7c3d4e2...", "created_at": "2026-02-03 11:00:00 +0100", "url": "/workflows/images/b7c3d4e2...", "content_type": "image/webp", "byte_size": 12044, "width": 400, "height": 120 }
Put the returned url into an <img src> inside section content: because
the picture's bytes are already stored under that url, it renders
correctly in the generated PDF.
| Status | Body | When |
|---|---|---|
| 201 | image | success |
| 422 | {"errors": ["..."]} | missing (no file/bytes), too_large (over the organization's byte ceiling), wrong_type (checked against the re-encoded bytes) |
| 400 | {"error": "..."} | body missing the image root key |
| 404 | {"error": "..."} | not a workflow, archived, or another organization's |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Delete an image
204 No Content, addressed by token. Succeeds even while a section's
content still cites the picture: the <img src> string is left exactly
as written. At the next render, the whole <img> frame is dropped silently
rather than printed as a broken box; validate's stored.unreachable_images
is the only place a dangling citation is ever reported.
| Status | Body | When |
|---|---|---|
| 204 | none | success |
| 404 | {"error": "..."} | wrong token or another workflow's image |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |