Cereal docs
Configure agents

Write tools in TypeScript

defineAgent puts the versioned definition and the code its tools run in one object.

Use defineAgent to keep an agent definition beside its tool handlers. Cereal stores the definition and runs the session. Your app serves the handlers, including from serverless functions. The example below assumes your app provides sources and recordTranscript.

// lib/cereal.ts
import { defineAgent, defineProvider, tool } from "@trycereal/sdk"
import { z } from "zod"

export const primary = defineProvider({
  slug: "primary",
  type: "anthropic",
  secret: process.env.ANTHROPIC_API_KEY
})

export const research = defineAgent({
  agent: {
    slug: "research-agent",
    name: "Research agent",
    model: {
      provider: primary,
      model: "claude-sonnet-4.6",
      reasoning: "low"
    },
    system: "Research the question, read source documents, and cite the evidence.",
    limits: { maxTurns: 12 },
    overrides: { model: false, reasoning: true, system: "append" }
  },
  tools: [
    tool({
      name: "read_source",
      // Descriptions are what the model chooses on, and, with deferred loading,
      // what discovery searches. Say what it does, not what it is.
      description: "Read a source document by its ID",
      input: z.object({ sourceId: z.string() }),
      annotations: { readOnlyHint: true },
      handler: async ({ sourceId }, ctx) => {
        const source = await sources.get(sourceId)
        // ctx: { session?, connection, claims, signal }, all verified.
        if (!ctx.session?.identity || ctx.session.identity.scope !== "user" ||
            source?.workspaceId !== ctx.session.metadata?.workspaceId) {
          return { found: false, error: "source not found" }
        }
        return { found: true, source }
      }
    })
  ],
  hooks: {
    onSessionEvent: async (event) => {
      if (event.type === "agent.message") await recordTranscript(event)
    }
  }
})

Tool inputs validate through Standard Schema. Zod 4.2+ and ArkType work directly; Valibot and other validators can supply inputJsonSchema. See tool schema interoperability.

For independent agents and reusable sub-agent references, see multiple agents.

Serving it

One catch-all route serves both the MCP endpoint and the hooks endpoint.

// app/api/cereal/[...cereal]/route.ts
import { research } from "@/lib/cereal"

export const { GET, POST, DELETE } = research.handlers

The handlers take a Web Request and return a Web Response, so the same export works in Next, Hono, Remix, SvelteKit, Deno, Bun and Cloudflare Workers. The SDK does not depend on Next. Defaults are /api/cereal/mcp and /api/cereal/hooks; override with mcpPath and hooksPath.

For Express, Fastify, Hono, Node HTTP, and Fetch-native runtimes, see the framework integrations. research.fetch(request) is the framework-neutral entry point and returns 404 for unknown paths and 405 for unresearched methods.

Verification happens before anything reaches a tool. See MCP and request signing.

hooks takes three callbacks, and sync registers whichever are defined: onSessionEvent for event hooks, onToolCall for the decision hook, and onSandboxPrepare for the sandbox prepare hook.

Application configuration and initialization

Set CEREAL_API_KEY to an environment key with read,data,control. For HTTP callbacks, set CEREAL_PUBLIC_URL to your application's HTTPS origin. These settings belong to the application runtime; the agent definition receives neither a client nor deployment context.

The first action initializes the referenced provider, registers the connection and upserts the agent. It learns the verification tenant from the API key, including in fresh serverless instances. Concurrent first actions share initialization.

await research.init() // optional early initialization at application startup

Unchanged agent definitions reuse their version. Provider secrets stay out of the serialized agent definition. research.definition() returns that document without a network request. Initialization registers configuration; it does not deploy your tool code.

See authoring helpers for the lifecycle and deployment for runtime credentials.

Driving it

const { id } = await research.sessions.start({
  message: "What are the Orion launch risks?",
  metadata: { workspaceId: "ws_ada" },
  identity: { scope: "user", id: "ada" }
})

for await (const event of research.sessions.stream(id)) {
  if (event.type === "agent.message") console.log(event.text)
}

Before you have a public URL

With NODE_ENV=development, these same handlers automatically connect through an outbound channel on the first agent action. See local development.

On this page