> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.rightbrain.ai/v-1/docs/api/run-agents/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=` 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": "" }] } ``` #### 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. > Build reliable AI agents that run inside your existing tools and workflows. Rightbrain developer documentation.