Routes & Hooks
Scripts handle outbound work — you invoke them, they run, they finish. Routes and hooks handle inbound work — the outside world triggers your agent.
The pattern is always the same: something external happens, a route receives it, the agent is invoked with context about what happened.
Routes
server/api/ is the complete inbound surface. File-based routing maps filenames to HTTP
paths and methods.
server/api/
├── chat.post.ts → POST /api/chat
├── status.get.ts → GET /api/status
└── webhooks/
└── github.post.ts → POST /api/webhooks/github
Routes are standard h3 handlers. They receive a request, do whatever work is needed, and return a response. Invoking the agent is one option — not automatic.
// server/api/webhooks/github.post.ts
export default defineEventHandler(async event => {
const payload = await readBody(event)
if (payload.action !== "opened") return { ignored: true }
const prompt = await axon.prompt("issue-triage", {
number: payload.issue.number,
title: payload.issue.title,
body: payload.issue.body,
})
// fire and forget — respond immediately, agent works asynchronously
void axon.request({ prompt, thread: `issue-${payload.issue.number}` })
return { received: true }
})
The agent runs in a named thread keyed to the issue number. Any follow-up call for the same issue continues that conversation.
Hooks
Hooks decouple event emission from event handling. A module emits a named hook when something happens. Your code subscribes to that hook in a plugin. The module doesn't know who's listening — it just fires the event.
// @axon/github module — emits a hook when an issue opens
await axon.hooks.callHook("github:issue.opened", { number, title, labels, body })
// server/plugins/triage.ts — your agent subscribes
export default defineAxonPlugin(async axon => {
axon.hooks.on("github:issue.opened", async ({ number, title, body }) => {
const prompt = await axon.prompt("issue-triage", { number, title, body })
void axon.request({ prompt, thread: `issue-${number}` })
})
})
This is how module integrations work. The module ships the webhook route and emits the hook. You subscribe in your plugin and decide what the agent does with it.
Plugins
Plugins run at boot and have access to the full axon API. They're the right place for
hook subscriptions, event listeners, and any setup that needs to happen once before the
server starts handling requests.
// server/plugins/setup.ts
export default defineAxonPlugin(async axon => {
// subscribe to module hooks
axon.hooks.on("linear:issue.created", async ({ id, title }) => {
const prompt = await axon.prompt("triage", { id, title })
void axon.request({ prompt, thread: `linear-${id}` })
})
// subscribe to platform hooks
axon.hooks.on("email:received", async ({ from, subject, body }) => {
const prompt = await axon.prompt("email-process", { from, subject, body })
void axon.request({ prompt, thread: `email-${Date.now()}` })
})
})
Platform hooks
These are emitted by the Axon runtime itself — no module required.
| Hook | When |
|---|---|
boot | Agent is starting |
shutdown | Agent is stopping |
server:ready | HTTP server is listening |
email:received | Inbound email arrived |
Every deployed agent gets an email address at agent-name@axon.run. The email:received
hook fires automatically. Subscribe to it in a plugin to make your agent respond to email
without installing anything.
Fire and forget
When a route needs to acknowledge receipt immediately and let the agent work in the background, void the invocation and return early:
void axon.request({ prompt, thread: "background-task" })
return { received: true }
The route returns before the loop completes. The caller gets an immediate response. The agent runs to completion in the background and writes its output to the thread.
Use this for webhooks and any integration where the caller has a short timeout. Don't use
it when the caller needs the agent's response to construct their own response — use
axon.stream() and pipe the entries instead.