Skip to content
JamFrame
Docs menu

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.
  • trigger says what started the run: manual, schedule, webhook, new_rows or api.
  • rows holds up to 1,000 rows. Each row has its values, the modifications it sent, its render, its files in urls, and the result of each delivery.
  • The links in urls are new on every call and work for 1 hour.
  • GET /v1/workflows/{id}/runs lists runs newest first, without rows. It takes limit (1 to 100, default 25) and cursor, 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/workflows with name, description and definition, all optional. The answer is 201 with the new workflow, always a draft. Without a definition, you get a blank 4-step draft.
  • Change: PATCH /v1/workflows/{id} with any of name, description, definition and status. Fields you send replace the old ones. "status": "active" turns the workflow on. It works only when nothing is missing; otherwise you get 400 VALIDATION_ERROR with detail.problems. "status": "draft" turns it off.
  • Delete: DELETE /v1/workflows/{id} answers 204. 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, data or items, or one object (one row). Nested objects become parent.child columns.
  • A call can send up to 256 KB. JamFrame uses the first 1,000 rows.
  • The answer is 202 with the run’s id. The workflow must be active, or you get 409 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.

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.