Your first tasks.yml
From an empty repository to a four-task dependency graph, running locally. About five minutes.
- 1. Create the file
- 2. Run it
- 3. Add a second task, and an order
- 4. Branch it
- 5. Know which shell you are in
- 6. Add values
- 7. Break it on purpose
- What you now have
1. Create the file
Local Workflows reads one folder: .local-workflows/, next to your code.
Create a file in it called tasks.yml:
version: 1
tasks:
hello: echo Hello World
That is a complete, valid pipeline. tasks: is a map keyed by task id,
and a bare string is shorthand for run: — so hello is a task that
runs one command.
You can skip the typing: open the Local Workflows view with no workflow files present and its empty state offers to create exactly this, as does the Local Workflows: Initialize tasks.yml command.
version: 1 is optional in a tasks.yml and worth writing anyway. It is
what lets a future build migrate your file instead of guessing at its
shape.
2. Run it
Click the Local Workflows icon in the Activity Bar. Your file appears, expandable into its tasks.
Click the hello row. Nothing runs. Clicking only ever opens —
the panel shows you what would run, drawn as a graph, before you commit
to it. Starting a run is always the explicit ▶ icon on the row, or the
Run button in the panel.
Hit ▶. The run panel streams the output, one row per command, with the task turning green when it exits 0.
A task can be a database reset or a destructive cleanup, so an accidental click must never start one. That is why clicking opens and only ▶ runs. The Tasks row above your tasks has no ▶ at all — there is no “run the whole file”, by design.
3. Add a second task, and an order
One task is not a pipeline. Add a second, and declare that it depends on the first:
version: 1
tasks:
setup: echo Restoring dependencies...
build:
name: Build
desc: Compiles the app
needs: setup
run: echo Compiling...
needs: is the whole ordering model. It takes one id or a list of them.
Now run build on its own. setup runs first — running any task
pulls in its full transitive needs: chain, and only that subset. Tasks
are composable and independently runnable; there is no separate
“job” concept to learn, and nothing in the file runs on its own until
you ask for it.
name: is the label in the tree, desc: is its tooltip. Both optional;
without name:, the id is shown.
4. Branch it
Dependencies form a graph, not a chain. Two tasks that both needs:
setup are declaring they do not depend on each other — so they run at
the same time:
version: 1
tasks:
setup: echo Restoring dependencies...
build:
name: Build
needs: setup
run: echo Compiling...
lint:
name: Lint
needs: setup
run: echo Linting...
test:
name: Test
needs: [build, lint]
run: echo Running tests...
Run test. The panel draws the diamond — setup, then build and
lint side by side, then test — and colours each node as it finishes.
Click any node to open its log in the side panel.
A run that quietly serialised build and lint would be making the
file’s declaration a lie, so it does not.
5. Know which shell you are in
A task with no shell: runs through whatever the OS already provides —
cmd on Windows, bash elsewhere. Never pwsh by default.
So the moment you write PowerShell syntax — $env:NAME,
Get-Location, a .ps1 invocation — say so:
build:
name: Build
needs: setup
shell: pwsh
run: echo "Compiling in $env:BUILD_CONFIGURATION configuration..."
This is the single most common first-run failure: a $env: reference
that silently comes back empty because the task ran in cmd.
6. Add values
Two blocks, and they are deliberately not one:
vars:— values to substitute into this file. Never exported to anything you spawn. Typed: a number stays a number.env:— the environment a process is spawned with. Exported. Strings only.
Put a value in vars: when you want to use it, and in env: only when
something you run needs to read it. Both can be set at the file level
and overridden per task.
Substitution is ${{ }}, and three directory anchors are always
available: ${{ workspace }} (where the editor opened),
${{ root }} (the project the file belongs to — the folder holding
.local-workflows), and ${{ cwd }} (where this command actually runs).
version: 1
vars:
platform: linux-x64
artifact: myapp-${{ vars.platform }} # a var may read a sibling var
env:
BUILD_CONFIGURATION: Release
ARTIFACT_NAME: ${{ vars.artifact }} # env: may read vars
tasks:
setup: echo Restoring dependencies...
build:
name: Build
needs: setup
shell: pwsh
env:
BUILD_CONFIGURATION: Debug # overrides the file-level value
run: echo "Compiling $env:ARTIFACT_NAME in $env:BUILD_CONFIGURATION..."
package:
name: Package
needs: build
shell: pwsh
cwd: ${{ root }}/dist
run: echo "Packaging from $(Get-Location)..."
Resolution runs one way only, so nothing can chase its own tail: anchors,
then vars:, then env:, then cwd: and run:. vars: cannot read
env: — vars are the file’s own constants, env is machine-dependent, and
letting constants depend on the environment inverts which of the two is
stable.
Because ${{ root }} is the project folder rather than the folder the
file sits in, ${{ root }}/scripts works from tasks.yml and from
workflows/deploy.yml alike. No ../.
7. Break it on purpose
Add a task that fails, and run it:
error-demo:
name: Error Demo
shell: pwsh
run: |
echo 'About to fail on purpose...'
exit 1
The node turns red, the error surfaces in the panel, and anything that
needs: it is marked Skipped rather than run. A failed prerequisite
stops its dependents; it does not stop unrelated branches.
Try a typo, too — nees: setup. It fails before anything executes,
naming the unknown key. The same is true of an unresolvable
${{ }}: it is caught by pre-run validation, not
discovered halfway through a build.
What you now have
That is the whole loop: edit tasks.yml → click a task → hit ▶ →
watch it run locally, instantly.
What you have built is a library of commands, each runnable on its own. Next comes the other shape: one pipeline, run as a whole, where the order is part of what it means.
Next: your first local workflow.
The repository’s Samples/HelloWorld/.local-workflows/tasks.yml
exercises every feature in one file, with a comment on each explaining
what it is there to demonstrate. It is the fastest way to see the rest.