Workflows
The second file format. A tasks.yml is a library of commands, each
independently runnable; a workflow is one pipeline, run as a whole,
with an order that is part of what it means.
Keys and defaults only: the cheat sheet.
- Which one you want
- Three levels
- What a workflow file can declare
- Two things that are deliberately missing
Which one you want
tasks.yml |
workflows/*.yml |
|
|---|---|---|
| Is | a library of commands | one pipeline |
| Lives at | .local-workflows/tasks.yml |
.local-workflows/workflows/*.yml |
| Ordering | needs: between tasks |
stages:, then needs: between jobs |
| Run granularity | any task, alone | the whole file |
version: |
optional | required |
name: |
ignored | the row’s label |
Reach for a workflow when the order is the point — a release that
drafts notes, waits for a human, then publishes. Reach for tasks.yml
when you have a pile of commands people run individually and some of them
happen to depend on others.
Both run through the same task executor, so env:, vars:, dotenv:,
cwd:, templating, trigger:, timeout:, retries:, if:,
continueOnError: and output: mean the same thing in each.
Three levels
stages: ordered, with a barrier between each
jobs: a DAG within the stage, ordered by needs:
tasks: a list, run in list order
Stages are sequential. No job in stage N+1 starts until every job in stage N has finished.
Jobs within a stage run together, unless one needs: another. Two
jobs with no relationship between them run at the same time — because the
file says they do not depend on each other, and a run that quietly
serialised them would be making the declaration a lie.
Tasks are a list, in order. This is the one place the two formats
genuinely differ: needs: on a task is a hard error, because a list
already carries the order and two mechanisms disagreeing about it is
worse than either alone.
Everything else about a task is identical to a tasks.yml task — a bare
string is still the run: shorthand, run: still works, and the same
closed set of keys applies. Both formats share one task parser; where a
task sits changes only how it is addressed — by map key in a
tasks.yml, by position in a job.
What a workflow file can declare
At the top level:
| Key | |
|---|---|
version: |
required, and must be 1 |
name: |
the row’s label in the tree |
desc: |
shown beside it |
params: |
values the run asks a human for before it starts |
env:, vars:, dotenv: |
the same three blocks, at file level |
plugins: |
file-local plugin aliases |
ai: |
sugar for the plugins: entry named ai |
stages: |
the pipeline |
A stage may declare only name, desc, if, artifact, and jobs.
A job may declare name, desc, needs, env, vars, dotenv,
cwd, and tasks.
Both key sets are closed, and a misspelling is a hard error rather
than a key nobody reads. That is the whole reason they are closed: a
mistyped if: on a stage would otherwise be a phase that quietly always
runs.
Two things that are deliberately missing
There is no job-level outputs: block. Data crosses job and stage
boundaries through run variables — output: on the task that produces
it, ${{ run.NAME.key }} where it is consumed —
which are already run-scoped. A second mechanism would only be a
narrower version of the one that exists.
version: cannot be omitted. A brand-new format with no legacy can
afford to be strict, and that strictness is what buys painless evolution
later. tasks.yml forgives an absent version only because every file
written before versions existed has none.
Next: writing a new workflow.