---
title: Callbacks
description: Documents Agency's callback hooks (e.g. `onNodeStart`) that can be registered from Agency files via `callback(...)` or passed in from TypeScript when invoking a node, including scoping rules and the no-interrupts-in-callbacks restriction.
---

# Callbacks

Agency exposes a number of hooks. It's possible to write callbacks for these hooks in Agency files or pass them in when you run the node through TypeScript. Here are both options.

## Callbacks in Agency files

```ts
import { callback } from "std::agency"

callback("onNodeStart") as data {
  print(`Node ${data.nodeName} started.`)
}
```

Callbacks registered with `callback(name, fn)` are scoped to the dynamic
extent of the function or node that calls `callback(...)`. When that function
or node returns, the callback is automatically unregistered. Callbacks
registered at module top-level (outside any function or node) are active for
the entire run.

## Callbacks in TypeScript

```ts
import { main } from "agency"
const callbacks = {
  onNodeStart: (data) => {
    console.log(`Node ${data.node.id} started.`)
  }
}

const result = main(param1, param2, { callbacks })
```

## `interrupt` is not allowed inside a callback body

Callback bodies cannot raise interrupts. The typechecker rejects any
`interrupt(...)` call (direct or transitive) inside a `callback(...) {
... }` body with an error like:

> `interrupt` is not allowed inside a callback body. Callbacks fire as
> side effects; their body cannot pause execution to ask the user a
> question. Move the `interrupt` into the calling node/function
> instead, or use a runtime guard if you wanted budget enforcement.

Callbacks fire as side effects: they run, they may throw a JS error
(which is caught and logged by the runtime), and that is it. If you
need to pause the agent for user input, put the `interrupt(...)` in
the node or function that contains the work, not in a hook callback.

## List of hooks
Here are all the hooks that Agency provides.

### onAgentStart
Called when an agent (graph) starts executing.

- `nodeName`: the name of the entry node
- `args`: the arguments passed to the agent
- `messages`: the initial message history
- `cancel(reason?)`: call this to cancel the agent before it runs

### onAgentEnd
Called when an agent finishes executing.

- `nodeName`: the name of the entry node
- `result`: the result of running the agent

### onNodeStart
Called when a graph node begins executing.

- `nodeName`: the name of the node

### onNodeEnd
Called when a graph node finishes executing.

- `nodeName`: the name of the node
- `data`: the data returned by the node

### onLLMCallStart
Called before an LLM call is made. You can return a `MessageJSON[]` array to override the messages sent to the LLM.

- `prompt`: the prompt string
- `tools`: the tools available to the LLM, each with `name`, `description`, and `schema`
- `model`: the model being used
- `messages`: the messages that will be sent

### onLLMCallEnd
Called after an LLM call completes. You can return a `MessageJSON[]` array to override the messages stored in the thread.

- `model`: the model that was used
- `result`: the full prompt result from the LLM
- `usage`: token usage statistics (if available)
- `cost`: estimated cost (if available)
- `timeTaken`: how long the call took in milliseconds
- `messages`: the messages that were sent

### onFunctionStart
Called when a function (tool) begins executing.

- `functionName`: the name of the function
- `args`: the arguments passed to the function
- `isBuiltin`: whether this is a built-in function
- `moduleId`: the module the function belongs to

### onFunctionEnd
Called when a function (tool) finishes executing.

- `functionName`: the name of the function
- `timeTaken`: how long the function took in milliseconds

### onEmit
Called when agency code invokes `emit(...)`. Receives whatever value was passed to `emit`.

### onToolCallStart
Called when the LLM invokes a tool call.

- `toolName`: the name of the tool being called
- `args`: the arguments passed to the tool

### onToolCallEnd
Called when a tool call finishes.

- `toolName`: the name of the tool
- `result`: the result returned by the tool
- `timeTaken`: how long the tool call took in milliseconds

### onStream
Called during streaming LLM responses. The data is a tagged union with one of these types:

- `{ type: "text", text }` — a chunk of streamed text
- `{ type: "tool_call", toolCall }` — a streamed tool call
- `{ type: "done", result }` — streaming is complete
- `{ type: "error", error }` — an error occurred during streaming

### onTrace
Called for each trace line emitted during execution. Providing this callback automatically activates tracing for the execution. Receives a `TraceEvent` object:

- `runId`: a unique id identifying this run (useful for distinguishing concurrent requests)
- `line`: the trace line, one of:
  - `{ type: "header", ... }` — trace metadata (first line)
  - `{ type: "chunk", hash, data }` — content-addressed data block
  - `{ type: "manifest", ... }` — checkpoint reference (one per step)
  - `{ type: "footer", checkpointCount, chunkCount, timestamp }` — emitted when execution completes
