# Subagents

Mark a block of code as a subagent so its work shows as its own lane in the conversation instead of mixing into the main transcript.

Agents delegate. A support agent hands research to a research agent, a planner fans work out to three workers, an orchestrator kicks off a long job on a queue. Captured as-is, every one of those calls lands in the same transcript, and the conversation reads as one agent talking to itself — the user's question, then a research prompt, then a planning prompt, then the answer.

Marking a block as a subagent tells Brizz that the work inside it belongs to a **named agent of its own**. Its LLM calls, tool calls, and replies are grouped under that name in the conversation view instead of mixing into the session's main transcript. Nothing else changes: the calls still run, the session is still one session, and everything you don't wrap stays exactly where it is today.

:::info
**Nothing to migrate.** Code you don't wrap is unaffected — it stays in the session's main transcript. This works with any framework or provider: the Vercel AI SDK, LangChain, Agno, or a provider loop you wrote yourself.
:::

## Mark a subagent

Wrap the call that runs the subagent and give it a name. Everything the block does — however many LLM calls, tool calls, or turns — belongs to that subagent.

:::tabs
:::tab[Python]
```python
import brizz

with brizz.agent("research"):
    result = research_subagent.run(task)
```
:::tab[Node.js]
```typescript
import { startAgent } from '@brizz/sdk';

await startAgent('research', () => researchAgent.generate({ prompt: task }));
```
:::

The name is what you'll see in the dashboard, so name it the way you think about it — `research`, `board-creator`, `sql-writer`.

Two runs of the same subagent are always two separate lanes, never merged. A fan-out that starts five `research` subagents shows five, each with its own work.

:::tabs
:::tab[Python]
```python
import asyncio

import brizz


async def research(topic: str) -> str:
    async with brizz.aagent("research"):
        return await research_subagent.arun(topic)


async def research_all(topics: list[str]) -> list[str]:
    return await asyncio.gather(*(research(t) for t in topics))
```
:::tab[Node.js]
```typescript
import { startAgent } from '@brizz/sdk';

const results = await Promise.all(
  topics.map((topic) => startAgent('research', () => researchAgent.generate({ prompt: topic }))),
);
```
:::

In Python, use `brizz.aagent` for `async with` blocks. In Node.js, `startAgent` takes sync and async callbacks alike and returns whatever the callback returns.

## Three ways to mark an agent

Pick by the shape of your code:

- **A block you can wrap** → the `with` block / `startAgent`.
- **A function that *is* the subagent** → the decorator / `withAgent`. Every call to it is its own run, so calling it three times gives you three lanes.
- **A handler where the agent starts and the function just carries on** — a job worker, a route — → `set_agent` / `setAgent`. No block, no callback: everything after the line belongs to the agent.

:::tabs
:::tab[Python]
```python
import brizz

# A block you can wrap
with brizz.agent("research"):
    research_subagent.run(task)


# A function that is the subagent — one lane per call
@brizz.agent_context("research")
def research(topic: str) -> str:
    return research_subagent.run(topic)


# A handler that just carries on
def handle_job(job):
    brizz.set_agent(token=job["brizz_agent"])
    board_creator.run(job["goal"])
```
:::tab[Node.js]
```typescript
import { setAgent, startAgent, withAgent } from '@brizz/sdk';

// A block you can wrap
await startAgent('research', () => researchAgent.generate({ prompt: task }));

// A function that is the subagent — one lane per call
const research = withAgent('research', runResearch);
await research(topicA);
await research(topicB);

// A handler that just carries on
export async function handleJob(job) {
  setAgent({ token: job.brizzAgent });
  await boardCreator.generate({ prompt: job.goal });
}
```
:::

All three take the same first argument — a name, your own ids, or a token — so you can switch forms without changing anything else. `brizz.aagent_context` is the async decorator.

`set_agent` / `setAgent` has no scope to close: it marks everything created after it in the same context, the way `set_message_id` / `setUser` do. Reach for a block or the decorator when you want the marking to end where the work ends. Both return the agent handle, so you can grab its token inline.

:::tabs
:::tab[Python]
```python
token = brizz.set_agent("board-creator").token
```
:::tab[Node.js]
```typescript
const { token } = setAgent('board-creator');
```
:::

## Subagents that run somewhere else

A subagent often doesn't run inline — the work happens in another process, minutes later. Open the agent where you hand the work off, send its **token** with the work, and re-enter the agent from that token on the other side.

The token is just a string, so the transport is up to you: put it wherever the rest of the job's data goes — a queue message, an RPC argument, a database row, an HTTP header.

:::tabs
:::tab[Python]
```python
import brizz

# Where you hand the work off
with brizz.agent("board-creator") as a:
    send_to_worker({"goal": goal, "brizz_agent": a.token})


# Wherever the work actually runs
def handle_job(job):
    brizz.set_agent(token=job["brizz_agent"])
    board_creator.run(job["goal"])
```
:::tab[Node.js]
```typescript
import { setAgent, startAgent } from '@brizz/sdk';

// Where you hand the work off
await startAgent('board-creator', ({ token }) => sendToWorker({ goal, brizzAgent: token }));

// Wherever the work actually runs
export async function handleJob(job) {
  setAgent({ token: job.brizzAgent });
  await boardCreator.generate({ prompt: job.goal });
}
```
:::

`set_agent` / `setAgent` suits the receiving side, since a handler rarely has a block worth wrapping — but a block or the decorator works there too if you have one.

The token carries the name, the session, and enough context for the detached run to join the conversation the work came from. Those calls land in that conversation, under the subagent's name — even though they ran later, elsewhere, and long after the session that dispatched them returned. Treat the token as an opaque string: pass it through, don't build or edit it.

:::info
**One token, one run.** The token identifies that particular run of the subagent, so hand a fresh one to each piece of work. Reusing one token for two hand-offs puts both in the same lane.
:::

## Nesting

A subagent that starts a subagent of its own composes with no extra work — just wrap it the same way inside. The dashboard shows it nested, so a supervisor's structure reads the way you built it.

:::tabs
:::tab[Python]
```python
import brizz

with brizz.agent("planner"):
    plan = planner.run(request)
    for step in plan.steps:
        with brizz.agent("worker"):
            worker.run(step)
```
:::tab[Node.js]
```typescript
import { startAgent } from '@brizz/sdk';

await startAgent('planner', async () => {
  const plan = await planner.generate({ prompt: request });
  for (const step of plan.steps) {
    await startAgent('worker', () => worker.generate({ prompt: step }));
  }
});
```
:::

## Bring your own agent ids

If your system already tracks its own agents — a run id per agent, a parent/child graph you keep in your own records — hand those ids to Brizz and the lanes line up with what you already have.

:::tabs
:::tab[Python]
```python
import brizz

with brizz.agent("research", id=run_id, parent_id=caller_run_id):
    research_subagent.run(task)
```
:::tab[Node.js]
```typescript
import { startAgent } from '@brizz/sdk';

await startAgent({ name: 'research', id: runId, parentId: callerRunId }, () => run());
```
:::

- `name` — the display name on the lane.
- `id` — your own id for **this run**. It only has to be unique per run; reuse one id for two runs and they merge into a single lane.
- `parentId` / `parent_id` — your own id for the agent that spawned this one. It overrides the enclosing block, so you can declare nesting your code's shape doesn't show.

Anything you leave out is filled in for you: `id` gets a fresh one on every run, and the parent defaults to the enclosing subagent — none means a top-level agent.

Every form takes these — the block, the decorator, and `set_agent` / `setAgent` alike.

:::warning
**Don't give a decorator an `id`.** A decorator runs once but its function is called many times, and a fixed `id` would put every one of those calls in the same lane. Leave `id` out and each call gets its own.
:::

## What shows up in the dashboard

Open a session and each subagent run is a collapsible **Subagent** card in the conversation, labeled with the name you gave it. Collapse it to read the conversation the user actually had; open it to see the subagent's own prompts, replies, and tool calls. Nested subagents are nested cards, and a subagent whose work errored is highlighted, so a failure deep in a delegation chain is visible without opening anything.

Anything outside a subagent block — your main agent's turns, the user's messages — stays in the session's main transcript.

Marking applies to the work that happens after it — inside the block, inside the decorated function, or after the `set_agent` line — so mark the subagent's own work, not your whole request handler. It also only applies going forward: conversations already captured are unchanged.

## See also

- [Sessions](/docs/instrument/sessions.md) — the session a subagent's work belongs to.
- [Mute messages](/docs/instrument/mute.md) — keep an internal call out of the conversation entirely.
