steps docs
start here Writing pipelines resourcesexprcontrol-flowagentsattempts-timeoutworkspaceinfratemplatingmcpcomplete Reference webagents-internalsaws-workersgcp-workersconformance

Workspace Isolation

Every step runs isolated: its working directory is materialized from its own declared inputs: and nothing else, and only its declared outputs: are captured back. A get fetches into the build's artifact store; a task/agent/put step sees an artifact only by naming it. There is no shared mutable directory and no way to opt into one — an artifact reaching a step that never declared it would be ambient data flow, and every flow in this DSL is opt-in.

jobs:
- name: build
  plan:
  - task: generate
    outputs: [meta]                  # an empty meta/ exists; its content is captured
    run: echo 42 > meta/answer.txt
  - task: consume
    inputs: [meta]                   # sees meta/ because it says so — and nothing else
    run: cat meta/answer.txt
    assert:
      stdout: "42"                   # the artifact really crossed the step boundary
  - task: blind
    run: test ! -e meta              # declared nothing, sees nothing
    assert:
      code: 0                        # test(1) agrees meta/ is absent here
  assert:
    execution: [generate, consume, blind]
    outcome: succeeded
resource_types:
- name: archive
  config:
    out: |
      ls a b            # the put's view really holds both artifacts

resources:
- name: bundle
  type: archive
  source: {}

jobs:
- name: publish
  plan:
  - task: one
    outputs: [a]
    run: echo 1 > a/one.txt
  - task: two
    outputs: [b]
    run: echo 2 > b/two.txt
  - put: bundle
    inputs: all         # every artifact produced so far; valid only on put steps
  assert:
    execution: [one, two, bundle]    # `ls a b` in out: would fail if either were missing
    outcome: succeeded

Read-modify-write

A name in both inputs: and outputs: materializes the artifact's current content and captures it back over the artifact after the step succeeds. This is how a revise loop carries state between visits — the looping step declares the same artifact both ways and each visit continues from the last captured state:

jobs:
- name: accumulate
  plan:
  - task: start
    outputs: [log]
    run: echo first > log/entries.txt
  - task: append
    inputs: [log]                     # starts from what `start` captured…
    outputs: [log]                    # …and its changes become the new content
    run: echo second >> log/entries.txt
  - task: show
    inputs: [log]
    run: wc -l < log/entries.txt
    assert:
      stdout: "2"                     # both entries survived — the read-modify-write held
  assert:
    execution: [start, append, show]
    outcome: succeeded

A step that fails captures nothing, so put the state-advancing write in a step that succeeds.

Name mapping (task steps only)

input_mapping:/output_mapping: are {task-config-name: plan-artifact-name}, mirroring Concourse: a reusable tasks: entry with pinned input/output names can be pointed at whatever a job actually produced without editing the task. The directory on disk keeps the task-config name; the artifact copied in / captured out uses the plan name:

tasks:
- name: count-lines            # a reusable task, written against "src"
  inputs: [src]
  outputs: [report]
  run: wc -l src/* > report/count.txt

jobs:
- name: audit
  plan:
  - task: fetch
    outputs: [handbook]
    run: printf 'a\nb\n' > handbook/pages.txt
  - task: count-lines
    input_mapping: { src: handbook }        # the plan's "handbook" appears as src/
    output_mapping: { report: audit-notes } # captured as "audit-notes"
  - task: check
    inputs: [audit-notes]
    run: cat audit-notes/count.txt
    assert:
      stdout: "2"                           # the mapped-in handbook is what got counted
  assert:
    execution: [fetch, count-lines, check]
    outcome: succeeded

Mapping keys must be a subset of the resolved task's declared inputs/outputs, and mapping values must be plain artifact names.

Tuning materialization: the workspace: block

Optional tuning for how trees are materialized — not a switch that turns isolation on:

workspace:
  strategy: btrfs       # default: copy. btrfs (Linux only) snapshots instead of copying
  root: /mnt/btrfs      # optional for copy (default: system temp); required for btrfs
  options:
    compression: zstd   # btrfs only: zstd | lzo | zlib | none

jobs:
- name: build
  plan:
  - task: hello
    run: echo hi

Cross-build resource cache (cache:)

Agent and put steps make their chains unskippable, so a real run re-fetches every get from scratch — the network and disk paid again every time. The cache keeps fetched versions across builds:

workspace:
  strategy: btrfs
  root: /mnt/btrfs
  cache:
    resources: true
    max_entries: 50    # optional; least-recently-used evicted first

jobs:
- name: build
  plan:
  - task: hello
    run: echo hi

Step output cache (volatile:)

The resource cache above keeps what a get fetched. This one keeps what a task or agent step produced, so the steps that cost money can skip too.

It turns on by itself as soon as workspace.root: names a durable directory — there is no second switch. A step is looked up before it runs, and on a hit its declared outputs: are restored into the artifact store and the plan carries on with the next step:

skip: reviewer (reused)

Opt a single step out with volatile::

jobs:
- name: publish
  plan:
  - task: stamp
    volatile: true          # produces an artifact, but reads the clock to do
    outputs: [stamp]        # it — so a recorded answer would be a stale one
    run: date +%s > stamp/at
    assert:
      files: [stamp/at]
  assert:
    execution: [stamp]
    outcome: succeeded

Resuming a failed run

If a step fails, re-running the job from the beginning pays for every expensive step again — and for agent steps it is lossy: an agent is not deterministic, so re-running does not reproduce the output that already passed review.

$ steps run pipeline.yml --job publish
... 50 minutes ...
steps: error: put "repo": failed to push some refs
run: K7QP2XM4  (resume with: steps run <pipeline> --resume K7QP2XM4)
run: K7QP2XM4  workspace kept at /tmp/steps-1837462

$ steps run pipeline.yml --resume K7QP2XM4
skip: planner       (already succeeded)
skip: coder         (already succeeded)
skip: reviewer      (already succeeded)
put: repo

See also infra.md for image:, which composes with workspace isolation — a containerized step still only sees its declared inputs/outputs.