Skip to main content
A machine spec describes a machine in one value: what it boots from, its size, labels, networks, isolation, egress allowlist, env vars, the secrets it gets, the integrations and agent logins it needs, and the shell commands it runs on its first boot. You build a spec in code with the TypeScript or Python SDK, from reusable pieces, and create machines from it. The machine keeps its spec. The console shows it, snapshots and forks carry it, and you can change it later with updateSpec. That makes a spec the way to hand someone a machine that is ready to work. Describe what every engineer on a team needs once (the team’s base snapshot, GitHub, Claude Code, an npm token, a few packages), add what one person needs on top, and create the machine, for yourself or for them.
Specs are available in the TypeScript and Python SDKs and the gRPC API. The CLI does not take a spec. The console shows a machine’s spec, what it still needs connected and how its packages went, but does not edit specs.

A complete example

An org owner describes what every engineering machine gets once, then builds Mike’s machine on top of it:
mike-dev boots from the team’s snapshot, joins the eng network, gets NODE_ENV and NPM_TOKEN, installs nginx on its first boot, and records that it needs the org’s shared GitHub and Jira connections, the owner’s own Slack, and a Claude Code login. To make it Mike’s machine rather than yours, add .assignTo(mikesUserId) (see Create a machine for someone else). As data, mike.resolve() (no machine is created) is:
A spec has this JSON form everywhere: the machine keeps one (merged with its snapshot’s own spec, when the base is a snapshot), the console shows it as YAML, and updateSpec patches it. A spec holds secret names, never values.

Build a spec

Machine.spec() starts an empty builder. Every method returns a new builder, so one base like engineering can be extended in several directions without the extensions affecting each other.

Pieces

A piece is part of a spec. The SDKs come with these: Integrations and agent logins are recorded on the machine as what it needs. A missing one never blocks the create. See Integrations the machine needs.

Write your own piece

A piece is a plain object (a dict in Python) holding spec fields, so a team’s own pieces are ordinary code:
A connection is "personal" or "shared:<tag>". The create refuses an integration boxd can’t connect, naming it (spec.connections.<toolkit>: ...), and accepts the GitHub App (github-app) only as "shared:org". Agents are "claude" and "codex".

How pieces merge

Pieces merge in the order they are added:
  • Maps (labels, env, connections) merge key by key, and the later piece wins.
  • Lists (networks, egress, secrets, agents, packages) combine in order, without duplicates.
  • isolated only turns on. A later piece can’t turn it off.
  • base, size and assignTo can be set by several pieces only to the same value. Two different values are an error. Sizes compare as boxd stores them, so "2X8" is the same as "2x8".
A key a spec doesn’t have is an error, and so is a name in both env and secrets. The SDKs check this as you build: TypeScript throws a BoxdError, Python raises MachineSpecError (a ValueError), each naming the field. mergeSpecs(...pieces) (Python merge_specs(*pieces)) applies the same rules to plain pieces and returns the resolved spec. The server checks every field again when you create the machine, and its errors name the field and the rule, such as spec.size: ... or spec.packages[2]: ....

Create a machine from it

Pass the builder, or a resolved spec, as spec:
A spec carries the machine’s base, size, labels, networks, isolation and env, so create takes none of image, env, labels, networks, isolated or the sizing options beside it. The other options still apply (name, org, shared, the idle timeouts, delete after), within what a restore allows when the base is a snapshot. A spec whose base is a snapshot, or a team with a default snapshot, restores that snapshot, so the machine comes up running whatever was captured. A snapshot keeps the spec of the machine it was taken from. Restoring it gives the new machine that spec, and a spec you pass is merged on top by the same rules as pieces. Its size, if it names one, must be the snapshot’s, since a restore keeps its size.

Read the spec back

Both are null (None) on a machine that wasn’t created from a spec and didn’t inherit one from a snapshot or a fork. A machine’s spec lists the labels and networks the machine really has, including those it took over from a snapshot or a fork source.

Secrets

A spec lists the secrets its machine gets by name. The values stay in boxd’s secret store and never appear in a spec. Usually they are unattached secrets: a secret with scope unattached reaches no machine by itself, only the machines whose spec lists it.
boundMachines (bound_machines) on a listed secret is how many machines list it in their spec. See Env vars and secrets for the scopes. Which secret a name means. A name resolves from the most private to the most shared: Who may list what. Only org owners and admins may list an org unattached secret. A personal secret never goes on a shared machine or on a machine created for someone else. An org shared secret only reaches shared machines, so a private machine can’t list one. A name that resolves to nothing, or to a secret the create may not list, fails the create, naming it. Listing a private, all or shared secret changes nothing about who gets it, since it already reaches the machine through its scope. The spec records that the machine needs it, and the create checks that it exists. Precedence on the machine. The spec’s env and its secrets win over your private vars, which win over the org’s shared vars (shared machines only), which win over the org’s all vars. A name can’t be in both env and secrets. A reference, not a copy. Rotating a secret reaches every machine that lists it: new shells and exec sessions get the new value. Deleting the secret takes it away from them, and setting it again under the same name brings it back. An unattached secret can be bound to hosts like any other, and the machine then holds its placeholder. Forks, snapshots and restores resolve the names again for the new machine and its owner, and fail naming any that no longer resolve.

Create a machine for someone else

An org owner or admin can create a machine for another member of the org: add .assignTo(userId) (.assign_to(user_id)) with the member’s user id, as orgs.getMembers() (orgs.get_members()) lists it.
The machine is the member’s from the start: private to them, billed to the org, and set up with their GitHub sign-in, agent logins and personal connections. The personal connections and agents in the spec are theirs to connect. Only org secrets count for it (Secrets). You can’t see or reach the machine afterwards, as with any member’s private machine. So create answers with its id and name only, and the same create can’t also set a delete-after timer. The member sees who created it: createdBy (your user id) and createdByName (created_by, created_by_name) on the machine, and a Created by line on the machine’s page in the console. The create is refused, naming spec.assignTo:
  • without org, or with shared: true (the machine is private to the member);
  • when you aren’t an owner or admin of the org;
  • with an org API key, or from inside a machine (use your login or a member API key);
  • for someone who isn’t a member of the org;
  • with a snapshot or a disk the member couldn’t use themselves.
Your own user id is a plain create. assignTo is never inherited: forks, snapshots and restores of the machine drop it.
The machine runs the spec you wrote, as the member’s own machine. Its packages run as root, and its env and its base are yours, alongside the member’s GitHub token, agent logins and personal connections. Assign a spec only if the member would run it themselves. The member can read the spec (machine.spec) and sees who created the machine.

Integrations the machine needs

A spec’s connections and agents are the machine’s requirements. Reading the machine (get, list, or what create returns) says, for each one, whether it is connected right now:
canFix (can_fix) says whether you could connect it: the owner for personal connections and agents, an org owner or admin for shared ones. Connect from the console: the machine’s page has an Integrations needed section with a Connect button on each one you can connect. For a shared connection you can’t, it says to ask an org admin to connect it, under its name. An unmet requirement is only unavailable on the machine. Nothing blocks creating, booting or using it. Some requirements can’t be met on the machine as it is, and reason says why:
  • An isolated machine gets no integrations or agent logins.
  • A shared machine never gets anyone’s personal connection or agent login. Require shared:<tag> instead. See Where connections are available.

Packages

packages are shell commands the machine runs once, in order, after its first boot:
  • as root, with bash (sh on an image without it);
  • with DEBIAN_FRONTEND=noninteractive and no stdin, so nothing waits for input;
  • with the env vars and secrets an exec session on the machine gets;
  • with a 30-minute timeout each.
Their output is appended to /var/log/boxd/packages.log in the machine, which only root can read. A command is done when its shell exits, so one that leaves a process running in the background (my-server &) doesn’t hold up the next. The machine doesn’t auto-suspend or hibernate while a command runs. The first failure (a non-zero exit or a timeout) stops the rest. The machine stays usable, and you have two ways on:
  • Run it again. Fix the cause, then run the failed command and every one after it again with retryPackages, or with Retry in the console’s Packages section.
  • Give up on it, or replace it. Remove it from the spec with updateSpec. A command that leaves the spec before it finished is skipped, and the ones after it run. So replacing a wrong command is one patch: drop it and add the corrected one.
Secrets in the output. In outputTail, the value of every secret the machine gets is shown as [secret NAME]. Only the value as it is: a value printed encoded or in pieces isn’t recognised. The log file isn’t masked. It is root’s, and root on the machine can read its env anyway. Interruptions. A command the platform interrupts (the machine stops, reboots, hibernates or moves to another host, or boxd restarts on its host) goes back to "pending" and runs again from the start once the machine is running, and the rest follow. After 3 interruptions it fails. Since a command can run more than once this way, write commands that are safe to repeat: apt-get install -y is, appending to a file is not. What finished never runs again: not on a reboot, a resume, a wake, or a move. Restores and forks. Restoring a snapshot marks done the commands that had already run on the machine the snapshot was taken from, and runs the rest of the snapshot spec’s commands (one that was running when the snapshot was taken runs again from the start), then the ones your spec adds. A fork keeps its source’s package states and runs nothing. Egress. Commands that download something need the hosts in the spec’s egress, if it has one: *.ubuntu.com for apt-get on Ubuntu, registry.npmjs.org for npm. At most 50 commands (a restore counts the snapshot’s too), each at most 4 KiB. The server names the one it refuses (spec.packages[2]: ...). Put a longer script in a file and run that.

Update a machine’s spec

updateSpec (update_spec) changes the spec of a machine created from one, and applies what changes. Pass a patch, or a function that adds to the current spec:
A patch is a JSON Merge Patch on the spec’s JSON form, so it can remove things. The builder form only adds, by the merge rules: a size that differs from the spec’s is an error, and nothing is removed. Use a patch to replace or remove. specPatch(from, to) (spec_patch) computes the patch the builder form sends. Each change in changes names its field and says how it applies (apply): A change also lists what was added, removed and changed (map keys only: values are never shown), before and after for a scalar, a one-sentence note on what happens, and warnings for what won’t. The response’s rebootRequired and rebootRecommended (reboot_required, reboot_recommended) sum it up, spec is the spec after the patch, and applied and rebooted say what happened. reboot: true only allows a reboot. It doesn’t reboot the machine for env or secrets: call machines.reboot() for that. All or nothing. A refused change refuses the whole patch, and so does a change that needs a reboot without reboot: true. Also refused: new packages or a retry on a fork, and a size change for a machine on standby or hibernated. Nothing is applied. The patched spec is checked like a create’s, so an unknown integration, a secret you may not list, or more than 50 commands in all are refused too, naming the field. Someone else changed it. If the spec changes while your update runs (another update, a label edit), nothing is applied and the call throws ConflictError. Reading the machine again and retrying is safe. The builder form does this itself: it sends the spec it read, and on a conflict reads the spec again and reapplies your function, up to 3 times. A patch you built from a spec you read can say so too, with expectedSpec: machine.spec (expected_spec=machine.spec): if the spec is no longer that one, nothing is applied and the call throws ConflictError. In order. The spec and the labels, networks, egress and secrets it drives change together, then the size, then the new packages run. If the size then fails (the org’s quota changed, the machine went to standby), the rest stays applied and the call still returns: resizeError (resize_error) says why, and spec has the size the machine has. createdSpec never changes, so the console’s spec badge reads UPDATED after a change. An update is allowed to whoever may edit the machine’s labels: its owner, or an org owner or admin on a shared machine. A machine without a spec can’t get one through updateSpec.

From inside a machine

An SDK running inside a machine with the machine’s own credentials can create machines from specs and update them, with three limits. They keep a machine from loosening its own confinement:
  • It can’t change that machine’s own egress or secrets with updateSpec (not even in a dry run), or its allowlist with setEgressAllow.
  • It can’t list an unattached secret, yours or the org’s, in any spec: not on a create, a restore’s own spec, or an updateSpec on another machine. Names a fork or a restore takes over from its source’s spec are fine.
  • It can’t create a machine for someone else (assignTo).
Make those changes from outside the machine, with an API key or in the console.

Forks, snapshots and restores

  • A snapshot keeps its machine’s spec, as it stands when the snapshot is saved. A machine restored from it gets that spec, with anything you pass merged on top.
  • A fork keeps its source’s spec.
  • Labels and networks. The new machine’s spec lists the labels and networks it really has: those of the snapshot or the source, plus its own.
  • Egress. A restore normally starts with no allowlist, but one whose spec lists egress gets that allowlist. A fork inherits its source’s allowlist either way. See Egress control.
  • Secrets are resolved again for the new machine and its owner. A name that no longer resolves fails the create, naming it and where it came from.
  • assignTo is never inherited.
  • Packages: see Restores and forks above. A fork can’t take new packages or a retry.

Limits

Checked before anything else, and named with what was given (spec.env: at most 128 variables (got 129)):

In the console

  • The SPEC badge. A machine created from a spec carries a SPEC badge in the machine list and on its page. Hover or tap it to see the current spec as YAML (secret names only). Once the spec has changed since the machine was created, the badge reads UPDATED, and a Current / As created toggle shows either one.
  • Created by. A machine created for someone else says who created it for whom.
  • Integrations needed. Each connection and agent login the spec needs, with Connect for the ones you can connect, or why it can’t be met on this machine. Once all are met, one line says so.
  • Packages. Each command with its state and how long it took. A failed command shows its exit code or reason and its output, and the section says where it stopped, with Retry for whoever may update the machine’s spec.
  • Secrets page. Unattached secrets are a scope you can pick, and each row says how many machines use it. See Env vars and secrets.
To change a spec, use updateSpec from an SDK or UpdateMachineSpec on the gRPC API.