---
title: "App agents"
description: "Give each agent your app runs an identity in the audit log, a budget, a person's approval for risky steps and a kill switch."
section: "interfaces"
status: "Building for 0.3 · ships as a experimental API"
spec: "agent-governance"
canonical: "https://mainsdk.dev/docs/interfaces/agents"
updated: "2026-10-28"
---

# App agents

> Give each agent your app runs an identity in the audit log, a budget, a person's approval for risky steps and a kill switch.

Status: Building for 0.3 · ships as a experimental API · Canonical: https://mainsdk.dev/docs/interfaces/agents · Updated: 2026-10-28

`agents` governs the agents your app runs, not the coding agent that builds it. Each gets an identity in the audit log, a budget, a person's approval for risky steps and a kill switch.

## At a glance

- Status: Building for 0.3 · ships as a experimental API
- Phase: 1 (Core, public beta)
- Milestone: 0.3 Every interface
- Plan: free in your app (Apache-2.0)
- Spec: https://github.com/hubxero/mainsdk/blob/develop/docs/specs/agent-governance.md
- Category: AI and agents

Calls:

- `agents.reserve(call)`
- `agents.settle(hold, actual)`
- `agents.release(hold)`
- `agents.record(call)`
- `agents.check(call)`
- `agents.ask(req)`
- `agents.remaining(name)`
- `agents.counters()`
- `agents.attach(budget)`
- `agents.admit(call)`
- `agents.charge(call)`
- `agents.run(call, fn)`
- `agents.approve(approvalId, by)`
- `agents.isApproved(approvalId)`
- `agents.approvals.list()`
- `agents.get(name)`
- `agents.reload(config)`
- `agents.close(approvalId, why)`
- `agents.decline(approvalId, by)`
- `agents.isDeclined(approvalId)`
- `agents.isPending(approvalId)`
- `agents.onApprove(fn)`
- `agents.remainingSpend(name)`
- `agents.request(req)`
- `agents.pendingApprovals()`
- `agents.onBudgetLow(fn)`

In main.yaml:

```yaml
agents:
  support-bot:
    budget: { spend: $5/day, calls: 500/day }
    approve: { risk: destructive, spend-over: $1 }
    enabled: true
```

Guarantees, whatever the driver:

- Every step is audited with the agent as actor and the person it acts for
- A budget is checked before the call; at the budget the call is refused
- A destructive or costly step waits for a person, and an agent never approves its own
- enabled false stops an agent on its next call, with no deploy
- Works offline from local config


## Drivers

Pick one in main.yaml with `agents.driver`.

| Driver | Id in main.yaml | Default | Note |
| --- | --- | --- | --- |
| Built in | `builtin` | yes |  |

## Codes

| Code | Where | Meaning |
| --- | --- | --- |
| `MAIN-AGENT-0001` | runtime | A call names an agent that is not under `agents` |
| `MAIN-AGENT-0002` | runtime | The agent is over its budget; the call is refused and nothing reaches the provider |
| `MAIN-AGENT-0003` | runtime | An agent tried to approve its own step |
| `MAIN-AGENT-0004` | runtime | The agent is switched off (`enabled: false`) |
| `MAIN-AGENT-0006` | runtime | The store that counts budgets did not answer, and the agent has a budget, so the call is refused (503, retryable) |
| `MAIN-AGENT-0007` | runtime | A sync budget call (`admit`, `charge`, `request`) on a store other instances share |
| `MAIN-AGENT-0005` | runtime | An approval id that isn't pending: unknown, already approved or expired |

## Features

| Feature | Phase | Milestone | Spec ticket |
| --- | --- | --- | --- |
| Agent and person in every audit event | 1 | 0.3 | `agent-governance#actor` |
| Spend and call budgets per agent | 1 | 0.3 | `agent-governance#budgets` |
| Approval above a risk or spend threshold | 1 | 0.3 | `agent-governance#approval` |
| Calls bound to a named agent | 1 | 0.3 | `agent-governance#bind-spend` |
| Kill switch without a deploy | 1 | 0.3 | `agent-governance#kill` |

## MainSDK Cloud

Everything on this page is free in your app.


## Quick start

```yaml title="main.yaml"
agents:
  support-bot:
    budget: { spend: $5/day, calls: 500/day }
    approve: { risk: destructive, spend-over: $1 }
    enabled: true
```

```ts title="lib/refund.ts"
import { agents } from "mainsdk";

declare function refundOrder(input: { orderId: string }): Promise<unknown>;   // your own action

export async function refund(user: { id: string }, input: { orderId: string }) {
  const out = await agents.run(
    { agent: "support-bot", action: "orders.refund", user: user.id, spend: "$5.00", risk: "destructive" },
    () => refundOrder(input),
  );
  if (out.status === "done") return Response.json(out.value);
  if (out.status === "pending") return Response.json({ approval: out.approval.id }, { status: 202 });
  throw new Error("unreachable: run returns done or pending");
}
```

Most apps never call `agents` directly: `ai.generate`, `pay.agent` and actions reached over MCP name the agent and pass through it. The walk-through is in [agents with an identity, a budget and approvals](https://mainsdk.dev/docs/agents/governance).

## Configuration

| Key | What it does |
| --- | --- |
| `agents.<name>.budget.spend` | The most the agent may spend in the window. Checked before each call |
| `agents.<name>.budget.calls` | The most calls in the window |
| `agents.<name>.approve.risk` | A step at this risk waits for a person |
| `agents.<name>.approve.spend-over` | A step that moves more than this waits for a person |
| `agents.<name>.enabled` | `false` stops the agent on its next call |
| `agents.<name>.run` | How `ai.run` treats the agent: `max-steps`, `timeout`, `handoff`, `delegate`. See [AI calls](https://mainsdk.dev/docs/interfaces/ai) |

## What you get

`agents` governs the agents your app runs, as opposed to the coding agent that builds it. Every call that names an agent is charged to that agent's budget before it runs, held for a person's approval above a risk or spend threshold, and audited with the agent as actor and the user as `on_behalf_of`. Setting `enabled: false` stops an agent on its next call, with no deploy. It works offline and is free. Spec `agent-governance` is `experimental`, so the shape may change. The calls on this page are built and tested.

## Examples

### Reserve, settle and release a call

`agents.reserve(call)` checks the kill switch, holds the call's estimate and one call on each budget the agent has, and returns `{ status: "run", hold }` or a pending approval. The hold is atomic across instances when `runtime.store` is `redis` or `db`. `agents.settle(hold, actual)` changes it to the real cost; `agents.release(hold)` gives it back when the call failed. `agents.run(call, fn)` does all three around your function and returns `{ status: "done", value }` or the pending approval. When the store is down, an agent with a budget fails closed with `MAIN-AGENT-0006` and an agent with none keeps running. `agents.admit` and `agents.charge` are deprecated sync forms for the `local` store, removed in 1.0.

```ts title="lib/agent-step.ts"
import { agents } from "mainsdk";

declare function grantCredits(): Promise<void>;   // your own code

const call = { agent: "support-bot", action: "credits.grant", user: "user_42", spend: "$0.50" };
const a = await agents.reserve(call);
if (a.status === "run") {
  try {
    await grantCredits();
    await agents.settle(a.hold);
  } catch (err) {
    await agents.release(a.hold);
    throw err;
  }
}
```

### Approve a waiting step

A person approves. An agent approving its own step fails with `MAIN-AGENT-0003`.

```ts title="app/api/approvals/route.ts"
import { agents, auth } from "mainsdk";

export async function GET() {
  return Response.json(agents.pendingApprovals());   // oldest first
}

export async function POST(req: Request) {
  const session = await auth.require(req, { role: "admin" });
  const { id } = (await req.json()) as { id: string };
  const call = agents.approve(id, { kind: "user", id: session.user });
  return Response.json({ approved: agents.isApproved(id), call });
}
```

### Decline a waiting step

`agents.decline(id, { kind: "user", id })` closes the step without running it; `agents.isDeclined(id)` reports it. An agent declining fails with `MAIN-AGENT-0003`. `agents.onApprove(fn)` calls `fn` when a step is approved and returns an unsubscribe function.

### Read and reload an agent's config

```ts title="lib/agent-config.ts"
import { agents } from "mainsdk";

const bot = agents.get("support-bot");   // MAIN-AGENT-0001 for an unknown name
agents.reload({ "support-bot": { ...bot, enabled: false } });   // takes effect on the next call
```

### Check, record and ask without holding a budget

- `agents.check(call)` tests the kill switch and budgets across instances and holds nothing. It returns `run` now, or a pending approval. Use it for a plan made now and charged later.
- `agents.record(call)` charges spend that already happened, such as a child run's cost. It adds the spend and one call to the budgets and writes the allow audit event. A store that does not answer is logged, not thrown.
- `agents.ask(req)` puts a step in the approval queue and checks the named agent's kill switch and budget first. `agents.request(req)` is the deprecated sync form for the `local` store, removed in 1.0.

### Read budgets and watch them

- `agents.remaining(name)` reads the spend the agent may still use now from the shared store. It returns `undefined` with no spend budget or no answer.
- `agents.remainingSpend(name)` is the sync form: micro-dollars this instance last saw. It sizes a plan; `reserve` enforces the budget.
- `agents.onBudgetLow(fn)` calls `fn(agent, used, window)` once per window when an agent passes 80% of a budget. It returns a stop function.
- `agents.counters()` returns the budget counters agents use, so `ai` and the MCP client can count in the same ones. `agents.attach(budget)` replaces them.

### Inspect and close approvals

`agents.isPending(approvalId)` is true while an approval waits for a person. `agents.close(approvalId, why)` removes one nobody will answer, with `why` set to `timeout` or `cancelled`, and records it.

## Errors and checks

| Code | When | Fix |
| --- | --- | --- |
| `MAIN-AGENT-0001` | The call names an agent not under `agents` | Fix the name, or add the agent |
| `MAIN-AGENT-0002` | The agent is over budget | Wait for the reset, or raise it on purpose |
| `MAIN-AGENT-0003` | An agent tried to approve its own step | A person approves |
| `MAIN-AGENT-0004` | The agent is switched off | Set `enabled: true` when a person decides |
| `MAIN-AGENT-0005` | The approval id is not pending: unknown, already decided or expired | List `agents.approvals.list()` and use one of those ids |
| `MAIN-AGENT-0006` | The budget store did not answer and the agent has a budget | Check `runtime.store`; agents with no budget keep running |
| `MAIN-AGENT-0007` | A sync `admit`, `charge` or `request` on a shared store | Use `reserve`, `settle`, `release` or `ask` |

## Swapping drivers

`agents` has one built-in driver. MainSDK Cloud adds shared budgets and an approvals console, never as a requirement.

## Testing

Run the calls above on `createTestApp()` and move the clock with `clock.advance()` to cross a budget window. See [the testing kit](https://mainsdk.dev/docs/interfaces/testing).

## Security and privacy

- An agent never approves its own step.
- Audit events hold the agent, the person and the surface, never the prompt.
- `enabled: false` works with no network and no deploy.

## FAQ

**Is this the coding agent?** No. See [build with agents](https://mainsdk.dev/docs/agents) for that.

## Next

- [Agents with an identity, a budget and approvals](https://mainsdk.dev/docs/agents/governance)
- [Actions and agent tools](https://mainsdk.dev/docs/interfaces/action)
- [Payments](https://mainsdk.dev/docs/interfaces/pay): `pay.agent`.
