Your first local workflow
From one pipeline file to a release that builds, drafts its own notes, and stops for a person before it publishes. About five minutes.
This page assumes you have finished
your first tasks.yml.
- Why a second file at all
- 1. Create the file
- 2. Run it
- 3. Add a stage, and see the barrier
- 4. Two jobs at once, and tasks in order
- 5. Park the run on a human
- 6. Let AI draft it, and keep the decision yours
- What you now have
Why a second file at all
A tasks.yml is a library of commands — you run one, and it pulls in
what it needs. A workflow is one pipeline, run as a whole, with an
order that is part of what it means.
Reach for a workflow when the order is the point: build, then draft the
notes, then wait for a human, then publish. Reach for tasks.yml when you
have a pile of commands people run individually.
Both run through the same task executor, so everything you learned about
run:, env:, vars:, cwd: and templating means the same thing here.
1. Create the file
Workflows live one folder deeper:
.local-workflows/workflows/. Create release.yml in it:
version: 1
name: Release
desc: Builds, then publishes.
stages:
- name: verify
jobs:
build:
name: Build
tasks:
- name: Compile
run: echo Compiling...
Three levels, and all three are mandatory: stages hold jobs, jobs hold tasks. Even for one command.
Two keys behave differently to tasks.yml:
version: |
required here, and must be 1 |
name: |
the row’s label in the tree — in a tasks.yml it is ignored at file level |
version: cannot be omitted because this format has no legacy to forgive.
tasks.yml forgives an absent version only because files written before
versions existed have none.
2. Run it
The file appears in the Local Workflows view beside your tasks. Click it to open — same rule as before, clicking never runs anything — then hit ▶.
There is no way to run one job. A workflow is addressed as a whole;
that is the difference from a tasks.yml, where every task has its own ▶.
If you want the parts individually runnable, they belong in a tasks.yml.
3. Add a stage, and see the barrier
Stages are sequential, with a hard barrier between them. Nothing in stage two starts until everything in stage one has finished:
stages:
- name: verify
jobs:
build:
name: Build
tasks:
- name: Compile
run: echo Compiling...
- name: release
jobs:
publish:
name: Publish
tasks:
- name: Publish the build
run: echo Publishing...
Run it. The panel draws the two stages in order, and publish sits
waiting until build is green.
4. Two jobs at once, and tasks in order
Inside a stage, jobs with no needs: between them run at the same
time — because the file says they do not depend on each other:
- name: verify
desc: Two checks that do not depend on each other.
jobs:
build:
name: Build
tasks:
- name: Compile
run: echo Compiling...
- name: Archive
run: echo Archiving...
lint:
name: Lint
tasks:
- name: Analyse sources
run: echo Linting...
build and lint start together. Inside build, Compile and
Archive do not: a job’s tasks are a list, and a list already carries
its order.
needs: on a task is a hard error, not a no-op. A list and a needs:
key disagreeing about order is worse than either one alone, so only jobs
take needs:.
To make a job wait on a sibling, give it needs: — one id, or a list of
them, exactly as in a tasks.yml.
5. Park the run on a human
trigger: manual stops the run and waits for a person:
publish:
name: Publish
tasks:
- name: Publish the build
trigger: manual
run: echo Publishing to ${{ vars.target }}...
Add vars: { target: production } at the top of the file and run it. The
run parks, and the panel offers Approve and Reject.
Two things worth knowing about that pause:
- What you are shown is the resolved command — already
echo Publishing to production..., not the template. Approving a template would be approving a promise rather than a command. - Nothing is executing while it waits. No process, no timer. Close VS Code, reopen it, and run Local Workflows: Show Runs Waiting for Approval — the run is still there.
Reject it and the tasks after the gate are reported as Skipped, not left looking like they are still to come.
6. Let AI draft it, and keep the decision yours
This is the shape the whole engine exists for: a model proposes, a human approves, a task executes.
This step needs a GitHub Copilot sign-in. Everything above works without one — if you have not got Copilot, you already have a working workflow and can stop here.
Declare what runs the AI once, at the top of the file, then the whole file:
version: 1
name: Release
desc: Builds, drafts the notes, then waits for a person before publishing.
vars:
target: production
plugins:
ai:
uses: ai@1
args:
provider: ghcp
stages:
- name: verify
desc: Two checks that do not depend on each other.
jobs:
build:
name: Build
tasks:
- name: Compile
run: echo Compiling...
- name: Archive
run: echo Archiving...
lint:
name: Lint
tasks:
- name: Analyse sources
run: echo Linting...
- name: release
desc: AI drafts, a human approves, a task publishes.
jobs:
notes:
name: Release notes
tasks:
- name: Draft the notes
uses: ai
args:
prompt: Draft release notes from the commits on this branch.
output: NOTES
publish:
name: Publish
needs: notes
tasks:
- name: Approve the release
trigger: manual
run: echo Publishing to ${{ vars.target }} - "${{ run.NOTES.summary }}"
- name: Announce
run: echo Announced
Run it, and read the gate. It shows you the sentence the model actually wrote, because the run variable resolved before anybody was asked.
Three lines in there are the entire argument:
uses: aiis a name you declared. Swapping the vendor is one word at the top of the file; no task mentions a vendor or a model.output: NOTESstores what it drafted as a value. Not a decision — a value.trigger: manualis on the task that acts, and it is there because you put it there.ai@1enforces no gate of its own — it is an agent, and it can write to the repository. The publishing command is an ordinaryrun:you wrote and someone reviewed; all the model did was fill in${{ run.NOTES.summary }}.
The drafting task cannot act and the acting task cannot draft. That split is not a limitation waiting to be lifted — it is the reason a person is asked at all.
What you now have
Two formats, and a sense of which is which:
tasks.yml |
workflows/*.yml |
|
|---|---|---|
| Is | a library of commands | one pipeline |
| Ordering | needs: between tasks |
stages:, then needs: between jobs |
| Run granularity | any task, alone | the whole file |
version: |
optional | required |
Next: your first spec — the same engine, running requirements, design, plan and implementation as a process whose prompts are files in your repository.
Samples/HelloWorld/.local-workflows/workflows/release-pipeline.yml
exercises every shape the format can express, each case numbered and
commented. approve-a-deploy.yml beside it is the gate on its own.