Skip to content

Workflow API

The workflow API runs published workflows from your own systems and reads their results. It is a deliberately small surface: two authenticated endpoints plus the public webhook ingest.

Base URL: https://api.app.getcontroller.ai

Authenticate with an API key (sk- prefix) as a Bearer token:

Authorization: Bearer sk-...

A cai_ CLI credential is a different credential type: it authenticates against the same host for CLI operations but is rejected by these endpoints (403). Server-to-server integrations use sk- keys.

API-initiated runs are billable like any other run, and a request can be refused up front with 400 "You have no remaining usage balance for this period."

POST /api/v1/run-workflow
{
"workflow_id": 123,
"flow_id": "aB3xK9pQr2Zm",
"flow_inputs": { "customer_email": "jane@acme.com" },
"mode": "live",
"callback_url": "https://example.com/hooks/controller"
}
  • flow_id is a server-generated id, not a name. Get it (with each flow’s declared inputs and outputs) from cai flow list, or via POST /workflow/get-workflow-flows with the same API key.
  • flow_inputs is validated strictly before anything runs: keys may be input names or display names; unknown keys are rejected (not ignored); values are type-checked; required inputs must be present; the same input under two keys is an error.
  • mode is live (default — latest published release) or dev (the development version). ⚠️ dev is not a simulation: it is a real, billable run that performs real external actions; only the data/file rail is isolated. See dev runs.

Success response: { "status": "pending", "execution_id": "...", "error": null }.

Errors from this endpoint are 400s with the same envelope — including “Workflow not found” (unknown or not yours: API access is strictly per-account; org sharing does not extend keys). There is no 404 and no rate-limit 429. A 401 (bad/missing key) comes from the auth layer with a different body shape: {"statusCode":401,"error":{...},"data":null} — parse defensively.

GET /api/v1/workflow-results/:execution_id

Response: { "status": "...", "execution_id": "...", "workflow_outputs": [{ "name": "...", "value": ... }], "error": "" }.

Statuses: pending, running, paused, completed, completed_with_error, failed, cancelled. Terminal states are completed, completed_with_error, failed, and cancelled — a polling loop must stop on all four, not just the first. paused (an approval gate or manual pause) can last indefinitely and cannot be resumed via this API — resolution happens in the app.

Error contract: 404 when the execution doesn’t exist, 403 when it belongs to another account, and 400 when it has aged past your plan’s run-history window — results are not retrievable forever (retention).

Outputs are the merged results of every Return response node that completed in the run — last write wins per field. A flow with no Return response completes with workflow_outputs: []: a silent-success trap if you expected data. Design one Return per path (flow control).

callback_url receives a POST when the run reaches a terminal state (all four — not only success). Delivery is at-most-once: a single unsigned POST, not retried, not persisted across restarts. Treat callbacks as a latency optimization and keep polling as the source of truth for anything that matters. (Shape quirk: a run with no outputs sends workflow_outputs: null in the callback but [] on the GET.)

POST /api/events/:webhookId

The URL shown on a webhook trigger. It is unauthenticated — the opaque id is the only secret, so treat the URL like a credential. It returns 200 even when no enabled trigger matches (events are silently dropped), so a 200 is delivery, not processing, confirmation. What happens next is the trigger lifecycle.

Write a client for this workflow: fetch the flow id and inputs with cai flow list, call run-workflow, and poll with a loop that handles all four terminal states plus paused.