# Workflow file reference

Workflows live in `.oak/workflows/` and are read from the commit being tested. This page is the complete format.

Oak's workflow files look like GitHub Actions at the trigger / job / step level, but they're a **small, strict subset of YAML that Oak parses itself**. That keeps behavior predictable: anything outside the subset is a line-numbered error, never a silent misread. A workflow that fails to parse still produces a run — one that fails immediately and shows the error.

## Files

- **Location:** directly in `.oak/workflows/`, ending in `.yml` or `.yaml`. Subdirectories are ignored.
- **Name:** the file name without its extension. `.oak/workflows/ci.yml` is the workflow `ci` — that's the name `oak ci` and the API use.
- **Size:** at most 64 KiB.
- **Independence:** each file that matches an event starts its own run. Separate files are the way to run independent checks in parallel.

## A complete example

```yaml
# .oak/workflows/ci.yml
on: [push, branch.created, merge]

env:
  CARGO_TERM_COLOR: always
  DATABASE_URL: postgres://postgres@localhost:5432/test

jobs:
  test:
    env:
      RUST_BACKTRACE: "1"
    cache:
      key: cargo
      paths:
        - ~/.cargo/registry
        - ~/.cargo/git
        - target
    steps:
      - name: start postgres
        run: bash scripts/ci/start-postgres.sh
      - name: lint
        run: cargo clippy --all-targets -- -D warnings
      - name: test
        run: cargo test --locked
```

## Top-level keys

Only these three are allowed.

| Key | Required | Value |
|---|---|---|
| `on` | yes | The events that start this workflow. |
| `env` | no | Environment variables for every step. |
| `jobs` | yes | One or more named jobs. |

## on

Which events start the workflow. Three spellings are accepted:

```yaml
on: push
on: [push, branch.created, merge]
on:
  - push
  - branch.created
```

| Event | Fires when |
|---|---|
| `push` | An existing branch gets new commits (including a revert landing on it). |
| `branch.created` | A branch is pushed for the first time. |
| `merge` | A branch is squash-merged onto `main`; the run tests the new `main` commit. |
| `manual` | Someone triggers it with `oak ci trigger` or the API. |

Event names are case-insensitive; anything else is an error. Because a branch's **first** push is `branch.created`, not `push`, use `[push, branch.created]` to test every push.

## env

Flat `KEY: value` lines, at the top level and/or inside a job:

```yaml
env:
  NODE_ENV: test
  RETRIES: "3"
```

- Values are strings. A single pair of matching quotes (`"…"` or `'…'`) around a value is removed, so quote values that contain `#` or start with special characters.
- A job's `env` overrides the top-level `env` for the same key.
- [CI secrets](/docs/ci#secrets) override both.
- Keys starting with `OAK_CI_` are reserved and rejected.
- An `env:` block can't be empty; leave it out instead.

## jobs

A map of job names to job definitions. Each job accepts only these keys:

| Key | Required | Value |
|---|---|---|
| `steps` | yes | The commands to run, in order. At least one. |
| `env` | no | Variables for this job's steps (see [env](#env)). |
| `cache` | no | Paths to persist between runs (see [cache](#cache)). |
| `image` | no | Accepted, but **currently ignored** — every job runs in the [standard runner image](/docs/ci-runners). |

> **Jobs run one after another, in one container.** Today a workflow's jobs are flattened into a single ordered list of steps that run sequentially in the same container — not in parallel, and not isolated from each other. Their `env` maps are merged (later jobs win), and only the first job with a `cache:` block gets a cache. Until that changes, use **one job per workflow**, and put independent checks in separate workflow files.

## steps

```yaml
steps:
  - name: unit tests
    run: cargo test --lib
  - run: ./scripts/lint.sh
```

| Key | Required | Value |
|---|---|---|
| `run` | yes | One shell command line. |
| `name` | no | Label shown in the UI and `oak ci logs`. Defaults to `step N`. |

How steps execute:

- Each step runs in its **own shell process**, starting in `/workspace` (the repository root at the commit being tested). A `cd` or `export` in one step doesn't carry into the next — chain with `&&` within a step instead.
- Background processes you start (a database, say) **keep running** for the rest of the run.
- A non-zero exit fails the step and the run; later steps are marked `skipped`.
- `run` is **one line**. Block scalars (`run: |`) aren't supported. For anything longer, put a script in the repo and call it: `run: bash scripts/ci/deploy.sh`. `&&`, `||`, pipes and `;` work as usual within the line.

## cache

```yaml
cache:
  key: cargo          # optional; defaults to the job name
  paths:              # required; 1 to 16 entries
    - ~/.cargo/registry
    - node_modules
    - target
```

- **Paths** may be absolute, start with `~/`, or be relative to `/workspace`. They can't contain `"`, `$`, backticks, `\`, or control characters.
- **Restore** happens before the first step: the run's own branch's cache if there is one, otherwise `main`'s.
- **Save** happens after the last step, **only if every step succeeded**, and only to the run's own branch. The newest save replaces the old one.
- Change `key` to start a fresh cache (for example after a toolchain upgrade).
- A single cache archive can be up to 12 GiB, and a repo's caches together up to 10 GiB before the least recently used are evicted.
- Caching is best-effort: a failed restore or save is logged, never fatal.

## Environment variables in every step

| Variable | Value |
|---|---|
| `OAK_CI_RUN_ID` | The run's id, as shown by `oak ci runs`. |
| `OAK_CI_COMMIT` | The full hash of the commit being tested. |
| `CI` | Not set by Oak — set it in `env:` if your tools look for it. |

Plus your workflow and job `env`, and every [CI secret](/docs/ci#secrets). Steps run as `root`; `/root/.cargo/bin` and `/root/.bun/bin` are on `PATH`.

## Syntax rules

- Indent with **spaces**; tabs are rejected.
- `#` starts a comment at the beginning of a line or after whitespace, but not inside quotes.
- **Unknown keys are errors** at every level — a typo like `step:` is caught, not ignored.
- Lists are `- item` lines or inline `[a, b]`.

## Not supported (yet)

These GitHub Actions features have no equivalent today. The table says what to do instead.

| Feature | Instead |
|---|---|
| `uses:` / marketplace actions | Install tools in a step (`apt-get`, `curl`, `npm`, `cargo install`). |
| `needs:`, parallel jobs | Separate workflow files run as separate, parallel runs. |
| `if:` conditions | Check in your script: `[ "$X" = y ] \|\| exit 0`. |
| `matrix:` | One workflow file per variant. |
| `timeout-minutes:` | Runs are capped by your [plan's wall-clock limit](/docs/limits); use `timeout 600 cmd` within a step. |
| `services:` | Start the service in a step — PostgreSQL is pre-installed. |
| `working-directory:` | `cd subdir && cmd` within the step. |
| Artifacts | Upload from a step (`rsync`, `curl`, an object store CLI) with credentials in a secret. |
| Branch filters | Not available yet — every matching event runs, and runs aren't told their branch name. Split work that should only run after merging into a workflow with `on: [merge]`. |
| Multi-line `run: \|` | Put the commands in a script file in the repo. |
