> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vendo.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Run it like a function

> run() with an output schema returns validated, typed data instead of prose, a run that needed consent comes back interrupted, and the usage totals are what you meter and bill on.

Sometimes the caller is code, not a person, and prose is the wrong answer.
Hand `run()` a schema and the result comes back with typed `output`.

## Typed output

```ts lib/triage.ts focus={11-14} theme={null}
import { support } from "@/lib/agent";
import { z } from "zod";

const Triage = z.object({
  category: z.enum(["billing", "bug", "feature", "other"]),
  urgency: z.number().int().min(1).max(5),
  reply: z.string(),
});

export async function triage(ticketId: string, ownerId: string) {
  const result = await support.run(`Triage support ticket ${ticketId}.`, {
    as: `user:${ownerId}`,
    output: Triage,
  });

  if (result.status !== "ok") {
    throw new Error(`triage ${result.status}: ${result.text}`);
  }

  return result.output; // { category, urgency, reply }, typed
}
```

`output` takes any AI SDK `FlexibleSchema`, and a zod schema is the usual one.
The value is validated before it reaches you, and `T` is inferred from the
schema, so `result.output` is typed without a cast.

Leave `output` unset and it costs nothing. No schema is sent, and `T` is
`void`.

Read `status` once and everything you then touch is on the object: `output`
lives on the `ok` arm alone, so the check above is what types it.

## What an unattended run may touch

Nobody is watching a `run()`, so nobody can be tapped mid-turn. A call the
guard wants a person for parks, and parking ENDS the turn: the result comes
back `interrupted` with the cards on it, in the time the run actually took.

<div style={{ display: "flex", flexWrap: "wrap", alignItems: "stretch", gap: 10, margin: "1.25rem 0" }}>
  <div style={{ flex: "1 1 190px", border: "1px solid #e9e6f1", borderRadius: 10, padding: "12px 14px", background: "#fdfcff" }}>
    <div style={{ fontSize: 10.5, fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: "#a8a5b4", marginBottom: 6 }}>Never offered</div>
    <div style={{ fontSize: 13, lineHeight: 1.55, color: "#4b4857" }}>Destructive and ungraded tools are filtered out of the listing the model sees.</div>
  </div>

  <div style={{ flex: "1 1 190px", border: "1px solid #e9e6f1", borderRadius: 10, padding: "12px 14px", background: "#fdfcff" }}>
    <div style={{ fontSize: 10.5, fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: "#a8a5b4", marginBottom: 6 }}>Offered, then checked</div>
    <div style={{ fontSize: 13, lineHeight: 1.55, color: "#4b4857" }}>Read and write tools are listed, and every call still needs authority captured while a person was present.</div>
  </div>

  <div style={{ flex: "1 1 190px", border: "1px solid #ddd0ff", borderRadius: 10, padding: "12px 14px", background: "#f5f1ff" }}>
    <div style={{ fontSize: 10.5, fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: "#6c3bff", marginBottom: 6 }}>No authority</div>
    <div style={{ fontSize: 13, lineHeight: 1.55, color: "#7b6fa6" }}>The call parks and the run answers <code>interrupted</code>, carrying the card.</div>
  </div>
</div>

Put those cards in front of a person, then carry on where the run stopped:

```ts focus={4} theme={null}
const result = await support.run(task, { as: subject });

if (result.status === "interrupted") {
  const done = await result.resume({ [result.interruptions[0]!.id]: "approve" });
}
```

`resume()` is not a rerun. The turn picks up byte for byte from where it
parked, and a denied call is a refusal the model reads and works around.

The result channel is the one exception. It reaches nothing, so it is never
guarded and never parks, which is what keeps a typed run from stranding on a
card nobody can answer.

## Meter on usage

Every result carries the run's token totals. This is the number to bill and
budget on.

```ts focus={3,5-9} theme={null}
const result = await support.run(task, { as: subject, output: Triage });

if (result.status !== "error") {
  await meter.record(ownerId, {
    input: result.usage.inputTokens,
    output: result.usage.outputTokens,
    cacheRead: result.usage.cacheReadTokens ?? 0,
    cacheWrite: result.usage.cacheWriteTokens ?? 0,
    model: result.usage.model,
  });
}
```

A turn that broke never reached a model, so `usage` is on the three statuses
that ran and the `error` check is what types it. `cacheReadTokens`,
`cacheWriteTokens`, and `model` are optional on `TurnUsage`; the two token
counts are always there. The same figures land on the run's audit rows in your
Cloud console.

## Cancel it

There is no `cancel()`. Cancellation is an `AbortSignal`, the same as
everywhere else on the platform.

```ts focus={3} theme={null}
const result = await support.run(task, {
  as: subject,
  signal: AbortSignal.timeout(60_000),
});
// result.status === "stopped", result.reason === "aborted"
```

An aborted run still answers. What it said, the calls it made, and the usage it
spent are all preserved.

## What the result carries

Four ends and no fifth. Read `status` once, and every field you then touch is
there.

| `status`      | Also carries                                         |
| ------------- | ---------------------------------------------------- |
| `ok`          | `output`, the typed value when you asked for one     |
| `interrupted` | `interruptions`, and `resume(decisions)` to carry on |
| `stopped`     | `reason`, either `"aborted"` or `"maxToolCalls"`     |
| `error`       | `error: { code, message }`, and `text` is empty      |

And the run itself:

| Field       | Type                       | What it holds                                            |
| ----------- | -------------------------- | -------------------------------------------------------- |
| `text`      | `string`                   | The model's own words — what it actually said            |
| `threadId`  | `string`                   | The conversation this run wrote to                       |
| `turnId`    | `string`                   | This turn, stable across a park and a resume             |
| `toolCalls` | `Array<{ call, outcome }>` | Every call attempted, in order, with the guard's outcome |
| `usage`     | `TurnUsage`                | Token totals for the whole run                           |

`text`, `threadId` and `turnId` are on all four arms. `toolCalls` and `usage`
are on the three that ran: a turn that broke never spoke, so `error` carries
the failure and an empty `text` instead of a sentence in the agent's voice.

## Options

| Option         | Type                      | Default                                       |
| -------------- | ------------------------- | --------------------------------------------- |
| `as`           | `string`                  | The agent's own subject, `vendo:agent:<name>` |
| `output`       | `FlexibleSchema<T>`       | Unset, and `T` is `void`                      |
| `maxToolCalls` | `number`                  | `20`                                          |
| `signal`       | `AbortSignal`             | Unset, and the run has no time bound          |
| `threadId`     | `string`                  | Unset, and the run mints a fresh thread       |
| `user`         | `Record<string, Json>`    | Unset                                         |
| `context`      | `Record<string, unknown>` | Unset                                         |

<CardGroup cols={2}>
  <Card title="Your own surface" href="/backend/your-own-surface">
    The same run, streamed to a screen you built.
  </Card>

  <Card title="Automate — .on()" href="/backend/automate">
    The same run, on a schedule or an event, with no caller at all.
  </Card>
</CardGroup>
