Workflows

Variables, datasets & images

API v115 August 2026·9 min read

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

GET/api/v1/workflows/templates/:id/variables
cURL
curl https://app.doclift.io/api/v1/workflows/templates/100050/variables \
  --header "X-Api-Key: <your-api-key>"
200 OK · application/json
[
  { "id": 200100, "name": "investor_name", "description": "Full legal name", "field_type": "text", "allowed_values": [], "seed_value": "Jane Doe", "required": true, "fields": null }
]
StatusBodyWhen
200 arraysuccess
404 {"error": "..."}not a workflow, archived, or another organization's
403 {"error": "..."}see Shared behaviour

Show a variable

GET/api/v1/workflows/templates/:id/variables/:id
200 OK · application/json
{ "id": 200100, "name": "investor_name", "description": "Full legal name", "field_type": "text", "allowed_values": [], "seed_value": "Jane Doe", "required": true, "fields": null }
StatusBodyWhen
200 variablesuccess
404 {"error": "..."}variable belongs to another workflow, or the workflow itself is out of scope
403 {"error": "..."}see Shared behaviour

Create a variable

POST/api/v1/workflows/templates/:id/variables
cURL
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 }
      ]
    }
  }'
201 Created · application/json
{
  "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 }
  ]
}
FieldTypeRequiredNotes
namestringyes[a-z0-9_]+ only (refused, not rewritten, on any other character); unique per workflow
descriptionstringyes
field_typestringnoone of text, checkbox, radio, select, collection; default text
seed_valuestringnomust be one of allowed_values if both are set; refused on a collection
requiredbooleannomeaningful only for workflows (see required variables)
allowed_valuesarraynorefused on a collection
fieldsarraycollection onlynon-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.

StatusBodyWhen
201 variablesuccess
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

PATCH/api/v1/workflows/templates/:id/variables/:id

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.

StatusBodyWhen
200 variablesuccess
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

DELETE/api/v1/workflows/templates/:id/variables/:id
cURL
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.

StatusBodyWhen
204 nonesuccess
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

GET/api/v1/workflows/templates/:id/datasets
200 OK · application/json
[
  { "id": 400100, "name": "natural_person", "values": { "investor_type": "natural" } }
]

Alphabetical by name.

StatusBodyWhen
200 arraysuccess
404 {"error": "..."}not a workflow, archived, or another organization's
403 {"error": "..."}see Shared behaviour

Show a dataset

GET/api/v1/workflows/templates/:id/datasets/:id
200 OK · application/json
{ "id": 400100, "name": "natural_person", "values": { "investor_type": "natural" } }
StatusBodyWhen
200 datasetsuccess
404 {"error": "..."}dataset or workflow out of scope
403 {"error": "..."}see Shared behaviour

Create a dataset

POST/api/v1/workflows/templates/:id/datasets
cURL
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 €" }]
      }
    }
  }'
201 Created · application/json
{
  "id": 400100,
  "name": "natural_person",
  "values": { "investor_type": "natural", "investments": [{ "product": "SCPI", "amount": "10 000 €" }] }
}
FieldTypeRequiredNotes
namestringyes≤ 60 characters, unique per workflow (case-insensitive)
valuesobjectyesflat: a scalar, or an array of flat row objects, per key; nothing nested deeper
StatusBodyWhen
201 datasetsuccess
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

PATCH/api/v1/workflows/templates/:id/datasets/:id

Same fields as create; rewrites name/values in full.

StatusBodyWhen
200 datasetsuccess
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

DELETE/api/v1/workflows/templates/:id/datasets/:id

204 No Content.

StatusBodyWhen
204 nonesuccess
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

GET/api/v1/workflows/templates/:id/images
200 OK · application/json
[
  { "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.

StatusBodyWhen
200 arraysuccess
404 {"error": "..."}not a workflow, archived, or another organization's
403 {"error": "..."}see Shared behaviour

Show an image

GET/api/v1/workflows/templates/:id/images/:id

: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.

200 OK · application/json
{ "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 }
StatusBodyWhen
200 imagesuccess
404 {"error": "..."}wrong token, a numeric id, or another workflow's image
403 {"error": "..."}see Shared behaviour

Upload an image

POST/api/v1/workflows/templates/:id/images

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
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,<...>"
    }
  }'
201 Created · application/json
{ "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.

StatusBodyWhen
201 imagesuccess
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

DELETE/api/v1/workflows/templates/:id/images/:id

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.

StatusBodyWhen
204 nonesuccess
404 {"error": "..."}wrong token or another workflow's image
409 {"error": "..."}edit lock held
403 {"error": "..."}see Shared behaviour