Skip to content
loom
Esc
↑↓navigate↵open⌘Jpreview
On this page

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));

Was this page helpful?