Workflow runs
Executing a workflow runs the whole pipeline on Corte's servers and records a run — a status plus one result entry per node. API and MCP runs use the same runner as the app canvas, so behavior is identical wherever a run starts.
The loop is: fetch the workflow to learn its input node ids
(GET /v1/workflows/:id),
execute with overrides, then poll the run.
The run object
| Field | Type | Description |
|---|---|---|
id | string | Run id (UUID). |
workflowId | string | The workflow that produced it. |
status | enum | running · complete · error · stopped. |
nodeResults | object | Map of node id → node result. Fills in incrementally while running. |
userId | string | The member who started the run. |
createdAt | string | ISO 8601. |
completedAt | string | null | ISO 8601, once terminal. |
Node results
nodeResults[nodeId]
| Field | Type | Description |
|---|---|---|
status | string | This node's state. |
detail | string | Extra context — the failure reason on an errored node. |
output | object | The node's primary out port: { type, value, storageId? }. |
outputs | object | Every output port, keyed by port id. Nodes that fan out (a text splitter emitting 1–12 branches) are only fully represented here. |
In a port value, type is image, video, audio, or text. For media,
value is a signed URL that expires and storageId is the durable handle —
use storageId to chain results into further API calls. For text, value is
the string itself.
Read output for the common case; read outputs when a node has multiple
ports. output is always populated alongside outputs for compatibility.
:::tip Run URLs are re-signed on every read Media URLs recorded in a run expire well before most runs are looked at again, so every fetch re-signs them from storage. A run you cached a week ago will have dead URLs; re-fetch it. :::
POST /v1/workflows/:id/execute
Run a saved workflow, optionally overriding input nodes for this run only. Saved values are never modified.
Body
| Name | Type | Required | Description |
|---|---|---|---|
inputs | object | no | Map of node id → { text } or { storageId }. Omitted nodes keep their saved values. |
wait | boolean | no | true blocks until the run finishes. Default false — returns immediately with a running run. |
projectId | string | no | Billing context for a personal workflow run inside a shared project. A workflow that already belongs to a project inherits it; passing a different one is an error. |
inputs rules
- Keys are node ids from the workflow's
graph.nodes, not node types. promptandgenerateTextnodes require{ "text": "…" }— non-empty.imageInputandvideoInputnodes require{ "storageId": "…" }— a committed object you can read.- Any other node type is rejected:
inputs.<id>: <type> nodes don't take run inputs. - An unknown node id is rejected:
inputs: no node <id> in this workflow.
Returns { "run": Run }.
curl -s -X POST https://api.corte.so/v1/workflows/7c93b1de-…/execute \
-H "Authorization: Bearer corte_sk_…" \
-H "Content-Type: application/json" \
-d '{
"inputs": {
"concept": { "text": "Alpine trail-running capsule, cold dawn light" },
"productPhoto": { "storageId": "b0e7c9a1-4d52-4f0e-8c31-77a2e5d9f6b8" }
},
"wait": false
}'
{
"run": {
"id": "d4a80b13-6c7e-42f9-8a05-91b2c7e3f660",
"workflowId": "7c93b1de-2a48-4f60-91b7-5d0e6a2c8f13",
"status": "running",
"nodeResults": {},
"userId": "3f8c1a92-b5d7-4e60-9c14-8a72d0e5b391",
"createdAt": "2026-07-28T16:40:12.774Z",
"completedAt": null
}
}
With wait: true the same call returns once the run settles, with
nodeResults fully populated. Fine for short pipelines; for anything long,
poll instead — a blocked HTTP connection is a fragile way to wait several
minutes.
Errors
| Code | When |
|---|---|
not_found | Unknown workflow, or one you can't reach. |
invalid_params | inputs isn't an object, names an unknown node, targets a node type that takes no inputs, omits text/storageId where required, references an unreadable storageId, or the workflow has no nodes. Also when projectId conflicts with the workflow's own project. |
insufficient_credits · owner_insufficient_credits | The payer's balance can't cover a step. |
GET /v1/workflows/:id/runs/:runId
Fetch one run. This is the polling endpoint, and it streams progress: while
status is running, each poll returns more entries in nodeResults as nodes
finish.
Runs of a project workflow are visible to every member. Runs of a personal workflow are visible only to you.
Returns { "run": Run }.
curl -s https://api.corte.so/v1/workflows/7c93b1de-…/runs/d4a80b13-… \
-H "Authorization: Bearer corte_sk_…"
{
"run": {
"id": "d4a80b13-6c7e-42f9-8a05-91b2c7e3f660",
"workflowId": "7c93b1de-2a48-4f60-91b7-5d0e6a2c8f13",
"status": "complete",
"nodeResults": {
"productPhoto": {
"status": "complete",
"output": { "type": "image", "value": "https://media.corte.so/…/in.jpg?X-Amz-Signature=…", "storageId": "b0e7c9a1-…" }
},
"concept": {
"status": "complete",
"output": { "type": "text", "value": "Alpine trail-running capsule, cold dawn light" }
},
"reshoot": {
"status": "complete",
"output": { "type": "image", "value": "https://media.corte.so/…/out.jpg?X-Amz-Signature=…", "storageId": "c74a1e08-…" },
"outputs": {
"out": { "type": "image", "value": "https://media.corte.so/…/out.jpg?X-Amz-Signature=…", "storageId": "c74a1e08-…" }
}
},
"final": {
"status": "complete",
"output": { "type": "image", "value": "https://media.corte.so/…/out.jpg?X-Amz-Signature=…", "storageId": "c74a1e08-…" }
}
},
"userId": "3f8c1a92-b5d7-4e60-9c14-8a72d0e5b391",
"createdAt": "2026-07-28T16:40:12.774Z",
"completedAt": "2026-07-28T16:41:58.203Z"
}
}
A failed run is a 200 with status: "error"; the node that broke carries the
reason:
{
"run": {
"status": "error",
"nodeResults": {
"reshoot": { "status": "error", "detail": "Upstream provider timed out after 180s" }
},
"completedAt": "2026-07-28T16:44:10.554Z"
}
}
Read your output node's entry for the pipeline's final result — or any intermediate node if you want the in-between artifacts too.
Polling
import time, requests
def wait_for_run(workflow_id, run_id, interval=3, timeout=1800):
url = f"https://api.corte.so/v1/workflows/{workflow_id}/runs/{run_id}"
deadline = time.time() + timeout
while time.time() < deadline:
run = requests.get(url, headers={"Authorization": f"Bearer {KEY}"}).json()["run"]
done = sum(1 for r in run["nodeResults"].values() if r["status"] == "complete")
print(f"{run['status']}: {done} nodes done")
if run["status"] != "running":
return run
time.sleep(interval)
raise TimeoutError(run_id)
GET /v1/workflows/:id/runs
List runs for a workflow, newest first.
Same visibility rule as fetching a single run: all members' runs for a project workflow, only yours for a personal one.
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
limit | number | 20 | Max 100. |
offset | number | 0 | Page through with limit. |
Returns { "runs": Run[], "total": number } — total counts every visible
run, not the page.
curl -s https://api.corte.so/v1/workflows/7c93b1de-…/runs \
-H "Authorization: Bearer corte_sk_…"
{
"runs": [
{ "id": "d4a80b13-…", "workflowId": "7c93b1de-…", "status": "complete", "nodeResults": { "…": "…" }, "userId": "3f8c1a92-…", "createdAt": "2026-07-28T16:40:12.774Z", "completedAt": "2026-07-28T16:41:58.203Z" },
{ "id": "9e2f7c40-…", "workflowId": "7c93b1de-…", "status": "error", "nodeResults": { "…": "…" }, "userId": "3f8c1a92-…", "createdAt": "2026-07-27T09:38:01.442Z", "completedAt": "2026-07-27T09:41:22.006Z" }
],
"total": 47
}
POST /v1/workflows/:id/runs/:runId/stop
Abort a run that's still running. Partial nodeResults are kept; status
becomes stopped.
Only the member who started the run may stop it. Idempotent — stopping an already-settled run returns it unchanged.
Nodes that already completed have already been billed. Stopping prevents further spend; it doesn't refund.
Returns { "run": Run }.
curl -s -X POST https://api.corte.so/v1/workflows/7c93b1de-…/runs/d4a80b13-…/stop \
-H "Authorization: Bearer corte_sk_…"
{
"run": {
"id": "d4a80b13-6c7e-42f9-8a05-91b2c7e3f660",
"workflowId": "7c93b1de-2a48-4f60-91b7-5d0e6a2c8f13",
"status": "stopped",
"nodeResults": { "productPhoto": { "status": "complete", "output": { "…": "…" } } },
"userId": "3f8c1a92-b5d7-4e60-9c14-8a72d0e5b391",
"createdAt": "2026-07-28T16:40:12.774Z",
"completedAt": "2026-07-28T16:40:51.318Z"
}
}
The abort settles the row within a poll tick, so a response may still read
running for a moment — poll once to confirm.
Cost
A run bills per node, exactly as the same graph would in the app: generation nodes at their model's rate, Generate Text nodes by token usage. The run is billed to the key's account, or to the project owner for project-billed work.
Each step checks the balance before executing, so a run can settle as error
part-way through with insufficient_credits on the node that couldn't be
afforded. Completed nodes stay charged.