# Workflows

A workflow runs a job as a series of steps. Each step runs in a sandbox of its own, and each step that finishes saves a checkpoint. The run keeps going with no client attached. If a step fails, you resume the run from that step instead of starting over, and you can replay a run from any of its checkpoints.

You describe a workflow in one YAML or JSON file, then run it from the CLI, the console or the API.

## Write your first workflow

Save this as `build-and-test.yaml`. Replace the image with one you pushed to your workspace's registry (see [Images and the registry](/docs/images)), and the repository with yours:

```yaml
workflow: build-and-test
image: registry.caas.sh/ci:node24
egress: [github.com, registry.npmjs.org]
inputs:
  repo: https://github.com/your-org/app
  branch: main

steps:
  - id: checkout
    name: Check out and install
    run:
      - git clone --depth 1 --branch "${{ inputs.branch }}" "${{ inputs.repo }}" app
      - cd app && npm ci
    outputs: [app]

  - id: test
    name: Run tests
    run: cd app && npm test
    inputs: [checkout]

  - id: build
    name: Build
    run: cd app && npm run build
    inputs: [checkout]
    outputs: [app/dist]
```

When this runs:

- Each step starts in a fresh sandbox with an empty `/workspace`, and runs its commands there.
- `checkout` hands its `app` directory on through `outputs`. `test` and `build` list `checkout` under `inputs`, so they start with that directory unpacked into their own `/workspace`.
- Steps run in file order, because a step without `needs` waits for the step before it. [Run steps in parallel](#run-steps-in-parallel) shows how to change that.
- A command that exits with an error fails its step, and no further step starts.
- Outbound requests go only to the hosts in `egress`: `github.com` and `registry.npmjs.org`.

## Run a workflow

### From the CLI

Install the CLI and connect it to your workspace as [The CLI](/docs/cli) describes:

```sh
npm i -g @caascade/cli
```

Then start the run:

```sh
caas workflows run build-and-test.yaml --input branch=release --follow
```

- `--input NAME=VALUE` sets one of the file's inputs. Repeat it for each input you set. An input you don't set keeps its default.
- The CLI checks the file and its inputs before it sends anything. It also pins each image tag to its digest in your registry, so the run uses exactly the image you pushed, even if you push the tag again later.
- Without `--follow`, the CLI prints the run's name, number and ID, plus a link to the run in the console, and returns.
- With `--follow`, it prints each step as its state changes, and a running step's new output lines, until the run ends. It exits with status 1 unless the run succeeded.

Starting a run needs an API key that may create sandboxes and run commands in them (`sandbox:write` and `sandbox:execute`). Resolving image tags also needs registry read access.

### From the console

1. Open **Workflow runs** and choose **New run**.
2. Under **Workflow file**, choose a `.yaml`, `.yml` or `.json` file, or paste the file into **Or paste it**. The console reads and checks it in your browser.
3. The console only accepts images pinned to a digest, such as `registry.caas.sh/<tenant-id>/ci@sha256:…`. `docker push` prints the digest. To use tags, run the file with `caas workflows run`, which pins them for you.
4. Once the file parses, a card shows the workflow's name and its number of steps, with a field for each input. A required input has no default and must be filled in.
5. Choose **Start run**. The run's page opens.

Workspace owners and operators can start and control runs. Viewers can follow runs, but can't start or control them.

### From the API

`POST /v1/tenants/<tenant-id>/workflow-runs` starts a run from the same file. See [The API](/docs/api).

## The workflow file

A workflow file is YAML or JSON, at most 64 KiB. If its first non-blank character is `{`, it's read as JSON. The CLI, the console and the API all read files the same way, so a file one accepts, the others accept too. An unknown field, or a value outside the limits, is refused with a message that names the line or the field.

### Top-level fields

| Field | Required | What it does |
| --- | --- | --- |
| `workflow` | Yes | The workflow's name: up to 63 lowercase letters, digits and hyphens, starting and ending with a letter or digit. Runs are named after it: `build-and-test #12`. |
| `steps` | Yes, unless you use `run` | The steps, 1 to 50 of them. |
| `inputs` | No | Values a run takes, each with a default or none. See [Run inputs](#run-inputs). |
| `image` | No | The default image for every sandbox in the run. See [Images and outbound hosts](#images-and-outbound-hosts). |
| `egress` | No | The default list of hosts every sandbox may reach. |
| `sandboxes` | No | Named sandboxes that several steps share. See [Share a sandbox between steps](#share-a-sandbox-between-steps). |
| `run`, `secrets` | No | The one-command shorthand, in place of `steps`. See [The one-command shorthand](#the-one-command-shorthand). |

### Step fields

| Field | Required | What it does |
| --- | --- | --- |
| `id` | Yes | A unique ID: lowercase letters, digits and hyphens. Other steps refer to the step by its ID. |
| `name` | Yes | What the console and the CLI show, up to 80 characters. |
| `run` | One of `run` or `agent` | A command, or a list of 1 to 32 commands. See [Commands](#commands). |
| `agent`, `task` | One of `run` or `agent` | `agent: claude-code` runs Claude Code on `task`. See [Agent steps](#agent-steps). |
| `needs` | No | The IDs of the steps this one waits for. Without it, the step waits for the step before it. See [Run steps in parallel](#run-steps-in-parallel). |
| `inputs` | No | The IDs of earlier steps whose outputs this step receives. |
| `outputs` | No | Up to 64 paths, relative to `/workspace`, that the step hands on to later steps. |
| `secrets` | No | Up to 16 names of credentials the step's requests may use. See [Secrets](#secrets). |
| `sandbox` | No | The name of a sandbox under `sandboxes` to run on. Without it, the step gets a fresh sandbox of its own. |
| `image`, `egress` | No | This step's own image and outbound hosts. They replace the file's defaults, and aren't allowed on a step that runs on a named sandbox. |

A step's `inputs` and the file's `inputs` mean different things. The file's `inputs` are values you pass in when you start a run. A step's `inputs` name earlier steps whose files it receives.

### Commands

The commands in a step's `run` run one after another in a single `/bin/sh` script, with `/workspace` as the working directory:

- The first command that exits with an error stops the script and fails the step, as with `set -e`. To let a command fail, end it with `|| true`.
- The commands share one shell, so a `cd` carries over to the commands after it, but not to the next step.
- Standard input is empty (`/dev/null`). Everything the commands print, standard output and standard error together, goes to the step's log.
- A step may run for 2 hours. After that it fails.
- caas.sh keeps its own files in `/workspace/.caas`, so `/workspace` is never quite empty. Clone a repository into a subdirectory, as in `git clone URL app`, rather than into `.`.

### Run inputs

Declare inputs at the top of the file, each with a default value, or with nothing after the colon to make it required:

```yaml
inputs:
  repo: https://github.com/your-org/app
  branch: main
  ticket:
```

- Input names start with a letter or `_`, followed by up to 63 letters, digits or `_`. A file declares at most 32 inputs, and each value is at most 4,096 characters.
- Set inputs with `--input NAME=VALUE` in the CLI, or in the fields the console's **New run** page shows. A run can't start until every required input has a value.
- Write `${{ inputs.NAME }}` in a command or a task to use an input's value. The name must be one the file declares.
- In a command, `${{ inputs.NAME }}` becomes a reference to the environment variable `CAAS_INPUT_NAME`, which the shell expands. Put it in double quotes, as in `"${{ inputs.repo }}"`, if a value could contain spaces.
- In a task, `${{ inputs.NAME }}` is replaced with the value itself.
- Every step's commands can also read each input as `$CAAS_INPUT_NAME`.

### Pass files between steps

A step's sandbox is deleted after the step. To keep files, list them under `outputs`. Each output is a path relative to `/workspace`:

- `.` stands for the whole workspace.
- A path can't start with `/` or `~`, contain `..`, or name `.caas`.

When the step finishes, its checkpoint saves those paths. A later step that names the step under `inputs` gets those paths unpacked into its `/workspace`, at the same paths, before its commands run:

```yaml
  - id: build
    name: Build
    run: cd app && npm run build
    inputs: [checkout]
    outputs: [app/dist]

  - id: package
    name: Package the build
    run: tar -czf build.tar.gz -C app/dist .
    inputs: [build]
    outputs: [build.tar.gz]
```

- A step's `inputs` must name steps listed earlier in the file, and the step waits for each of them to finish.
- If two inputs hold the same path, the one listed later wins.
- What checkpoints hold is billed as carried files. See [Pricing](#pricing).

### Run steps in parallel

A step waits for the steps named in its `needs`. A step without `needs` waits for the step before it in the file. Steps waiting on the same steps start together, each in its own sandbox.

This file fans out after `checkout`, then fans back in:

```yaml
workflow: test-matrix
image: registry.caas.sh/ci:node24
egress: [github.com, registry.npmjs.org]
inputs:
  repo: https://github.com/your-org/app

steps:
  - id: checkout
    name: Check out and install
    run:
      - git clone --depth 1 "${{ inputs.repo }}" app
      - cd app && npm ci
    outputs: [app]

  - id: lint
    name: Lint
    run: cd app && npm run lint
    needs: [checkout]
    inputs: [checkout]

  - id: unit
    name: Unit tests
    run: cd app && npm test
    needs: [checkout]
    inputs: [checkout]

  - id: build
    name: Build
    run: cd app && npm run build
    needs: [checkout]
    inputs: [checkout]
    outputs: [app/dist]

  - id: package
    name: Package the build
    run: tar -czf build.tar.gz -C app/dist .
    needs: [lint, unit, build]
    inputs: [build]
    outputs: [build.tar.gz]
```

`lint`, `unit` and `build` run side by side once `checkout` finishes. `package` starts once all three have finished.

- A step that names several steps in `needs` waits for all of them.
- A step also waits for every step it takes `inputs` from.
- `needs` names steps listed earlier in the file, each once. A step that names an unknown step, itself or a later step is refused. If the order would make a step wait for itself, the error shows the cycle, such as `a → b → a`.
- `needs: []` starts the step right away, alongside the first step.
- If a step fails, no new step starts. Steps that are already running finish and save their checkpoints, then the run fails.

### Agent steps

An agent step runs a coding agent on a task, in place of commands:

```yaml
  - id: fix
    name: Fix the failing tests
    agent: claude-code
    task: Make the failing tests in the app directory pass without changing the tests.
    inputs: [checkout]
    outputs: [app]
```

- `claude-code` is the only agent. The task can be up to 4,096 characters, and `${{ inputs.NAME }}` in it is replaced with the input's value.
- The step runs Claude Code non-interactively in `/workspace`, with the task as its prompt and permission prompts turned off.
- The step uses the `claude` command if your image has it on its `PATH`. If not, it installs Claude Code with `npx`, which needs npm in the image and `registry.npmjs.org` in the step's `egress`.
- The step's sandbox can always reach `api.anthropic.com`. You don't need to add it to `egress`.

An agent step needs your Anthropic API key at egress. Before you start the run, connect the key and add a workspace egress rule for it. You need to be a workspace owner. `--api-key` reads the key from standard input:

```sh
caas credentials connect ANTHROPIC_API_KEY --api-key < anthropic-key.txt
caas egress rules add --credential ANTHROPIC_API_KEY --preset anthropic
```

The key never enters the sandbox. In the sandbox, `ANTHROPIC_API_KEY` holds a placeholder, and caas.sh adds the real key to requests to `api.anthropic.com`. A run with an agent step won't start until a workspace egress rule adds a usable credential at `api.anthropic.com`. See [Credentials and outbound access](/docs/credentials).

### Secrets

List a credential under a step's `secrets` so the step's requests can use it:

```yaml
  - id: checkout
    name: Check out a private repository
    run: git clone --depth 1 https://github.com/your-org/private-app app
    outputs: [app]
    secrets: [GITHUB_TOKEN]
```

- Secret names are capitals, digits and `_`, such as `GITHUB_TOKEN`.
- Each secret must be a connected credential with a workspace egress rule that adds it. A rule that applies to one sandbox only doesn't count. For example, `caas egress rules add --credential GITHUB_TOKEN --preset github` adds it to requests to `api.github.com` and `github.com`.
- If a secret isn't connected, has no usable value or has no workspace rule, the run doesn't start, and the error names the step and the secret.
- In the step, a variable named after the secret holds a placeholder, never the value. caas.sh adds the real value to requests to the hosts the rule names. The step's `egress` must still allow those hosts.
- On a named sandbox, the secrets of all the steps that run there apply to every one of those steps.

See [Credentials and outbound access](/docs/credentials) to connect credentials and add rules.

### Share a sandbox between steps

By default, every step gets a fresh sandbox, which is deleted when the step ends. To let several steps work in the same `/workspace`, declare a named sandbox under `sandboxes` and point the steps at it with `sandbox`:

```yaml
workflow: fix-failing-tests
image: registry.caas.sh/node-agent:24
egress: [github.com, registry.npmjs.org]
inputs:
  repo: your-org/app

sandboxes:
  dev:
    size: XL

steps:
  - id: checkout
    name: Check out and install
    sandbox: dev
    run:
      - git clone --depth 1 "https://github.com/${{ inputs.repo }}" app
      - cd app && npm ci
    secrets: [GITHUB_TOKEN]

  - id: fix
    name: Fix the failing tests
    sandbox: dev
    agent: claude-code
    task: Make the failing tests in the app directory pass without changing the tests.

  - id: test
    name: Run the tests
    sandbox: dev
    run: cd app && npm test
```

- A named sandbox takes `size`, `image` and `egress`. Its `image` and `egress` replace the file's defaults, and an agent step on it adds `api.anthropic.com` to its hosts.
- Steps on the same named sandbox run one at a time, in file order. Each one waits for the step before it on that sandbox, as well as for the steps it `needs`.
- `/workspace` carries over from one step to the next on the sandbox. You don't need `outputs` and `inputs` between them.
- A step on a named sandbox can't set its own `image` or `egress`. Set them under `sandboxes`.
- Each checkpoint of a step on a named sandbox also saves the sandbox's whole `/workspace`. A resume or a replay restores the workspace from it.
- A file can declare up to 16 named sandboxes. Each one is deleted after the last step that runs on it.

#### Sizes

Only a named sandbox takes a `size`. A step on its own sandbox, and a named sandbox without `size`, gets 2 vCPU, 6 GiB of memory and 10 GB of disk.

| `size` | vCPU | Memory | Disk |
| --- | --- | --- | --- |
| `L` | 1 | 6 GiB | 12 GB |
| `XL` | 2 | 8 GiB | 16 GB |
| `2XL` | 4 | 12 GiB | 20 GB |
| Custom, such as `3 vCPU · 9 GiB · 16 GB` | 1 to 4, whole | At least 3 GiB per vCPU, at most 12 GiB | 2 to 20 GB |

- Size names aren't case-sensitive. The smaller sandbox sizes, XS, S and M, aren't available to workflows.
- You can also write a custom size with commas: `3 vCPU, 9 GiB, 16 GB`.
- A custom size is rounded up to whole GiB and GB, to at least 3 GiB per vCPU, and on your own image to at least 4 GiB and 8 GB. The CLI and the **New run** page show a note when a size was rounded.
- To give a single step a different size, put it on a named sandbox of its own.

### Images and outbound hosts

`image` and `egress` can be set at the top of the file, on a named sandbox, or on a step that has its own sandbox. A value set at a lower level replaces the file's default; it's never merged with it.

- **`image`** names an image in your workspace's registry: `registry.caas.sh/NAME:TAG`, `registry.caas.sh/<tenant-id>/NAME:TAG`, or either one pinned with `@sha256:…`. Without a tag, the CLI uses `latest`. The CLI pins tags to digests when it starts the run, and the console needs digests. Without `image`, steps run on the Default image, Alpine Linux with bash, coreutils, Node and Docker. It doesn't include Claude Code. Use your own image for what else your steps need, such as Git, npm or your project's toolchain. See [Images and the registry](/docs/images).
- **`egress`** lists the exact host names a sandbox may reach, such as `api.github.com`, up to 32 per sandbox, including an agent's hosts. Wildcards aren't allowed. Requests to any other host are blocked, except to `registry.caas.sh`. Some hosts are never reachable, whatever you list: see [Hosts that are always or never reachable](/docs/credentials#hosts-that-are-always-or-never-reachable).

### The one-command shorthand

For a job that's a single step, write `run` at the top of the file in place of `steps`:

```yaml
workflow: nightly-tests
image: registry.caas.sh/ci:node24
egress: [github.com, registry.npmjs.org]
run:
  - git clone --depth 1 https://github.com/your-org/app app
  - cd app && npm ci && npm test
```

This is one step, with the ID `run`, named after its first command, on a sandbox of its own. The shorthand takes `workflow`, `inputs`, `image`, `egress`, `run` and `secrets`, but not `steps` or `sandboxes`. A top-level `secrets` list is only allowed with `run`. In a file with `steps`, give each step its own `secrets`.

### JSON files

A JSON file has the same fields:

```json
{
  "workflow": "nightly-tests",
  "image": "registry.caas.sh/ci:node24",
  "egress": ["github.com", "registry.npmjs.org"],
  "steps": [
    {
      "id": "test",
      "name": "Run tests",
      "run": ["git clone --depth 1 https://github.com/your-org/app app", "cd app && npm ci && npm test"]
    }
  ]
}
```

### The YAML that workflow files accept

Workflow files use a subset of YAML:

- Block mappings and `- ` lists, indented with spaces. Tabs aren't allowed.
- Flow lists of plain or quoted values, such as `[github.com, registry.npmjs.org]`, written on one line.
- Plain, single-quoted and double-quoted values, and `#` comments. `true`, `false` and whole numbers are read as such.
- At most one leading `---`.

Block scalars (`|` and `>`), anchors, aliases, tags, flow mappings, several documents and duplicate keys are refused. Quote a value that contains `: `. For a multi-line script, write a list of commands.

## Follow a run

A run is named after its workflow and numbered in your workspace, such as `build-and-test #12`. Its status is one of:

| Status | Meaning |
| --- | --- |
| Queued | Accepted, and waiting to start. |
| Running | Steps are running or about to start. |
| Paused | Paused: no new step starts until you resume it. |
| Failed | A step failed, or the run couldn't continue. |
| Succeeded | Every step finished. |
| Cancelled | You cancelled it. |

In the CLI:

```sh
caas workflows list
caas workflows show <run-id>
caas workflows show <run-id> --follow
```

- `list` shows the newest 100 runs, with each one's status, progress, cost so far and ID.
- `show` prints each step's state, its sandbox and checkpoint, and output: a failed step's exit code and the last 40 lines of its log, or a running step's newest lines. For a failed run, it also prints the command that resumes it.
- `--follow` checks the run every 5 seconds until it ends.
- Add `--json` to `list` or `show` to print the API's view.

In the console, open **Workflow runs** and choose a run:

- The summary shows when the run started, how long it has taken, its progress, how many checkpoints it saved and its cost.
- **Steps** lists the steps in the order they run. Steps that run side by side share one entry. Each step shows its sandbox, how long it took and its checkpoint.
- A running step shows the newest 40 lines of its log, refreshed about every 15 seconds. A failed step shows its exit code and the last 40 lines.

A step's sandbox is named `<workflow>-<number>-<step-id>`, or after the named sandbox it runs on, with `-<attempt>` added from the second attempt on. These sandboxes appear on the run's page, not under **Sandboxes**. Failed runs also appear under **Needs attention** on the console's Overview for a day.

## Checkpoints

Each step that finishes saves a checkpoint, including steps with no outputs. A checkpoint holds the step's `outputs`, and for a step on a named sandbox, the sandbox's whole `/workspace`. It also notes which files the step added, changed or deleted, not counting `.git`, `node_modules` and `.caas`.

A checkpoint is labeled `cp-N`, where `N` is its step's position in the file: `cp-3` is the third step's checkpoint. The run's page lists them, newest first, under **Checkpoints**. **Carried forward** shows the files the newest checkpoint's step changed.

Checkpoints are kept while the run is active and for 7 days after it ends. After that, the run can't be resumed or replayed, and you start it again.

## Pause, resume and cancel

### Pause a run

Pausing a running run stops new steps from starting. Steps that are already running finish and save their checkpoints.

```sh
caas workflows pause <run-id>
caas workflows resume <run-id>
```

In the console, choose **Pause** on the run's page, then **Resume** to continue.

### Cancel a run

You can cancel a run that's queued, running or paused. Its sandboxes are stopped, and the run ends as Cancelled.

```sh
caas workflows cancel <run-id>
```

In the console, choose **Cancel run**. The checkpoints a cancelled run saved are kept. `caas workflows resume <run-id>` continues it from the steps that didn't finish. On the run's page, you can also restart it from one of its checkpoints.

### Resume a failed run

When a step fails, no new step starts, and the run fails once the running steps have finished. To resume it:

```sh
caas workflows resume <run-id> --follow
```

In the console, choose **Resume from step N** on the run's page, or in the runs list.

Resuming starts a new attempt of the run. The attempt keeps every checkpoint and runs only the steps that didn't finish. Named sandboxes restore their workspace from their last checkpoint. The run's list entry shows how many times it was resumed.

A resume runs the same file, with the same inputs and the same pinned images. It suits failures whose cause lies outside the file: a flaky test, a service that was down, a credential you've since connected, or a balance you've since topped up. To change the file or its inputs, start a new run.

Resuming checks the same things as starting: your balance, the active run limit, your quota, and the secrets and keys at egress. A run that's queued or running can't be resumed or replayed.

### Replay from a checkpoint

To run part of a run again, replay it from a checkpoint:

```sh
caas workflows resume <run-id> --from cp-2
```

In the console, choose **Restart from here** next to a checkpoint on the run's page.

- A replay from `cp-N` keeps the checkpoints of steps 1 to N, and runs every step after step N in the file again, with the run's own inputs.
- You can replay any run that has ended: succeeded, failed or cancelled. A run that succeeded can only be replayed, not resumed.
- The checkpoint must be one listed under the run's **Checkpoints**.

## Limits

| Limit | Value |
| --- | --- |
| Workflow file | 64 KiB |
| Steps | 50 per file |
| Commands | 32 per step, each up to 4,096 characters |
| Agent task | 4,096 characters |
| Step name | 80 characters |
| Outputs | 64 per step |
| Secrets | 16 per step |
| Inputs | 32 per file, each value up to 4,096 characters |
| Named sandboxes | 16 per file |
| Outbound hosts | 32 per sandbox, including an agent's |
| Step running time | 2 hours |
| Active runs | 10 queued, running or paused at once per workspace |
| Checkpoints kept | 7 days after the run ends |

Runs also count against your workspace's vCPU and memory quota, together with your sandboxes. Each active run counts the largest group of steps that can run at once. A start or a resume that would go over the quota is refused.

A run starts or resumes only while your workspace could start a sandbox. If your balance runs out or the workspace is suspended, every active run is stopped and fails. Resume it once the workspace can run again. See [Billing and usage](/docs/billing).

## Pricing

| Meter | Price | What counts |
| --- | --- | --- |
| Steps | $1.00 / 1,000 steps | Each time a step starts, including when a resume or a replay runs it again |
| Step sandboxes | Sandbox rates | Active vCPU at $0.15 / hour while computing, plus memory at $0.018 / GiB-hour and disk at $0.001 / GB-hour at the sandbox's size, from the step's start to its checkpoint or failure. Egress is $0.06 / GB. |
| Carried files | $0.10 / GiB-month | Step outputs held by checkpoints, from the checkpoint until 7 days after the run ends |
| Named sandbox workspaces | $0.025 / GB-month | The workspace each checkpoint on a named sandbox saves, for the same time, at the snapshot storage rate |
| Checkpoints, resumes and replays | Included | No charge for the operation itself |

A step on the default 2 vCPU, 6 GiB, 10 GB sandbox costs $0.118 an hour for memory and disk while it runs, plus $0.15 for each vCPU-hour of CPU it actually uses, plus $0.001 for the step.

The cost on the run's page and in `caas workflows list` is an estimate at the published rates. It never comes out below what you're billed. See [pricing](/pricing.html) for every rate, and [Billing and usage](/docs/billing) for how usage draws down your balance.
