> See the full index of documentation pages at https://docs.zettabyte.com/llms.txt.

# Launch and specs

> zb init, zb launch, and everything a spec can say about code, results, environment, and scheduling.

Source: https://docs.zettabyte.com/docs/cli/launch-and-specs

Last updated: 2026-08-21

## Generate a spec with zb init

New to specs? `zb init` builds one for you. It asks the essentials per kind, with live GPU, image, and model suggestions when you are logged in. It writes `NAME.yaml` with every optional setting as a commented stub, then offers to launch. Without a TTY it prompts nothing and emits a ready-to-edit template. That makes it safe for scripts and agents.

```bash
zb init                              # interactive, per kind
zb init -k serve --model qwen3-8b -  # print a template to stdout
```

## Launch from a spec, flags, or both

`zb launch [SPEC.yaml] [flags]` starts a workload. The spec file is the paved road, and flags cover one-liners. Flags override the file. Passing `-` reads the spec from stdin.

| Flag | What it does |
| --- | --- |
| `--env K=V` | merges over the spec's `env` per key |
| `--secret K[=V]` | `K` alone reads `$K` locally, keeping secrets out of shell history |
| `--env-file FILE`, `--secrets-file FILE` | fold a local dotenv in at the lowest precedence |
| `--volume NAME:/path` | replaces the spec's volume list |
| `--set path=value` | dotted spec override, e.g. `--set serve.replicas.max=3` or `--set env.SEED=42`; made for sweeps |
| `--hf-token` | inject your Hugging Face token as the `HF_TOKEN` secret. The bare form reads `$HF_TOKEN`, then the token saved by `zb auth hf set`. Never sent without this flag |
| `--debug` | hold a crashed container instead of restarting it, so logs keep the traceback (job/serve; see Everyday commands) |
| `--dry-run` | print the exact API request without launching (secret values are redacted) |
| `-w, --wait` | block until the workload is running |

## Composing a spec

A spec starts at three lines and grows only when the run needs more. The example below ships code from your laptop, pulls results back, and connects Weights & Biases.

```yaml
# train.yaml
kind: job
name: llama-ft
image: pytorch                  # a preset, @TAG, or any pullable ref
run: torchrun --nproc-per-node 8 train.py

resources:
  gpu: h100:8

workdir: .                      # ship this directory; run executes in /workspace
outputs: /outputs               # whatever lands here comes home with zb pull
volumes:
  - training-data:/data

env:
  WANDB_PROJECT: llama-ft       # plain env vars: your code reads them as usual
ttl: 12h                        # auto-clean when you walk away
```

```bash
zb launch train.yaml --secret WANDB_API_KEY -w   # bare --secret K reads your local $WANDB_API_KEY
zb logs -f llama-ft
zb pull llama-ft ./results
```

- Anything your training code reads from the environment goes in `env:`. Use `--secret` when a value should not sit in the file. `--secret K` alone reads your local `$K`, so tokens stay out of the spec and out of shell history.
- Every key has a flag twin (`--gpu`, `--env`, `--volume`, `--ttl`). Flags override the file: one spec, many runs.
- `zb` rejects unknown keys, so a typo never silently no-ops. The Manifest reference page lists every key the spec accepts.

## Images and presets

The platform curates a small set of GPU-ready preset images. `zb image ls` lists them (for example `pytorch` and `vllm`). Use a preset by its bare name anywhere an image is accepted (`--image pytorch`). Or omit `--image` entirely on `job` and `box`, and the default preset (PyTorch + CUDA) fills in. Either way the CLI prints the image it chose (`image: default → …`), so the launch stays transparent. On boxes, preset images get a keep-alive shell automatically. `zb box --gpu h100:1` alone gives you a GPU-ready PyTorch container.

`--image @TAG` and `--image @fav` resolve from your imported images. Curate them with `zb image tag` and `zb image fav`. `@TAG` picks the newest completed match for that tag. `@fav` picks your starred image, and errors if several are starred. `job`, `dev`, and `box` get the pullable source ref. `serve` gets the runtime image id.

`serve` is the exception. A custom serve runtime needs an imported runtime image id, which `zb image import` prints. A raw registry ref does not work, and presets cannot back a serve. A serve composes two ways. Managed mode is `--model` alone: the platform runs its stock runtime with the weights mounted at `/home/model`. The advanced lane adds `--image` and `--run` to serve a managed model on your own runtime. Image-only mode (no `--model`, with `--served-name`) also exists and requires tenant enablement.

## The annotated manifest

```yaml
# train.yaml: everything a manifest can say
kind: job                        # REQUIRED: job | dev | serve | box (or --kind)
name: llama-ft                   # optional; auto-generated when omitted
description: llama finetune      # optional

image: ghcr.io/acme/train:cu124  # job/dev/box: any pullable image, or a preset name like pytorch; omit on job/box for the default preset
run: |                           # job + image-mode serve; FORBIDDEN on dev and box
  torchrun --nproc-per-node 8 train.py

resources:
  gpu: h100:8                    # TYPE[:COUNT], per node (job) / per replica (serve); types from zb gpus
  cpu: 96                        # cores; omit on GPU workloads to size per card
  memory: 1024                   # GB; omit on GPU workloads to size per card
nodes: 2                         # job: >1 = distributed gang; serve: shard one replica across N nodes
max_restarts: 1                  # job only
roles: []                        # job only: multi-role gang (see Manifest reference)
priority: 5                      # 1 to 6, higher schedules first (job/serve only)
queue: true                      # default; false = fail fast when GPU quota is exhausted
debug: true                      # job/serve: hold a crashed container instead of restarting it

env:                             # plain env vars
  WANDB_PROJECT: llama
secrets:                         # env vars marked sensitive (tiered by kind: see the callout below)
  HF_TOKEN: hf_xxx
env_file: .env                   # fold a local dotenv into env: (lowest precedence)
secrets_file: .env.secrets       # same, into secrets:; a bare KEY reads local $KEY
volumes:                         # persistent volumes (zb volume create)
  - training-data:/data
  - checkpoints:/ckpt

serve:                           # kind: serve only
  model: M01ABC...               # managed mode (model id or name); add image + run for a custom runtime
  served_name: my-model          # image mode: the model name on the OpenAI API
  replicas: {min: 1, max: 3}     # default {1, 1}; min 0 = launch stopped; zb stop scales to zero later
dev:                             # kind: dev only
  ide: vscode                    # jupyter (default) | vscode; want your image's ENTRYPOINT? use kind: box
```

Unknown keys are rejected, so typos never silently no-op. The Manifest reference page lists every key with its allowed kinds.

## Sizing defaults

Omit `cpu` and `memory` on GPU workloads and the platform sizes them per card. For example, an H100 brings 10 cores and 200 GiB per card, times the requested count. The count is per node on jobs and per replica on serve. Explicit values always win. The two values also fill independently: set just `--cpu 4` and memory still sizes per card. CPU-only workloads default to 8 cores and 32 GB. `zb status NAME --wide` shows the effective sizing.

## Code in, results out

- `workdir: .` ships the spec file's directory into the workload, read-only at `/workspace`. `run` executes there. `.zbignore` controls exclusions with simple ignore patterns (names, `*.ext`, `dir/`). It replaces `.gitignore`. `.git/` never ships. The cap is 1 GiB.
- Relaunching unchanged code skips the upload entirely, so the edit-relaunch loop stays fast.
- `outputs: /outputs` mounts a results path. Whatever the workload writes there, `zb pull NAME` downloads, even after `zb down`. `zb pull` is for small results: reports, or a checkpoint or two (5 GB per pull). Large output belongs in your own bucket via `outputs_s3` (below).
- `outputs_s3:` writes results straight to your own S3 bucket instead of platform storage. Set `{url, region, access_key, secret_key}` in the spec, or use the `--outputs-s3-url/--outputs-s3-region/--outputs-s3-key/--outputs-s3-secret` flags. The platform uses your keys only to mount the bucket. It never stores them or shows them in `zb status`. Use a bucket-scoped, fine-grained key. With `outputs_s3` there is nothing to `zb pull`, because the results are already in your bucket. It requires `outputs:` to be set.
- `share: /share` mounts the tenant's persistent share volume at the given path. The share volume is the same storage behind your imported models and the console's shared files. `job`, `dev`, and `box` only.
- `ttl: 8h` cleans up automatically, anchored at launch. The platform deletes `job`, `dev`, and `box`, and stops `serve` (scale to zero). `zb ttl NAME 8h` extends the TTL. `zb ttl NAME off` clears it.

## Environment and secrets files

`env_file: .env` and `secrets_file: .env.secrets` load a local dotenv file into `env:` and `secrets:` at launch. `zb` never auto-loads a file, and a missing file is a hard error. File values are the lowest layer. Any key written explicitly wins (in the spec map, via `--env` or `--secret`, or via `--set env.K=`). Relative paths resolve against the spec file's directory. The `--env-file` and `--secrets-file` flags resolve against your working directory and replace the spec's path.

The dotenv grammar is small. `KEY=VALUE` lines and full-line `#` comments work (a mid-line `#` stays literal). An `export` prefix is optional. Double quotes unescape `\n \t \" \\`, and single quotes are literal. There is no `${VAR}` expansion. In `secrets_file` a bare `KEY` line reads local `$KEY`, like `--secret KEY`. The same key in both files with no explicit winner is an error.

> [!WARNING]
> **Secrets are tiered by kind**
> Platform-deep redaction of secrets applies only on `serve`. A `job` keeps secrets out of logs, but not out of job detail. `dev` and `box` reject `secrets:` outright. Anything passed via `env:` there is stored and shown in plain text. Prefer short-lived scoped credentials.

## Queueing and priority

Queueing is the default. When a launch fails only because GPU quota is exhausted, it parks server-side as `QUEUED`. It then launches automatically as quota frees. You do not babysit or resubmit. A queued launch that gets no quota within 24 hours expires on its own.

```bash
zb launch train.yaml --name exp-7   # quota full? parks as QUEUED instead of failing
zb wait exp-7 && zb logs -f exp-7   # polls straight through QUEUED into RUNNING
zb down exp-7                       # cancels a queued launch the same way
```

Only quota exhaustion queues. Billing problems and validation errors still fail fast. `--no-queue` (or `queue: false`) restores fail-fast behavior entirely. TTL counts from the actual launch, not from enqueue. You are never billed, or TTL-reaped, for time spent waiting.

`priority: 1` to `6` (or `--priority`) is the scheduler's ordering knob. Higher priority sits ahead of lower in the pending set and in the server-side launch queue. Ties launch in submit order. It applies to `job` and `serve` only. The platform pins `dev` and `box` scheduling. There is no preemption. Priority orders waiting workloads, and never evicts running ones.

## Iterate and sweep

Sweeps are one spec plus `--set`. Template the experiment once, then vary the override and the name. A name collision is a clean 4xx failure, never a silent duplicate. Every launch queues independently when quota is tight.

```bash
for seed in 1 2 3; do
  zb launch train.yaml --set env.SEED=$seed --name exp-$seed
done
zb status                           # one table: exp-1, exp-2, exp-3
```

While a workload runs, `zb push` and `zb sync` update code in place. `sync` transfers only files whose content changed. `zb pull NAME ./results --watch` keeps results syncing to your machine. Relaunching unchanged code skips the bundle upload entirely.
