# Handlers

Back one side of an agent with a function that takes its turns

## What is a handler?

A handler is a function that takes one side's turns in a conversation. When a handler backs a side of an [agent](/docs/core-concepts/agents), the runtime calls the handler once for each inbound message. The runtime sends no model request for that side.

A handler has one role: it backs a side of an agent. Three other definitions cover the remaining roles:

- A [hook](/docs/core-concepts/hooks) changes or observes a turn that a prompt or a handler owns.
- A [tool](/docs/core-concepts/tools) is a capability that a prompt chooses to call.
- An [effect](/docs/core-concepts/effects) runs outside the conversation flow.

## Defining a handler

Create a file in `agents/handlers/` and export a `defineHandler` call as the default export. The file name is the handler name: `agents/handlers/relay_pipe.ts` defines the handler `relay_pipe`.

```typescript
// agents/handlers/relay_pipe.ts
import { defineHandler } from '@standardagents/spec';

export default defineHandler({
  description: 'Forwards each user message to the assistant child.',
  execute: async (state, message) => {
    const child = state.children.find((c) => c.relationship === 'assistant_agent');
    if (!child) return;
    const thread = await state.getChildThread(child.reference);
    await thread?.queueMessage({ role: 'user', content: message.content ?? '' });
  },
});
```

| Property      | Type                                                      | Required | Description                                  |
| ------------- | --------------------------------------------------------- | -------- | -------------------------------------------- |
| `description` | `string`                                                  | No       | A summary for people who read the definition |
| `execute`     | `(state: ThreadState, message: Message) => Promise<void>` | Yes      | The turn implementation                      |

`message` is the complete persisted message, with its `id`, `role`, `content`, `silent` flag, `metadata`, and `created_at` timestamp.

## Referencing a handler

Reference a handler from the `handler` property of a side in `defineAgent`. This is the only place that can reference a handler.

```typescript
// agents/agents/relay_agent.ts
import { defineAgent } from '@standardagents/spec';

export default defineAgent({
  name: 'relay_agent',
  sideA: {
    handler: 'relay_pipe',
    subagents: [
      {
        agent: 'assistant_agent',
        maxInstances: 1,
        immediate: true,
        parentCommunication: 'explicit',
      },
    ],
  },
  sideB: { human: true },
});
```

A handler-backed side accepts `handler`, `label`, and `subagents`. It does not accept `maxSteps`, `stopOnResponse`, `stopTool`, or the session bindings, because a handler turn has no steps.

Standard Agents Builder rejects a prompt whose `tools` or `hooks` array names a handler. The error is raised before the prompt's side runs.

## Example: a chat command

This agent closes a GitHub issue when a person sends `/close` and an issue number. The `close_command` handler backs side A, so the agent sends no model request.

Diagram: The agent close_bot has two sides. Side B is a human. Side A is the close_command handler.

```ts agents/agents/close_bot.ts
import { defineAgent } from '@standardagents/spec';

export default defineAgent({
  name: 'close_bot',
  sideA: { handler: 'close_command' },
  sideB: { human: true },
});
```

```ts agents/handlers/close_command.ts
import { defineHandler, type ThreadState } from '@standardagents/spec';

const REPOSITORY = 'acme/widgets';

async function closeIssue(state: ThreadState, number: string): Promise<string> {
  const token = await state.env('GITHUB_TOKEN').catch(() => null);
  if (!token) return `Issue ${number} is still open. No GITHUB_TOKEN is set.`;

  const response = await fetch(
    `https://api.github.com/repos/${REPOSITORY}/issues/${number}`,
    {
      method: 'PATCH',
      headers: {
        Accept: 'application/vnd.github+json',
        Authorization: `Bearer ${token}`,
        'User-Agent': 'standard-agent-example',
      },
      body: JSON.stringify({ state: 'closed' }),
    },
  ).catch(() => null);

  if (!response?.ok) {
    const reason = response ? `GitHub returned ${response.status}` : 'GitHub did not respond';
    return `Issue ${number} is still open. ${reason}.`;
  }

  const issue = (await response.json()) as { title: string };
  return `Closed issue ${number}: ${issue.title}.`;
}

export default defineHandler({
  description: 'Closes one GitHub issue when a person sends /close and its number.',
  execute: async (state, message) => {
    const number = message.content?.trim().match(/^\/close\s+(\d+)$/)?.[1];
    const content = number
      ? await closeIssue(state, number)
      : 'Send /close and an issue number, such as /close 42.';

    await state.injectMessage({ role: 'assistant', content });
  },
});
```

The handler replies with `state.injectMessage`. It reads `GITHUB_TOKEN` with `state.env`. A handler declares no variables, so set the token for your user account or your instance.

## Execution model

A turn of a handler-backed side has three stages:

1. An inbound message arrives on the side's perspective. Side A acts on `user` messages. Side B acts on `assistant` messages.
2. The runtime calls `execute` once with the thread's `ThreadState` and that message.
3. `execute` returns or throws. The turn is complete.

When the agent sets `maxSessionTurns`, a handler turn counts toward that limit in the same way as a prompt turn.

Each handler call writes one row to the thread's logs. The row names the handler and records its duration.

## Delivery

Standard Agents Builder delivers each inbound message to the handler one time:

- The handler runs once for each inbound message, in the order the messages were stored.
- One thread never runs two handler calls at the same time. A message that arrives during a handler call waits and is delivered after that call returns.
- A message whose handler call failed is not delivered again. Send the message again to retry.
- The runtime records the delivery position before each call. A restart during a call does not deliver that message a second time.

> **Warning:**
>
> A handler that runs for a long time delays every later message on its thread. A handler that waits for a `blocking` child session holds the thread for the whole session. If that child can finish only after another message arrives on this thread, the thread stops making progress. Forward with `queueMessage()` and handle the child's reply as a later turn.

## Errors

A handler that throws fails that delivery only:

- The log row for the call records the error with `error_type` `handler_execution_error` and a runtime origin.
- The inbound message stays stored. Nothing is rolled back.
- The session continues. The next inbound message starts a new handler call.

To end the session after an error, catch the error and call `state.stopSession()`.

## What a handler can do

`execute` receives the same `ThreadState` that tools receive:

- **Forward**: `(await state.getChildThread(reference))?.queueMessage(...)` sends a message to a child. `state.notifyParent(...)` sends one to a parent.
- **Respond**: `state.injectMessage({ role, content })` writes a message in the side's own role. The role is `assistant` for side A and `user` for side B.
- **Use children**: the side's `subagents` declarations make child instances available through `state.children`. A handler needs no lifecycle tools.
- **Control**: `state.stopSession()`, `state.setStatus()`, `state.scheduleEffect()`, the environment, the thread values, and the file system.

```typescript
// agents/handlers/echo_once.ts
import { defineHandler } from '@standardagents/spec';

export default defineHandler({
  description: 'Replies to the first message and then ends the session.',
  execute: async (state, message) => {
    await state.injectMessage({
      role: 'assistant',
      content: `Received: ${message.content ?? ''}`,
    });
    await state.stopSession();
  },
});
```

The runtime gives a handler no reply channel of its own. A handler that must reach a person or an external system calls that system in its own code.

## Avoiding message loops

A handler runs only when a message arrives. A handler that replies to every inbound message can still exchange messages with a prompt-backed side without end. Use one of these controls:

- Reply only when a condition holds.
- Set `maxSessionTurns` on the agent.
- Call `state.stopSession()` when the work is done.

## Next steps

- [Agents](/docs/core-concepts/agents)

  Choose what backs each side of an agent

* [Subagents](/docs/core-concepts/subagents)

  Declare the child relationships a handler forwards to

- [ThreadState](/docs/core-concepts/flowstate)

  See the state object a handler receives

* [Effects](/docs/core-concepts/effects)

  Schedule work that runs outside a turn
