Source: https://docs.cumuluslabs.io/runs
Content version: ac60a26f

# Runs

A run executes one publication once: a cloud task or chat, or a phone call. Start it with one request, follow its events live, and read its transcript, recording, analysis, effects and cost when it ends.

## Start a run {#start}

`POST /api/v1/runs` (member or admin key, `Idempotency-Key` required) admits a run and answers `201` with it at once. It runs the agent's head unless you name a `version`, and keeps that version and its digest for its whole life. `metadata` (string values) is stored with the run and never changes.

| Channel | Body | What happens |
| --- | --- | --- |
| `cloud · task` | `{ channel: "cloud", agent_id, mode: "task", input }` | The input is checked against the publication's input schema, becomes the agent's variables and its one user turn. The run ends with a result validated against the output schema. |
| `cloud · chat` | `{ channel: "cloud", agent_id, mode: "chat", input }` | A conversation: send each user turn with POST …/messages. It ends at the agent's end node, its turn limit or its idle timeout. |
| `phone` | `{ channel: "phone", agent_id, to, from?, dynamic_variables }` | An outbound call from one of your numbers. See [Phone](/docs/phone). |

**A chat run**

```
# Start a chat run: input fills variables; the conversation happens in messages.
POST /api/v1/runs
{ "channel": "cloud", "agent_id": "…", "mode": "chat", "input": { "customer_name": "Ada" } }

# Send the user's turn (Idempotency-Key required). 202: { "run_id": "…", "index": 0 }
POST /api/v1/runs/{run_id}/messages
{ "text": "Where is my order?" }

# The agent's reply arrives as a run.message event on GET /api/v1/runs/{run_id}/events.
```

Step-by-step samples in curl, TypeScript and Python are in [Using the API](/docs/api#cloud-run).

## Dynamic variables {#dynamic-variables}

An agent's prompts, transitions, transfer numbers and voicemail message can use `{{variables}}`, such as `{{debtorName}}`. A phone run fills them from `dynamic_variables` (string values), and so does a browser test (`POST /api/v1/test-sessions`). A chat test of a phone agent (`mode: "chat"`, `test: true`) fills them from `input`. Every name the agent uses must have a value when the run starts. It can come from:

*   the run's `dynamic_variables` (or `input`, for a chat test);
*   the flow's own defaults (`defaultDynamicVariables`);
*   a value the call produces itself: an `extract_dynamic_variables` node or a tool's response;
*   the runtime: `tenant_id`, `call_id`, `workflow_id`, `agent_id` and `system.channel`.

If any name is left without a value, the run is refused before it dials. You get `400 invalid_request` with `details.reason: "missing_dynamic_variables"`, and the message names every missing variable at once. No run is created, so fix the body and send it again with a new Idempotency-Key.

<!-- code-tabs: A call without its variables -->


**A call without its variables · curl**

```curl
curl -sS -X POST https://api.cumuluslabs.io/api/v1/runs \
  -H "Authorization: Bearer $CUMULUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "$(jq -n --arg agent "$PHONE_AGENT_ID" \
        '{channel: "phone", agent_id: $agent, to: "+14155550123", from: "+14155550100", dynamic_variables: {}}')" \
  | jq '.error // {run_id, state}'
# 400, and no run is created:
# { "code": "invalid_request", "retryable": false, "outcome": "none",
#   "message": "Flow references dynamic variables that were not supplied: agencyName, debtorName",
#   "details": { "reason": "missing_dynamic_variables" } }
```

**A call without its variables · typescript**

```typescript
import { PlatformError } from "@cumulus/sdk";

try {
  await client.runs.create({
    body: { channel: "phone", agent_id: phoneAgentId, to: "+14155550123", from: "+14155550100", dynamic_variables: {} },
  });
} catch (error) {
  if (!(error instanceof PlatformError) || error.details.reason !== "missing_dynamic_variables") throw error;
  console.error(error.message); // "Flow references dynamic variables that were not supplied: agencyName, debtorName"
}
```

**A call without its variables · python**

```python
from cumulus import PlatformError

try:
    client.runs.create(
        body={"channel": "phone", "agent_id": phone_agent_id, "to": "+14155550123", "from": "+14155550100", "dynamic_variables": {}}
    )
except PlatformError as error:
    if error.details.get("reason") != "missing_dynamic_variables":
        raise
    print(error)  # "Flow references dynamic variables that were not supplied: agencyName, debtorName"
```




<!-- /code-tabs -->

To see what an agent needs before you call, read the flow of its head publication. It is in `GET /api/v1/agents/{agent_id}/publications/{version}`, under `plan.flow`. This recipe lists the names it references, minus the ones it provides itself:

<!-- code-tabs: List the variables an agent needs -->


**List the variables an agent needs · curl**

```curl
VERSION=$(curl -sS "https://api.cumuluslabs.io/api/v1/agents/$PHONE_AGENT_ID" \
  -H "Authorization: Bearer $CUMULUS_API_KEY" | jq -r .head.version)
curl -sS "https://api.cumuluslabs.io/api/v1/agents/$PHONE_AGENT_ID/publications/$VERSION" \
  -H "Authorization: Bearer $CUMULUS_API_KEY" \
  | jq -r '.plan.flow as $flow
      | ([$flow | .. | strings | scan("\{\{\\s*([^{}]+?)\\s*\}\}")[0]] | unique)
        - ($flow.defaultDynamicVariables | keys)
        - [$flow | .. | objects | select(.kind? == "extract_dynamic_variables") | .variables[]?.name]
        - [$flow | .. | objects | (.responseVariables? // {}) | keys[]]
        - ["tenant_id", "call_id", "workflow_id", "agent_id", "system.channel"]
      | .[]'
```




<!-- /code-tabs -->

The recipe also counts names that only appear in tool URLs, so it can list more than the API requires, never fewer. Publishing a new version can change the list. During a call, `PATCH /api/v1/runs/{run_id}` can add or change variables.

## Test runs {#test}

`"test": true` marks a run as a test. It runs exactly like a live one, on the same numbers and capacity, but no customer webhook or event trigger ever sees it and its usage is not billed. Talos's tests are test runs. List them apart with `GET /api/v1/runs?test=true` (or `false`).

> **A test phone run still places a real call**
> 
> Use a number you control as `to` while you test.

## Lifecycle {#lifecycle}

`state` is the same on every channel: `admitted`, `running`, `ending`, `completed`, `failed`, `cancelled`, `rejected`, `expired`. A phone run also has `phone_state` for the call itself. When a run ends, `outcome` says how on four axes: transport (did the call connect), execution (did the agent finish), contact (human, voicemail or IVR) and the business result; `disposition` is the analysis verdict.

*   `POST …/cancel` ends an active run and returns `202` with the run. A live call is hung up at once and the run becomes `cancelled` when the call has ended. Repeating the request is safe.
*   `PATCH /api/v1/runs/{run_id}` changes a live phone call's dynamic variables or metadata and returns a receipt: `202` while it is applied, `200` once settled.
*   `GET /api/v1/runs` filters by `agent_id`, `channel`, `state`, time (`after`, `before`), `disposition`, `contact`, `successful`, `version` and `test`.

## Events {#events}

Every run writes an ordered log of what it did: admitted, state changes, each node entered, a chat's messages, each action it took for you (a dial, a transfer, an HTTP action) when it settled, recording and analysis updates, and ended. `GET /api/v1/runs/{run_id}/events` returns them as a JSON page or streams them live ([format and resume](/docs/api#events)); add `?progress=live` to a stream to also hear a call's transcript grow and each action's attempts land. Messages carry a reference to their content; read the words with the transcript, and every action's steps and receipts with `GET …/effects`.

## What a run produced {#outputs}

| Read | Returns |
| --- | --- |
| GET …/result | A cloud task's output object (state pending, ready, none or expired). |
| GET …/transcript | Every turn with its role and text; for calls, timing and interruptions. |
| GET …/recording | Phone and browser runs: a URL to the recording that expires after five minutes. |
| GET …/analysis | Post-run analysis: disposition, whether it succeeded and the fields the agent extracts. |
| GET …/effects | Each action the run took, its authorization decision and its receipts. |
| GET …/usage | What the run metered and cost. |

## Capacity {#capacity}

Each channel has a limit of concurrent runs for your organization. `GET /api/v1/runs/capacity` returns each channel's `limit` and `active` runs. A run beyond the limit is refused with `429 rate_limited` and `details.reason: "capacity_exhausted"`; nothing is created, so retry later with the same key.

## Operations {#operations}

| Operation | MCP tool | Key role | Purpose |
| --- | --- | --- | --- |
| [`POST /api/v1/runs`](https://docs.cumuluslabs.io/reference/runs.create) | `runs_create` | member | Admit a cloud or phone run of a publication (201; an idempotent replay returns 200) |
| [`GET /api/v1/runs`](https://docs.cumuluslabs.io/reference/runs.list) | `runs_list` | viewer | List runs |
| [`GET /api/v1/runs/capacity`](https://docs.cumuluslabs.io/reference/runs.capacity) | `runs_capacity` | viewer | Read admission capacity per channel: configured limit and runs holding it |
| [`GET /api/v1/runs/{run_id}`](https://docs.cumuluslabs.io/reference/runs.get) | `runs_get` | viewer | Get one run |
| [`PATCH /api/v1/runs/{run_id}`](https://docs.cumuluslabs.io/reference/runs.update) | `runs_update` | member | Update a live phone run's dynamic variables or metadata; returns a durable receipt (202 while pending, 200 once settled) |
| [`GET /api/v1/runs/{run_id}/updates/{update_id}`](https://docs.cumuluslabs.io/reference/runs.update_get) | `runs_update_get` | viewer | Read a live update's receipt |
| [`POST /api/v1/runs/{run_id}/cancel`](https://docs.cumuluslabs.io/reference/runs.cancel) | `runs_cancel` | member | Cancel an active run (accepted: a live call settles cancelled when its media session ends) |
| [`POST /api/v1/runs/{run_id}/messages`](https://docs.cumuluslabs.io/reference/runs.send_message) | `runs_send_message` | member | Send a user message to a cloud chat run |
| [`GET /api/v1/runs/{run_id}/events`](https://docs.cumuluslabs.io/reference/runs.events) | `runs_events` | viewer | Read the run's semantic events (JSON page or text/event-stream) |
| [`GET /api/v1/runs/{run_id}/effects`](https://docs.cumuluslabs.io/reference/runs.effects) | `runs_effects` | viewer | Read the run's governed effects and receipts |
| [`GET /api/v1/runs/{run_id}/transcript`](https://docs.cumuluslabs.io/reference/runs.transcript) | `runs_transcript` | viewer | Read the run transcript |
| [`GET /api/v1/runs/{run_id}/result`](https://docs.cumuluslabs.io/reference/runs.result) | `runs_result` | viewer | Read what a cloud task run produced: its output object, validated against the publication's output schema |
| [`GET /api/v1/runs/{run_id}/recording`](https://docs.cumuluslabs.io/reference/runs.recording) | `runs_recording` | viewer | Get a short-lived recording URL |
| [`GET /api/v1/runs/{run_id}/analysis`](https://docs.cumuluslabs.io/reference/runs.analysis) | `runs_analysis` | viewer | Read post-run analysis |
| [`GET /api/v1/runs/{run_id}/usage`](https://docs.cumuluslabs.io/reference/runs.usage) | `runs_usage` | viewer | Read metered usage and cost of one run |
