# Hooks

Hooks observe session events after they are recorded and run side effects such
as updating metrics or sending alerts. They never change the turn, prompt, or
reply. Author them under `bot/hooks/` with `defineHook` from
`@grok/agent-sdk/hooks`.

## Meter token usage

With [OpenTelemetry](/docs/guides/opentelemetry.md) configured, this hook adds a live
turn's reported input and output tokens to your counters. Eval runs stay quiet,
so the dashboard reflects live traffic instead of the test suite.

```ts
// bot/hooks/usage.ts
import { metrics } from "@opentelemetry/api";
import { defineHook } from "@grok/agent-sdk/hooks";

const meter = metrics.getMeter("acme");
const inputTokens = meter.createCounter("acme.tokens.input");
const outputTokens = meter.createCounter("acme.tokens.output");

export default defineHook({
  events: {
    async "turn.completed"(event, ctx) {
      if (ctx.session.purpose === "eval") {
        return;
      }
      if (event.data.usage === undefined) {
        return;
      }

      inputTokens.add(event.data.usage.inputTokens);
      outputTokens.add(event.data.usage.outputTokens);
    },
  },
});
```

## Alert on failure

Set `PAGER_WEBHOOK_URL` to your pager's webhook. When a live turn fails, this
hook posts the agent, session, and failure message there. Eval runs and
interrupted turns stay quiet, so tests and preemptions do not page anyone.

```ts
// bot/hooks/page-on-failure.ts
import { defineHook } from "@grok/agent-sdk/hooks";

export default defineHook({
  events: {
    async "turn.failed"(event, ctx) {
      if (ctx.session.purpose === "eval") {
        return;
      }
      if (event.data.message === "turn interrupted") {
        return;
      }

      const pagerUrl = process.env.PAGER_WEBHOOK_URL;
      if (pagerUrl === undefined) {
        return;
      }

      await fetch(pagerUrl, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          agent: ctx.agent.name,
          session: ctx.session.id,
          channel: ctx.channel.id,
          message: event.data.message,
        }),
        signal: AbortSignal.timeout(5_000),
      });
    },
  },
});
```

## Events / when hooks run

Use event names from the
[session event vocabulary](/docs/reference/sessions.md#stream-events). A hook
receives each matching event after it is recorded, and the model does not wait
for the handler.

Within one session, handlers run one at a time. A slow handler delays later
handlers for that session, but it does not delay the model or handlers for
other sessions. Hooks also fire for evals, so check
`ctx.session.purpose === "eval"` before metering or paging. A restart does not
replay recorded events into hooks.

## When not to use a hook

| Want | Use instead |
| --- | --- |
| Add context before the model | `instructions.md`, skills, or `workspaceFiles` |
| Deliver to Slack or a PR | Channel [`events`](/docs/reference/channels.md#events) or packs |
| Gate behavior | [Evals](/docs/evals.md) |

To act on a turn's decision, have the model record it with an authored
[tool](/docs/reference/tools.md), then act on it from a `turn.completed` hook.

[Cursor Agent hooks](https://cursor.com/docs/agent/hooks) in
`.cursor/hooks.json` are a different product. They can observe, block, or
modify the local agent loop.

## Related

- [Hooks reference](/docs/reference/hooks.md): payloads, context, and discovery
- [Sessions: stream events](/docs/reference/sessions.md#stream-events): event
  vocabulary and payload sequence
- [OpenTelemetry](/docs/guides/opentelemetry.md): export traces and custom metrics
- [Channels: events](/docs/reference/channels.md#events): deliver replies back to
  Slack, source control, or another surface
