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

# Core concepts

> The mental model behind Avocado Studio — your components as blocks, the 19-operation vocabulary that is the safety boundary, and how drafts, approval and publishing fit together.

Avocado Studio is the operations layer for a website that already exists. Building the site is the part you have already solved; this is the weekly work that follows — copy, images, sections, metadata, translations — done by the people who own the content rather than the people who own the repository.

Six ideas carry the whole product. Blocks are your components. Operations are the only thing anything can do. A plan is a proposal. A draft is what you see before visitors do. Publishing hands content back to wherever it lives. And the operation vocabulary is the safety boundary — not a policy layered on top of one.

## A block is a component your site already has

A **block** is not an Avocado widget you have to adopt. On an existing site it is one of your own components, registered with a schema that declares which of its props are content.

```ts theme={null}
import { registerBlock, z } from "@avocadostudio-ai/site-sdk/blocks"

registerBlock("PricingTier", {
  schema: z.object({
    name: z.string().min(1),
    price: z.string().default(""),
    blurb: z.string().default(""),
    badgeVariant: z.enum(["none", "accent"]).default("none"),
  }),
  meta: {
    displayName: "Pricing tier",
    category: "content",
    fields: {
      name: { kind: "text", label: "Tier name" },
      price: { kind: "text", label: "Price" },
      blurb: { kind: "richtext", label: "Description", multiline: true },
    },
  },
})
```

That registration is the boundary you author. `name`, `price` and `blurb` have field metadata, so they appear in the property panel, the AI can edit them, and the preview can mark them for inline editing. `badgeVariant` is in the schema but has no field entry — it stays a developer concern, and no amount of asking will move it.

Three properties of that declaration matter more than they look:

* **The schema decides what is legal.** Every write is checked against it before it lands. An AI that tries to set `price` to a number is rejected, not coerced into something surprising.
* **Undeclared props survive.** A block carrying its own bookkeeping — a CMS source snapshot, the locale it was read in, an origin slot a migration recorded — keeps it through an edit. Validation decides what is *legal*; it does not decide what is *kept*.
* **Adding a field is one line.** When marketing needs something the panel does not expose, you widen the boundary deliberately, in your own repo, through your own review.

The same declaration can narrow the boundary further. `fixed: true` on a block pins it where the site draws it: nobody can move, remove or duplicate it. `readOnly: true` on a field shows the value with a `readOnlyReason` and refuses every write to it. `shared: true` makes a block site-wide content, so editing it on one page edits it on every page that holds it. See [fixed blocks and read-only fields](/integration/block-system#fixed-blocks-and-read-only-fields).

Twenty block types ship built in — Hero, FeatureGrid, Testimonials, FAQAccordion, CTA, Card, CardGrid, RichText, Stats, TwoColumn, Footer, SiteHeader, Embed, Banner, Carousel, Gallery, Tabs, Table, Quote and Video. They are a starting catalogue for a site built from scratch. On a site that already exists, the blocks are yours.

See [custom blocks](/integration/custom-blocks) for the full registration reference and [the field table](/integration/field-table) for deriving a schema, a panel, a projection and a merge from one declaration.

## Pages, blocks and props

Content is structured data, never raw HTML.

```mermaid theme={null}
flowchart LR
    Page["<b>PageDoc</b><br/>id · slug · title<br/>updatedAt · meta"]
    B1["<b>BlockInstance</b><br/>type: Hero<br/>id: hero-1"]
    B2["<b>BlockInstance</b><br/>type: PricingTier<br/>id: tier-2"]
    B3["<b>BlockInstance</b><br/>type: CTA<br/>id: cta-3"]
    P1["props<br/>heading · subheading<br/>imageUrl · ctaHref"]
    P2["props<br/>name · price<br/>blurb · badgeVariant"]
    P3["props<br/>label · href"]

    Page --> B1 --> P1
    Page --> B2 --> P2
    Page --> B3 --> P3
```

* **`PageDoc`** — one page. It carries `id`, `slug`, `title`, `updatedAt`, an ordered `blocks` array and optional `meta` (`title`, `description`, `ogImage`, `path`).
* **`BlockInstance`** — one section on that page: an `id`, a `type`, and a `props` object.
* **Props** — the fields that make up a block's content, governed by the Zod schema the block was registered with.

<Note>
  **The slug is an identity, not necessarily a URL.** A slug is a path beginning with `/`; the home page is `/`, never `""`. When the URL a visitor uses differs from that identity — a locale prefix, a `basePath`, a CMS that owns its own routing — the URL goes in `meta.path` and the slug stays the identity. A field-level-i18n CMS keeps one editable page per document per language, so `/de/events` can be a page identity on a site that serves German at the root. See [multilingual sites](/integration/multilingual).
</Note>

## Operations are the safety boundary

An **operation** is a structured, schema-validated edit action. When someone says *"change the hero heading to Welcome"*, nothing writes HTML and nothing writes a file. The system produces this:

```json theme={null}
{
  "op": "update_props",
  "pageSlug": "/about",
  "blockId": "hero-1",
  "patch": { "heading": "Welcome" }
}
```

There are exactly nineteen operations. The list is not a summary of a larger capability — it *is* the capability, a discriminated union in `packages/shared/src/schemas.ts`, and every surface writes through it: the AI chat editor, the visual editor, the MCP server, the Jira integration and any direct `POST /ops` caller all submit operations against the same union.

| Operation | What it does |
| - | - |
| `add_block` | Insert a block, optionally after a named block |
| `update_props` | Merge a patch into an existing block's props |
| `remove_block` | Delete a block from a page |
| `move_block` | Move a block to a new position on its page |
| `duplicate_block` | Copy a block within a page or onto another page |
| `reorder_blocks` | Restate a page's final block order in one atomic op |
| `add_item` | Add an entry to a list field — a testimonial, a feature, a row |
| `update_item` | Patch one list entry, addressed by `itemId` or `index` |
| `remove_item` | Delete one list entry |
| `move_item` | Move one list entry |
| `reorder_items` | Restate a list field's final order in one atomic op |
| `create_page` | Create a page from a full `PageDoc` |
| `duplicate_page` | Copy an existing page to a new slug |
| `rename_page` | Change a page's slug, title, or both |
| `remove_page` | Delete a page |
| `move_page` | Move a page's position in the site navigation |
| `update_page_meta` | Update `title`, `description` or `ogImage` |
| `update_site_config` | Update site name, logo, nav labels, nav groups |
| `update_theme` | Update semantic theme tokens and raw CSS variables |

Read that table for what is missing. There is no `write_file`, no `run_command`, no `install_dependency`, no `edit_component`, no `change_route`. A model driving Avocado at full confidence with every guardrail switched off cannot express any of them, because the vocabulary has no verb for them. This is the difference between safety that is **structural** and safety that is **procedural**: a pull request, a reviewer and an audit log are human steps that can be rushed or skipped, and when they are, the blast radius is your production deploy. A type has no bad day.

<CardGroup cols={2}>
  <Card title="Inside the boundary" icon="check">
    Block props, list items, block order, page metadata, pages, navigation, theme tokens — validated, previewable, undoable.
  </Card>

  <Card title="Outside the boundary" icon="lock">
    Components, routes, dependencies, middleware, build config, CI. Not gated — unexpressible.
  </Card>
</CardGroup>

### How an operation is validated

Every operation is checked before it changes anything.

1. **Shape.** The operation must parse against `operationSchema`. A malformed op is rejected with a category, not silently dropped.
2. **Target.** The page must exist, the block must exist, the anchor must exist, a list index must be in range.
3. **Props.** When the site supplied a block manifest, the resulting props are checked against that manifest's `propsSchema` — which is how your custom blocks are enforced. In library mode, `/ops` uses the mount's own manifest when a caller sends none, so an MCP agent or a script is held to the same rules as the editor. With no manifest entry, the props are checked against the registered Zod schema instead.
4. **Declared limits.** A fixed block cannot be moved, removed or duplicated, and a read-only field cannot be written. A new list row may only use keys the list already knows — from its field metadata, its schema, or another row — so a row written in the wrong shape is refused instead of stored where nothing reads it.
5. **Atomicity.** A batch either applies in full or leaves the page as it was.

Failures are classified rather than lumped together: `schema_violation`, `not_found`, `ambiguity`, `no_effective_change`, `malformed_output`, `planner_refusal`, `incomplete_output`, `operation_failed`, `unsupported_by_site`, `canceled`, `internal_error`. `unsupported_by_site` means the op is valid but the site behind the session has declared it cannot honour it. Only `schema_violation` is worth re-prompting a model about, so only that category triggers a repair pass — the rest either need a person or need nothing.

## The block manifest

The editor and the orchestrator both need to know what blocks a *particular* site has, not what happens to be registered in their own process. Your site answers that at `GET /api/editor/blocks`.

```json theme={null}
{
  "version": 1,
  "blocks": [
    {
      "type": "PricingTier",
      "displayName": "Pricing tier",
      "category": "content",
      "editablePaths": ["name", "price", "blurb"],
      "propsSchema": {
        "type": "object",
        "properties": {
          "name":  { "type": "string" },
          "price": { "type": "string" },
          "blurb": { "type": "string" }
        },
        "required": ["name"]
      },
      "defaultProps": { "name": "Starter", "price": "$0" },
      "fields": { "blurb": { "kind": "richtext" } }
    }
  ]
}
```

A block definition may also carry `listFields`, a `category` and `description`, `chrome`, `fixed` and `shared` flags, and `role`, `insertable` and `group` for the add-block picker. A chrome block — a site header or footer — is structurally pinned: always present, never added, moved or removed. A fixed block stays where the site draws it. An agent reading the manifest needs to be told both, or it plans an operation the engine will refuse.

The manifest is the AI's instruction manual as much as it is the validator's rulebook. A block with a thin schema is a block the AI edits badly, so completeness here pays for itself. [Coverage checks](/integration/coverage) grade how much of it your pages actually expose.

## Plans and approval

A chat turn produces an **edit plan**: an intent, a summary, a change log and a list of operations.

```
"Add a testimonials section below the hero with 3 customer quotes"

→ intent: edit_plan
→ ops:    add_block { type: "Testimonials", afterBlockId: "hero-1", props: { items: [ … ] } }
→ summary_for_user + change_log
```

Most plans apply straight away, streaming into the preview as they are generated, and stay undoable. Some are **held for explicit approval** before anything is applied. The destructive-action gate holds a plan when:

* it removes a page, always, whatever that page contains;
* its operations touch more than one page in a single turn;
* it contains three or more `remove_block` operations;
* it would remove more than half the blocks on any one page.

Undo protects recovery. It does not protect intent: it is per page rather than atomic across pages, it does not help if nobody notices until the redo stack is cleared, and a model that deleted the wrong thing deleted it confidently. So these plans stop and wait.

Approving a held plan applies the plan that was already produced — no second model call, no re-planning, no chance of a different answer than the one you read. Discarding it drops it.

## Draft mode

**Draft mode** is how your own site renders unpublished changes.

* **Published content** — what visitors see, stored in your CMS, database or files.
* **Draft content** — what the editor shows, held in the orchestrator's session state, invisible to visitors.
* **Publishing** — promoting draft content to published, on your explicit action.

In draft mode your pages fetch from the orchestrator instead of your content store: `GET /draft/pages`, `/draft/slugs` and `/draft/site-config`, scoped by `session` and `siteId`. Your renderers are unchanged — they receive the same `PageDoc` either way.

Two things authorise a draft request in production: Next.js draft mode already being enabled through your secret-gated `/api/draft` handler, or a valid `secret` on the request itself. The second exists because the editor renders your site in a cross-origin iframe where the draft cookie is frequently blocked outright. In development the `__editor=1` routing hint alone is enough; in production it authorises nothing.

A request also has to ask for a draft. The editor's iframe URL carries `?siteId=&session=`, and the routing hint, the draft cookie and the secret each count as asking. A plain visit carries none of them, so it gets the published page — in development too. That is how you check that publish did something.

## Undo, redo and the version log

Every applied change snapshots the page it touched before writing.

* **Undo and redo** are per page, per session, capped at 50 entries in each direction.
* **A chat turn is one entry.** A plan with six operations undoes as one action, because a sentence is what the person meant, not the six ops it decomposed into. Direct edits from the property panel and the visual editor push their own entries.
* **The version log** keeps up to 100 entries per session with restorable snapshots, so you can go back to a named point rather than stepping. Each entry records who made the change — a person, AI chat, an MCP client — and what it changed. A restore is itself an entry, so it can be undone.
* **A shared block undoes everywhere.** Undo on one page reverts a site-wide block on every page that holds it.

## The three services

```mermaid theme={null}
flowchart LR
    subgraph Editor["Editor :4100"]
        Chat["AI chat editor"]
        Visual["Visual editor"]
    end
    Orch["Orchestrator<br/>planning · ops engine<br/>draft state · history · publish"]
    Site["Your site :3000<br/>Next.js · your components"]

    Editor -- "POST /chat · POST /ops" --> Orch
    Orch -- "draft pages" --> Site
    Site -- "iframe + postMessage" --> Editor
```

| Service | Role |
| - | - |
| **Orchestrator** | Planning, operation validation, session state, undo and redo, publishing. Mounted inside your own Next.js app in library mode (`/api/avocado`), or run as the standalone Fastify server (`:4200`). |
| **Editor** (Vite + React, `:4100`) | Two surfaces over the same operations — the AI chat editor and the visual editor. Embeds your site in an iframe. English and German. |
| **Your site** (Next.js 15 or 16 on the App Router, or Astro 5+, `:3000`) | Renders `PageDoc` content. In draft mode it fetches from the orchestrator. |

The two editing surfaces are not two products. The **AI chat editor** is the default: describe the change, watch the plan stream into the live preview, approve or undo. The **visual editor** — Puck-based, enabled per site — is click-and-drag on the same preview, with the AI chat alongside it. Both emit operations from the same nineteen. See [visual editor mode](/integration/puck-mode).

The site talks back over a `postMessage` protocol (`site-editor/v1`): clicking a block tells the editor which block is selected, and the editor pushes `liveDraft` frames during streaming and a `draftUpdated` frame when a change lands.

## Bring your own model

The orchestrator calls **your** Anthropic, OpenAI or Google API keys. There is no token resale and no per-seat markup — you pay your own provider for your own usage.

Each provider is exposed as four tiers. You pick one in the editor under **Settings** → **Model**, and each tier's model name is overridable by environment variable:

| Tier | Used for | Override |
| - | - | - |
| `fast` | Intent routing, and edits the router judges simple | `OPENAI_MODEL_FAST`, `ANTHROPIC_MODEL_FAST`, `GOOGLE_GENAI_MODEL_FAST` |
| `balanced` | The default planner (Claude Sonnet 5.5 on Anthropic) | `*_MODEL_BALANCED` |
| `reasoning` | A planner you pick for harder edits | `*_MODEL_REASONING` |
| `codex` | The largest model on offer (Claude Opus 5.5 on Anthropic) | `*_MODEL_CODEX` |

Not every request reaches a model at all. Simple, unambiguous edits are answered by a deterministic planner with no model call; a fast intent router handles the next tier up; the full planner runs only when it is needed. See [AI providers](/ai-providers) and [how it works](/how-it-works).

## Where to go next

<CardGroup cols={2}>
  <Card title="How it works" icon="diagram-project" href="/how-it-works">
    A sentence, followed all the way to an applied, reviewable, publishable change.
  </Card>

  <Card title="Architecture" icon="sitemap" href="/architecture">
    Services, packages and the data flow between them.
  </Card>

  <Card title="Custom blocks" icon="cube" href="/integration/custom-blocks">
    Register your own components and declare which props are content.
  </Card>

  <Card title="Next.js integration" icon="plug" href="/integration/nextjs-integration">
    Routes, markers, draft mode, preview and publishing.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.