Workflow API
How does your service run a flow and read its answer? Send an authenticated request, keep the execution ID it returns, then poll for status and outputs. For Invoice intake, call Check invoice with an invoice ID and an amount, then read its review decision.
| Request | Purpose |
|---|---|
POST /api/v1/run-workflow | Start one flow. |
GET /api/v1/workflow-results/:execution_id | Read status and outputs. |
The default host is https://api.app.getcontroller.ai. These endpoints run workflows. You author those workflows through the app, the CLI, or MCP.
Read the selected version’s inputs
Section titled “Read the selected version’s inputs”In the app: open Settings → API keys → Create new secret key, then choose Copy API key on its row. Use the copied value as <apiKey> below. Supply it as Authorization: Bearer …. Keys start with sk-.
Open Invoice intake, select the intended Dev / Live version and Check invoice, then open Triggers → Start this flow via API. The panel generates sample values. For saved-record inputs it generates objects, even though those inputs actually require ID strings. Check the selected version’s Inputs before you use its sample.
With an assistant: this page passes workflow context explicitly with --workflow <workflowId>. Start by listing your workflows:
cai workflow list --limit 100 --jsonMCP uses workflow_list with limit: 100, its maximum.
Note Invoice intake’s data.items[].id as <workflowId>, then check data.hasMore. The default is 25 rows, and neither interface offers a next-page cursor. If the workflow is absent with hasMore: true, find it through app search and copy its ID from /workflows/<workflowId>/versions/….
For the Live request below, read the newest published release:
cai workflow releases --workflow <workflowId> --jsonMCP uses workflow_release_list with workflowId.
Note data.releases[0].workflowVersionStateId as <workflowVersionStateId>. An empty list means nothing is published.
Now read that release’s document:
cai workflow document --workflow <workflowId> --state <workflowVersionStateId> --jsonMCP uses workflow_document_get with workflowId and stateId.
Find Check invoice in data.document.flows, note its id as <flowId>, and inspect its flowInputs.
For a Dev request, read the development document instead:
cai workflow document --workflow <workflowId> --jsonflow list and flow get read development. A flow or input change that exists only in Dev does not describe Live.
Start Check invoice
Section titled “Start Check invoice”Save this as invoice-request.json, replacing the placeholders with the IDs just read:
{ "workflow_id": "<workflowId>", "flow_id": "<flowId>", "mode": "live", "flow_inputs": {"Invoice ID": "INV-104", "Amount": 120}}Send exactly "dev" or "live". If you omit mode, or send "", null, false, or 0, the request selects Live. Any other value is rejected. Live selects the current live release, and Dev selects development. Both perform real actions and spend usage. Development and Live select different data and file state.
Post that file to the run endpoint:
curl --silent --show-error \ 'https://api.app.getcontroller.ai/api/v1/run-workflow' \ --header 'Authorization: Bearer <apiKey>' \ --header 'Content-Type: application/json' \ --data @invoice-request.jsonAcceptance returns HTTP 200 and a numeric execution ID:
{"status":"pending","execution_id":301,"error":null}Which inputs are accepted?
Section titled “Which inputs are accepted?”| Field or value | Rule |
|---|---|
workflow_id | Positive integer or numeric string. |
flow_id | Nonempty string, trimmed before lookup. |
flow_inputs | JSON object; omission supplies {}. |
| Input keys | Exact ID, stored name, or display name, checked in that order. Unknown keys and conflicting aliases reject. |
| Required inputs | Missing or null rejects. |
| Optional inputs | Omission or null becomes null; neither saved test values nor declared defaults are substituted. |
| Non-null values | Must match the declared type; Amount takes 120, not "120". Lists require their declared item type. Custom objects receive no nested-field validation. |
0, false, and empty text count as supplied values when their types match. Dates accept strict ISO-8601 strings, or finite epoch milliseconds that produce a valid date. An empty "" passes even for a required date and becomes null, so supply a real date.
Saved-record inputs accept ID strings or lists of strings at submission. An empty ID, a record missing from the selected version, and an inactive collection each fail after acceptance, before nodes run. An existing active record’s collection is not checked against the declared input type, so supply an ID from the declared collection.
Poll until the run ends
Section titled “Poll until the run ends”Note execution_id as <workflowExecutionId>, then read the result:
curl --silent --show-error \ 'https://api.app.getcontroller.ai/api/v1/workflow-results/<workflowExecutionId>' \ --header 'Authorization: Bearer <apiKey>'| Status | Next action |
|---|---|
pending, running | Poll the same ID again. |
paused | Inspect the existing run; these endpoints cannot resume it. |
completed | Stop polling and check values. |
completed_with_error | Stop polling; inspect the handled error and continued path. |
failed, cancelled | Stop polling; inspect prior effects before repeating. |
For INV-104, expect:
{ "status": "completed", "execution_id": 301, "workflow_outputs": [ {"name":"invoice_id","value":"INV-104"}, {"name":"review_needed","value":false} ], "error": ""}Outputs come from the entry flow’s completed Return response nodes. When two of them share a name, the later node execution ID overwrites the earlier value. With no outputs, GET returns workflow_outputs: []. Compare the values, not just the status, and debug the run when they differ.
Handle rejected requests
Section titled “Handle rejected requests”Synchronous request and input checks, and dispatch failures, return HTTP 4xx/5xx with {"status":"failed","execution_id":null,"error":"…"}. Once a request reaches pending, graph, record-loading, and runtime failures appear when you poll that ID.
Authentication, GET, and JSON-parser failures instead return {statusCode, error: {code, message}, data: null}. Malformed JSON gives HTTP 400 with Invalid JSON body. Check your request payload syntax. A JSON body over 10 MB is rejected before execution.
| HTTP status | Cause |
|---|---|
| 400 POST | Invalid inputs or truthy mode other than live/dev; missing/foreign workflow; unavailable version/flow; no usage balance. |
| 401 | Missing/invalid authentication. |
| 404 / 403 GET | Missing execution / another owner’s execution. |
| 400 GET | Outside the plan’s run-history window. |
| 503 / 500 POST | Unavailable engine / unexpected failure; message: Internal server error. |
A dispatch failure can leave a failed history entry despite execution_id: null, because the record is created before dispatch. Each accepted POST creates another execution, so do not resubmit because a response is delayed.
Receive a callback
Section titled “Receive a callback”API-key callers can add a receiver they control:
{"callback_url":"https://example.com/hooks/controller"}callback_url is not validated. A falsey value is ignored. An invalid or unreachable truthy value still allows acceptance. Delivery errors are only logged, and do not change execution status.
The server waits for completion, then attempts one unsigned POST, only if the status is terminal. Pausing delays completion. If the wait fails while the status is still nonterminal, delivery is skipped. Delivery has no retry, and does not survive an API-process restart. Keep polling, and use a callback to schedule a GET check.
The body has status, execution_id, workflow_outputs, and error. Absent outputs can be null here, while GET returns an array.
Run through an assistant
Section titled “Run through an assistant”Assistants normally use this command instead of POST. It defaults to Dev, and Live requires --confirm-live.
cai run flow <flowId> --workflow <workflowId> \ --inputs '{"Invoice ID":"INV-104","Amount":120}' \ --target live --confirm-live --jsonMCP uses run_flow with workflowId, flowId, parsed inputs, target: "live", and confirmLive: true.
CLI credentials need workflows:write for POST, cannot include callback_url, and cannot use GET-results. A Dev REST request made with a CLI credential is also checked against the workflow’s daily assistant test-run cap. Inspect results through Debug a Run.
Webhook ingestion uses the separate POST /api/events/:webhookId. Follow Start with Triggers.