---
title: Agents
description: 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.

```ts
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](/docs/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](/docs/data). |
| `store` | Where threads are saved. See [Stores and connectors](/docs/data). |
| `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](#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`.

```ts
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:

```ts agents/support/tools/refund.ts
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:

```ts agents/support/agent.ts
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:

```ts app/api/agents/[name]/route.ts
import { createRouteHandler } from "useloom/next";
import { agents } from "@/.loom/agents";

export const { GET, POST } = createRouteHandler(agents);
```

Anywhere else that speaks `Request` and `Response`:

```ts
import { agents } from "./.loom/agents";

app.post("/api/agents/:name", (c) => agents[c.req.param("name")].fetch(c.req.raw));
```
