# Steps and approvals

Jobs form a graph of steps inside each rollout phase, ordered by depends_on, with block steps that wait for a person in your environments.

Every job on `deploy`, `seed` or `start` is a step. Phases run in a fixed order. Inside a phase, steps run in parallel unless `depends_on` orders them. A block step runs nothing and waits for someone to approve it. The keys are in the [jobs reference](/manifest/jobs#steps).

```yaml title="parity.yaml"
jobs:
  init-db: { label: Create the database, service: web, command: npm run db:init, on: deploy }
  migrate: { service: web, command: npm run migrate, depends_on: [init-db] }
  assets:  { service: web, runner: tools, command: ./scripts/upload-assets.sh, on: deploy }
  release:
    block: Release to customers
    prompt: Check the canary dashboard and #support first.
    depends_on: [migrate, assets]
    environments: [production]
  seed:    { service: web, command: npm run seed, on: seed }
```

`init-db` and `assets` start together. `migrate` starts when `init-db` succeeds. `release` waits for both branches. `seed` starts after the whole `deploy` phase.

## Phases [#phases]

Phases are barriers. A phase starts only when every step of the one before it has succeeded.

| Phase | Steps | Runs |
| --- | --- | --- |
| `deploy` | Jobs with `on: deploy` | When a sandbox is first set up, and on every rollout after |
| `seed` | Jobs with `on: seed` and not `deploy` | Once, when a sandbox is first set up |
| `start` | Jobs with `on: start` | Once per sandbox, after `deploy` and `seed` |

In your [environments](/manifest/environments), a rollout runs the `deploy` steps; see [Builds](/concepts/builds#rollout-stages). Jobs on a schedule, on `destroy` or with no `on` are not steps.

## Order steps with depends_on [#order-steps-with-depends_on]

`depends_on` names jobs and services in one list. A job name makes the step run after that job succeeds. A service name makes the job wait for that service, as before.

- **Parallel by default.** Steps with nothing between them run at the same time, up to 8 per sandbox. Past 8, the rest start in file order as slots free up.
- **Inherited phase.** A job without `on` that depends on jobs runs in the latest phase among them. `migrate` above runs on `deploy`. A job without `on` that depends only on services stays on demand.
- **Same or earlier phase.** A step can depend on steps of its own phase or an earlier one. A `seed` step can list a `deploy` step; a `deploy` step can't list a `start` step.
- **No cycles.** Parity names the cycle.
- **One name, one meaning.** A name that is both a service and a job is an error. Rename one.

## Labels [#labels]

`label` is the name the console and CLI show for a step. The job's name stays its key: `depends_on`, `parity run` and the API use the key. A label is one line, at most 200 characters.

## Block steps [#block-steps]

A block step gates the rollout. It runs nothing: no `service`, `command`, `runner` or service `depends_on`.

```yaml
release:
  block: Release to customers       # or: block: true, with label
  prompt: Check the canary dashboard and #support first.
  depends_on: [migrate, assets]
  environments: [production]
```

| Key | Meaning |
| --- | --- |
| `block` | `block: <label>`, or `block: true` with an optional `label`. Not both a block label and `label`. |
| `prompt` | Optional plain text, up to 1,000 characters, shown when someone approves. Block steps only. |
| `environments` | Names of declared environments. Omitted means every environment. Block steps only. |
| `depends_on` | Jobs only. A block runs on `deploy`, so every job it lists must be a `deploy` step. |

**Sandboxes pass blocks.** A sandbox belongs to an agent, and agents never approve. The block records as succeeded the moment its dependencies are done, so `parity up` never stalls on one.

**Environments wait.** A block holds the rollout until a person approves or rejects it. That takes `environments.deploy`, which Developers have. Approve lets the steps after it run. Reject fails the rollout, and the current revision keeps serving. Agent tokens can't approve.

Jobs that run something can't vary by environment. What a sandbox tested is what every environment runs. `environments` exists only on blocks because a block runs nothing.

## When a step fails [#when-a-step-fails]

| What | Result |
| --- | --- |
| The failed step | `failed`, with the end of its log in `log_tail`. A job pod gets 3 attempts before the step fails. |
| Steps that depend on it, directly or not | `skipped`. They never start. |
| Steps already running | Run to completion. |
| Later phases | Never start. |
| The sandbox | Fails once nothing is still running. `parity up` prints the log tail. |

Rerunning `parity up` retries the failed step. Steps that succeeded on `seed` or `start` don't run again; `deploy` steps run on every rollout.

Parity runs one copy of a job at a time. A rollout that lands while an earlier run of `migrate` is still going waits for it, so two migrations never meet one database.

## Read status [#read-status]

Before you deploy, `parity check` prints each step with its label, phase and what it runs after:

```text title="parity check"
jobs
  init-db (Create the database) in web, deploy: when a sandbox is set up and on every rollout
  migrate      in web, deploy: when a sandbox is set up and on every rollout, after init-db
  assets       in web, deploy: when a sandbox is set up and on every rollout
  release (Release to customers) block, waits for a person in staging and production (sandboxes pass it), deploy: when a sandbox is set up and on every rollout, after migrate, assets
  seed         in web, seed: once, when a sandbox is first set up, after the deploy jobs
```

While it runs, `parity status`, the `sandbox_status` MCP tool and `GET /v0/sandboxes/{id}` return each step under `jobs`, in run order:

```json title="parity status (jobs, trimmed)"
[
  { "name": "init-db", "label": "Create the database", "event": "deploy", "status": "succeeded", "attempts": 1 },
  { "name": "migrate", "event": "deploy", "status": "running", "needs": ["init-db"], "attempts": 1 },
  { "name": "assets", "event": "deploy", "status": "failed", "attempts": 3, "log_tail": "…" },
  { "name": "release", "label": "Release to customers", "event": "deploy", "status": "skipped", "needs": ["migrate", "assets"], "block": true, "attempts": 0 }
]
```

| Field | Meaning |
| --- | --- |
| `status` | `pending`, `running`, `succeeded`, `failed` or `skipped` |
| `label` | The step's label, when it has one |
| `needs` | The jobs it runs after |
| `block` | `true` for a block step |
| `event` | The phase the run was for. On a sandbox's first setup, `deploy` steps report `seed`. |

## Validation [#validation]

`parity check` and `parity up` refuse a manifest with any of these, and list them all at once.

| Problem | Message |
| --- | --- |
| Depends on itself | `jobs.<job>.depends_on: a job can't depend on itself` |
| A cycle | `jobs.a.depends_on: a cycle, a → b → a` |
| Depends on a later phase | `jobs.migrate.depends_on: warm runs on start, after this job's deploy; depend on jobs of the same or an earlier phase` |
| Depends on a job that isn't a step | `jobs.<job>.depends_on: <dep> runs on no seed, deploy or start, so it is never a step to wait for` |
| A non-step depends on a job | `jobs.<job>.depends_on: <dep> is a job; only steps (jobs on seed, deploy or start) run after other jobs` |
| Name is both a service and a job | `jobs.<job>.depends_on: "<name>" is both a service and a job; rename one of them` |
| A block that runs something | `jobs.<job>: a block step waits for a person and runs nothing; drop service, command, runner and service depends_on` |
| A block off `deploy` | `jobs.<job>.on: a block step runs on deploy, not start (it gates a rollout)` |
| Block label and `label` | `jobs.<job>: block and label both name the step; keep one (block: true with label, or block: <label>)` |
| Empty or non-text `block` | `block must be a label, such as "Release to customers", or true` |
| `prompt` on a job | `jobs.<job>.prompt: only block steps have a prompt; add block: true, or drop prompt` |
| `environments` on a job | `jobs.<job>.environments: only block steps vary by environment (a job that runs something runs the same everywhere, so a sandbox tests it)` |
| An undeclared environment | `jobs.<job>.environments: environment "<name>" is not declared (sandboxes pass block steps)` |
| Long text | `jobs.<job>.prompt: at most 1000 characters, got <n>` and `jobs.<job>.label: at most 200 characters, got <n>` |
| Multi-line label | `jobs.<job>.label: one line; put longer text in prompt` |

## Migrate from file order [#migrate-from-file-order]

<Callout type="warning" title="File order no longer orders jobs">
Jobs of one phase used to run one at a time, in file order. They now run in parallel. If a job relied on the job above it, list that job in `depends_on`.
</Callout>

```yaml title="Before: order came from the file"
jobs:
  init-db: { service: web, command: npm run db:init, on: deploy }
  migrate: { service: web, command: npm run migrate, on: deploy }
```

```yaml title="After"
jobs:
  init-db: { service: web, command: npm run db:init, on: deploy }
  migrate: { service: web, command: npm run migrate, on: deploy, depends_on: [init-db] }
```

Order between phases is unchanged. A `seed` job still runs after every `deploy` job and needs no `depends_on` for that. Run `parity check` to see each step's `after` list before you push.
