> ## 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.

# In your backend

> One package, one agent() call, one tool. Answer in a line with chat(), put the same agent on HTTP with handler(), and drive unattended work with run().

A governed agent in any Node backend, from an empty project. One package, no
CLI, no `init`, no files moved around. The UI stays yours.

<Steps>
  <Step title="Compose the agent">
    Install one package and write one `agent()` call at module scope. Only `name`
    is required.

    <CodeGroup>
      ```bash npm theme={null}
      npm install @vendoai/agents ai zod
      ```

      ```bash pnpm theme={null}
      pnpm add @vendoai/agents ai zod
      ```
    </CodeGroup>

    ```ts lib/agent.ts focus={4-16} theme={null}
    import { agent, tool } from "@vendoai/agents";
    import { z } from "zod";

    export const support = agent({
      name: "support",
      instructions: "Answer in the product's voice; never invent account numbers.",
      tools: [
        tool({
          name: "order_status",
          description: "Look up one order by id and return its shipping status and ETA.",
          risk: "read",
          inputSchema: z.object({ orderId: z.string() }),
          execute: ({ orderId }) => lookupOrder(orderId),
        }),
      ],
    });
    ```

    `description` is required, and it is the only thing the model reads when it
    decides whether to call the tool. `risk` is `read`, `write`, or `destructive`,
    and your label is final. Grade it: leave `risk` off and the tool is ungraded,
    which the guard asks a person about every time — so the very first `chat()`
    comes back `interrupted` with nothing run.

    Nothing else is configured. Left unset, the agent thinks in this process, and
    threads and audit rows are persisted automatically, with zero setup.

    <CardGroup cols={4}>
      <Card title="tools">
        `tool()` for your own code, `api()` for your existing HTTP API.
      </Card>

      <Card title="guard">
        Risk grade, approval, and an audit row on every call.
      </Card>

      <Card title="store">
        Threads and audit rows. Embedded, your Postgres, or Cloud.
      </Card>

      <Card title="harness">
        `vendo()` by default, `claudeCode()` on request.
      </Card>
    </CardGroup>

    <Note>
      **Already have an HTTP API?** `vendo init` reads it into tool definitions and
      `tools: [api()]` hands the whole thing to the agent, graded, in one line. It
      is an addition to this page, not a prerequisite for it — see
      [API tools](/capabilities/api-tools).
    </Note>
  </Step>

  <Step title="Ask it something">
    `chat()` is one turn and the answer, with no route and no stream in the way.

    ```ts theme={null}
    const turn = await support.chat("Where is order A-1001?");

    console.log(turn.text);
    ```

    That is the whole hello world. The agent needs a model to think with, and the
    shortest way to give it one is `npx vendoai@latest login`, which mints a
    `VENDO_API_KEY` into `.env.local` and never prints it.

    `.env.local` is a Next.js convention, and nothing in `@vendoai/agents` reads it.
    A plain Node backend — which this page is — has to be handed the variable:

    ```bash theme={null}
    node --env-file=.env.local ./dist/chat.js   # or: export VENDO_API_KEY=…
    ```

    To bring your own model instead, install `@ai-sdk/anthropic` — the major that
    pairs with your `ai`, 4 with `ai@7` and 3 with `ai@6` — and pass it beside
    `name`. That is two lines:

    ```ts lib/agent.ts focus={1,6} theme={null}
    import { anthropic } from "@ai-sdk/anthropic";
    import { agent } from "@vendoai/agents";

    export const support = agent({
      name: "support",
      model: anthropic("claude-sonnet-4-6"),
    });
    ```

    Either way the credential is read at the first turn, never at build time — see
    [Model credentials](/production/model-credentials).

    `turn` is a `TurnResult`: read `status` once and everything you then touch is
    there. `ok` carries the typed `output`; `interrupted` carries the
    `interruptions` a person has to answer and a `resume()` that carries on from
    where the turn parked. [Converse](/backend/converse) covers both.
  </Step>

  <Step title="Put it on HTTP">
    `handler()` is the whole agent as one fetch handler — the chat turn, the thread
    list and transcript, and the approvals wire. Mount it on one catch-all route.

    ```ts app/api/agent/[[...path]]/route.ts focus={4-11} theme={null}
    import { support } from "@/lib/agent";
    import { auth } from "@/lib/auth";

    const handle = support.handler({
      basePath: "/api/agent",
      // Your session, read per request. `null` answers 401.
      resolveUser: async (request) => {
        const user = await auth(request);
        return user === null ? null : { subject: user.id };
      },
    });

    export { handle as GET, handle as POST, handle as DELETE };
    ```

    In the browser, `useVendoChat` from `@vendoai/ui` speaks to that mount and
    keeps nothing of its own:

    ```tsx app/support/page.tsx focus={3} theme={null}
    import { useVendoChat } from "@vendoai/ui";

    const { messages, sendMessage, interruptions, resume } = useVendoChat({
      api: "/api/agent",
    });
    ```

    Render `messages` however your app renders anything else, and `interruptions`
    as approve/deny cards that call `resume`.

    <Note>
      Prefer to own the route yourself? `respond()` is one turn as a streamed
      `Response` you return unchanged, and it is not going anywhere.
      [Converse](/backend/converse) has both.
    </Note>
  </Step>

  <Step title="Run unattended work">
    `run()` is work nobody is watching. The same object is awaitable and iterable,
    so `run.events` is the live feed and awaiting it is the result.

    ```ts app/api/reports/route.ts focus={10,15-18,29} theme={null}
    import { THREAD_ID_HEADER } from "@vendoai/agents";
    import { support } from "@/lib/agent";
    import { auth } from "@/lib/auth";

    export async function POST(req: Request) {
      const user = await auth(req);
      if (user === null) return new Response("unauthorized", { status: 401 });
      const { brief } = (await req.json()) as { brief: string };

      const run = support.run(brief, { as: user.id, signal: req.signal });

      const encoder = new TextEncoder();
      const stream = new ReadableStream<Uint8Array>({
        async start(controller) {
          for await (const event of run.events) {
            controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
          }
          const result = await run;
          controller.enqueue(
            encoder.encode(`event: result\ndata: ${JSON.stringify(result)}\n\n`),
          );
          controller.close();
        },
      });

      return new Response(stream, {
        headers: {
          "content-type": "text/event-stream",
          [THREAD_ID_HEADER]: run.threadId,
        },
      });
    }
    ```

    <Note>
      `run.threadId` is there before the first event, so the header goes out with
      the response. `signal: req.signal` ties the run to the connection, and
      closing the tab stops it.
    </Note>

    Away, the menu is already narrower: destructive and ungraded tools are never
    offered, and a read or write call still needs authority a person captured while
    they were present — a grant. Without one the call parks, and the run comes back
    `interrupted` carrying the cards for someone to answer. [Run](/backend/run)
    covers what an unattended run may touch.
  </Step>
</Steps>

***

## One run, seen from your own UI

Events leave your server on the left. Your own progress screen fills on the
right.

<div style={{ border: "1px solid #e9e6f1", borderRadius: 14, overflow: "hidden", background: "#fff", boxShadow: "0 18px 40px -22px rgba(24,18,54,.3)", margin: "1.25rem 0 0.5rem" }}>
  <div style={{ display: "flex", flexWrap: "wrap" }}>
    <div style={{ flex: "1 1 280px", minWidth: 0, background: "#151322" }}>
      <div style={{ padding: "8px 12px", background: "#1c1930", borderBottom: "1px solid #2a2542", fontFamily: "ui-monospace, monospace", fontSize: 10.5, color: "#8b83ad" }}>
        POST /api/reports · text/event-stream
      </div>

      <div style={{ padding: "12px", fontFamily: "ui-monospace, monospace", fontSize: 11, lineHeight: 1.9 }}>
        <div><span style={{ color: "#7a72a0" }}>status</span> <span style={{ color: "#c6c1de" }}>started</span></div>
        <div><span style={{ color: "#b79bff" }}>tool-call</span> <span style={{ color: "#c6c1de" }}>listInvoices</span></div>
        <div><span style={{ color: "#4fc6b1" }}>tool-result</span> <span style={{ color: "#c6c1de" }}>42 rows</span></div>
        <div><span style={{ color: "#b79bff" }}>tool-call</span> <span style={{ color: "#c6c1de" }}>getPayments</span></div>
        <div><span style={{ color: "#4fc6b1" }}>tool-result</span> <span style={{ color: "#c6c1de" }}>38 rows</span></div>
        <div><span style={{ color: "#e6e2f5" }}>text</span> <span style={{ color: "#fff" }}>Three invoices are overdue.</span></div>

        <div style={{ marginTop: 8, paddingTop: 9, borderTop: "1px solid #2a2542" }}>
          <span style={{ color: "#4fc6b1" }}>result</span> <span style={{ color: "#8ff0dc" }}>ok · 4 tool calls</span>
        </div>
      </div>
    </div>

    <div style={{ flex: "1 1 280px", minWidth: 0, background: "#fbfbfc" }}>
      <div style={{ padding: "8px 12px", background: "#f6f4fa", borderBottom: "1px solid #e9e6f1", fontFamily: "ui-monospace, monospace", fontSize: 10.5, color: "#a8a5b4" }}>
        app.maple.com/reports
      </div>

      <div style={{ padding: "15px 16px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
          <span style={{ width: 13, height: 13, borderRadius: 4, background: "#0f7b6c" }} />

          <span style={{ fontSize: 13, fontWeight: 600, color: "#16211f" }}>Monthly close</span>
          <span style={{ marginLeft: "auto", fontSize: 10, fontWeight: 600, background: "#e7f4f1", color: "#12403a", border: "1px solid #cbe6e0", borderRadius: 999, padding: "2px 9px" }}>Done</span>
        </div>

        <div style={{ height: 6, borderRadius: 3, background: "#eaeeed", overflow: "hidden", marginBottom: 6 }}>
          <span style={{ display: "block", height: "100%", width: "100%", background: "#0f7b6c" }} />
        </div>

        <div style={{ display: "flex", fontSize: 10, color: "#8a9491", marginBottom: 12 }}>
          <span>August 2026</span>
          <span style={{ marginLeft: "auto", fontWeight: 600, color: "#16211f" }}>4 steps</span>
        </div>

        <div style={{ display: "flex", alignItems: "center", gap: 9, fontSize: 11.5, color: "#4b5654", padding: "7px 0" }}>
          <span style={{ color: "#0f7b6c" }}>✓</span> Pulled invoices
          <span style={{ marginLeft: "auto", fontFamily: "ui-monospace, monospace", fontSize: 10, color: "#9aa4a1" }}>42</span>
        </div>

        <div style={{ display: "flex", alignItems: "center", gap: 9, fontSize: 11.5, color: "#4b5654", padding: "7px 0", borderTop: "1px solid #eef2f1" }}>
          <span style={{ color: "#0f7b6c" }}>✓</span> Matched payments
          <span style={{ marginLeft: "auto", fontFamily: "ui-monospace, monospace", fontSize: 10, color: "#9aa4a1" }}>38</span>
        </div>

        <div style={{ display: "flex", alignItems: "center", gap: 9, fontSize: 11.5, color: "#4b5654", padding: "7px 0", borderTop: "1px solid #eef2f1" }}>
          <span style={{ color: "#0f7b6c" }}>✓</span> Drafted the summary
          <span style={{ marginLeft: "auto", fontFamily: "ui-monospace, monospace", fontSize: 10, color: "#9aa4a1" }}>1</span>
        </div>

        <div style={{ marginTop: 12, border: "1px solid #e6e8ea", borderRadius: 9, background: "#fff", padding: "10px 11px" }}>
          <div style={{ display: "flex", fontSize: 10.5, fontWeight: 600, color: "#16211f" }}>
            <span>Overdue</span>
            <span style={{ marginLeft: "auto", fontWeight: 500, color: "#9aa4a1" }}>3 invoices</span>
          </div>

          <div style={{ fontSize: 18, fontWeight: 600, letterSpacing: "-0.03em", color: "#16211f", marginTop: 4 }}>\$12,480</div>
        </div>
      </div>
    </div>
  </div>
</div>

<p style={{ fontSize: 13, color: "#7c7989", textAlign: "center", marginTop: 0 }}>your chrome, not Vendo's</p>

***

## Where to go next

Every verb in depth, and the surface you build around them.

<CardGroup cols={3}>
  <Card title="Converse" href="/backend/converse">
    Threads, forwarded credentials, and approvals with a person present.

    `support.chat(message)`
  </Card>

  <Card title="Run" href="/backend/run">
    Typed output from a schema, plus the usage you meter on.

    `await support.run(task)`
  </Card>

  <Card title="Your own surface" href="/backend/your-own-surface">
    Forward the event feed over SSE and end on the result.

    `for await (const e of run.events)`
  </Card>
</CardGroup>

The whole of this page as a project you can run is
[`examples/standalone-agent`](https://github.com/runvendo/vendo/tree/main/examples/standalone-agent):
one `agent()`, one `tool()`, `chat()` in the terminal, and `handler()` on a
Node server.
