BashBrainTeammatesPricingDevelopersGet a test key

deploy

Publish a new version of your agent's setup. Edit vestra.yaml, run deploy, and Vestra saves it as a new version and runs your tests. If they pass, the new version starts handling test traffic. Deploy with your live key to put it in front of real users, and roll back if something goes wrong. This setup is called a deployment.

Publish a change

Vestra saves vestra.yaml as a new version and runs its tests. When they pass, the new version handles test traffic. If the file has not changed, no new version is made.

TypeScript
const applied = await vestra.deploy('vestra.yaml');console.log(applied.version, applied.promote.status);// The first deployment you promote becomes the default, and every ask that names no deployment goes to it.// became_default is true when this call made it the default. If the call waits for tests, that happens when they pass.if (applied.became_default) console.log('default deployment: an ask without a deployment reaches it');

Go live with real users

Deploy the same file with a live key.

TypeScript
const live = new Vestra({ apiKey: process.env.VESTRA_LIVE_KEY });await live.deploy('vestra.yaml', { live: true });

Roll back to the previous version

The version that was live before comes back at once.

TypeScript
await live.deploy.rollback('trip-rescue');

Publish a change

Vestra saves vestra.yaml as a new version and runs its tests. When they pass, the new version handles test traffic. If the file has not changed, no new version is made.

Python
applied = vestra.deploy("vestra.yaml")print(applied.version, applied.promote.status)# The first deployment you promote becomes the default, and every ask that names no deployment goes to it.# became_default is true when this call made it the default. If the call waits for tests, that happens when they pass.if applied.became_default:    print("default deployment: an ask without a deployment reaches it")

Go live with real users

Deploy the same file with a live key.

Python
live = Vestra(api_key=os.environ["VESTRA_LIVE_KEY"])live.deploy("vestra.yaml", live=True)

Roll back to the previous version

The version that was live before comes back at once.

Python
live.deploy.rollback("trip-rescue")

Publish a change

Vestra saves vestra.yaml as a new version and runs its tests. When they pass, the new version handles test traffic. The first deployment you promote becomes the default once its tests pass, and every ask without --deployment goes to it.

CLI
vestra deploy

Go live with real users

Deploy with a live key.

CLI
VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --live

Roll back to the previous version

The version that was live before comes back at once.

CLI
VESTRA_API_KEY=$VESTRA_LIVE_KEY vestra deploy --rollback trip-rescue

While you build, vestra dev --tools ./tools.mjs publishes every save to test mode, and your tools run on this computer. It runs until you press Ctrl-C.

Publish a change

Send the whole file as JSON, by its name. The first call creates the deployment, and a file that changes nothing makes no new version. Its tests run, and when they pass the version goes live in your key's mode: test traffic with a test key, real users with a live key. A test plays your own tools with tool_results. The first deployment you promote becomes the default, and every ask that names no deployment goes to it. became_default is true when this call made it the default. If the call waits for tests, that happens when they pass.

HTTP
curl -X PUT "$VESTRA_BASE_URL/v1/deployments/trip-rescue?promote=after_tests" \  -H "Authorization: Bearer $VESTRA_API_KEY" \  -H "Content-Type: application/json" \  -d '{    "name": "trip-rescue",    "instructions": "You help Tripnest travellers fix broken trips. Always offer two options.",    "apps": ["gmail", "google-calendar"],    "tools": [{ "function": "get_booking", "description": "A booking by its PNR", "input": { "pnr": "string" }, "read_only": true }],    "approvals": ["payment", "booking", "cancellation"],    "tests": [{      "name": "Bag allowance",      "ask": "How many kilos can I take on booking TN-48213?",      "expect": "Says 7 kg of cabin baggage.",      "tool_results": { "get_booking": { "cabin_kg": 7 } }    }]  }'

Read the file and its versions

The file comes back as YAML.

HTTP
curl "$VESTRA_BASE_URL/v1/deployments/trip-rescue?format=yaml" \  -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/deployments/dep_4kq/versions" \  -H "Authorization: Bearer $VESTRA_API_KEY"

Roll back to the previous version

The version that was live before comes back at once. This needs a live key.

HTTP
curl -X POST "$VESTRA_BASE_URL/v1/deployments/dep_4kq/rollback" \  -H "Authorization: Bearer $VESTRA_LIVE_KEY"

/v1/mcp never deploys: publishing a version stays with you. In your editor, vestra mcp offers a deploy tool that runs on your own computer, with the key vestra login saved.

Publish a change

Send the whole file as JSON, by its name. The first call creates the deployment, and a file that changes nothing makes no new version. Its tests run, and when they pass the version goes live in your key's mode: test traffic with a test key, real users with a live key. A test plays your own tools with tool_results. The first deployment you promote becomes the default, and every ask that names no deployment goes to it. became_default is true when this call made it the default. If the call waits for tests, that happens when they pass.

HTTP
curl -X PUT "$VESTRA_BASE_URL/v1/deployments/trip-rescue?promote=after_tests" \  -H "Authorization: Bearer $VESTRA_API_KEY" \  -H "Content-Type: application/json" \  -d '{    "name": "trip-rescue",    "instructions": "You help Tripnest travellers fix broken trips. Always offer two options.",    "apps": ["gmail", "google-calendar"],    "tools": [{ "function": "get_booking", "description": "A booking by its PNR", "input": { "pnr": "string" }, "read_only": true }],    "approvals": ["payment", "booking", "cancellation"],    "tests": [{      "name": "Bag allowance",      "ask": "How many kilos can I take on booking TN-48213?",      "expect": "Says 7 kg of cabin baggage.",      "tool_results": { "get_booking": { "cabin_kg": 7 } }    }]  }'

Read the file and its versions

The file comes back as YAML.

HTTP
curl "$VESTRA_BASE_URL/v1/deployments/trip-rescue?format=yaml" \  -H "Authorization: Bearer $VESTRA_API_KEY" curl "$VESTRA_BASE_URL/v1/deployments/dep_4kq/versions" \  -H "Authorization: Bearer $VESTRA_API_KEY"

Roll back to the previous version

The version that was live before comes back at once. This needs a live key.

HTTP
curl -X POST "$VESTRA_BASE_URL/v1/deployments/dep_4kq/rollback" \  -H "Authorization: Bearer $VESTRA_LIVE_KEY"

Good to know

Your first deploy

A deployment holds your agent's instructions, the apps and tools it may use, the actions that need the user's approval, and its limits. deploy always sends your whole file. The first time, it creates the deployment. A file with no changes makes no new version. A deployment created with POST /v1/deployments starts with its draft as version 1, so its first saved version is 2.

How deploy waits for your tests

vestra deploy and vestra deploy --live wait for the new version's tests, then promote it. Promoting a version makes it the one your agent runs in that mode, test or live. The command prints each result as it arrives (failed: <name>: <reason>), and then promoted in test mode, version 5.

The exit code tells CI and coding agents what happened. 0 means the version now runs in that mode, or there was nothing to wait for (an unchanged file, or --no-promote). 1 means a test failed, the run ended in an error or was interrupted, or the promotion was refused or another promotion got there first. 2 means the command was written wrong.

Ctrl-C stops the waiting (exit 130), but not the test run. If the run passes, it still promotes the version. --no-wait returns at once with the run's id, which you can follow with vestra events <trun_id>. If you retry a deploy whose version is already being tested, it follows that run. A version that vestra test already passed is promoted without testing it again. The deploy tool in vestra mcp only promotes in test mode, and follows the tests for up to 45 seconds. If the run is still going, it returns the run's id, which you can read with its test tool.

Your default deployment

The first deployment you promote becomes your company's default, and this happens only once. If the promotion waits for tests, the deployment becomes the default when they pass. If you deploy the file vestra init saved without changing it, it becomes the default at once. From then on, every ask that names no deployment goes to the default. If you set or clear the default yourself, Vestra leaves it as you set it.

If an ask goes to the default before it has a version in your key's mode, plain Bash answers it, without your setup. If you name a deployment that has no such version, the ask fails with deployment_not_live. Archiving the default deployment clears the default when it has no live version. When it has one, archiving it is refused with deployment_is_default.

Connecting MCP servers

Your file registers each MCP server by its URL, and one URL is one server. Its headers are shared by every deployment and mode that uses it. So if another deployment runs the server and your file would give it different headers, the deploy fails with 409 mcp_server_shared, naming those deployments.

The tool policy is not shared. The deny, require_approval and run_without_asking lists in your file belong to the version the file makes, stored as manifest.mcp_tool_policy. They take effect when that version is promoted. The policy set on the server itself (PATCH /v1/mcp-servers/{id}) is a minimum that every version runs under, and a version's own policy can only add to it.

A tool that the server does not mark read-only (readOnlyHint) needs the person's approval in every version, unless that version's run_without_asking lists it. Bash trusts your server's read-only mark, so mark only tools that change nothing. If your file's run_without_asking names a tool your server does not list, the deploy fails with 400 tool_not_on_server. A test key cannot change or delete a server that a live version uses. That fails with 409 mcp_server_live, so use a live key, or make the change as an owner or admin. Every version reaches every company server as it is, so while a deployment runs a version live, a test key (or a developer in the console) cannot add, change or delete any server. A file that would register a new one fails with 409 shared_change before anything is written.

Skills and knowledge

Skills and knowledge are not versioned yet: every version, the live one included, reads them as they are now. So while a deployment runs a version live, a test key cannot change them. A test key's write to a skill or a knowledge notebook, including the uploads vestra deploy makes, fails with 409 shared_change before anything is written, and names the live deployments. vestra deploy then exits with code 2 and names what your file changes.

Apply those changes with a live key (vestra deploy --live), which changes them for your users at once, or as an owner or admin. Until a version is live, a test key can change them freely.

Functions that need approval

Before Bash calls a function your code runs, it asks the person for approval. Two marks in your file skip that: read_only: true for a function that changes nothing, such as a lookup, and run_without_asking: true for one that changes something but needs no approval. Either mark lets the function run at once, and you can use both together. Bash trusts the marks in your file, so mark only functions that do what the mark says.

The approval shows every input of the call. So a function that charges, books or changes something should take each value the person agrees to as an input. For example, extend_rental takes rental_id, charge_usd and new_due_back, not rental_id alone. Your code then checks those values against its own before it acts, and throws if one is different. The throw becomes the call's error, and nothing is charged.

In a conversation, Bash starts a separate task to call a function that needs approval, and that task asks first. A version saved before 3 October 2026 has no marks, so every one of its functions needs approval: mark your lookups read_only: true. vestra init marks lookups for you. A routine, a batch, or an ask with no person in your app has nobody to approve, so a function it calls waits until the approval expires. Mark a function that may run there run_without_asking: true.

Going live and rolling back

A version goes live only after its tests pass. Rolling back needs a live key, and puts the version that was live before back in service at once, without running tests. Your webhooks receive deployment.version_created, deployment.promoted and deployment.rolled_back.

The key you need

Deploying and the other setup commands need a key with the setup scope, or all.

The routes behind deploy32 routes, each with every field in the API reference
GET/v1/mcp-serversYour registered MCP servers.Key scope: setup (or all)
POST/v1/mcp-serversRegister one: URL, auth (none, or headers using ${secret:NAME} or ${user_secret:NAME}), and its tool policy floor: tools never run and tools that always need approval, for every deployment in both modes. A deployment version's own tool policy adds to the floor and never removes from it.Key scope: setup (or all)
GET/v1/mcp-servers/{server_id}One server and its settings.Key scope: setup (or all)
PATCH/v1/mcp-servers/{server_id}Change URL, auth or the tool policy floor, for every deployment in both modes. Users pick it up at their next task. While a deployment's live version uses the server, a test key's change answers 409 mcp_server_live: change it with a live key, or as an owner or admin.Key scope: setup (or all)
DELETE/v1/mcp-servers/{server_id}Remove it from every user. While a deployment's live version uses it, a test key answers 409 mcp_server_live.Key scope: setup (or all)
POST/v1/mcp-servers/{server_id}/checkConnect now and list its tools, to prove it works.Key scope: setup (or all)
GET/v1/mcp-servers/{server_id}/toolsThe tools we found, each with its setting in the server's tool policy floor. A deployment version's own policy can add to it.Key scope: setup (or all)
GET/v1/skillsYour company skills, plus the skills Bash already has. A company skill carries sha256, the hash of its files as uploaded, computed the way the SDKs compute a folder's, so an unchanged folder is never uploaded again.Key scope: setup (or all)
POST/v1/skillsUpload a skill folder as a zip, or as JSON files.Key scope: setup (or all)
GET/v1/skills/{skill_id}One skill and its files.Key scope: setup (or all)
PUT/v1/skills/{skill_id}Upload a new version.Key scope: setup (or all)
DELETE/v1/skills/{skill_id}Remove it from every user.Key scope: setup (or all)
GET/v1/skills/{skill_id}/versionsEvery version, each with a content hash.Key scope: setup (or all)
POST/v1/notebooksCreate a notebook.Key scope: setup (or all)
GET/v1/notebooksYour notebooks.Key scope: setup (or all)
GET/v1/notebooks/{notebook_id}One notebook and who may read it.Key scope: setup (or all)
PATCH/v1/notebooks/{notebook_id}Rename it, or set who may read it: every user, or named users only.Key scope: setup (or all)
DELETE/v1/notebooks/{notebook_id}Delete it.Key scope: setup (or all)
POST/v1/notebooks/{notebook_id}/documentsAdd a document, as a file or as text.Key scope: setup (or all)
GET/v1/notebooks/{notebook_id}/documentsIts documents.Key scope: setup (or all)
DELETE/v1/notebooks/{notebook_id}/documents/{document_id}Remove one.Key scope: setup (or all)
GET/v1/deploymentsYour deployments.Key scope: setup (or all)
POST/v1/deploymentsCreate one on base 'bash'. It starts with a draft version.Key scope: setup (or all)
GET/v1/deployments/{deployment_id}One deployment, by its id or its name: the live version and the draft. With format=yaml, its latest version as vestra.yaml.Key scope: setup (or all)
PUT/v1/deployments/{deployment_id}Apply a whole vestra.yaml, sent as JSON, by the deployment's name: the first call makes it, and a file that changes nothing makes no version (changed: []). MCP servers are registered by URL, and the tool policy the file gives each one is this version's own, on top of the server's floor, from the moment the version runs. Tests become scenarios, and the routines and webhooks the file declares are its own. ?promote=after_tests (the default), now or no promotes in the key's mode. 400 secret_missing names the secrets to store first; 409 mcp_server_shared names the other deployments that run an MCP server whose headers the file would change.Key scope: setup (or all)
PATCH/v1/deployments/{deployment_id}Rename, describe or archive it.Key scope: setup (or all)
POST/v1/deployments/{deployment_id}/versionsSave a new version: instructions, language, MCP servers and this version's own tool policy for them (mcp_tool_policy, on top of each server's floor), allowed apps, approval rules, model, memory mode, notebooks, limits per task.Key scope: setup (or all)
GET/v1/deployments/{deployment_id}/versionsEvery version.Key scope: setup (or all)
GET/v1/deployments/{deployment_id}/versions/{version}One version, in full.Key scope: setup (or all)
GET/v1/deployments/{deployment_id}/diffWhat changed between two versions (?from=&to=).Key scope: setup (or all)
POST/v1/deployments/{deployment_id}/versions/{version}/promoteMake it live. Refused unless its latest test run passed.Key scope: setup (or all)
POST/v1/deployments/{deployment_id}/rollbackPut an earlier version back live, instantly.Key scope: setup (or all)