Loopfour
API reference

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.

ScopeEndpoints
runs:read (or legacy workflows:read)List runs, get a run, logs, retry preview
workflows:executeCancel, pause, resume, stop, retry, restart
approvals:decide (or legacy workflows:write)Decide an approval
workflows:readList approvals

Runs are scoped to the company that owns the API key. A run in another company returns 404.

Run statuses

StatusMeaning
pendingQueued, not started
runningRunning steps
waiting_approvalPaused at an approval step until someone decides
pausedPaused by a person or an API key until it is resumed or cancelled
suspendedCheckpointed while it waits inside the run. Not a pause; Studio shows it as running
completedFinished without error
failedStopped by an error
cancelledCancelled

triggerType is one of api, manual, webhook, schedule, event, connection_event or subworkflow.

List runs

GET /api/v1/runs
Query parameterTypeMeaning
workflowIdstring (UUID)Only runs of this workflow
statusstringA status above, or active for pending, running, waiting_approval, paused and suspended together
triggerTypestringA trigger type above
from, tostring (ISO 8601)Start time range
searchstringRun ID prefix
limitintegerDefault 20, maximum 100
cursorstringmeta.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"
  }
}
FieldMeaning
triggerDevRunIdID 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
errorRun-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
done

Get run logs

GET /api/v1/runs/{id}/logs

Returns 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}/cancel

Cancels 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"
  }
}
StatusCodeCause
400INVALID_STATEThe run already ended, ended before the cancel was written, or is already being cancelled
502TRIGGER_CANCEL_FAILEDThe 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 running or suspended run to wait. It takes effect between blocks and between loop iterations, so the running block finishes first and the run shows pausing until 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 in autoCancelAt.
  • Resume resumes a paused run, or withdraws a pause request the run has not reached yet. If the run cannot be woken, it returns 502 RESUME_FAILED; retry.
  • Stop cancels the run like Cancel a run, and returns 409 CONFLICT instead of 400 when 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}/retry picks the start step itself. Use it for automatic retries.
  • GET .../retry-preview then POST /runs/{id}/restart lets 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}/retry

Retries 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-preview

Shows 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 parameterMeaning
fromStep ID to start from. Default: the failed step, else the first step
pickedA 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 fieldMeaning
fromBlockIdThe step the preview starts from
reusedCount, rerunCountSteps that keep their result, and steps that run again
writesOrSendsCountSteps that run again and write data or send a message
riskytrue when a step that already wrote or sent outside Loopfour runs again
repeatedExternalCount, repeatedInternalCountRepeated steps that reach other systems, and those that only email your team or write a table
safeNote, riskNote, ackLabel, warningText to show the person confirming, or null
planFingerprintSend it to restart with fromStepId set to fromBlockId

Restart from a step

POST /api/v1/runs/{id}/restart
FieldTypeRequiredMeaning
fromStepIdstringYesStep to start from. It always runs, even if the original run skipped it
planFingerprintstringYesFrom the preview for the same fromStepId
overrideInputobjectNoInput 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.

StatusCodeCause
400INVALID_STEPfromStepId is not in the workflow
409RETRY_PLAN_CHANGEDThe steps or the start step changed since the preview. Preview again
409RETRY_START_MISMATCHThe 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 parameterTypeDefaultMeaning
statusstringpendingComma-separated: pending, approved, rejected, expired, escalated or superseded. An unknown or empty value returns 400
workflowIdstring (UUID)Only this workflow
qstringText search, up to 200 characters
needsInputtrue or falsetrue keeps pending requests that ask for input fields
sortstringoldestoldest, newest or deadline
limitinteger501 to 50
offsetinteger0

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}/decide

id is the approval id (a UUID) from the list.

FieldTypeRequiredMeaning
approvedbooleanYestrue to approve, false to reject
reasonstringNoStored 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.

StatusCodeCause
400APPROVAL_ALREADY_DECIDEDAlready approved, rejected or closed
400APPROVAL_EXPIREDThe deadline passed
404APPROVAL_NOT_FOUNDNo approval with this ID in your company
409RUN_NOT_WAITINGThe run is no longer waiting for this approval
500APPROVAL_ERRORThe decision was saved but the run could not be resumed

Errors

StatusCodeCause
400INVALID_STATEThe run's status does not allow the action
400ZodError in error.nameBody or query does not match the schema
401UNAUTHORIZEDMissing or invalid API key
403FORBIDDENKey lacks the scope
404NOT_FOUNDRun not found
409CONFLICT, RETRY_*See the endpoint
429RATE_LIMITEDRate limit exceeded

See Errors, retries and timeouts for the shared error format.

On this page