> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.rightbrain.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.rightbrain.ai/_mcp/server.

# Run agents via the API

> Run a Rightbrain agent via REST with SSE streaming — read text, tool calls, tool results, and formatted output; continue multi-turn sessions; handle approval pauses; and attach input files.

Running an [agent](/docs/concepts/agents) is different from running a task: an agent takes turns, calls tools, and can pause for approval, so its run **streams** back as Server-Sent Events rather than returning a single response body.

```
POST https://app.rightbrain.ai/api/v1/org/{org_id}/project/{project_id}/task-agent/{agent_id}/run
```

The response is a `text/event-stream`. All requests use bearer authentication — see [Authentication](/docs/api/authentication).

## Request body

| Field        | Required | Description                                                     |
| ------------ | -------- | --------------------------------------------------------------- |
| `message`    | Yes      | The user message to send to the agent this turn.                |
| `session_id` | No       | Continue an existing conversation. Omit to start a new session. |
| `reference`  | No       | Your own correlation string (max 64 chars).                     |
| `context_id` | No       | Group related runs under a shared context ID (max 64 chars).    |

For a new session, select a revision with the `revision_id` query parameter; otherwise an active revision is selected. Existing sessions stay pinned to their original revision.

## Stream a run

Set `RB_TOKEN`, `RB_ORG`, `RB_PROJECT`, and `RB_AGENT` to your credential and resource IDs. These examples send a review-triage message; change it to match your agent. Python requires `httpx`; TypeScript uses the built-in `fetch` in Node.js 22+.

**`cURL`**

```bash title="cURL"
curl --fail-with-body -N -X POST "https://app.rightbrain.ai/api/v1/org/$RB_ORG/project/$RB_PROJECT/task-agent/$RB_AGENT/run" \
  -H "Authorization: Bearer $RB_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"message": "Triage this review: My toaster exploded during breakfast and set the bread on fire."}'
```

**`Python`**

```python title="Python"
import json
import os
import httpx


def read_agent_stream(response):
    response.raise_for_status()
    if "text/event-stream" not in response.headers.get("content-type", ""):
        raise RuntimeError("Expected a text/event-stream response")

    session_id = None
    data = []
    for line in response.iter_lines():
        if line.startswith("data:"):
            data.append(line[5:].removeprefix(" "))
        elif line == "" and data:
            event = json.loads("\n".join(data))
            data = []
            kind = event["event_type"]
            if kind == "session_id":
                session_id = event["metadata"]["session_id"]
            elif kind == "text":
                print(event.get("content") or "", end="", flush=True)
            elif kind == "formatted_output":
                print("\nStructured output:", event.get("content"))
            elif kind == "error":
                raise RuntimeError(f"Agent run failed: {event.get('error')}")
            elif kind in ("done", "approval_required"):
                return {
                    "status": "completed" if kind == "done" else "waiting_for_human",
                    "session_id": session_id,
                    "metadata": event.get("metadata"),
                }
            elif kind in ("tool_call", "tool_result"):
                print("\nTool event:", event)
            # Ignore unknown event types so new events remain compatible.

    raise RuntimeError("Stream ended without done or approval_required; inspect the run before retrying")


base = f"https://app.rightbrain.ai/api/v1/org/{os.environ['RB_ORG']}/project/{os.environ['RB_PROJECT']}"
url = f"{base}/task-agent/{os.environ['RB_AGENT']}/run"
headers = {
    "Authorization": f"Bearer {os.environ['RB_TOKEN']}",
    "Accept": "text/event-stream",
}
payload = {"message": "Triage this review: My toaster exploded during breakfast and set the bread on fire."}

with httpx.stream("POST", url, headers=headers, json=payload, timeout=None) as response:
    outcome = read_agent_stream(response)
    print("\nOutcome:", outcome)
```

**`TypeScript`**

```typescript title="TypeScript"
async function readAgentStream(response: Response) {
  if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  if (!response.headers.get("content-type")?.includes("text/event-stream") || !response.body) {
    throw new Error("Expected a text/event-stream response");
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let data: string[] = [];
  let sessionId: string | undefined;

  try {
    while (true) {
      const { value, done } = await reader.read();
      buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
      let newline: number;
      while ((newline = buffer.indexOf("\n")) !== -1) {
        const line = buffer.slice(0, newline).replace(/\r$/, "");
        buffer = buffer.slice(newline + 1);
        if (line.startsWith("data:")) {
          data.push(line.slice(5).replace(/^ /, ""));
        } else if (line === "" && data.length) {
          const event = JSON.parse(data.join("\n"));
          data = [];
          switch (event.event_type) {
            case "session_id":
              sessionId = event.metadata.session_id;
              break;
            case "text":
              process.stdout.write(event.content ?? "");
              break;
            case "formatted_output":
              console.log("\nStructured output:", event.content);
              break;
            case "error":
              throw new Error(`Agent run failed: ${event.error}`);
            case "done":
            case "approval_required":
              return {
                status: event.event_type === "done" ? "completed" : "waiting_for_human",
                sessionId,
                metadata: event.metadata,
              };
            case "tool_call":
            case "tool_result":
              console.log("\nTool event:", event);
              break;
            // Ignore unknown event types so new events remain compatible.
          }
        }
      }
      if (done) throw new Error("Stream ended without done or approval_required; inspect the run before retrying");
    }
  } finally {
    await reader.cancel().catch(() => {});
    reader.releaseLock();
  }
}

const base = `https://app.rightbrain.ai/api/v1/org/${process.env.RB_ORG}/project/${process.env.RB_PROJECT}`;
const response = await fetch(`${base}/task-agent/${process.env.RB_AGENT}/run`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RB_TOKEN}`,
    "Content-Type": "application/json",
    Accept: "text/event-stream",
  },
  body: JSON.stringify({
    message: "Triage this review: My toaster exploded during breakfast and set the bread on fire.",
  }),
});
const outcome = await readAgentStream(response);
console.log("\nOutcome:", outcome);
```

HTTP success only means the stream opened. The cURL example prints events; its exit code does not tell you whether the agent completed. The Python and TypeScript readers return `completed` on `done`, return `waiting_for_human` on `approval_required`, and raise an error on a run failure or unexpected end of stream. `formatted_output.content` is a string; parse it separately if your formatter produces JSON.

## The event stream

Each event is one SSE frame: a line beginning with `data:` followed by a JSON object, terminated by a blank line. The JSON always carries an `event_type`; the fields populated depend on the type.

| `event_type`        | Meaning                                                                    | Key fields                                                                        |
| ------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `session_id`        | First event of the stream; carries the session to reuse for the next turn. | `metadata.session_id`                                                             |
| `text`              | A chunk of the agent's natural-language response.                          | `content`                                                                         |
| `tool_call`         | The agent invoked a Task, Integration, MCP, or registered tool.            | `tool_name`, `tool_display_name`, `tool_args`, `tool_call_id`                     |
| `tool_result`       | A tool returned a result.                                                  | `tool_name`, `tool_result`, `tool_outcome` (`success` or `error`), `tool_call_id` |
| `formatted_output`  | The run's structured output, produced by the output-formatter tool.        | `content`                                                                         |
| `approval_required` | A gated tool needs human approval; the run is now `waiting_for_human`.     | `metadata` (approval request details)                                             |
| `error`             | The run failed.                                                            | `error`                                                                           |
| `done`              | Terminal event; the run finished.                                          | `metadata` (`run_id`, `session_id`, `duration_ms`, `total_tokens`)                |

A representative sequence:

**`Event stream`**

```text title="Event stream" wordWrap
data: {"event_type": "session_id", "metadata": {"session_id": "01909843-3596-da54-4756-28af46917e74"}}

data: {"event_type": "tool_call", "tool_name": "summarize_tickets", "tool_display_name": "Summarize tickets", "tool_args": {"month": "July"}, "tool_call_id": "call_01"}

data: {"event_type": "tool_result", "tool_name": "summarize_tickets", "tool_outcome": "success", "tool_call_id": "call_01", "tool_result": {"summary": "..."}}

data: {"event_type": "text", "content": "Here is the summary of this month's tickets..."}

data: {"event_type": "formatted_output", "content": "{\"summary\": \"...\", \"top_themes\": [\"billing\", \"latency\"]}"}

data: {"event_type": "done", "metadata": {"run_id": "0195d207-32bb-d03d-cfdc-f4516e9222c8", "session_id": "01909843-3596-da54-4756-28af46917e74", "duration_ms": 8421, "total_tokens": 5120}}
```

A run has one of four statuses: `running`, `waiting_for_human`, `completed`, or `failed`. Only `completed` confirms successful completion. A tool error is a tool outcome that the agent may handle; an `error` event is a run failure.

A client disconnect cancels the live execution. Do not automatically repeat the POST after a disconnect: inspect the recorded run and session first, because tools may already have executed. The stream does not support replay via `Last-Event-ID`. The approval `/resume` endpoint returns the same SSE format and can use the same readers.

If the stream ended before returning a run ID, list runs with `?session_id=<session_id>` using the session ID from the first event. Check `status`, `termination_reason`, and the tool execution records. A disconnected run can be recorded as `failed` with `termination_reason: "client_disconnect"`. Continuing that session starts another run; it does not resume the cancelled execution. Reconcile any completed tool actions before submitting the next turn.

An HTTP 200 stream can also end with `error` and no `done`, for example when an output formatter cannot resolve an uploaded file. Treat that as failure and inspect its error metadata and the stored run. Do not wait for a completion event after a terminal error.

## Multi-turn sessions

The first `session_id` event and the final `done` event both carry the session ID. Pass it back as `session_id` on the next run to continue the same conversation with its full history intact.

**`cURL`**

```bash title="cURL"
curl -N -X POST https://app.rightbrain.ai/api/v1/org/{org_id}/project/{project_id}/task-agent/{agent_id}/run \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message": "Now break that down by product area.", "session_id": "01909843-3596-da54-4756-28af46917e74"}'
```

Manage sessions directly:

**`Session endpoints`**

```text title="Session endpoints"
GET     .../task-agent/{agent_id}/session              list sessions
GET     .../task-agent/{agent_id}/session/{session_id} fetch a session
DELETE  .../task-agent/{agent_id}/session/{session_id} delete a session
```

## Approval pauses

If a tool is configured to require approval, the run emits an `approval_required` event and its status becomes `waiting_for_human`. The stream ends without a `done` event. Approval records the decision; call `/resume` separately to execute the approved tool and continue the run. Rejection follows the tool's configured behavior. See [Approvals (HITL)](/docs/production/approvals) for the full lifecycle.

## Attach input files

Send files with a run the same two ways a task does.

#### Base64 in JSON

Add a `files` array. Each entry has base64 `content` and a `filename`.

**`Request body`**

```json title="Request body"
{
  "message": "Analyze this document",
  "files": [{ "filename": "report.pdf", "content": "<base64-encoded bytes>" }]
}
```

#### Multipart form data

Send `message` as a text field and each file under the `files` field.

**`cURL`**

```bash title="cURL"
curl -N -X POST https://app.rightbrain.ai/api/v1/org/{org_id}/project/{project_id}/task-agent/{agent_id}/run \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F 'message=Analyze this document' \
  -F 'files=@report.pdf'
```

## Runtime-generated files

A run also has a file registry for files acquired or produced after execution starts. A producer tool stores the bytes once and returns a run-scoped file reference. A later Task tool can consume that reference as file input without copying binary content into the prompt or through each tool response.

File promotion is explicit and format-aware; arbitrary base64-shaped tool output is not treated as a file. Input files and runtime-generated files remain distinguishable in the run record.

The run's top-level `files` array contains file metadata and provenance, including whether each file was an input or generated by a Task tool. A paused approval run retains its file registry when resumed. Use the run file endpoint to download a retained file.

See [Tasks](/docs/concepts/tasks) for direct model file input and [Input processors](/docs/concepts/input-processors) when a file must be converted before the Task model runs.

## Retrieve a run after the fact

The stream is the live view. To read a completed run's events or download files it produced, use the run endpoints — handy when a client disconnected mid-stream or you need the transcript later.

**`Run endpoints`**

```text title="Run endpoints"
GET  .../task-agent/{agent_id}/run                          list runs (paginated)
GET  .../task-agent/{agent_id}/run/{run_id}                 fetch a run
GET  .../task-agent/{agent_id}/run/{run_id}/events          fetch the run's events
GET  .../task-agent/{agent_id}/run/{run_id}/file/{file_name} download a run file
```

#### [Approvals (HITL)](/docs/production/approvals)

Pause runs on sensitive tool calls and resume them after review.

#### [Agents](/docs/concepts/agents)

What an agent is and how it uses the primitives you make available.

#### [Memory & context](/docs/concepts/memory-and-context)

Control session history and inspect context attribution.