Loopfour
API reference

Workflows API

Create, list, update, deploy and delete workflows with the Workflows API, with the parameters, request bodies and responses of each endpoint.

A workflow is a saved definition: one trigger and an ordered list of steps. Use these endpoints to create and edit definitions, publish and pause them, start runs, and read version history. The definition itself is described in Workflow definition format.

ScopeEndpoints
workflows:readList, get, versions, compare, schedule (read)
workflows:writeCreate, update, canvas, delete, archive, unarchive, activate, pause, clone, rollback, schedule (update)
workflows:executeRun, trigger a scheduled run
runs:read (or legacy workflows:read)List a workflow's runs

Every endpoint is scoped to the company that owns the API key. A workflow in another company returns 404.

Workflow object

FieldTypeMeaning
idstring (UUID)Workflow ID
namestring1 to 255 characters
descriptionstring or null
statusstringdraft, active, paused or archived
versionintegerInternal version number. Goes up by 1 for each new version. Use it for expectedVersion, rollback and compare
versionLabelstringDisplay name, v{major}.{minor}, for example v2.1
versionMajor, versionMinorinteger or nullParts of the label
publishedVersioninteger or nullversion of the last publish; null if never published
triggerobjectTrigger configuration
stepsarraySteps (single-workflow responses only)
inputSchema, outputSchema, settingsobject or nullSingle-workflow responses only
canvasState, isVisualEditor layout. Returned by get only
nextRunAt, scheduleTimezonestring or nullList only. Set when the workflow is active, has an enabled schedule and the next run time is in the future
createdAt, updatedAtstring (ISO 8601)

List workflows

GET /api/v1/workflows
Query parameterTypeDefaultMeaning
statusstringdraft, active, paused or archived. Without it, archived workflows are left out
searchstringCase-insensitive match on the name
limitinteger201 to 100
cursorstringmeta.cursor from the previous page
curl 'https://workflow.loopfour.ai/api/v1/workflows?status=active&limit=10' \
  -H 'x-api-key: wfk_live_your_api_key'
{
  "success": true,
  "data": [
    {
      "id": "c3a1f7d2-58e4-4b9a-8f21-6d0e9b7c4a15",
      "name": "Month-end close report",
      "description": "Run the close checklist each weekday",
      "status": "active",
      "version": 3,
      "versionLabel": "v2.1",
      "versionMajor": 2,
      "versionMinor": 1,
      "publishedVersion": 2,
      "trigger": { "type": "schedule", "cron": "0 18 * * 1-5", "timezone": "America/New_York" },
      "createdAt": "2026-02-01T08:00:00.000Z",
      "updatedAt": "2026-02-10T09:15:00.000Z",
      "nextRunAt": "2026-02-12T23:00:00.000Z",
      "scheduleTimezone": "America/New_York"
    }
  ],
  "meta": { "cursor": "MTcwNjc3NDQwMDAwMDpjM2ExZjdkMi4uLg", "hasMore": true }
}

Results are ordered newest first. When meta.hasMore is true, pass meta.cursor as cursor to get the next page.

Create a workflow

POST /api/v1/workflows

New workflows start as draft at v0.1 (version 1), unpublished. The response is 201 with the full workflow object.

FieldTypeRequiredMeaning
namestringYes1 to 255 characters
descriptionstringNo
triggerobjectYesTrigger configuration, see Triggers
stepsarrayYesAt least one step. Each step needs id, type, name and config; action steps also need action
inputSchemaobjectNoJSON Schema checked against input when the workflow is run
outputSchemaobjectNo
settingsobjectNotimeoutMs, maxRetries, concurrencyLimit, machine, maxDurationSeconds (5 or more)

Step type is one of action, condition, loop, parallel, wait, transform, slack, email, gmail, approval, agent, code, subworkflow, data_table or workspace_files. Steps are checked when you create the workflow; a step that refers to a step that does not exist returns 400.

This example creates a workflow that you start by API call. It reads a QuickBooks invoice and posts a Slack message with the result:

curl -X POST 'https://workflow.loopfour.ai/api/v1/workflows' \
  -H 'x-api-key: wfk_live_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Invoice status to Slack",
    "description": "Look up an invoice and post its balance",
    "trigger": { "type": "api" },
    "inputSchema": {
      "type": "object",
      "required": ["invoiceId"],
      "properties": { "invoiceId": { "type": "string" } }
    },
    "steps": [
      {
        "id": "get_invoice",
        "type": "action",
        "name": "Get invoice",
        "action": "quickbooks.getInvoice",
        "config": { "invoiceId": "{{input.invoiceId}}" }
      },
      {
        "id": "notify",
        "type": "action",
        "name": "Post to finance channel",
        "action": "slack.sendMessage",
        "config": {
          "channel": "#finance",
          "text": "Invoice {{input.invoiceId}} balance: {{steps.get_invoice.Invoice.Balance}}"
        }
      }
    ]
  }'

Find each action's ID and settings with block and action discovery. References such as {{steps.get_invoice.Invoice.Balance}} are explained in Variable references.

Get a workflow

GET /api/v1/workflows/{id}

Returns the full workflow object, including steps, canvasState and isVisual. Archived workflows can still be read. A deleted workflow returns 404.

Update a workflow

PUT /api/v1/workflows/{id}

Every field is optional. Send only what you want to change.

FieldTypeMeaning
name, descriptionstringChanging only these never starts a new version
trigger, steps, inputSchema, outputSchema, settingsDefinition fields. A change follows the version rules
changeSummarystringUp to 500 characters. Stored with the outgoing version when this save starts a new one
expectedStatusstringThe status you read. A different current status returns 409
expectedVersionintegerThe version you read. A different current version returns 409
expectedUpdatedAtstring (ISO 8601)The updatedAt you read. A different value returns 409. An in-place save keeps version but moves updatedAt, so this is the field that detects it

The response is the full workflow object. Compare version with the value you sent to see whether a new version started.

  • An archived workflow returns 409 (Workflow is archived; unarchive it before editing).
  • If the workflow changes between the server's read and its write, the request returns 409 and nothing is saved. Reload and retry.
  • On an active workflow whose schedule timing changes, the schedule is replaced or removed after the save. If that fails, the request returns 502 (UPSTREAM_ERROR); the new version is already saved, so repeating the same edit does not fix the schedule.

Save the canvas

PATCH /api/v1/workflows/{id}/canvas

Saves the editor layout and, optionally, the compiled steps and trigger. Studio uses this endpoint for autosave.

FieldTypeRequiredMeaning
canvasStateobjectYesEditor blocks, edges, loop and parallel membership, notes and viewport. Each top-level key you send replaces the stored value; keys you leave out stay as stored
stepsarrayNoReplacement steps. Left out, the saved steps do not change
triggerConfigobjectNoReplacement trigger. null returns 400
expectedUpdatedAtstring (ISO 8601)NoThe updatedAt you read. A later save returns 409

canvasState.notes is a patch keyed by note ID: { "body": "...", "position": { "x": 0, "y": 0 } } creates or replaces a note, null deletes it, and IDs you do not send stay as stored. A save that would leave more than 100 notes returns 400.

A save that sends steps or triggerConfig follows the version rules. A save of only canvasState never changes the version. On an active workflow, new steps must pass full validation, and changing the trigger type returns 409 (Pause the workflow before changing its trigger).

The response has id, version, versionLabel, canvasState, isVisual, lastSavedAt, lastModifiedAt and updatedAt.

Every trigger type accepts enabled (boolean; missing means true). It is the trigger's on/off switch: while it is false, a run the trigger starts (schedule, webhook, inbound email, event) is recorded as cancelled with error code TRIGGER_DISABLED and message Trigger disabled, and no step runs. Runs started with POST /api/v1/workflows/{id}/run, restarts and retries still run. A sub-workflow step whose workflow has its trigger off fails. On an active workflow, a save can change enabled alone; any other trigger change still returns 409. A triggerConfig that leaves out enabled keeps the stored value, as long as the trigger type is the same.

Activate (deploy) a workflow

POST /api/v1/workflows/{id}/activate

Makes the workflow active so its trigger can start it. This is what Deploy does in Studio. Allowed from draft, paused and active; an archived workflow returns 400 with details.allowedTransitions.

Activating also publishes. If the definition changed since the last publish, it becomes a new major version (for example v2.2 becomes v3.0) and publishedVersion is set to the new version. With nothing to publish, an active workflow returns 200 and nothing changes.

Before it activates, the API checks the steps and the trigger. Invalid steps, an event trigger with no event chosen, or a schedule trigger with neither cron nor interval return 400.

{
  "success": true,
  "data": {
    "id": "c3a1f7d2-58e4-4b9a-8f21-6d0e9b7c4a15",
    "status": "active",
    "version": 4,
    "versionLabel": "v3.0",
    "schedule": {
      "id": "0b6c2c55-1d8f-4a2b-9e37-5f4a8d1c2e90",
      "triggerScheduleId": "sched_123",
      "nextRunAt": "2026-02-12T23:00:00.000Z"
    }
  }
}

schedule appears only for schedule triggers. If the schedule or inbound address cannot be set up, the status goes back to what it was and the request returns 400; the publish stays, so the next activation does not bump the version again. A status change by another request while this one runs returns 409.

Pause a workflow

POST /api/v1/workflows/{id}/pause

Moves an active workflow to paused. Runs already in progress are not cancelled. The response is { "id": "...", "status": "paused" }.

A schedule trigger's schedule is turned off. If that fails, the workflow is still paused and the request returns 502; scheduled starts are skipped while it is paused.

Archive and unarchive

POST /api/v1/workflows/{id}/archive
POST /api/v1/workflows/{id}/unarchive

Archive stops a draft, active or paused workflow from running. It removes the schedule, deactivates the inbound email address and keeps the workflow readable under status=archived. Update, canvas save, rollback and schedule update then return 409 until you unarchive. Archiving an archived workflow returns 200 and changes nothing.

Unarchive returns the workflow to draft. Activate it again to make it runnable. An inbound email workflow gets a new address when it is activated again. Unarchiving a workflow that is not archived returns 400.

Both return { "id": "...", "status": "archived" } or { "id": "...", "status": "draft" }.

Delete a workflow

DELETE /api/v1/workflows/{id}

Soft-deletes the workflow from any status and removes its schedule. It no longer appears in any list or lookup. Its past runs stay readable through the Runs API. To keep the workflow viewable, archive it instead.

{ "success": true, "data": { "deleted": true } }

Run a workflow

POST /api/v1/workflows/{id}/run

Starts a run of an active workflow. Any other status returns 400 with code WORKFLOW_NOT_ACTIVE.

FieldTypeDefaultMeaning
inputobject{}Run input. Checked against the workflow's inputSchema; a mismatch returns 400 VALIDATION_ERROR with details.errors
idempotencyKeystringIf a run of this workflow already has this key, that run is returned instead of a new one
modestringasyncasync returns at once; sync waits for the result
maxExecutionTimeMsnumber30000Sync only. 1,000 to 300,000
onTimeoutstringreturn_run_idSync only. return_run_id or throw_error
onFailurestringthrow_errorSync only. throw_error or switch_to_async
curl -X POST 'https://workflow.loopfour.ai/api/v1/workflows/c3a1f7d2-58e4-4b9a-8f21-6d0e9b7c4a15/run' \
  -H 'x-api-key: wfk_live_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{ "input": { "invoiceId": "1042" }, "idempotencyKey": "invoice-1042-status" }'

Async (default), 202:

{
  "success": true,
  "data": {
    "run_id": "4e1f9c02-7b3d-4a1e-9c85-2f6d0b8e1a47",
    "status": "pending",
    "workflow_id": "c3a1f7d2-58e4-4b9a-8f21-6d0e9b7c4a15",
    "mode": "async"
  }
}

Poll GET /api/v1/runs/{id} with run_id until the status is completed, failed or cancelled.

Sync, completed, 200: the same fields with "mode": "sync", output and duration_ms.

Sync, timed out: with onTimeout: "return_run_id", 202 with "timed_out": true and "mode": "async". With throw_error, 408 with code SYNC_TIMEOUT and details.run_id.

Sync, failed: with onFailure: "throw_error", 500 with the run's error code (or WORKFLOW_FAILED) and details.run_id. With switch_to_async, 202 with data.error and "switched_to_async": true.

Idempotency match, 200: run_id, status, workflow_id and message: "Existing run returned (idempotency key matched)".

If the run cannot be queued, the request returns 500 with code TRIGGER_ERROR (or 202 with switched_to_async when onFailure is switch_to_async).

List a workflow's runs

GET /api/v1/workflows/{id}/runs
Query parameterTypeMeaning
statusstringpending, running, waiting_approval, completed, failed, suspended, cancelled or paused
triggerTypestringapi, manual, webhook, schedule, event, connection_event or subworkflow
from, tostring (ISO 8601)Start time range
searchstringRun ID prefix
limit, cursorDefault 20, maximum 100

Each entry has id, workflowId, workflowName, status, triggerType, startedAt, completedAt, durationMs, error and createdAt. Pagination works as in List workflows.

Clone a workflow

POST /api/v1/workflows/{id}/clone
FieldTypeRequiredMeaning
namestringYes1 to 255 characters

Copies the trigger, steps, schemas and settings into a new draft at v0.1. The response is 201 with the new workflow and clonedFrom set to the source ID.

Versions

How versions change

A save that changes the definition (trigger, steps, input or output schema, or settings) starts a new minor version (for example v3.0 to v3.1) when any of these is true:

  • the current version is the published one;
  • more than 30 minutes passed since the last definition save;
  • a run used the current version.

Otherwise it updates the current version in place: version stays the same and updatedAt moves. A save that does not change the definition never starts a version. Activating publishes a new major version, and rollback always starts a new minor version.

List versions

GET /api/v1/workflows/{id}/versions
{
  "success": true,
  "data": {
    "workflowId": "c3a1f7d2-58e4-4b9a-8f21-6d0e9b7c4a15",
    "currentVersion": 4,
    "currentVersionLabel": "v2.2",
    "publishedVersion": 2,
    "workflowStatus": "active",
    "versions": [
      { "version": 4, "versionLabel": "v2.2", "versionMajor": 2, "versionMinor": 2, "status": "active", "isCurrent": true },
      {
        "version": 3,
        "versionLabel": "v2.1",
        "versionMajor": 2,
        "versionMinor": 1,
        "name": "Month-end close report",
        "description": null,
        "changeSummary": "Added the bank balance check",
        "status": "archived",
        "createdAt": "2026-02-09T10:00:00.000Z",
        "isCurrent": false
      }
    ]
  }
}

When publishedVersion differs from currentVersion, the workflow has unpublished edits.

Get one version

GET /api/v1/workflows/{id}/versions/{version}

version is the integer version, not the label. A value that is not a number returns 400; an unknown version returns 404. The response has the version's name, description, trigger, steps and labels.

Compare two versions

GET /api/v1/workflows/{id}/versions/compare?from=2&to=4

from and to are required integers. The response lists changes.name and changes.description (from, to, changed), changes.trigger.changed, and changes.steps with the added, removed and modified step IDs.

Roll back to a version

POST /api/v1/workflows/{id}/versions/{version}/rollback
FieldTypeMeaning
changeSummarystringReason, up to 500 characters
expectedStatus, expectedVersion, expectedUpdatedAtAs in Update a workflow

Copies the chosen version into a new minor version. On an active workflow the next run uses it; activate again to publish it. Rolling back to the current version returns 400, and an archived workflow returns 409. The response includes rolledBackFrom, rolledBackTo and their labels, plus the new version, versionLabel, trigger and steps.

Schedule

These endpoints apply to workflows with a schedule trigger. For cron syntax and time zones, see Schedule trigger.

Get the schedule

GET /api/v1/workflows/{id}/schedule

Returns null in data when the trigger is not a schedule or no schedule exists yet. Otherwise the schedule has id, workflowId, scheduleType, cronExpression, interval, timezone, enabled, nextRunAt, lastRunAt, lastRunId, runCount, consecutiveFailures, lastError, createdAt and updatedAt.

Update the schedule

PUT /api/v1/workflows/{id}/schedule
FieldTypeMeaning
cronstringCron expression
intervalobjectvalue (1 to 525,600) and unit (minutes, hours, days or weeks)
timezonestringIANA time zone, for example America/New_York
enabledbooleanLeft out, a schedule that is off stays off
{ "cron": "0 10 * * 1-5", "timezone": "America/Los_Angeles", "enabled": true }

The response is { "updated": true, "scheduleId": "...", "nextRunAt": "..." }. The change is saved to the trigger, so it follows the version rules. A workflow whose trigger is not a schedule returns 400, and so does a request that leaves neither cron nor interval. An archived workflow returns 409. If the schedule cannot be turned off when enabled is false, the request returns 502 and the trigger is not changed.

Start a scheduled run now

POST /api/v1/workflows/{id}/schedule/trigger

Starts one run now, as if the schedule had fired. The workflow must be active with a schedule trigger, otherwise 400. When the trigger is turned off (enabled is false), the response is 409; to test the workflow, start a run with POST /api/v1/workflows/{id}/run. The response is 202:

{
  "success": true,
  "data": {
    "runId": "4e1f9c02-7b3d-4a1e-9c85-2f6d0b8e1a47",
    "status": "pending",
    "workflowId": "c3a1f7d2-58e4-4b9a-8f21-6d0e9b7c4a15",
    "triggeredAt": "2026-02-12T18:30:00.000Z"
  }
}

Status transitions

FromAllowed to
draftactive, archived
activepaused, archived
pausedactive, archived
archiveddraft (unarchive)

Delete is allowed from any status. An invalid transition returns 400:

{
  "success": false,
  "error": {
    "code": "BAD_REQUEST",
    "message": "Cannot transition from 'archived' to 'active'",
    "requestId": "req_123",
    "details": { "currentStatus": "archived", "targetStatus": "active", "allowedTransitions": ["draft"] }
  }
}

Errors

StatusCodeCause
400BAD_REQUESTInvalid steps (Invalid workflow steps: ...), invalid transition, schedule problems
400ZodError in error.nameBody or query does not match the schema. error.issues lists each problem
400WORKFLOW_NOT_ACTIVERun requested on a workflow that is not active
400VALIDATION_ERRORRun input does not match inputSchema
401UNAUTHORIZEDMissing or invalid API key
403FORBIDDENKey lacks the scope
404NOT_FOUNDWorkflow or version not found
408SYNC_TIMEOUTSync run timed out with onTimeout: "throw_error"
409CONFLICTArchived workflow edited, expected* mismatch, or a concurrent change
429RATE_LIMITEDRate limit exceeded
500TRIGGER_ERROR, WORKFLOW_FAILEDRun could not be queued, or a sync run failed
502UPSTREAM_ERRORSchedule could not be updated after a save or pause

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

On this page