Run workflows from the API
List, create, change and run workflows from your own code, follow each run row by row, and cancel runs. The calls, the run answer and the definition JSON.
A workflow reads rows from a source, renders a template for each row, and delivers each file. You usually build workflows in the app. The API lets your code start them, follow their runs, and create or change them.
| Call | What it does |
|---|---|
GET /v1/workflows |
List the workflows of the workspace |
POST /v1/workflows |
Create a workflow (a draft) |
GET /v1/workflows/{id} |
Get one workflow |
PATCH /v1/workflows/{id} |
Change a workflow, or turn it on or off |
DELETE /v1/workflows/{id} |
Delete a workflow and its run history |
POST /v1/workflows/{id}/run |
Start a run |
GET /v1/workflows/{id}/runs |
List a workflow’s runs |
GET /v1/workflows/{id}/runs/{runId} |
Get one run, with each row’s result |
POST /v1/workflows/{id}/runs/{runId}/cancel |
Cancel a run |
POST /v1/workflows/{id}/webhook-token |
Get a new incoming webhook URL |
Run a workflow
POST /v1/workflows/{id}/run starts a run now. Drafts can run this way too. The body is optional:
| Field | What it does |
|---|---|
maxRows |
Render only the first N rows, from 1 to 10,000. 1 is a cheap way to test the mapping. |
payload |
The rows, for a workflow whose source is Webhook payload: an array of objects (one row each), or one object. Required for that source. Up to 1 MB; the first 1,000 rows are used. |
curl -X POST https://api.jamframe.com/v1/workflows/YOUR_WORKFLOW_ID/run \
-H "Authorization: Bearer $JAMFRAME_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "maxRows": 1 }'const res = await fetch(
`https://api.jamframe.com/v1/workflows/${workflowId}/run`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.JAMFRAME_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ maxRows: 1 }),
},
);
const { run } = await res.json(); // 202: run.status is "queued"import os
import requests
res = requests.post(
f"https://api.jamframe.com/v1/workflows/{workflow_id}/run",
headers={"Authorization": f"Bearer {os.environ['JAMFRAME_API_KEY']}"},
json={"maxRows": 1},
)
run = res.json()["run"] # 202: run["status"] is "queued"The answer is 202 Accepted with the run, and a Location header with the run’s link. The run happens in
the background. Each row costs the usual render credits.
A run can be refused before it starts:
| Status | error |
Why |
|---|---|---|
400 |
VALIDATION_ERROR |
A step isn’t finished, or a Webhook payload workflow got no payload. detail.problems lists what’s missing. |
402 |
PLAN_LIMIT_EXCEEDED |
Your plan’s runs for this month are used up. |
402 |
QUOTA_EXCEEDED |
You have no render credits left. |
404 |
NOT_FOUND |
No workflow with this id in the workspace. |
Follow a run
Get the run with GET /v1/workflows/{id}/runs/{runId} every few seconds until it ends. Or subscribe to the
workflow.run.completed and workflow.run.failed webhook events instead.
{
"run": {
"id": "cmw9v2k1a0003abcd1234efgh",
"workflowId": "cmw9uoeb9000binw52g648888",
"trigger": "api",
"status": "succeeded",
"rowsTotal": 1,
"rowsSucceeded": 1,
"rowsFailed": 0,
"rowsSkipped": 0,
"credits": 1,
"error": null,
"warnings": [],
"createdAt": "2026-11-14T09:00:00.000Z",
"startedAt": "2026-11-14T09:00:01.000Z",
"finishedAt": "2026-11-14T09:00:05.000Z",
"rows": [
{
"index": 0,
"status": "succeeded",
"values": { "Quote": "Stay hungry", "Author": "Jobs" },
"rowNumber": null,
"modifications": { "quote": "Stay hungry", "author": "Jobs" },
"renderId": "clx8n0q1r0009abcd1234efgh",
"urls": ["https://cdn.jamframe.com/renders/…/0.png?Expires=…"],
"credits": 1,
"deliveries": [{ "stepId": "hook", "kind": "webhook", "status": "succeeded" }],
"error": null
}
]
}
}
A run’s status is one of:
status |
Meaning |
|---|---|
queued, running |
Not finished yet. |
succeeded |
Every row rendered and delivered. |
partial |
Some rows failed, the others went through. |
failed |
No row went through, or the source couldn’t be read. error says why. |
canceled |
Someone canceled it. Rows not started were skipped. |
triggersays what started the run:manual,schedule,webhook,new_rowsorapi.rowsholds up to 1,000 rows. Each row has its values, themodificationsit sent, its render, its files inurls, and the result of each delivery.- The links in
urlsare new on every call and work for 1 hour. GET /v1/workflows/{id}/runslists runs newest first, withoutrows. It takeslimit(1 to 100, default 25) andcursor, like the other lists.
Cancel a run
POST /v1/workflows/{id}/runs/{runId}/cancel stops a queued or running run. Rows not started yet are skipped.
The row in progress finishes. The answer is the run. A run that already ended answers 409 CONFLICT.
List and read workflows
GET /v1/workflows returns {"workflows": […]}, most recently changed first. It returns at most 200
workflows and has no paging. GET /v1/workflows/{id} returns {"workflow": {…}}.
| Field | What it is |
|---|---|
id, name, description |
The workflow. |
status |
draft or active. Only active workflows run on their trigger. Drafts run only when started by hand or by the API. |
definition |
The steps, as JSON. See The definition. |
steps |
How many steps it has, as your plan counts them. |
problems |
What still stops it from running, as {"stepId", "message"} items. Empty when it’s ready. |
trigger |
kind and a summary in words, such as “Run Now only”. |
nextRunAt, lastRunAt |
When it runs next on a schedule, and when it last ran. |
lastRun |
The latest run, without rows, or null. |
webhookUrl |
For the incoming webhook trigger: the URL that starts it. |
Create, change and delete
- Create:
POST /v1/workflowswithname,descriptionanddefinition, all optional. The answer is201with the new workflow, always a draft. Without adefinition, you get a blank 4-step draft. - Change:
PATCH /v1/workflows/{id}with any ofname,description,definitionandstatus. Fields you send replace the old ones."status": "active"turns the workflow on. It works only when nothing is missing; otherwise you get400 VALIDATION_ERRORwithdetail.problems."status": "draft"turns it off. - Delete:
DELETE /v1/workflows/{id}answers204. It deletes the run history too. Renders it made stay in the render log.
Your plan limits how many workflows you can have and how many steps each one can have. Over a limit, you get
402 PLAN_LIMIT_EXCEEDED. See Pricing.
The definition
A definition is one JSON object with five parts, in this order: trigger, source, transforms, render, deliver. For what each step does, see Workflows. This one reads a CSV file, removes duplicate quotes, renders a card per row and sends each card to your webhook:
{
"version": 1,
"trigger": { "kind": "manual" },
"source": { "kind": "file_url", "url": "https://example.com/quotes.csv" },
"transforms": [{ "kind": "dedupe", "column": "Quote" }],
"action": {
"kind": "render",
"templateId": "YOUR_TEMPLATE_ID",
"mapping": { "quote": "{{Quote}}", "author": "{{Author}}" },
"format": "png"
},
"deliver": [{ "kind": "webhook", "url": "https://hooks.example.com/cards" }]
}
| Part | kind and its fields |
|---|---|
trigger |
manual (Run Now and the API only). schedule: frequency (hourly, daily, weekly, monthly or custom), minute, time ("HH:MM"), dayOfWeek (0 = Sunday), dayOfMonth (1 to 28), cron (for custom) and timezone. webhook: the incoming webhook. new_rows: intervalMinutes, one of 15, 30, 60, 180, 360, 720 or 1440. |
source |
google_sheets: url and optional sheet (the tab). Share the sheet as Anyone with the link → Viewer. file_url: url of a CSV or XLSX file, format (auto, csv or xlsx), sheet. airtable: baseId, table, view, token. webhook: the rows sent to the incoming webhook, or the payload of an API run. |
transforms |
A list, run in order, up to 10. filter: column, operator, value. sort: column, direction (asc or desc). limit: count. pick: which (first, last or random) and count. dedupe: column (or the whole row). |
action |
render: templateId, mapping (template key to value; {{Column}} is filled from the row), format (png, jpeg, webp or pdf), scale (1 to 3), quality (1 to 100), includePages. |
deliver |
A list, up to 6. url: keep the file’s link in the run. webhook: url to POST each row to. slack: webhookUrl (a Slack incoming webhook) and message. |
The filter operator is one of equals, not_equals, contains, not_contains, starts_with, is_empty,
not_empty, greater_than or less_than.
Each step can have an id (1 to 32 letters, digits, _ or -). JamFrame adds one when you leave it out.
problems and run deliveries name steps by this id.
Secrets. Send the Airtable token and the Slack webhookUrl as plain text. JamFrame stores them
encrypted, and the answer shows them as {"set": true, "tail": "…"}, with the last 4 characters. To keep a
stored secret when you send the definition back, send that object as it is.
The incoming webhook URL
A workflow with the webhook trigger has its own secret URL, in webhookUrl. Anything that can send an HTTP
POST can start the workflow, with no API key:
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '[{ "name": "Ada", "role": "Engineer" }, { "name": "Grace", "role": "Admiral" }]'
- The body is an array of objects (one row each), an object that holds such an array in
rows,records,dataoritems, or one object (one row). Nested objects becomeparent.childcolumns. - A call can send up to 256 KB. JamFrame uses the first 1,000 rows.
- The answer is
202with the run’s id. The workflow must be active, or you get409 CONFLICT. - Each URL takes up to 60 calls a minute.
- Anyone with the URL can start runs that spend your credits. Keep it secret.
POST /v1/workflows/{id}/webhook-token makes a new URL and answers {"webhookUrl": "…"}. The old URL stops
working at once.
Related
Last updated
Still stuck?
Email [email protected] and a real person will reply. Tell us the template you were using and what you expected to happen.