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

# 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](https://docs.standardagents.ai/core-concepts/agents.md), 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](https://docs.standardagents.ai/core-concepts/hooks.md) changes or observes a turn that a prompt or a handler owns.
* A [tool](https://docs.standardagents.ai/core-concepts/tools.md) is a capability that a prompt chooses to call.
* An [effect](https://docs.standardagents.ai/core-concepts/effects.md) 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 theme={null}
// 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 timestamps.

## 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 theme={null}
// 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.

## 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 theme={null}
// 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

> [card] [Agents](https://docs.standardagents.ai/core-concepts/agents.md)
>
> Choose what backs each side of an agent

> [card] [Subagents](https://docs.standardagents.ai/core-concepts/subagents.md)
>
> Declare the child relationships a handler forwards to

> [card] [ThreadState](https://docs.standardagents.ai/core-concepts/flowstate.md)
>
> See the state object a handler receives

> [card] [Effects](https://docs.standardagents.ai/core-concepts/effects.md)
>
> Schedule work that runs outside a turn


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.