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

# Machine specs

> Describe a ready-to-work machine once, in code: what it boots from, its size, networks, egress, env, secrets, integrations and first-boot commands. Create machines from it, for yourself or a teammate, and change it later.

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](/reference/typescript-sdk.md#machine-specs) or [Python](/reference/python-sdk.md#machine-specs) 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.

> [!NOTE]
>
> Specs are available in the TypeScript and Python SDKs and the [gRPC API](/reference/grpc-api.md#machine-specs). 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:

```typescript TypeScript theme={"theme":"github-dark"}
import { Boxd, Machine, team, github, slack, jira, claude, secrets } from "@boxd-sh/sdk";

const boxd = new Boxd({ apiKey: "bxd_..." });

// A secret that reaches no machine until a spec lists it.
await boxd.secrets.set("NPM_TOKEN", "npm_...", { scope: "unattached", org: "acme" });

// What every engineering machine has.
const engineering = Machine.spec()
  .from(team("eng"))                          // the eng team's default snapshot
  .network("eng")
  .label("team", "eng")
  .env("NODE_ENV", "development")
  .packages("apt-get update", "apt-get install -y nginx")
  .with(github({ shared: "eng-bot" }), claude());

// Mike's machine: the engineering spec, a size, and what he needs on top.
const mike = engineering
  .size("2x8")
  .with(slack(), jira({ shared: "jira" }), secrets("NPM_TOKEN"));

const m = await boxd.machines.create({ name: "mike-dev", org: "acme", spec: mike });
m.spec;          // the spec as it stands now
m.createdSpec;   // the spec it was created with, never changed
```

```python Python theme={"theme":"github-dark"}
from boxd import Boxd, Machine, team, github, slack, jira, claude, secrets

boxd = Boxd(api_key="bxd_...")

# A secret that reaches no machine until a spec lists it.
boxd.secrets.set("NPM_TOKEN", "npm_...", scope="unattached", org="acme")

# What every engineering machine has.
engineering = (
    Machine.spec()
    .from_(team("eng"))                       # the eng team's default snapshot
    .network("eng")
    .label("team", "eng")
    .env("NODE_ENV", "development")
    .packages("apt-get update", "apt-get install -y nginx")
    .with_(github(shared="eng-bot"), claude())
)

# Mike's machine: the engineering spec, a size, and what he needs on top.
mike = engineering.size("2x8").with_(slack(), jira(shared="jira"), secrets("NPM_TOKEN"))

m = boxd.machines.create("mike-dev", org="acme", spec=mike)
m.spec           # the spec as it stands now
m.created_spec   # the spec it was created with, never changed
```

`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](#create-a-machine-for-someone-else)).

As data, `mike.resolve()` (no machine is created) is:

```json theme={"theme":"github-dark"}
{
  "version": 1,
  "base": { "team": "eng" },
  "size": "2x8",
  "labels": { "team": "eng" },
  "networks": ["eng"],
  "env": { "NODE_ENV": "development" },
  "secrets": ["NPM_TOKEN"],
  "connections": { "github": "shared:eng-bot", "slack": "personal", "jira": "shared:jira" },
  "agents": ["claude"],
  "packages": ["apt-get update", "apt-get install -y nginx"]
}
```

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.

| TypeScript | Python | Sets |
| - | - | - |
| `.from(team(name))` | `.from_(team(name))` | `base`: the default snapshot of a team you belong to. A team without one boots the default image in the team's org. |
| `.from(snapshot(name))` | `.from_(snapshot(name))` | `base`: a snapshot, restored as `fromSnapshot` would. Leave `base` out for the default image. |
| `.size("2x8")` | `.size("2x8")` | `size`, as `<vCPU>x<memory GiB>`: `1x4`, `2x8`, `4x16` or `8x32`. Leave it out for the org's default size. |
| `.label(key, value)`, `.label({...})` | the same | `labels`: `key=value` tags for grouping and finding machines |
| `.network(...names)` | `.network(*names)` | `networks`, see [Machine to machine](/guides/vm-to-vm.md) |
| `.isolated()` | `.isolated()` | `isolated`, see [Sandboxes](/use-cases/sandboxes.md) |
| `.egress(...hosts)` | `.egress(*hosts)` | `egress`: the machine's [egress allowlist](/guides/egress-allowlist.md), from birth |
| `.env(key, value)`, `.env({...})` | the same | `env`: the machine's own env vars |
| `.packages(...commands)` | `.packages(*commands)` | `packages`: shell commands run once after the first boot ([Packages](#packages)) |
| `.assignTo(userId)` | `.assign_to(user_id)` | `assignTo`: create the machine for another member ([below](#create-a-machine-for-someone-else)) |
| `.with(...pieces)` | `.with_(*pieces)` | merges pieces, or other builders |
| `.resolve()` | `.resolve()` | returns the spec as plain data, without creating anything |

### Pieces

A piece is part of a spec. The SDKs come with these:

| Piece | Adds |
| - | - |
| `slack()`, `jira()`, `github()`, `asana()` | A connection the machine needs: the owner's own (`"personal"`). Pass `{ shared: "tag" }` (Python `shared="tag"`) for the org connection shared under that name instead (`"shared:tag"`). |
| `claude()`, `codex()` | An agent login the machine needs: the owner's Claude Code or Codex sign-in. |
| `secrets(...names)` | Secrets the machine gets, by name. See [Secrets](#secrets). |

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](#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:

```typescript TypeScript theme={"theme":"github-dark"}
import type { MachineSpecPiece } from "@boxd-sh/sdk";

// Any integration in the catalog, not only the four with helpers.
const notion: MachineSpecPiece = { connections: { notion: "personal" } };

// A piece is just data, so a function can build one.
const service = (name: string): MachineSpecPiece => ({
  labels: { service: name },
  env: { SERVICE_NAME: name },
  packages: [`git clone https://github.com/acme/${name} /srv/${name}`],
});

const spec = engineering.with(notion, service("billing"));
```

```python Python theme={"theme":"github-dark"}
# Any integration in the catalog, not only the four with helpers.
notion = {"connections": {"notion": "personal"}}

# A piece is just data, so a function can build one.
def service(name: str) -> dict:
    return {
        "labels": {"service": name},
        "env": {"SERVICE_NAME": name},
        "packages": [f"git clone https://github.com/acme/{name} /srv/{name}"],
    }

spec = engineering.with_(notion, service("billing"))
```

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](/guides/integrations/connections.md#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`:

```typescript TypeScript theme={"theme":"github-dark"}
await boxd.machines.create({ name: "mike-dev", org: "acme", spec: mike });

// A snapshot base restores the snapshot.
await boxd.machines.create({ name: "api-1", spec: Machine.spec().from(snapshot("api-base")).label("env", "dev") });

// Or add a spec to a snapshot's own.
await boxd.machines.create({ name: "api-2", fromSnapshot: "api-base", spec: Machine.spec().network("ops") });
```

```python Python theme={"theme":"github-dark"}
boxd.machines.create("mike-dev", org="acme", spec=mike)

# A snapshot base restores the snapshot.
boxd.machines.create("api-1", spec=Machine.spec().from_(snapshot("api-base")).label("env", "dev"))

# Or add a spec to a snapshot's own.
boxd.machines.create("api-2", from_snapshot="api-base", spec=Machine.spec().network("ops"))
```

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](/guides/delete-after.md)), 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

| TypeScript | Python | |
| - | - | - |
| `machine.spec` | `machine.spec` | The spec as it stands. `updateSpec` changes it, and so does editing the machine's labels, networks, egress allowlist or size any other way. |
| `machine.createdSpec` | `machine.created_spec` | The spec the machine was created with. It never changes. |

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.

```typescript TypeScript theme={"theme":"github-dark"}
// The org's: only org owners and admins can set, move or delete one.
await boxd.secrets.set("NPM_TOKEN", "npm_...", { scope: "unattached", org: "acme" });

// Your own, in that org.
await boxd.secrets.set("MY_TOKEN", "...", { scope: "private-unattached", org: "acme" });

const m = await boxd.machines.create({ name: "mike-dev", org: "acme", spec: engineering.with(secrets("NPM_TOKEN")) });
(await boxd.secrets.list({ org: "acme" })).map((s) => [s.name, s.scope, s.boundMachines]);
```

```python Python theme={"theme":"github-dark"}
# The org's: only org owners and admins can set, move or delete one.
boxd.secrets.set("NPM_TOKEN", "npm_...", scope="unattached", org="acme")

# Your own, in that org.
boxd.secrets.set("MY_TOKEN", "...", scope="private-unattached", org="acme")

m = boxd.machines.create("mike-dev", org="acme", spec=engineering.with_(secrets("NPM_TOKEN")))
[(s.name, s.scope, s.bound_machines) for s in boxd.secrets.list(org="acme")]
```

`boundMachines` (`bound_machines`) on a listed secret is how many machines list it in their spec. See [Env vars and secrets](/guides/env-secrets.md#unattached-secrets) for the scopes.

**Which secret a name means.** A name resolves from the most private to the most shared:

| Machine | A name resolves to |
| - | - |
| Your own private machine | your `private-unattached` secret, then your `private` one, else the org's `unattached`, `all` or `shared` one |
| A shared machine, or one created for someone else | the org's `unattached`, `all` or `shared` secret only |

**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](/guides/env-secrets.md#host-bound-secrets) 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.

```typescript TypeScript theme={"theme":"github-dark"}
const { members } = await boxd.orgs.getMembers("acme");
const mikeId = members.find((x) => x.githubUsername === "mike")!.userId;

const m = await boxd.machines.create({
  name: "mike-dev",
  org: "acme",                                // required: the org the machine is for
  spec: engineering.size("2x8").assignTo(mikeId),
});
m.id; m.name;                                 // all you get back: the machine is Mike's
```

```python Python theme={"theme":"github-dark"}
roster = boxd.orgs.get_members("acme")
mike_id = next(x.user_id for x in roster.members if x.github_username == "mike")

m = boxd.machines.create(
    "mike-dev",
    org="acme",                                # required: the org the machine is for
    spec=engineering.size("2x8").assign_to(mike_id),
)
m.id, m.name                                   # all you get back: the machine is Mike's
```

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](#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](/guides/delete-after.md) 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.

> [!WARNING]
>
> **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:

```typescript TypeScript theme={"theme":"github-dark"}
const m = await boxd.machines.get("mike-dev");
for (const r of m.requirements) {
  // { kind: "connection", name: "jira", scope: "shared", tag: "jira", met: false, canFix: true, reason: null }
  console.log(r.name, r.met ? "connected" : r.canFix ? "connect it" : r.reason ?? "ask an org admin");
}
```

```python Python theme={"theme":"github-dark"}
m = boxd.machines.get("mike-dev")
for r in m.requirements:
    # kind="connection" name="jira" scope="shared" tag="jira" met=False can_fix=True reason=None
    print(r.name, "connected" if r.met else "connect it" if r.can_fix else r.reason or "ask an org admin")
```

| Requirement | Met by |
| - | - |
| `personal` connection | The machine owner's own connection for that integration in the machine's org. On a machine created for someone else, the member's. |
| `shared` connection | The org's connection [shared](/guides/integrations/connections.md#personal-and-shared-connections) under `tag`. |
| Agent | The owner's Claude Code or Codex sign-in. |

`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](/guides/integrations/connections.md#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](/guides/suspend-resume.md) 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.

```typescript TypeScript theme={"theme":"github-dark"}
const m = await boxd.machines.get("mike-dev");
for (const p of m.packages) {
  // { command: "apt-get install -y nginx", state: "failed", exitCode: 100, outputTail: "E: Unable to locate…", reason: null, … }
  console.log(p.state, p.exitCode, p.command);
}

// Run the failed command and the ones after it again:
await boxd.machines.updateSpec("mike-dev", {}, { retryPackages: true });

// Or replace it: the old one is skipped, the corrected one and the rest run.
await boxd.machines.updateSpec("mike-dev", {
  packages: ["apt-get update", "apt-get install -y nginx-light"],
});
```

```python Python theme={"theme":"github-dark"}
m = boxd.machines.get("mike-dev")
for p in m.packages:
    # command="apt-get install -y nginx" state="failed" exit_code=100 output_tail="E: Unable to locate…" reason=None …
    print(p.state, p.exit_code, p.command)

# Run the failed command and the ones after it again:
boxd.machines.update_spec("mike-dev", {}, retry_packages=True)

# Or replace it: the old one is skipped, the corrected one and the rest run.
boxd.machines.update_spec(
    "mike-dev", {"packages": ["apt-get update", "apt-get install -y nginx-light"]}
)
```

| Field | |
| - | - |
| `state` | `"pending"`, `"running"`, `"done"`, `"failed"` or `"skipped"`. The commands after a failure stay `"pending"`. A pending or failed command that an update removes from the spec becomes `"skipped"`: it never runs and no longer holds up the ones after it. A skipped one that failed keeps its exit code and output. Listing it in the spec again puts it back to `"pending"`, in its place. A running command can't be removed until it ends, and removing a done one undoes nothing. |
| `exitCode` (`exit_code`) | The exit code, once the command exited. Empty for a timeout or an interrupted run. |
| `outputTail` (`output_tail`) | The last 4 KiB of the output. Empty in `list()`, which leaves it out to keep lists small. `get()` has it. |
| `reason` | Why a failed command has no exit code (it timed out, or was interrupted too often), why a pending one is waiting, or that a done one ran on the machine a snapshot was taken from. |
| `startedAt`, `finishedAt` (`started_at`, `finished_at`) | When it ran. |

**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:

```typescript TypeScript theme={"theme":"github-dark"}
// A patch: a field left out is untouched, a scalar replaces, maps merge key by
// key (null removes a key), a list replaces the whole list, null removes a field.
await boxd.machines.updateSpec("mike-dev", {
  labels: { owner: "mike", team: null },
  networks: ["eng", "ops"],
  packages: ["apt-get install -y nginx", "npm i -g pnpm"],
});

// A builder: reads the spec, adds with the merge rules, sends the difference.
await boxd.machines.updateSpec("mike-dev", (s) => s.network("ops").env("LOG_LEVEL", "debug"));

// A dry run: the diff, nothing changed.
const diff = await boxd.machines.updateSpec("mike-dev", { size: "4x16" }, { dryRun: true });
for (const c of diff.changes) console.log(c.field, c.apply, c.note, c.warnings);
if (diff.rebootRequired) {
  await boxd.machines.updateSpec("mike-dev", { size: "4x16" }, { reboot: true });
}
```

```python Python theme={"theme":"github-dark"}
# A patch: a field left out is untouched, a scalar replaces, maps merge key by
# key (None removes a key), a list replaces the whole list, None removes a field.
boxd.machines.update_spec("mike-dev", {
    "labels": {"owner": "mike", "team": None},
    "networks": ["eng", "ops"],
    "packages": ["apt-get install -y nginx", "npm i -g pnpm"],
})

# A builder: reads the spec, adds with the merge rules, sends the difference.
boxd.machines.update_spec("mike-dev", lambda s: s.network("ops").env("LOG_LEVEL", "debug"))

# A dry run: the diff, nothing changed.
diff = boxd.machines.update_spec("mike-dev", {"size": "4x16"}, dry_run=True)
for c in diff.changes:
    print(c.field, c.apply, c.note, c.warnings)
if diff.reboot_required:
    boxd.machines.update_spec("mike-dev", {"size": "4x16"}, reboot=True)
```

A patch is a [JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7396) on the spec's JSON form, so it can remove things. The builder form only adds, by the [merge rules](#how-pieces-merge): 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`):

| Field | `apply` |
| - | - |
| `labels`, `networks`, `egress` | `"live"`. New egress rules are in force within about a second of the call returning. |
| `connections`, `agents` | `"live"`: what the machine needs (`requirements`) changes. |
| `packages` | `"live"`: added commands run now, after the ones before them. Removing a done command undoes nothing, and a command that already ran isn't run again. A pending or failed command the update removes is skipped (`skipped` in the change), so the ones after it run; a skipped one listed again goes back to pending (`unskipped`). Commands after a failed one don't run unless the update also retries (`retryPackages`): then the failed command and the rest run again first, except skipped ones, and a failed command the same update removes is skipped, not retried. Removing a running command is `"refused"` until it ends. |
| `env`, `secrets` | `"reboot_recommended"`: new shells and `exec` sessions get them now. Processes already running keep the old values until they restart. |
| `size` | `"reboot"` on a running machine, which is rebooted to apply it, as [resize](/guides/resources.md#machine-sizes) does, so it needs `reboot: true`. `"live"` on a stopped machine, which starts at the new size. |
| `base`, `isolated`, `assignTo` | `"refused"`: fixed when the machine is created. |

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](/guides/egress.md#forks-snapshots-and-isolation).
* **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](#packages) 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)`):

| | Limit |
| - | - |
| The whole spec | 256 KiB as JSON |
| `env` | 128 variables. Each value at most 32 KiB, all values together at most 64 KiB. |
| `secrets` | 64 names |
| `networks` | 16, each name at most 63 bytes |
| `egress` | 256 entries, none empty. Leave `egress` out for no allowlist. |
| `connections` | 64 |
| `packages` | 50 commands (200 listed, 50 once duplicates are dropped), each at most 4 KiB. A restore counts the snapshot's commands too. |
| `labels` | the usual label rules: at most 64 per machine, and the `boxd/` prefix is reserved |
| An `updateSpec` patch | 256 KiB |

## 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](/guides/env-secrets.md#unattached-secrets).

To change a spec, use `updateSpec` from an SDK or `UpdateMachineSpec` on the [gRPC API](/reference/grpc-api.md#machine-specs).


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