> ## Documentation Index
> Fetch the complete documentation index at: https://docs.impellabs.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Responses and agent runs

> The stateless execution endpoint, non-conversational agent runs, and the options a caller may set.

Three endpoints execute an agent, and they share one request shape, one option
set, one error envelope and one stream contract.

| Endpoint                            | Scope                 | Use it when                                                  |
| ----------------------------------- | --------------------- | ------------------------------------------------------------ |
| `POST /responses`                   | `responses:create`    | You want an answer and no transcript                         |
| `POST /conversations/{id}/messages` | `conversations:write` | You want the turn recorded in a thread                       |
| `POST /agents/{id}/runs`            | `agents:run`          | You have a structured payload and a task, not a chat message |

## `POST /responses` — stateless

No conversation is named and none comes back. Internally a conversation is opened
on the `api` channel because a turn needs somewhere to live, and **no messages
are written to it** — so the request leaves a record that it happened and what it
cost, and no copy of what was said.

```bash theme={null}
curl "$IMPEL_API/responses" \
  -H "Authorization: Bearer $IMPEL_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c…" \
  -d '{
    "agent": "agt_support",
    "input": "Does the 30-day window include weekends?",
    "knowledge_policy": "required"
  }'
```

## `POST /agents/{id}/runs` — a payload and a task

For work that is not a conversation: an object to reason over plus a line saying
what to do with it.

```bash theme={null}
curl "$IMPEL_API/agents/agt_triage/runs" \
  -H "Authorization: Bearer $IMPEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "ticket": { "subject": "Charged twice", "tier": "enterprise" },
      "task": "Classify urgency and name the owning team."
    }
  }'
```

The structured payload reaches the model **in the user message** — fenced and
labelled — and never in the system prompt. That is one code path for every entry
point on the surface, so a payload cannot become an instruction.

Read a run back with `GET /agents/{id}/runs/{run_id}`, which needs `agents:read`.
Reading a run is a different authority from starting one.

## The input

```json theme={null}
{
  "agent": "agt_…",
  "input": "a string, or a JSON object",
  "channel": "api",
  "locale": "en-GB",
  "metadata": { "your": "labels" },
  "attachments": [{ "type": "document", "ref": "doc_…", "name": "invoice.pdf" }],
  "request_context": { "resource_id": "…" },
  "external_ref": "your-own-thread-id"
}
```

* **`channel`** decides both rendering and which ledger line the turn bills to.
  `""` means "not stated" and falls through to the conversation's own channel —
  it is **not** a synonym for `api`. Values: `api`, `web`, `website`, `whatsapp`,
  `phone`, `voice`.
* **`attachments`** carry a `ref` to something already stored. Never bytes.
* **`request_context`** is caller-supplied context. The one key the runtime acts
  on is `resource_id`, which narrows the execution context to that resource — and
  **raises rather than narrowing to nothing** when the caller could not reach it.
  That refusal is `invalid_context`, a 403.

<Warning>
  The input carries no identity field of any kind. Who you are comes from the
  credential.

  About thirty reserved keys are refused **by name** inside `request_context` —
  `tenant_id`, `company_id`, `scopes`, `permissions`, `entitlements`, `billed_to`,
  `principal`, `role`, `token`, `usage_category` and the rest. They are refused
  rather than dropped silently, so a caller who believed one of them worked finds
  out on the first call instead of in production.
</Warning>

## The options

Send them flat in the body or nested under `options`; both are read and the
nested value wins. An unrecognised key inside `options` is a 400.

| Option             | Default | Notes                                                                              |
| ------------------ | ------- | ---------------------------------------------------------------------------------- |
| `stream`           | `false` | [Server-Sent Events](/v2/streaming) instead of one JSON body                       |
| `response_format`  | `text`  | `text`, `json_object`, `json_schema`. [Structured outputs](/v2/structured-outputs) |
| `tool_policy`      | `auto`  | `auto`, `none`, `required`                                                         |
| `memory_policy`    | `auto`  | `auto`, `none`                                                                     |
| `knowledge_policy` | `auto`  | `auto`, `none`, `required`                                                         |
| `timeout_ms`       | `60000` | Between `1000` and `120000`                                                        |
| `preview`          | `false` | Run the unpublished configuration. Needs `agent:preview`                           |

<Note>
  The three policy fields can only ever **subtract**. There is no value meaning
  "retrieve more than this agent is configured for", or "use a tool this agent
  does not have".
</Note>

`timeout_ms` out of range is **refused, not clamped**. A caller who asked for ten
minutes and silently got sixty seconds reads the resulting timeout as a platform
fault.

### What you may not set

These are refused with a 400 that names the field and gives the reason, because
each is one a caller reasonably expects to work and the answer is a decision
rather than an omission.

| Refused                                   | Because                                                         |
| ----------------------------------------- | --------------------------------------------------------------- |
| `model`, `provider`                       | Selection is decided by runtime routing                         |
| `temperature`, `top_p`, `max_tokens`      | Sampling and token limits are not caller-controlled             |
| `system_prompt`, `prompt`, `instructions` | Prompt assembly is the runtime's, not the caller's              |
| `tools`                                   | Tool availability comes from the agent configuration            |
| `knowledge_groups`                        | Knowledge scope comes from the agent and your own authorization |
| `memory_namespace`                        | Derived from the execution context                              |
| `trace_id`, `request_id`                  | Minted by the runtime                                           |
| `billing_account`, `usage_category`       | Billing is derived from the authenticated tenant                |
| `idempotency_key`                         | Send the `Idempotency-Key` **header** instead                   |

## The response

```json theme={null}
{
  "id": "run_…",
  "status": "completed",
  "output": [{ "type": "text", "text": "…" }],
  "request_id": "req_…",
  "trace_id": "trc_…",
  "usage": { "input_tokens": 0, "output_tokens": 0, "cached_tokens": 0, "total_tokens": 0 },
  "conversation": "cnv_…",
  "agent": "agt_…",
  "agent_version": 7,
  "metadata": {}
}
```

`status` is one of six: `queued`, `running`, `completed`, `failed`, `cancelled`,
`blocked`. Four of those are terminal — `completed`, `failed`, `cancelled`,
`blocked`. Anything else is still in flight and a caller polling it may see it
change.

`id` is the public run id (`run_…`) and is the same id the stored run carries, so
a caller quoting it in a ticket lands on one row. Database primary keys are never
exposed.

## Cancelling

```bash theme={null}
curl -X POST "$IMPEL_API/responses/run_01J8Z.../cancel" \
  -H "Authorization: Bearer $IMPEL_KEY"
```

Needs `responses:create`. **Any run you may read can be cancelled here**, not
only ones from `POST /responses` — the ids are one namespace.

Two success statuses, and the difference is information rather than success:

* **202** — the run was in flight and will stop at its next checkpoint.
* **200** — there was nothing left to stop.

Cancellation is cooperative, so 202 is the honest code for "accepted" rather than
204\. It is idempotent: a second call answers the same way as the first, and a
call for a finished run answers 200, because "it already stopped" is the outcome
you wanted. A run that reached a terminal status is left exactly as it is — a
cancel that lost the race does not reopen it.

A cancelled run returns **no output**. You asked for it to stop; handing you a
partial answer as though it were the answer is how a cancelled report ends up
quoted in a board pack.

## Preview and drafts

`"preview": true` runs the agent's **unpublished** configuration. Asking is free;
being allowed is not.

```json theme={null}
{ "agent": "agt_support", "input": "…", "preview": true }
```

Preview needs the `agent:preview` scope, which is issued on its own and is
implied by nothing — a key that may edit an assistant does not thereby preview
its drafts.

<Warning>
  Without permission the answer is **404, not 403**. Confirming that a draft
  exists but may not be run is exactly the confirmation the not-found rule exists
  to withhold.
</Warning>

Statuses a preview run may execute: `active`, `draft`, `paused`, `preview`.
Without preview, only `active` — asking for anything else is
`agent_not_available`, a 409.
