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.

HookWhen
bootAgent is starting
shutdownAgent is stopping
server:readyHTTP server is listening
email:receivedInbound 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.