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

# Generated UI and apps: the app.tsx screen and the machine model

> How Vendo generates an app as one app.tsx screen, what that screen may write, the three-layer machine model from screen-only to machine-served, and the user-owned import and fork model.

An app is one file: `app.tsx`, a single default-exported React component. The
row that owns it is an `AppDocument` with format `vendo/app@1`, and every user
owns their own copy — import, fork, sharing, and publishing mint a fresh `app_`
id, so artifacts carry no data, grants, or authority.

Zero config: layer 1 needs nothing. `createVendo()` mounts the apps block
unless you pass `apps: false`, and a screen renders in the host-embedded
renderer with no sandbox account, no key, and no flag. Layer 2 is gated on one
thing, a configured `sandbox` adapter.

## The screen

A screen is plain TSX with a small, closed surface:

```tsx theme={null}
import { useQuery, tools, Stack, Text, Stat, Button } from "@vendo/screen";

export default function Spending() {
  const invoices = useQuery("host_invoices_list", { status: "open" });
  return (
    <Stack gap={12}>
      <Text text="Open invoices" variant="heading" />
      <Stat label="Owed" value={invoices.rows.reduce((t, r) => t + r.amount_cents, 0) / 100} />
      <Button label="Send reminders" onClick={() => tools.host_invoices_remind({ ids: [] })} />
    </Stack>
  );
}
```

Four rules, and each one is enforced rather than advised:

* **Two imports, no more**: `react` (its hooks) and `@vendo/screen`. There is no
  bundler and no `node_modules` inside a screen.
* **Data through `useQuery("tool_name", input?)`.** The tool name and the input
  are written-out literals, because a screen's queries are read out of the file
  and executed *before* the component renders — so no prop, state, or other
  query's result can reach one. Only read tools, one result per tool.
* **Actions through `tools.tool_name(args)`, from a handler.** A tool call in the
  render body would fire on every render, so it is refused.
* **No DOM.** `document`, `fetch`, timers, `process` and `<div>` do not exist
  here. Layout is `<Stack>`/`<Row>`/`<Grid>`; text is `<Text>`.

What `@vendo/screen` exports is exactly the component Kit plus the components
*your* host registered — so a name the screen renders is a name your app really
has. Props are checked against each component's own schema.

A screen stores no tree. Its tree is what *rendering* it produces, so the screen
is re-run on every open: the queries resolve against the world as it is that
instant, and the payload carries today's numbers. There is no snapshot to go
stale.

## The checks

Every save faces the same five stages, whoever wrote it — the generation loop,
Claude Code, or a person with an editor:

1. **Compile** — it has to be valid TSX.
2. **Scan** — the two rules a compiler cannot state: the import surface, and the
   query/tool discipline above.
3. **Type-check** — the real TypeScript compiler, against declarations derived
   from the Kit's schemas and your tools' own input and output schemas. A
   misspelled response field or a wrong payload key is a type error.
4. **Run once** — the query plan is executed for real and the screen is booted on
   the answers a tool actually gave.
5. **Tree** — the rendered tree is validated, and every component in it must be
   one the host registered.

Each stage is the next one's precondition, and the first one that finds something
is the last one that runs. A refusal is a repair instruction naming the line and
what to write instead. Nothing paints and no row lands: the last good screen
keeps serving. One AI reviewer pass runs on top, over the TSX and the rows its
queries really returned.

## The receipt your agent gets

Whoever called `vendo_make` gets four fields of words, never pixels:

```json theme={null}
{
  "id": "app_790892b0…",
  "title": "August Spending",
  "status": "ready",
  "say": "August Spending is on your screen."
}
```

That is the whole contract, not a summary of a richer one. An earlier version
handed the agent the whole document, and a model handed a tree eventually talks
about the tree: narrating a screen it has not seen, to someone looking at a
different one. So the receipt is deliberately unusable for narration. Say `say`,
close to verbatim, and stop.

| `status`     | What it means                                                                            |
| ------------ | ---------------------------------------------------------------------------------------- |
| `"ready"`    | the screen is on their page                                                              |
| `"building"` | an escalated build outlived the call. Honest, not an error, and there is nothing to poll |
| `"partial"`  | the screen IS painted; the server-side work its plan required is not                     |
| `"failed"`   | the checks floor rejected it and nothing is painted                                      |

`"failed"` means the bindings claimed data your host does not return, or the
props did not type-check. `"partial"` is the one a host branching on `status`
most needs: a half-built app reads as half-built instead of as plain success.

In process the call returns fast. The first streamed view part carries the app's
permanent id, so your loop gets a `vendo/app-ref@1`
([envelope contract](/existing-agents/embeds)) while the build streams over the
wire.

## Where the screen lands

By default a screen lands in the person's own list of views. A slot puts it in
your product instead, and a slot is your markup:

```tsx theme={null}
import { VendoSlot } from "@vendoai/ui/chrome";

<VendoSlot id="home-hero">
  <YourOriginalCard />
</VendoSlot>
```

Add `@vendoai/ui` as a direct dependency. Empty, the slot renders your children
untouched with no wrapper, so you can inline one anywhere. Filled, it shows the
build skeleton, then the live view, then a failure with a retry. Never a blank
hole. One view per slot per person, and a second view evicts the first.

When a person presses a guarded button inside the view — one that parks on the
approval guard — `VendoSlot` auto-mounts the [approval modal](/customize/surfaces#approval-modal-for-screen-initiated-presses)
centered over the page. Nothing to wire. If you render a bespoke slot with
`AppFrame` or `TreeView`, use [`useApprovalModal`](/reference/hooks#approval-modal-useapprovalmodal)
to mount it yourself.

Your agent aims at a slot by id:

```json theme={null}
{ "request": "This month's spending by category, largest first", "slot": "home-hero" }
```

Or you place it yourself, from the `appId` in the `vendo/app-ref@1` envelope
your loop already received:

```tsx theme={null}
<VendoSlot id={block.id} appId={block.vendoAppId} />
```

<Note>
  **Building a document or block editor? That is this shape.** One `<VendoSlot>`
  per block, with the block's own id as the slot id. A generated view then lives
  in a block like any other: it moves when the block moves, it is scoped to the
  person with that document open, and your editor keeps owning layout.
</Note>

Slot ids are yours, and nothing enumerates them for an agent. Tell the agent the
id, or let the person say it. That is why the
[prompt block](/customize/instructions#teach-your-own-agent-when-to-build-ui)
forbids inventing one.

### Moving a screen after the fact

"Put that on my dashboard", said about a view they already have, is
`vendo_apps_pin`. It answers with what it displaced, so your agent can say what
moved:

```json theme={null}
// in:  { "app": "app_5a3948bc…", "slot": "home-hero" }
{ "app": "app_5a3948bc…", "slot": "home-hero", "evicted": "app_c0d6b562…" }
```

`vendo_apps_unpin` clears the slot, and the view itself is untouched. Both are
writes, so under a `cautious` policy the first one parks in your approvals
queue, where the person sees the tool, the view, the slot, and the exact
arguments before anything moves. Routes:
[HTTP routes](/reference/http-routes).

<Warning>
  Both pin tools reach an agent over the MCP door only. The in-process tool pack
  carries no `vendo_apps_*` tool. On that path you move a view from your own
  code: write the app id into your own record and re-render the slot.
</Warning>

## The three layers

1. **Screen app**: no server. `app.tsx`, rendered by the host-embedded renderer,
   interactive through React state and guarded host tools. Most apps stay here.
   Server-shaped needs (schedules, away runs) usually ride an **automation** on
   the same app, not a machine.
2. **Screen app + machine**: the same screen plus a persistent per-app sandbox
   where execution lives — custom server code, third-party egress with secrets,
   heavy logic, working data. The machine never draws UI. Gated by one thing: a
   configured `sandbox` adapter (`createVendo({ sandbox })`). There is no flag
   beside it.
3. **Machine everything**: the machine also serves a real web app and the host
   embeds its URL as the app surface. Same sandbox gate; see
   [Layer 3 reachability and venues](#layer-3-reachability-and-venues) for what
   it additionally needs and its current caveats.

The agent escalates when an instruction demands it, preferring the cheapest rung
that can express the work: a **steps automation** (deterministic tool calls on a
trigger, created in seconds, no machine), then an **agentic automation**
(per-run judgment, still tool-only), and a **box machine** only when actual
custom code is required. With machines off (the default), a request only a box
can express refuses with a typed error naming the flag; apps that already carry a
machine keep working. Only new graduation is gated. Users do not choose a layer.
The last working surface keeps serving while the next layer builds.

Screen-only creates and edits run without an approval card because they cannot
reach host tools unguarded, cannot perform egress, and render in the sandboxed
surface. Edits that add server behavior stay approval-gated. See
[Effective risk resolution](/concepts/tools-and-safety#effective-risk-resolution).

A screen renders in a sandboxed surface by default. An approved version of an app
can also render in the host page; see
[In-client venue and approvals](/capabilities/in-client-venue) for how approvals are
recorded, how the version hash is pinned, and how new versions drop back.

## The machine

A graduated app owns one persistent sandbox. It sleeps as a snapshot, wakes in
about a second when poked, and auto-sleeps after five minutes idle. Vendo owns
only the boundary of the box (the environment in, HTTP on `$PORT` out, and a
`vendo.json` manifest); inside it, any language and framework works. A coding
agent lives in the box and does every server build and edit there, verifying its
own endpoints before reporting done.

The box holds no host credentials. Its single authority path is the `/box`
callback surface on the host's Vendo server, authenticated by a per-app bearer
token: durable rows over plain HTTP, and host tools through the same guard-bound
registry chat uses, with approvals and audit intact.

Anything the box wants beyond that boundary, a secret value or an outbound
domain, is an approve-once owner grant. See
[Secrets and outbound requests](#secrets-and-outbound-requests) for the exact
grant and injection rules.

## Progressive generation

A build writes a plan first, and the plan's skeleton is on screen within
seconds. The screen itself paints once — when it is saved and the five stages
have passed — replacing the skeleton in place under the same stream id. The
stock ai-SDK client reconciles the payload rather than appending duplicates.

You do not need to change your client. Existing consumers of `data-vendo-view`
parts see the skeleton followed by the finished screen under a single stable
part id.

## Graduation

**Layer 1 to 2** is invisible and additive. When an instruction needs server
capability (a schedule, egress with secrets, heavy logic, app-owned state), the
runtime provisions a machine, sends the build to the in-box agent, syncs the
box's `vendo.json` declarations, and parks any needed egress approval. The
existing screen keeps serving throughout.

**Layer 2 to 3** is an honest UI rewrite in the same box, gated behind the
experimental served-apps flag. The box agent builds a real web app beside the
`/fn` endpoints, the screen keeps serving through the whole build, and the
document flips to `ui: "http"` only after the host itself verifies the served
root answers. Opening a served app wakes the machine and embeds its public URL in
a sandboxed iframe; the wake latency is the loading state, and the URL is only
valid for the current wake.

Failure handling for both hops is specified in
[Graduation rollback](#graduation-rollback).

## Wire reference

The exact flags, environment contracts, and failure rules behind the model above.

### Component limits

These three bound a stored **wire** document's generated components, and the
document validator enforces all three. A generated app carries
at most **16** generated components, each at most
**64 KB** of source, and **256 KB** across all of them together.

An `app.tsx` screen has no byte budget — it is measured for compilation, types,
and one real render, never for size.

### Environment

The [app machine environment](/reference/environment-variables#app-machine-environment)
lists the exact variables injected into the box and the `/box` callback routes.

### Layer 3 reachability and venues

Layer 3 has no gate of its own: it is a narrowing of layer 2, so a configured
`sandbox` adapter is the only opt-in. What it additionally needs is a door to be
served THROUGH — the mounted wire (`createVendo().handler`, which answers
`/apps/:appId/serve/**`) plus a `VENDO_BASE_URL` to make that URL absolute. A
deployment missing either is told so before it plans: the layer-3 lane is shut,
and "this host cannot serve its own web pages for an app" arrives as a plain
line in the plan instead of a build that runs and then fails.

**Every** served app — the owner's own included — is opened at that authenticated
proxy URL, never at the sandbox provider's public ingress. The proxy re-checks
`can(viewer)` against live rows on every request, so a revoke bites the next one,
and it wakes the machine only after that check passes.

Layer 3 works on both venues: BYO e2b (`sandbox: e2bSandbox()`, which reads
`E2B_API_KEY` as its credential) and the Vendo Cloud hosted sandbox. Cloud ingress is a
single-label hostname on the existing `*.vendo.run` certificate:
`https://<id-suffix>-m.vendo.run`, where `<id-suffix>` is the machine id minus
its two-character prefix. The console mints that canonical-port URL; for any
other port the SDK rewrites the host string SDK-side, in your Node process,
inserting the port before the suffix as `<id-suffix>-<port>-m.vendo.run`.

### Graduation rollback

A failed server build rolls back to the pre-edit snapshot; a failed screen
rewrite keeps the working screen and reports the miss. In both cases the last
working surface keeps serving.

### Secrets and outbound requests

Machine egress is deny-by-default at the provider network layer, and both
secrets and egress are approve-once owner grants:

* An app declares secret names. Each declared secret needs one owner approval
  before its value enters the box environment. Only declared and granted
  secrets inject, at provision and at the env re-injection before each in-box
  edit. An ordinary wake resumes the snapshot's environment, so a grant or
  revocation decided while the machine slept fully lands at the next edit or
  re-provision. Grants are never carried by shares, forks, or publishes.
* An app declares the domains it talks to in `vendo.json`. Each declared domain
  parks one approval card; a machine never provisions or wakes with an
  unapproved declared domain. An app that declares nothing can reach only its
  own boundary (the host callback origin and the inference endpoint).

The egress allowlist, unlike the environment, is re-applied at every wake: a
domain grant decided or revoked while the machine slept counts at the next
resume. A redaction guard scrubs known secret values from everything that crosses
back out of the box. This is the SSRF and exfiltration answer, including for a
bring-your-own model key: the key sits in the box, and the box can only talk to
approved domains.
