Runs API
Start, list, inspect, retry and cancel workflow runs with the Runs API, including how to record a decision on a pending approval request.
A run is one execution of a workflow, with a record of each step. Start runs with POST /api/v1/workflows/{id}/run. Use these endpoints to read runs, follow their steps, stop or pause them, retry them, and decide pending approvals.
| Scope | Endpoints |
|---|---|
runs:read (or legacy workflows:read) | List runs, get a run, logs, retry preview |
workflows:execute | Cancel, pause, resume, stop, retry, restart |
approvals:decide (or legacy workflows:write) | Decide an approval |
workflows:read | List approvals |
Runs are scoped to the company that owns the API key. A run in another company returns 404.
Run statuses
| Status | Meaning |
|---|---|
pending | Queued, not started |
running | Running steps |
waiting_approval | Paused at an approval step until someone decides |
paused | Paused by a person or an API key until it is resumed or cancelled |
suspended | Checkpointed while it waits inside the run. Not a pause; Studio shows it as running |
completed | Finished without error |
failed | Stopped by an error |
cancelled | Cancelled |
triggerType is one of api, manual, webhook, schedule, event, connection_event or subworkflow.
List runs
GET /api/v1/runs| Query parameter | Type | Meaning |
|---|---|---|
workflowId | string (UUID) | Only runs of this workflow |
status | string | A status above, or active for pending, running, waiting_approval, paused and suspended together |
triggerType | string | A trigger type above |
from, to | string (ISO 8601) | Start time range |
search | string | Run ID prefix |
limit | integer | Default 20, maximum 100 |
cursor | string | meta.cursor from the previous page |
curl 'https://workflow.loopfour.ai/api/v1/runs?status=failed&limit=10' \
-H 'x-api-key: wfk_live_your_api_key'{
"success": true,
"data": [
{
"id": "4e1f9c02-7b3d-4a1e-9c85-2f6d0b8e1a47",
"workflowId": "c3a1f7d2-58e4-4b9a-8f21-6d0e9b7c4a15",
"workflowName": "Invoice status to Slack",
"status": "failed",
"triggerType": "api",
"startedAt": "2026-02-12T18:30:01.000Z",
"completedAt": "2026-02-12T18:30:04.000Z",
"durationMs": 3000,
"error": { "code": "STEP_ERROR", "message": "Invoice not found" },
"createdAt": "2026-02-12T18:30:00.000Z"
}
],
"meta": { "cursor": "MTcwNzc2MjYwMDAwMDo0ZTFmOWMwMi4uLg", "hasMore": false }
}Get a run
GET /api/v1/runs/{id}Returns the full run. While the run is in progress, its status is refreshed from the execution engine before the response is sent.
{
"success": true,
"data": {
"id": "4e1f9c02-7b3d-4a1e-9c85-2f6d0b8e1a47",
"workflowId": "c3a1f7d2-58e4-4b9a-8f21-6d0e9b7c4a15",
"triggerDevRunId": "run_cm1abc",
"status": "completed",
"triggerType": "api",
"input": { "invoiceId": "1042" },
"output": null,
"stepResults": [
{
"stepId": "get_invoice",
"status": "success",
"startedAt": "2026-02-12T18:30:01.000Z",
"completedAt": "2026-02-12T18:30:02.000Z",
"durationMs": 1000,
"input": { "invoiceId": "1042" },
"output": { "Invoice": { "Id": "1042", "Balance": 362.07 } },
"attempt": 1
}
],
"startedAt": "2026-02-12T18:30:01.000Z",
"completedAt": "2026-02-12T18:30:03.000Z",
"durationMs": 2000,
"error": null,
"createdAt": "2026-02-12T18:30:00.000Z"
}
}| Field | Meaning |
|---|---|
triggerDevRunId | ID of the execution engine run behind this run; null if it was never dispatched. Quote it when you report a run that failed without a step error |
stepResults[] | One entry per step: stepId, status (success, failed, skipped or suspended), input, output, error, startedAt, completedAt, durationMs and attempt |
error | Run-level error with code and message; null when the run did not fail |
Wait for a run to finish
Poll this endpoint until the status is completed, failed or cancelled. A run at waiting_approval or paused waits for a person, so do not treat it as stuck.
while true; do
status=$(curl -s 'https://workflow.loopfour.ai/api/v1/runs/4e1f9c02-7b3d-4a1e-9c85-2f6d0b8e1a47' \
-H 'x-api-key: wfk_live_your_api_key' | jq -r '.data.status')
case "$status" in completed|failed|cancelled) echo "$status"; break ;; esac
sleep 2
doneGet run logs
GET /api/v1/runs/{id}/logsReturns runId, workflowId, triggerDevRunId, status, startedAt, completedAt, durationMs, error, logs and warnings.
Each entry in logs has stepId, stepName, stepType, status, startedAt, completedAt, durationMs, input, output, error and attempt. warnings lists problems the run carried on with, such as an approval request that reached nobody (type: "approval_delivery", with approvalId, stepId and message).
Cancel a run
POST /api/v1/runs/{id}/cancelCancels a run that is pending, running, suspended, waiting_approval or paused. The block that is running is interrupted and no later block starts. A cancel does not undo an action that already finished. When the run waits for approval, the approval is closed, and a later decision on it returns 400 APPROVAL_ALREADY_DECIDED.
{
"success": true,
"data": {
"id": "4e1f9c02-7b3d-4a1e-9c85-2f6d0b8e1a47",
"status": "cancelled",
"cancelled": true,
"triggerDevCancelled": true,
"message": "Run cancellation requested"
}
}| Status | Code | Cause |
|---|---|---|
400 | INVALID_STATE | The run already ended, ended before the cancel was written, or is already being cancelled |
502 | TRIGGER_CANCEL_FAILED | The execution engine refused the cancel. Nothing changed; retry |
Pause, resume and stop
These endpoints return the run's state and the controls that remain, in one shape:
POST /api/v1/run-observability/runs/{id}/pause
POST /api/v1/run-observability/runs/{id}/resume
POST /api/v1/run-observability/runs/{id}/cancel- Pause asks a
runningorsuspendedrun to wait. It takes effect between blocks and between loop iterations, so the running block finishes first and the run showspausinguntil then. A subworkflow run cannot be paused (pause its parent). A run does not pause while it holds an open browser session. A run that stays paused for 7 days is cancelled; the time is inautoCancelAt. - Resume resumes a paused run, or withdraws a pause request the run has not reached yet. If the run cannot be woken, it returns
502RESUME_FAILED; retry. - Stop cancels the run like Cancel a run, and returns
409CONFLICTinstead of400when the run cannot be stopped.
A run that cannot be paused or resumed returns 409 CONFLICT with a message that says why.
{
"success": true,
"data": {
"summary": {
"id": "4e1f9c02-7b3d-4a1e-9c85-2f6d0b8e1a47",
"shortId": "4e1f9c02",
"status": "pausing",
"pausedBy": { "kind": "api_key", "keyId": "key_123", "name": "Close automation" },
"stoppedBy": null
},
"control": {
"canPause": false,
"canResume": true,
"canStop": true,
"canRetry": false,
"pauseRequestedAt": "2026-02-12T18:31:00.000Z",
"autoCancelAt": null,
"currentBlockId": "get_invoice"
}
}
}summary is shortened here; it also carries the workflow, times, progress and outcome. summary.status is a display status: queued, running, pausing, paused, waiting_approval, completed, failed or cancelled.
pausedBy and stoppedBy are null or an actor. Read kind first: user (userId, name), api_key (keyId, name) or system (reason: "auto_cancel_paused_7d").
Retry a run
Two ways to run a finished run again from a step. Both reuse earlier step results when the earlier run shows they are still valid, keep untaken branches skipped, and run the steps of the version the original run used.
POST /runs/{id}/retrypicks the start step itself. Use it for automatic retries.GET .../retry-previewthenPOST /runs/{id}/restartlets a person see what runs again before it starts.
If the API cannot prove which steps the original run ran, all three return 409 RETRY_SOURCE_UNAVAILABLE and nothing starts. Start the workflow again instead; that runs every step again.
Retry
POST /api/v1/runs/{id}/retryRetries a failed or cancelled run. A failed run starts again at its first failed step; a cancelled run at the first step it did not finish. The original input is used. Any other status returns 400 INVALID_STATE.
{
"success": true,
"data": {
"originalRunId": "4e1f9c02-7b3d-4a1e-9c85-2f6d0b8e1a47",
"newRunId": "9a7d3e11-2c4b-4f6a-8e19-0b5c7d2a3f64",
"workflowId": "c3a1f7d2-58e4-4b9a-8f21-6d0e9b7c4a15",
"restartFromStepId": "get_invoice",
"status": "pending",
"createdAt": "2026-02-12T18:40:00.000Z"
}
}The response is 202. If the new run was created but could not be queued, it also has warning.
Preview a retry
GET /api/v1/run-observability/runs/{id}/retry-previewShows what a retry reuses and what it runs again, and changes nothing. Needs runs:read (or workflows:read); a key with only workflows:execute cannot call it. The run must be failed, completed or cancelled, otherwise 409 INVALID_STATE.
| Query parameter | Meaning |
|---|---|
from | Step ID to start from. Default: the failed step, else the first step |
picked | A step ID to offer as an extra start option |
A from or picked step that is not in the workflow returns 400 INVALID_STEP.
| Response field | Meaning |
|---|---|
fromBlockId | The step the preview starts from |
reusedCount, rerunCount | Steps that keep their result, and steps that run again |
writesOrSendsCount | Steps that run again and write data or send a message |
risky | true when a step that already wrote or sent outside Loopfour runs again |
repeatedExternalCount, repeatedInternalCount | Repeated steps that reach other systems, and those that only email your team or write a table |
safeNote, riskNote, ackLabel, warning | Text to show the person confirming, or null |
planFingerprint | Send it to restart with fromStepId set to fromBlockId |
Restart from a step
POST /api/v1/runs/{id}/restart| Field | Type | Required | Meaning |
|---|---|---|---|
fromStepId | string | Yes | Step to start from. It always runs, even if the original run skipped it |
planFingerprint | string | Yes | From the preview for the same fromStepId |
overrideInput | object | No | Input fields to replace |
Restarts a failed, completed or cancelled run; other statuses return 400 INVALID_STATE. The response is 202 with the same fields as retry.
| Status | Code | Cause |
|---|---|---|
400 | INVALID_STEP | fromStepId is not in the workflow |
409 | RETRY_PLAN_CHANGED | The steps or the start step changed since the preview. Preview again |
409 | RETRY_START_MISMATCH | The fingerprint was issued for another start step. Preview with from set to fromStepId |
# 1. Preview from the failed step and read planFingerprint
curl 'https://workflow.loopfour.ai/api/v1/run-observability/runs/4e1f9c02-7b3d-4a1e-9c85-2f6d0b8e1a47/retry-preview?from=get_invoice' \
-H 'x-api-key: wfk_live_your_api_key'
# 2. Restart with corrected input
curl -X POST 'https://workflow.loopfour.ai/api/v1/runs/4e1f9c02-7b3d-4a1e-9c85-2f6d0b8e1a47/restart' \
-H 'x-api-key: wfk_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{
"fromStepId": "get_invoice",
"planFingerprint": "b7f3a9c1e04d58a26f19c3d7e8a05b4c91d2e6f073a8b5c4d1e9f20a63b7c8d5",
"overrideInput": { "invoiceId": "1043" }
}'Approvals
A run at waiting_approval has a pending approval request. Find it, then record a decision. For how approval steps are set up, see the Approval block.
List approvals
GET /api/v1/approvals| Query parameter | Type | Default | Meaning |
|---|---|---|---|
status | string | pending | Comma-separated: pending, approved, rejected, expired, escalated or superseded. An unknown or empty value returns 400 |
workflowId | string (UUID) | Only this workflow | |
q | string | Text search, up to 200 characters | |
needsInput | true or false | true keeps pending requests that ask for input fields | |
sort | string | oldest | oldest, newest or deadline |
limit | integer | 50 | 1 to 50 |
offset | integer | 0 |
Each entry has id, runId, workflowId, workflowName, stepId, stepName, reason, status, runStatus, approvers, requireAll, requestedAt, deadline, isOverdue, decidedAt, decidedBy and decisionReason. The response also has counts and pagination (limit, offset, total).
Decide an approval
POST /api/v1/approvals/{id}/decideid is the approval id (a UUID) from the list.
| Field | Type | Required | Meaning |
|---|---|---|---|
approved | boolean | Yes | true to approve, false to reject |
reason | string | No | Stored with the decision and shown in the audit trail |
curl -X POST 'https://workflow.loopfour.ai/api/v1/approvals/5d2e8c71-9a3b-4f0e-8c62-1b7a4d9e3f05/decide' \
-H 'x-api-key: wfk_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{ "approved": true, "reason": "Matches the signed PO" }'{
"success": true,
"data": {
"approvalId": "5d2e8c71-9a3b-4f0e-8c62-1b7a4d9e3f05",
"status": "approved",
"decidedAt": "2026-02-12T19:02:00.000Z",
"decidedBy": "api_key:key_123",
"reason": "Matches the signed PO"
}
}The run then continues on the approved or rejected path. An API key decides for the company, so the named-approver check that applies to people signed in to Studio does not apply to it. The decision is recorded in the audit trail as made through the API.
| Status | Code | Cause |
|---|---|---|
400 | APPROVAL_ALREADY_DECIDED | Already approved, rejected or closed |
400 | APPROVAL_EXPIRED | The deadline passed |
404 | APPROVAL_NOT_FOUND | No approval with this ID in your company |
409 | RUN_NOT_WAITING | The run is no longer waiting for this approval |
500 | APPROVAL_ERROR | The decision was saved but the run could not be resumed |
Errors
| Status | Code | Cause |
|---|---|---|
400 | INVALID_STATE | The run's status does not allow the action |
400 | ZodError in error.name | Body or query does not match the schema |
401 | UNAUTHORIZED | Missing or invalid API key |
403 | FORBIDDEN | Key lacks the scope |
404 | NOT_FOUND | Run not found |
409 | CONFLICT, RETRY_* | See the endpoint |
429 | RATE_LIMITED | Rate limit exceeded |
See Errors, retries and timeouts for the shared error format.
Workflows API
Create, list, update, deploy and delete workflows with the Workflows API, with the parameters, request bodies and responses of each endpoint.
Webhooks API
Manage webhook endpoints with the Webhooks API: create an endpoint for a workflow, list and delete endpoints, and read the delivery details.
Loopfour