Skip to main content

API

Start a saved Start-compatible ActionFlow with POST /api/runs, including authentication, request options, validation timing, and queue handoff.

API

Use the public run endpoint to start a saved graph from your backend. The endpoint is POST /api/runs; there is no per-flow /flows/{actionFlowId}/run URL and no API start-node type.

Use a saved graph with a Start node for this path. API and webhook calls run the saved Start-compatible graph; they do not add or change a start node.

Authentication

External integrations should use an API key:

Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

The same REST route also accepts an authenticated application session, such as a valid signed-in app session cookie. A session is appropriate for an authenticated application request; an external integration should use an API key rather than a session cookie.

API key scope is enforced separately:

  • An organization-scoped key is fixed to its organization. The request cannot switch to another organization, and the organizationId in the payload must match that key's organization.
  • A user-scoped key has no activeOrganizationId; it must supply an authorized organizationId in the request, and the route checks the caller's organization permission.
  • An authenticated application session may use its active organization or an authorized requested organization, and the route checks the session user's organization permission.

The gateway assigns run attribution from the authenticated context. The current REST route ignores a client-supplied payload userId when assigning attribution; do not send it to change who owns or is associated with the run.

Request envelope

{
  "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"],
  "maxAttempts": 3,
  "maxDuration": 3600
}

payload identifies the saved flow. The REST schema validates the top-level options (delay, idempotencyKey, tags, maxAttempts, and maxDuration); it does not deeply schema-validate the nested payload. The route separately checks that payload.actionFlowId and payload.organizationId are present before it continues. payload.inputs contains per-run overrides keyed as {nodeId}_{inputName}. payload.trigger is a separate envelope whose body, headers, query, and method fields are merged onto the saved Start node. The webhook path supplies its own envelope; do not put API run options inside payload.

FieldLocationMeaning
organizationIdpayloadOrganization that owns the flow
actionFlowIdpayloadSaved ActionFlow to run
inputspayloadPer-run input overrides using {nodeId}_{inputName} keys
triggerpayloadOptional body, headers, query, and method envelope merged onto Start
triggerSourcepayloadCanonical source metadata; the route does not enforce an enum, and API-key calls default to api
idempotencyKeytop levelStable key used by local queue/direct admission, scoped by organization and ActionFlow; request payloads are not compared
delaytop levelDelay string passed to the execution service; PT5M is the schema example
tagstop levelCaller tags; the platform adds and protects its organization tag
maxAttemptstop levelPositive retry-budget override passed to the execution service
maxDurationtop levelPositive run-duration override in seconds

The current Flow task definition defaults to one attempt because a retry can repeat non-idempotent side effects. A supplied retry value is forwarded as an execution-service override; it is not a promise of checkpoint-safe replay.

Validation timing and inputs

The REST route first validates the top-level run options, then treats payload as an unvalidated nested value and separately checks that payload.actionFlowId and payload.organizationId are present. Nested inputs, trigger, and triggerSource are not schema-validated at this boundary. A payload object missing either required ID is rejected with 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.

The canonical triggerSource values are manual, api, webhook, schedule, subflow, and unknown. The route does not reject a non-canonical string at this boundary, but only those values are supported by the product contract; an arbitrary value is not a supported trigger or authorization mechanism.

After the ID checks, the gateway loads the saved graph and calls validateFlow before enqueueing. API validation examines the saved graph before payload.inputs and payload.trigger are merged into the execution copy. Consequently, a blank required field in the saved graph can still produce a validation error even when the request contains a run input for it. A request rejected by these pre-enqueue checks does not create a run-history row.

Responses

201 Created: dispatched immediately

When the run is admitted directly, the endpoint returns 201 and a data object containing the runtime run ID and its history ID:

{
  "success": true,
  "data": {
    "id": "run_123",
    "runHistoryId": "runhist_123",
    "status": "dispatched",
    "triggerRunId": "run_123"
  }
}

201 means the execution service accepted the run; it does not mean the graph completed.

202 Accepted: placed in the queue

When the organization run queue is enabled, the endpoint admits the run to the queue and returns 202 (the queue consumer handles concurrency):

{
  "success": true,
  "data": {
    "position": 2,
    "queueId": "queue_123",
    "runHistoryId": "runhist_123",
    "status": "waiting",
    "triggerRunId": null
  }
}

Save queueId and use it with Get Queued Run, which is the detail endpoint for observing the item after admission. The List Queued Runs endpoint is an active-queue view: it returns only waiting and dispatching items, so it will not show a dispatched, failed, or cancelled item. 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. 202 is an admission response, not a completion response.

Status behavior

StatusCurrent POST /api/runs behavior
201The run was dispatched immediately.
202The run was accepted into the organization queue.
400A top-level run option failed schema validation, a required payload ID was missing, or the saved graph failed validateFlow.
401Authentication is missing or invalid.
403The authenticated caller cannot access the requested organization or flow.
404The flow does not exist in the authorized organization.
409Not emitted by the current REST route. A passive-flow 409 belongs to the webhook path; the API/Actionflow Studio run path has no equivalent active-state check.
422Not emitted by the current REST route. Saved-flow validation is returned as 400; 422 is used by the webhook path.
429The organization/user run-create rate limit was exceeded.
402Organization credits were insufficient during admission.
500The run could not be admitted or an unexpected admission error occurred.

For a 400 caused by a top-level option, the response can contain issues. A payload object missing either required ID returns the simpler invalid-request response. For graph validation, the response contains validationErrors and validationWarnings; warnings do not block admission. Do not treat any of these responses as a queued run.

Idempotency, delay, and tags

Use a stable idempotencyKey for a logical request and reuse it when retrying after a network timeout. 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. Retain the queue ID and original key while diagnosing a retry.

delay is passed through to the execution service. The gateway validates it as a string; use a supported duration such as PT5M (the documented schema example). Existing execution-service examples also use short forms such as 30s, 5m, 1h, and 2d.

Caller tags are sanitized so an organization ownership tag cannot be replaced. The organization tag is added automatically and omitted from public run metadata.

Example

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"]
  }'
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(result.data.id);
} else if (response.status === 202) {
  console.log(result.data.queueId);
} else {
  console.error(result.error, result.validationErrors, result.issues);
}

On this page