Trigger Run
Start an ActionFlow execution with POST /api/runs, including the public payload envelope, options, response envelopes, validation timing, and status behavior.
Trigger Run
Start a saved ActionFlow execution by sending a payload to the public run endpoint. The API runs a saved Start-compatible graph; it does not create an API-specific trigger node or a per-flow /run endpoint.
Endpoint
POST /api/runsTry it
Live requests run from your browser. Open the API Explorer to try every operation from one screen.
/api/runsTrigger a run
Requests go to the live API from your browser. The docs server never sees this key.
This request can create, change, or cancel real work.
curl -X POST 'https://api.actionflows.ai/api/runs' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -H 'Accept: application/json'
Response
Send a request to see the response.
Authentication
Required. External integrations should send an API key as a Bearer token:
Authorization: Bearer YOUR_API_KEYThe same route also accepts an authenticated application session, such as a valid signed-in app session cookie. Use an API key for an external integration rather than a session cookie.
API key scope is enforced separately:
- An organization-scoped key is fixed to its organization. The request cannot switch organizations, and the payload
organizationIdmust match that key's organization. - A user-scoped key has no
activeOrganizationId; it must supply an authorizedorganizationIdin the request, and the route checks the caller's organization permission. - An authenticated application session may use its active or requested organization and checks the session user's permission.
The run's user attribution comes from the authenticated context. The current REST route ignores a client-supplied userId when assigning it.
Request body
{
"payload": {
"organizationId": "org_123",
"actionFlowId": "flow_123",
"inputs": {
"node_abc_prompt": "Hello"
},
"trigger": {
"body": {
"event": "order.created"
},
"headers": {
"content-type": "application/json"
},
"method": "POST",
"query": {
"source": "backend"
}
},
"triggerSource": "api"
},
"idempotencyKey": "order-created-ORD-12345",
"delay": "PT5M",
"tags": ["production", "api-triggered"],
"maxAttempts": 3,
"maxDuration": 3600
}The request has two layers:
payloadidentifies the saved graph and contains optional run inputs and a trigger envelope.- The remaining top-level fields are execution options.
The REST schema validates the top-level options (delay, idempotencyKey, tags, maxAttempts, and maxDuration), not the nested payload in depth. The route separately checks that payload.actionFlowId and payload.organizationId are present. Nested inputs, trigger, and triggerSource are not schema-validated at this boundary. There is no public payload.data field; use payload.inputs for per-run input overrides.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
payload | object | Expected | Run payload envelope; the route does not deeply schema-validate it |
payload.organizationId | string | Required for a valid run | Organization that owns the flow; checked separately after top-level option validation |
payload.actionFlowId | string | Required for a valid run | Saved ActionFlow ID; checked separately after top-level option validation |
payload.inputs | object | No | Per-run overrides keyed as {nodeId}_{inputName}; not schema-validated at this boundary |
payload.trigger | object | No | Envelope merged onto the saved Start node: body, headers, query, and method; not schema-validated at this boundary |
payload.triggerSource | string | No | Canonical source metadata; the route does not enforce an enum, and API-key calls default to api |
idempotencyKey | string | No | Stable key used by local queue/direct admission, scoped by organization and ActionFlow; request payloads are not compared |
delay | string | No | Delay passed to the execution service; the schema documents ISO 8601 such as PT5M |
tags | string[] | No | Caller tags associated with the run |
maxAttempts | number | No | Positive retry-budget override passed to the execution service |
maxDuration | number | No | Positive maximum run duration in seconds |
payload.inputs uses the same {nodeId}_{inputName} form as the Actionflow Studio Flow Inputs form. A trigger body is separate from input overrides; the trigger fields are merged onto the Start node as body, headers, query, and method.
The canonical Flow triggerSource values are manual, api, webhook, schedule, subflow, and unknown. API-key requests default to api; webhook and scheduled paths set their own source. The route does not reject a non-canonical string at this boundary, so use only the canonical values and do not treat an arbitrary value as a supported trigger or authorization mechanism. subflow is the compatibility/display source for subflow-shaped execution paths; it does not enable the current nested-dispatch limitation described in Subflow.
The Flow task definition currently defaults to one attempt because a retry can repeat non-idempotent side effects. maxAttempts and maxDuration are passed as execution-service overrides; maxDuration is measured in seconds. The gateway validates delay as a string and passes it through; use a supported duration such as PT5M (short execution-service forms such as 30s, 5m, 1h, and 2d are also used).
Validation timing
POST /api/runs first authenticates the request and validates the top-level run options. It treats payload as an unvalidated nested value, then separately checks that payload.actionFlowId and payload.organizationId are present. A payload object missing either required ID returns 400; do not rely on a stable 400 contract for a missing or null payload because those cases are not handled as a documented malformed-envelope response.
After the ID checks, the gateway loads the saved graph and calls validateFlow before enqueueing. The saved graph is validated before payload.inputs and payload.trigger are merged into the execution copy. A run input therefore cannot bypass a missing required value in the saved graph.
A request rejected by these pre-enqueue checks may not create a run-history row. Scheduled dispatch and queue-dispatched runs have different timing; see Scheduled and Queue API.
Success responses
201 Created
The run was dispatched immediately:
{
"success": true,
"data": {
"id": "run_123",
"runHistoryId": "runhist_123",
"status": "dispatched",
"triggerRunId": "run_123"
}
}id and triggerRunId identify the runtime run. 201 confirms admission, not completion.
202 Accepted
The run was placed in the organization queue:
{
"success": true,
"data": {
"position": 2,
"queueId": "queue_123",
"runHistoryId": "runhist_123",
"status": "waiting",
"triggerRunId": null
}
}position can be null. Save queueId and runHistoryId, then use queueId with Get Queued Run. The List Queued Runs endpoint is an active-queue view: it returns only waiting and dispatching items, so it will not show dispatched, failed, or cancelled items. Poll the detail endpoint until it reports one of those states and retrieve triggerRunId when present from that response; then use the runtime ID with Get Run. A 202 does not mean the graph completed.
Error and status behavior
| Status | Current behavior |
|---|---|
201 | Immediate dispatch. |
202 | Queue admission. |
400 | Top-level option schema failure, a missing required payload ID, or saved-flow validation failure. |
401 | Missing or invalid authentication. |
403 | The caller is not authorized for the organization or flow. |
404 | The flow was not found in the authorized organization. |
409 | Not emitted by the current REST route. A passive-flow 409 is a webhook response; the API/Actionflow Studio run path has no equivalent active-state check. |
422 | Not emitted by the current REST route. Saved-flow validation is 400 here; webhook validation uses 422. |
429 | The organization/user run-create rate limit was exceeded. |
402 | Organization credits were insufficient during admission. |
500 | The run could not be admitted or an unexpected error occurred. |
400: top-level option or required-ID failure
A top-level option failure can return issues; for example, an invalid option produces:
{
"success": false,
"error": "Invalid request body",
"issues": [
{
"path": ["maxAttempts"],
"message": "Expected number"
}
]
}400: flow validation
A saved graph that fails validateFlow returns validation errors and warnings:
{
"success": false,
"error": "Flow validation failed",
"message": "Flow validation failed",
"validationErrors": [
{
"message": "Node \"sendEmail\" requires input \"to\""
}
],
"validationWarnings": []
}Fix and save the reachable graph before retrying. Do not rely on payload.inputs to bypass this check. Validation warnings are returned for review but do not block a run with no validation errors.
401 and 403
401 means the request has no valid API key or authenticated application session. 403 means the authenticated caller cannot access the requested organization or flow. Do not use a different organizationId to probe or access a foreign flow.
404
404 means the flow was not found in the authorized organization. The endpoint does not create a flow and does not expose a /flows/{actionFlowId}/run route.
429
The run-create limiter is keyed by the requested organization and authenticated user. Retry with backoff and keep the same idempotencyKey for the same logical request.
409 and 422 status ownership
The current REST route does not return 409 for a passive flow and does not return 422 for saved-flow validation. Those statuses belong to the current webhook handler:
409means the webhook found a passive flow and rejected it before enqueue.422means the webhook's saved graph failedvalidateFlowbefore enqueue.
This distinction prevents clients from applying webhook state codes to REST or assuming that a REST 400 is a webhook response.
Idempotency and queue handoff
Use a stable idempotencyKey for retries of one logical request. The local admission record is scoped by organization and actionFlowId; the lookup does not compare request payloads. Reusing a key for a different payload in the same organization and ActionFlow refers to the existing admission. For direct admission, success requires the execution response to contain a runtime id; an otherwise successful response without that id is an admission failure and may leave a history row in executing. A direct duplicate whose existing admission is executing/not dispatched and lacks triggerRunId also fails. These direct-admission failures return a generic REST 500. A dispatched direct duplicate with a triggerRunId is successful and returns 201. In queue mode, a waiting duplicate can be republished, and a dispatched duplicate with a triggerRunId returns HTTP 202 with status: dispatched. A dispatched duplicate without a triggerRunId and failed or cancelled terminal duplicates are unavailable and return a generic REST 500; none is admitted as a new run. Keep the queue ID and the idempotency key while polling or investigating a retry.
The organization tag is added automatically to the run. Caller-supplied organization ownership tags are removed, and organization tags are omitted from public run metadata.
Example requests
cURL
curl -X POST https://api.actionflows.ai/api/runs \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"payload": {
"organizationId": "org_123",
"actionFlowId": "flow_123",
"inputs": { "node_abc_prompt": "Hello" },
"trigger": { "body": { "event": "order.created" }, "method": "POST" }
},
"idempotencyKey": "order-created-ORD-12345",
"tags": ["production", "api"]
}'JavaScript
const response = await fetch("https://api.actionflows.ai/api/runs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ACTIONFLOWS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
payload: {
organizationId: "org_123",
actionFlowId: "flow_123",
inputs: { node_abc_prompt: "Hello" },
},
idempotencyKey: "order-created-ORD-12345",
}),
});
const result = await response.json();
if (response.status === 201) {
console.log("Run ID:", result.data.id);
} else if (response.status === 202) {
console.log("Queue ID:", result.data.queueId);
} else {
console.error(result.error, result.issues, result.validationErrors);
}