Source: https://docs.cumuluslabs.io/agents
Content version: b043053d

# Agents

An agent is a flow graph plus its settings. You change it through its draft, check it, and publish immutable versions; runs execute a version, never the draft.

## Create an agent {#create}

Start from a template, clone an existing agent, or import a Retell export. Each needs an `Idempotency-Key`.

| Template | Channel | Starts as |
| --- | --- | --- |
| blank | cloud or phone | One conversation node with an empty prompt. |
| cloud_task | cloud | A task: takes the run input, works through it, ends. Task mode. |
| cloud_chat | cloud | A conversation over messages you send to the run. Chat mode. |
| phone_outbound | phone | An outbound call: greet, say who it is calling for, ask how to help. Records both channels. |

`POST /api/v1/agents` takes `name`, `template`, `channels` and `languages`(`en`, `es`). A template must match a channel you choose. `POST /api/v1/agents/{agent_id}/clone` copies an agent's head, a `version` you name, or its draft if it was never published, into a new agent's first draft.

### Import from Retell {#import}

`POST /api/v1/agents/import` turns a Retell **conversation-flow** agent export into a phone agent's **draft**. An import never goes live: test the draft, then [publish](/docs/agents#publish) it. A `reference.id` no agent has creates a new agent; the ID of an existing agent replaces its draft and leaves its live version running. Split the export: its `conversationFlow` is `flow`, the rest is `agent`; `agent_version` is Retell's version, kept as provenance. Retell LLM (single-prompt) agents are not accepted. Check an export first with `POST /api/v1/agents/import/inspect`: it compiles the same body, returns the diagnostics, and says what the import would do in `target` (a new agent, a new or replaced draft, and whether that draft has unpublished changes). It writes nothing.

**POST /api/v1/agents/import**

```
{
  "reference": { "tenant_id": "<your environment's tenant_id>", "kind": "agent", "id": "fairway-rpc-outbound" },
  "agent_version": 6,
  "agent": { …the Retell agent object without conversationFlow, "version": 6, "webhook_url": null… },
  "flow": { …the export's conversationFlow: conversation_flow_id, version, nodes, tools… },
  "config": {
    "providers": {
      "llm": { "provider": "openai", "model": { "model_id": "gpt-5.4-mini", "provider": "openai", "tier": "cumulus-served" } },
      "stt": { "provider": "elevenlabs", "model": "scribe_v2_realtime" },
      "tts": { "provider": "elevenlabs", "model": "eleven_flash_v2_5", "voiceId": "<a voice_id from GET /api/v1/voices>" }
    },
    "recording": { "required": true, "channels": "dual" }
  },
  "model_bindings": {
    "$.flow.model_choice": { "model": { "model_id": "gpt-5.4-mini", "provider": "openai", "tier": "cumulus-served" } },
    "$.agent.post_call_analysis_model": { "model": { "model_id": "gpt-5.4-mini", "provider": "openai", "tier": "cumulus-served" } }
  }
}
```

*   `reference.tenant_id` is your environment's: `GET /api/v1/environments` lists production and sandbox with their `tenant_id` and which one your key is in (`current`).
*   `config.providers` chooses the models the agent runs on: a `ready` chat model from `GET /api/v1/models` for `llm` (with `provider: "openai"`, the platform route for every catalog model), and a speech model and voice from `GET /api/v1/voices`. The Retell voice is replaced by that voice (warning `voice_rebound`).
*   `model_bindings` maps each model the export declares to a catalog model. Run inspect with an empty object first: every `model_binding_required` diagnostic names one path to bind (usually `$.flow.model_choice` and `$.agent.post_call_analysis_model`).
*   Set `agent.webhook_url` to `null`, or pin the same URL as `config.webhook` (`{ url, secretRef: "<secret id>@<version>" }`, a `webhook_signing` secret); a different URL is refused with `webhook_mismatch`.
*   `config.telephony` (`{ trunkId, from }`) is optional. Leave it out until your trunk and numbers exist, then add them with the `agent.telephony` edit ([Phone](/docs/phone)).
*   Retell tools become HTTP actions. Inline secrets in tool headers or query values, and custom SIP headers, are refused (`private_inline_value`): move them to a [secret](/docs/triggers-and-webhooks#secrets).
*   Locales other than English and Spanish are dropped with a warning.
*   Replacing a draft with unpublished changes needs `replace_draft` with that draft's `draft_id` and revision (`expected_revision`); without it the import is refused with `409 conflict`, reason `draft_has_changes`, so no client discards work by accident.
*   Importing the same body onto its unedited draft again returns that draft (200) and writes nothing.

| Operation | MCP tool | Key role | Purpose |
| --- | --- | --- | --- |
| [`GET /api/v1/agents`](https://docs.cumuluslabs.io/reference/agents.list) | `agents_list` | viewer | List agents with their head publication, open draft and last run; search, filter and order server-side |
| [`GET /api/v1/agents/{agent_id}`](https://docs.cumuluslabs.io/reference/agents.get) | `agents_get` | viewer | Get one agent |
| [`POST /api/v1/agents`](https://docs.cumuluslabs.io/reference/agents.create) | `agents_create` | member | Create an agent from a template (201; an idempotent replay returns 200) |
| [`DELETE /api/v1/agents/{agent_id}`](https://docs.cumuluslabs.io/reference/agents.delete) | `agents_delete` | member | Delete (archive) an agent: it leaves the catalog and takes no new drafts, publications or runs; history is kept |
| [`POST /api/v1/agents/{agent_id}/clone`](https://docs.cumuluslabs.io/reference/agents.clone) | `agents_clone` | member | Clone an agent's head, chosen version or never-published draft into a new agent's first draft (201; replay 200) |
| [`POST /api/v1/agents/import`](https://docs.cumuluslabs.io/reference/agents.import) | `agents_import` | member | Import a Retell agent export as a draft, never live: a new agent gets it as its first draft; an existing agent's draft is replaced and its head is unchanged. Replacing a draft with unpublished changes needs replace_draft {draft_id, expected_revision} naming it (409 reason draft_has_changes otherwise). Publish the draft with publications.publish (201; re-importing the same export onto its unedited draft returns 200) |
| [`POST /api/v1/agents/import/inspect`](https://docs.cumuluslabs.io/reference/agents.import_inspect) | `agents_import_inspect` | member | Compile a Retell export and say what agents.import would do (new agent, new or replaced draft, unpublished changes it would discard) without importing it |

## The flow graph {#flow}

A flow has a global prompt, a start node and nodes joined by transitions. Each node has an instruction (a prompt, or static text to say) and edges whose conditions are prompts the model evaluates, equations on variables (`==`, `contains`, `is_set`, …) or always. Node kinds:

`conversation``end``branch``transfer_call``function``extract_dynamic_variables``component`

*   `function` nodes call a tool (an HTTP action you define), optionally waiting for its result.
*   `extract_dynamic_variables` nodes pull typed values out of the conversation into variables.
*   `transfer_call` nodes transfer a phone call, cold or warm, to a number or a `{{variable}}`.
*   `component` nodes run a reusable sub-flow; `end` ends the call or conversation.
*   Prompts use `{{variables}}`: a run's `dynamic_variables` fill them, and a phone run missing one the agent needs is refused ([Dynamic variables](/docs/runs#dynamic-variables)).

Talos's canvas edits the same graph; its node positions are presentation only and never change a version's digest.

## Drafts: edit and validate {#drafts}

An agent has at most one draft. `POST …/draft` opens one from the head (or from `base_version`). Every `PATCH …/draft` sends up to 100 typed edits with the `draft_id` and the `expected_revision` it was based on; if someone else edited first you get `409 conflict` and reload. There are no free-form paths: each change is one of these edits.

**Edit a draft**

```
PATCH /api/v1/agents/{agent_id}/draft
{
  "draft_id": "5b0e…",
  "expected_revision": 4,
  "edits": [
    { "type": "flow.prompt", "text": "You are Maya, calling on behalf of Acme Dental about {{appointment_time}}." },
    { "type": "languages.set", "languages": ["en", "es"] },
    { "type": "agent.telephony", "value": { "trunkId": "my-twilio-trunk", "from": "+14155550100" } }
  ]
}
```

`agent.name``agent.settings``agent.analysis``agent.provider``agent.telephony``agent.recording``flow.prompt``node.name``node.instruction``node.transitions``node.configure``node.create``node.remove``flow.configure``component.create``component.start``component.remove``transfer.credential``channels.set``languages.set``trigger.put``trigger.remove``action.put``action.remove``notification.put``notification.remove``knowledge.set``cloud.io.set``layout.set`

`POST …/draft/validate` compiles the draft and returns `{ ok, version, digest, diagnostics }`: the version and digest it would publish, and every warning or error. An error blocks publishing. A finding that has a mechanical fix lists it in `fixes` as `{ id, label, edits }`: send `edits` as a `PATCH …/draft` with the revision you validated, then validate again. A draft that changed since refuses the fix with `409 conflict`.

| Operation | MCP tool | Key role | Purpose |
| --- | --- | --- | --- |
| [`GET /api/v1/agents/{agent_id}/draft`](https://docs.cumuluslabs.io/reference/drafts.get) | `drafts_get` | viewer | Read the open draft |
| [`POST /api/v1/agents/{agent_id}/draft`](https://docs.cumuluslabs.io/reference/drafts.open) | `drafts_open` | member | Open a draft from the head or a chosen version |
| [`PATCH /api/v1/agents/{agent_id}/draft`](https://docs.cumuluslabs.io/reference/drafts.edit) | `drafts_edit` | member | Apply domain edits to the draft (revision compare-and-set) |
| [`DELETE /api/v1/agents/{agent_id}/draft`](https://docs.cumuluslabs.io/reference/drafts.discard) | `drafts_discard` | member | Discard the draft (revision compare-and-set) |
| [`POST /api/v1/agents/{agent_id}/draft/rebase`](https://docs.cumuluslabs.io/reference/drafts.rebase) | `drafts_rebase` | member | Rebase the draft onto the live version after a head change (keeps its definition; 409 head_moved when live moved again) |
| [`POST /api/v1/agents/{agent_id}/draft/validate`](https://docs.cumuluslabs.io/reference/drafts.validate) | `drafts_validate` | member | Compile the draft and return diagnostics and the would-be digest |

## Publish and roll back {#publish}

`POST …/publications` with the `draft_id`, `expected_revision` and the next `version` (0 for the first) compiles the draft into an immutable publication. By default it becomes the **head**, and runs that name no version use it from then on. Options:

*   `"live": false` publishes a test version: you can run it by version, but the head does not move and its triggers are not registered. Test-publishing a draft revision that already has a test version returns that version instead of a new one, so run the version in the response.
*   `"keep_draft": true` keeps the draft open for more changes.

To go live safely: validate, publish with `"live": false`, run your [test suites](/docs/agents#testing) against that version, then promote it.

`POST …/head` moves the head to any existing version: promote a test version, or roll back. It takes `expected_head_version`, the head you saw (`null` for none), and refuses the move with `409 conflict` if the head has moved since. Runs already admitted keep the version they started with.

| Operation | MCP tool | Key role | Purpose |
| --- | --- | --- | --- |
| [`POST /api/v1/agents/{agent_id}/publications`](https://docs.cumuluslabs.io/reference/publications.publish) | `publications_publish` | member | Publish the draft as the next immutable version; moves head unless live is false (201; replay 200) |
| [`GET /api/v1/agents/{agent_id}/publications`](https://docs.cumuluslabs.io/reference/publications.list) | `publications_list` | viewer | List publications of an agent |
| [`GET /api/v1/agents/{agent_id}/publications/{version}`](https://docs.cumuluslabs.io/reference/publications.get) | `publications_get` | viewer | Get one publication with its compiled plan |
| [`POST /api/v1/agents/{agent_id}/head`](https://docs.cumuluslabs.io/reference/publications.promote) | `publications_promote` | member | Move head to an existing version (manual promote or rollback), rebasing the named open draft in the same transaction |

## Actions and governed effects {#actions}

Everything an agent does outside the conversation is an action: calling your HTTP endpoint, transferring or ending a call, leaving a voicemail, scheduling a callback, searching knowledge, delivering a webhook. Each time a run takes one, Talos checks it against the run's live grant just before sending, records the decision, and settles it as an effect with a receipt (`intended → sending → succeeded | failed | uncertain`). A denied action sends nothing. An unacknowledged one is `uncertain` and is never blindly resent. Read them with `GET /api/v1/runs/{run_id}/effects`.

| Built-in action | What it does |
| --- | --- |
| webhook.deliver | Deliver run webhook |
| http.request | Customer HTTP action |
| call.transfer | Transfer call |
| run.end | End call or conversation |
| call.voicemail | Handle voicemail |
| callback.schedule | Schedule callback |
| knowledge.search | Search knowledge |
| call.dial | Dial outbound call |
| livekit.room | Create media room |
| livekit.dispatch | Dispatch voice agent |
| run.analyze | Analyze run |

Your own HTTP actions (`action.put`) call HTTPS endpoints with `none`, `bearer`, `basic` or header authentication, the credential held as a [secret](/docs/triggers-and-webhooks#secrets). Its `response_variables` set flow variables from the action's 2xx JSON response, each read at a path:`{"balance": "$.account.balance"}` makes the nested field `{{balance}}`.

| Operation | MCP tool | Key role | Purpose |
| --- | --- | --- | --- |
| [`GET /api/v1/actions`](https://docs.cumuluslabs.io/reference/actions.list) | `actions_list` | viewer | List the action descriptors available to publications |
| [`GET /api/v1/voices`](https://docs.cumuluslabs.io/reference/voices.list) | `voices_list` | viewer | List the speech models and platform TTS voices an agent can use |

## Test before going live {#testing}

Use [Test an agent](/docs/testing) for simulated caller suites, verdicts and browser voice sessions. Follow the [publish-and-test recipe](/docs/recipes#import-and-test) before promotion.

## Try it in the browser {#test}

> **Note**
> 
> `POST /api/v1/test-sessions` starts a browser voice test of one version of a phone agent: it admits a `web` run marked as a test and returns a short-lived LiveKit room token, so you talk to the agent from Talos. The same Idempotency-Key rejoins the live test with a fresh token.

| Operation | MCP tool | Key role | Purpose |
| --- | --- | --- | --- |
| [`POST /api/v1/test-sessions`](https://docs.cumuluslabs.io/reference/test_sessions.create) | `test_sessions_create` | member | Start a browser voice test of one agent version: admits a web run and returns a short-lived LiveKit room token (the same Idempotency-Key rejoins the live run with a fresh token) |
