Skip to main content
Installing the CLI package gives you two binaries pointed at the same bundle: vetta and naive. The name selects nothing — every command works under either — but a cloned blueprint repository documents the naive spellings below. naive claim is vetta naive claim, and so on.
The naive binary exists from @usenaive-sdk/vetta-cli 0.4.0. Earlier releases install only vetta, so on those every command below has to be spelled vetta naive …. The blueprint × template table below is bundled into the binary, so --template only knows the templates the release you installed knew: longform needs 0.5.0, and a data-only catalog template (one with no app, like media 2.x) needs 0.6.0 — older releases fail on it. A blueprint repo pins ^0.6.0 (and @usenaive-sdk/blueprints ^0.9.0) and gets the binary from its own pnpm install — nothing global is required: pnpm exec naive up. To install it yourself, npm i -g @usenaive-sdk/vetta-cli@^0.6.0.

Commands

claim

The key is verified with one authenticated read before anything is written; a refused key stores nothing. On success the profile is saved owner-only (0600) and the output confirms the organization and the key’s 12-character display prefix — never the key itself. The binding is the key: API keys are organization-scoped, so everything a later naive up creates lands in that key’s organization. The profile lives on this machine only — there is no server-side record tying a repository or project name to an organization, so the same clone claimed on another machine with another key deploys to another org. To move a project, claim again with the other organization’s key.

up

The report names the blueprint and the template it applied, when the config declares them: a crew provisioned into your organization is money, so which crew it was is not left to be inferred from the agent list. Loads the config, then reconciles each declared skill, identity, vault, app, agent and task by name — in that order, so agents can reference the rest and cards can name their assignee. The report lists every resource as created, updated, unchanged, deleted or refused (with the reason), and any refusal makes the exit code non-zero. Re-running is always safe: every resource is keyed by its config name, and nothing is deleted by omission — only a removed name, or a cron missing from an agent’s schedules. A live row the CLI cannot read — one written before a field’s rules tightened, say — is listed under skipped in the report and stepped over: it is neither reconciled nor deleted, and the rest of the apply runs normally. A row in skipped is a row to fix or delete by hand.

Config surface

Every cross-reference — allowed_apps, identity, a vault’s identity, a roster’s members — names a resource declared in the same config, and a config that names one that is not declared (or declares a name twice) is refused before anything is read. An agent’s system, skills, mcp_servers, window, output_schema, structured_output_required and metadata pass through to the platform verbatim, exactly as agents.create takes them; allowed_apps names apps and is written as their ids into the apps tool’s config. multiagent is the agent’s team setting as the platform spells it, except that a roster names its members by their declared name ({ type: "agent", name: "editor" } or just "editor") instead of an id — up creates every agent first and then writes the roster with the ids they were given. A roster may name an agent listed under kept, never one under removed. handoffs is true (the platform’s default: any agent of the organization), false, "team" (its own multiagent roster, so the agent must declare one), or the declared names this agent may send work to — checked against the same crew, so a name nobody declares, the agent’s own, or one written twice is refused before anything is read. A roster with handoffs: team and metadata: { role: group } is a channel in the dashboard — a place whose members are that roster. Nothing runs on it: a post with no mention asks every member, and an @mention asks just that member. A field the config leaves out is never drift: the live value stays (an app’s mcp is the one exception — see below). questions is the one key up does not reconcile, because it is not a resource: it is what a person is asked before the crew is provisioned. They use the same shape an agent asks a question with mid-run, so one form renders both, and the answers a person gives are recorded on the install rather than written into the tree — the tree is identical for every organization running the same template. Declaring them is what lets a published blueprint be set up before anything exists to configure. A template declares at most four (Templates); a hand-written config that names no template is not capped. help is the sentence shown under the label — where you say what a good answer looks like, so the label can stay short. optional: true says the field may be left blank, and a blank one comes back absent: the key is not in the answers at all, rather than present with an empty string, so an unanswered question is simply not in the project context the crew reads. It is for the question that would improve the work without gating it — a reference to imitate, say. Ask that one mandatorily and you force an answer out of a person who has none, and an invented answer is worse than an absent one, because the crew cannot tell them apart. A key you do send is checked like any other: a blank string or an unlisted option is refused whether or not the field is optional.
An agent’s tools is the platform’s toolset verbatim, with one field checked at define time: tools.configs.browser.config is the browser’s options (allowed_domains optional — the open web by default, timeout_minutes 1–360, context_name to reopen a saved login, …). A malformed one is refused by field (agents.0.tools.configs.browser.config.allowed_domains) rather than applied — the runtime would otherwise open a browser that reaches nothing. A skill’s file (resolved against the config’s directory) is pushed by slug; skills are content-addressed, so an unchanged file is unchanged and a changed one mints a new version (updated). An agent’s identity names a declared identity, which up attaches after the agent upsert; the grant is never revoked by up. An agent’s tools passes through as the same configs the API takes, so a blueprint can pin which models generate_image / generate_video may render with — the first named is the default, and any model must be in the live catalogue vetta media models --kind video lists (see generation tools):
An agent’s schedules are its cron deployments, owned as a complete set:
Deployments have no name, so each schedule is matched to the agent’s live deployments by its exact cron string (which must be unique per agent): a match with other fields drifted is patched, a missing cron is created, and any live deployment of that agent whose cron is not declared is deleted. The set is bounded to agents declared in this config — an agent without schedules keeps whatever it has, and other agents’ deployments are never touched. Each outcome is a report line named <agent> @ <cron> under schedules; a schedule identity names a declared identity.
The match is the cron string, not the schedule it denotes. "0 8 * * 1" and "0 08 * * 1" fire at the same minute and are two different keys, so re-spelling a declared cron is a delete plus a create, not a patch: the deployment gets a new dep_ id and a fresh next_run_at, and anything the old row was holding — an enabled: false, say — is gone. up deliberately does not parse cron; a client that normalised the string would have to agree with the server’s parser forever to stay idempotent, and the day they disagreed every apply would churn every schedule. Edit a cron and expect a new deployment.

Taking part of the crew

A template’s crew is provisioned whole unless you say otherwise. --only names the agents and apps to take; --without names the ones to leave out; both take declared names and either kind. The names left out come back in the report under deselected, apart from skipped (rows the CLI could not read) and from any refused line (things it tried and could not do): a deselection is a choice, and the report keeps choices apart from failures. A deselected agent is not created, its schedules are not armed and its intake does not open — a timer or a first session for an agent you declined is money spent on nobody’s instruction. A deselected agent that already exists is left running and reported deselected: narrowing the selection is not removed, and retiring a live agent stays an explicit act. A template may mark an agent or app required — the app every agent’s tools reach through, say. A selection that leaves one out is refused before a single route is called, and the message names every missing one, in the same words the platform’s own apply refuses it with:
A name the config does not declare is refused the same way. Selection is per apply and is not remembered: a later naive up with neither flag provisions everything. See Templates for what a crew, its first day and its selection are.

Tasks — the board the crew starts from

A project may declare tasks: the work its crew starts with, as cards on the organization’s shared board rather than as a briefing each agent reads alone.
key is the task’s stable name inside the project; assignee names an agent declared in the same config (or one listed under kept); blocked_by names other tasks by key, and a key that is not declared, an assignee that is not, a self-blocker, a cycle or a duplicate key is refused before anything is read. Every card lives on the board of the organization’s standing orchestrator — the CEO the studio’s Hire page creates — and up ensures that owner exists before it writes a card: finds it by its metadata.role, creates it (as CEO, on a modest monthly budget) when the organization has none, never renames one it finds, and seats this project’s agents on its roster so they can work the board. The owner is one line under agents in the report like any other agent; the board is created if missing; each task is one line under tasks. Cards are written blockers first, so blocked_by is always the ids of cards that already exist, and each is keyed <project>:<task.key>. That key is what makes re-apply honest: a second naive up finds every card already on the board and reports it unchanged, without a duplicate and without touching what an agent has since moved to doing or done. Nothing in the apply starts a session. A card with an assignee and no open blocker is picked up by the platform on its next tick, which starts the assignee on that card — so a crew declared through tasks starts working exactly once, and its first day is on the record where the whole crew can see it. A card whose assignee this apply did not provision — deselected, or refused — is refused rather than written, as are the cards behind it, so a board never names an agent that is not on it. --dry-run plans the owner, the board and every card and writes none of them.

The intake — the legacy first briefing

intake is the path tasks replaces: a private first session for one agent rather than a card on the shared board. It keeps working exactly as it did, and a project may declare both — the cards are seeded and the intakes opened, each reported under its own key — but a crew that starts from a board should not also be briefed in private, so new templates declare tasks and leave intake out. An agent may declare an intake: the message it is sent once, on the apply that creates it.
It is what turns a provisioned crew into a crew that has started working. It is not a schedule (a schedule fires again and again) and not system (that is standing instruction, re-read every turn): it is a session that is created and started with message as its first turn, reported under intake as one line named after the agent whose id is the ses_ that was opened. Nothing marks an agent as briefed, because nothing needs to: an agent is created exactly once, so a second naive up reports it unchanged and starts nothing. An agent that was merely patched is not re-briefed. Delete the agent and apply again and it intakes again, which is the correct reading of “this agent is new”. budget_micro_usd is optional and caps that one session; leave it out and the agent’s own budget governs. Intakes are opened last, after every other write in the apply, so an agent that delegates or reaches for an app finds the rest of the crew already there.

Blueprints and templates

A blueprint is the machine: the screens, the /api/* routes, the store, the build and the approval flow — or none of them, for a data-only blueprint whose crew works on the board. It is one repository. A template is data inside that repository: the crew and its prompts, the tool allow-lists and its schedules. A config names both:
media has no default on purpose: its three crews spend money in different ways — a long-form piece renders a minute or more of video where a clip only cuts one — so defaulting any of them would be a bill you did not choose. An unknown blueprint, an unknown template, or media with no template at all is refused when the config is loaded — before anything is read or written. The three media crews, by the id you pass to --template and the name the catalogue shows: The ids are the wire: they are what a config, an install and naive catalog --template carry, and they do not change when a name does. Each asks three setup questions. faceless and longform ask the niche, a reference (optional) and how often to post; clipping asks the channels to cut from, YouTube visibility (optional) and how often to post. None asks where to post: the crew posts to the accounts you connect. The chosen template’s agents become the project’s crew, and the config may declare agents of its own alongside them. Because one repository carries every template its blueprint has, switching is an edit plus naive up — never a re-clone, and never a new app: edit template, re-run, and the machine, the app, its URL and its data stay exactly where they are. Switching widens and never narrows. The new template’s agents are created; an agent only the previous template declared is reported and left alone — defineProject lists those names under kept, and up reports each one that is live as unchanged with not declared by template "…"; kept, never deleted. Your own rows — clients, posts, connections, the app, its URL, its token — are never touched by a switch. To retire an agent the old crew left behind, name it under removed yourself, which is the only thing that ever deletes.

App ownership

Every app records the project that created it, and up compares that stamp before it writes anything. An app whose row names a different project — or no project, because it was made by hand through the dashboard or the API — is refused, with the exit code non-zero and nothing written:
App names are unique per organization, and an app is the one resource where taking a name over is not recoverable: the deploy replaces a live site, an env write overwrites a secret value nothing can read back, and a removed name tears down its hosting and its database. So a name collision stops rather than converges. Fix it by renaming the app in your config, or, if the app really is this project’s, run naive up --adopt once: it takes every app the run touches, stamps each with this project’s name, and reports them updated with adopted from project "…" as the reason. From then on ordinary naive up converges them and the flag is not needed again. --adopt covers removed names too, since deleting another project’s app is worse than overwriting it, and under --dry-run it only says will adopt …. An app’s env is written as its secrets — a value is a literal or { from_env: "LOCAL_VAR" }, read from your shell at apply time so a secret never sits in the config file:
Secret values are write-only on the platform and never read back. Each write carries a hash of the value, which the platform echoes on reads, so up can tell an unchanged secret from a changed one without ever seeing a value: a secret whose live hash matches is skipped, and an app whose secrets all match reports unchanged. Changing a from_env variable’s value and re-running up rewrites just that secret — that is how rotation works for app secrets. A secret set before hashes existed (or by hand, through the dashboard or SDK) has no hash and is rewritten once. An unset from_env variable refuses the app before any write, naming the variable only. Secrets that exist on the app but are absent from env are listed in the report as unmanaged secrets: … and left alone — nothing is deleted by omission. With --dry-run the report says will set … for exactly the secrets an apply would write. An app’s deploy_dir ships as one file-map deployment carrying a hash of its contents. On the next apply, up hashes the directory again and compares it with the app’s newest deployment: equal means nothing is uploaded and the app reports unchanged (unless env or description made it updated); different, or a deployment made before hashes existed, deploys again. With --dry-run the report says will deploy <dir> or <dir> unchanged. A fullstack app’s mcp is the path it serves an MCP endpoint on ("/mcp"): agents connected to the app get its MCP tools automatically, and the platform mints a bearer token for that endpoint into the app’s VETTA_MCP_TOKEN secret, which the app should require. Declaring mcp on a frontend_only app is refused at parse time. An unchanged path is unchanged (the token is never rotated by a no-op apply); dropping mcp from the config clears the endpoint and forgets the token. With --dry-run the report says will set mcp <path> or will clear mcp. A fullstack app’s migrations are SQL files applied to its database after the app is provisioned and deployed — paths under the config’s directory, * allowed in the file name: ["sql/*.sql"]. Files match in name order within one entry and entries in declaration order, so 0001_todos.sql, 0002_index.sql is the whole ordering story. Each file is a migration named by its base name, applied once: a later apply re-sends the list and every already-applied one is a no-op, so the report says 2 migration(s), 0 new and the app is unchanged unless something was new. Changing an applied file’s contents under the same name is refused with version_conflict — add a new file. migrations on a frontend_only app is refused at parse time. A hosted apply (one the platform runs from a published blueprint) has no checkout, so the publisher ships the same files on the app’s built tree and the apply reads them from there; an artifact published before that carries none and refuses the app by name — republish it, or run naive up from a clone. A fullstack app’s buckets are the buckets its object store must hold: [{ name: "avatars", public: true }, { name: "exports" }], names in the platform’s bucket grammar (3–63 lowercase letters, digits and hyphens), public defaulting to false. Each apply lists the live buckets and creates the declared ones that are missing; one the app already holds is left exactly as it is — public included, since flipping visibility on a bucket that is serving URLs is a decision for a person — so the report says 2 bucket(s), 0 new and the app is unchanged unless one was made. Nothing is ever deleted here: a bucket dropped from the config, or made by the app’s own code, stays until vetta storage rb removes it. buckets on a frontend_only app is refused at parse time. With --dry-run the report says will ensure buckets <names>. Migrations run before buckets, and both run after the app’s secrets and deployment are written, whether naive up applies from a clone or the platform applies a published blueprint in the background. An agent’s browser is declared where every tool is — tools.configs.browser.config — with the browser options the platform accepts (allowed_domains, timeout_minutes, context_name, …), checked field by field at define time (above) and otherwise passed through unchanged. Computers, saved-login contexts and grants are not blueprint resources and cannot be declared: an agent opens its own computer, a saved-login context is what a live browser session saves, and a grant names that context’s id, so all three are made by running the agent (or with vetta browser), never by up. A vaults entry provisions a credential vault by name, optionally held by a declared identity. Credential values are real secrets, so they are { from_env } only — never a literal — and are read from your shell at apply time:
Vault credentials are write-only and immutable on the platform, so up reconciles their presence, not their value: a credential is identified by (kind, key, host / mcp_server_url). One that is live is unchanged, one that is missing is created (an unset from_env variable refuses that one credential, naming the variable only, and never echoes a value), and credentials on the vault that are not declared are never deleted or overwritten. Because a live value cannot be read or replaced through up, rotation is a manual operation: create the new credential and delete the old one on the platform (or with the SDK) — changing a from_env variable’s value and re-running up changes nothing. Each credential is a report line named <vault> / <key> under vaults; a vault’s identity cannot change after creation, so a live vault held by another identity is refused. env_var credentials are refused at parse time: they need substitution at the sandbox egress boundary, which the platform does not have, so the API would refuse them anyway — declare a static_bearer or mcp_oauth credential with an mcp_server_url instead, which is injected server-side before the sandbox sees it. To delete a resource, move its name from the declaration into the config’s removed block:
up deletes each removed name it finds live and reports it as deleted; a name that is already absent is unchanged (already absent), so removed entries can stay in the config and re-running stays safe. A name cannot be both declared and removed. Removing a vault deletes its credentials with it. Renaming is a delete plus a create: resources are keyed by name, so changing a declared name mints a new resource and leaves the old one live — put the old name under removed in the same apply, or it stays behind. The config is the source of truth: a field edited by hand in the dashboard is drift, and the next naive up converges it back to what the config declares (a field the config leaves out is never touched). Two load failures stay distinct: a config that loaded but is wrong is reported as config invalid (exit 2 — fix the file), while a config the toolchain could not evaluate (a missing install) is a config toolchain failure (exit 1). A .ts config is evaluated with the CLI’s own loader and needs only the blueprint repo’s install (defineProject comes from @usenaive-sdk/blueprints); a .json config needs nothing installed at all.

init

template

Clones a blueprint into a directory of its own. A blueprint is a public repository with a naive.config.ts in it, so what you get is exactly what is on the branch — its screens, its server and its tests — not a scaffold generated by string substitution. The git history is dropped: the first commit of your company should be yours.
--template writes your choice into the clone’s own naive.config.ts, so the next naive up provisions that crew. Nothing else is fetched — the repository already carries every template its blueprint has — and a template the blueprint does not have is refused before the clone.

catalog

The published catalogue: one row per blueprint × template × version, carrying the setup questions to ask first, the crew and apps that pair provisions, and the timers it runs.
Requires the agents:read scope. It reads and never applies — a published row becomes an install when you clone the blueprint (naive template) and run naive up, which puts the tree in front of you before it provisions a crew that costs money. It answers the question naive template cannot: which templates exist at which versions, right now, rather than which ones this binary was released knowing about. There is no catalogue inside the CLI, deliberately — publishing a blueprint is publishing a repository and a row, not shipping a new binary — so this is where the two meet.

installs

One row per project this organization has applied: which blueprint and template, at which artifact version, at which revision, and by which writer.
naive up reports the apply it just ran and nothing else. This reports what anyone applied, which is the thing to read before an apply that expects a particular revision: an install the dashboard advanced since you last ran up shows a higher revision here, and the row’s report carries what that apply did. A hosted apply the dashboard queued reads status: "pending" with report: null until it has run; --project is how to watch one row until it says applied or failed.

installs context

What the crew of the project in this directory knows about the business it runs: the setup answers with the questions they answer, the apps and their URLs, and the agents and their roles — the project context, the same object a template agent reads through its project_context tool.
The project is the config’s own name; its latest applied install is the one read, so a hosted apply still pending is not it. Without --set, the context is printed. With --set, the edits are laid over the install’s stored answers and the whole set is written back — the platform validates it against the template’s questions, so an undeclared key is refused — and the context is printed as the crew now reads it. A key set more than once, or a multiple choice set once, is a list. --clear is how an answer goes back to unanswered, and it is not the same as setting it to nothing: an empty value is an answer, and the platform refuses one. A question the template marked optional is answered by the key being absent, so --clear removes it rather than emptying it. Clearing a question that is not optional is refused, because the crew would then be working to a company setting nobody gave it. An edit is not an apply: the install’s revision does not move, nothing is re-provisioned and no agent is briefed again. The crew simply reads the new answer the next time it looks. A project with no applied install has no context yet: run naive up first. An install applied from a checkout rather than from the catalogue is validated against the questions of the config it applied, and naive up keeps its answers when it records the next apply. The record keeps only the config’s questions and its agents’ names and roles — never its env values. The repository is <org>/<blueprint>-blueprint — naive template agency clones usenaive/agency-blueprint — so publishing a new blueprint is publishing a repository: there is no catalogue inside this binary to drift from the blueprints that actually exist, and no release of the CLI is needed to add one. The suffix is the convention a fork follows too, so --org acme looks for acme/agency-blueprint. No API key is required: a blueprint is public, and claiming it comes after.