Agents
An agent is a folder. Learn what each file does and how Loom turns the folder into an endpoint.
Every folder in agents/ is an agent. The folder name is the agent’s name and its URL: agents/support is served at /api/agents/support.
agents/support/
instructions.md system instructions, plain markdown (optional)
agent.ts model, auth, connectors, store (required)
tools/*.ts one tool per file (optional)
Folders and tool files whose names start with _ are skipped, so you can keep drafts and shared helpers beside your agents.
agent.ts
Default-export defineAgent(). Only model is required.
import { anthropic } from "@ai-sdk/anthropic";
import { defineAgent } from "useloom";
import { sqliteStore } from "useloom/sqlite";
import { supabaseTools } from "useloom/supabase";
export default defineAgent({
model: anthropic("claude-sonnet-5-5"),
auth: (request) => getUser(request),
connectors: [supabaseTools({ client: ({ request }) => supabaseFor(request), tables: ["orders"] })],
store: sqliteStore(),
});
| Option | Does |
|---|---|
model |
Any AI SDK language model instance. See Models. |
instructions |
System instructions, if you don’t use instructions.md. Set one or the other, not both. |
tools |
Tools defined inline, in addition to tools/. |
connectors |
Tool bundles for external systems. See Stores and connectors. |
store |
Where threads are saved. See Stores and connectors. |
auth |
Returns the signed-in user, or null to reject the request with 401. |
maxSteps |
Most model steps per request. Defaults to the AI SDK’s 20. |
approval |
Which tools need the user’s approval, by name. See Approvals. |
approvalSecret |
Signs approvals. Defaults to LOOM_APPROVAL_SECRET. |
instructions.md
The agent’s system instructions. Write them as you would brief a colleague. Loom passes the file to the model as-is.
tools/
Each file default-exports a tool(). The file name becomes the tool name in camelCase: tools/lookup-order.ts is lookupOrder.
import { tool } from "useloom";
import { z } from "zod";
export default tool({
description: "Look up an order by its id",
input: z.object({ id: z.string() }),
run: async ({ id }, ctx) => db.orders.find(id, { for: ctx.user }),
});
ctx holds user (from auth, or null), threadId, request, and an abortSignal. Tool names must be unique across tools/, agent.ts, and connectors; Loom throws at startup when two collide.
Approvals
Any tool can wait for the user’s approval before it runs. Set it on the tool:
export default tool({
description: "Refund an order",
input: z.object({ id: z.string() }),
approval: true, // or (input, ctx) => input.amount > 100
run: async ({ id }) => payments.refund(id),
});
Or choose per agent, which covers connector tools too and overrides each tool’s own setting:
defineAgent({
model,
approval: { refund: true, supabaseUpdate: true, lookupOrder: false },
});
When a tool needs approval, the reply stops with that tool call waiting. useAgent lists it in pendingApprovals; call approve(id) or deny(id, reason) and the conversation continues by itself. A denied tool never runs, and the model is told it was denied.
Approvals are signed so a client cannot forge one. Set LOOM_APPROVAL_SECRET to 32 or more random bytes (openssl rand -base64 32). In development Loom uses a temporary secret and warns; in production, a request that needs approval fails until the secret is set.
The registry
Loom turns agents/ into a registry at .loom/agents.ts, which imports each agent.ts and tool and inlines instructions.md. In Next.js, withLoom keeps it current; elsewhere, run loom dev while developing and loom gen before you build. Add .loom/ to your .gitignore.
Always serve agents from the registry. Importing agent.ts directly skips the folder’s instructions.md and tools/.
Serving
Each agent has a web-standard handler, agent.fetch(request). POST with { id, messages } streams a reply; GET ?threadId= returns a saved thread when the agent has a store.
In Next.js, one route serves every agent:
import { createRouteHandler } from "useloom/next";
import { agents } from "@/.loom/agents";
export const { GET, POST } = createRouteHandler(agents);
Anywhere else that speaks Request and Response:
import { agents } from "./.loom/agents";
app.post("/api/agents/:name", (c) => agents[c.req.param("name")].fetch(c.req.raw));