Your first tasks.yml

From an empty repository to a four-task dependency graph, running locally. About five minutes.

  1. 1. Create the file
  2. 2. Run it
  3. 3. Add a second task, and an order
  4. 4. Branch it
  5. 5. Know which shell you are in
  6. 6. Add values
  7. 7. Break it on purpose
  8. 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.


Back to top

Local Workflows is a VS Code extension. Everything it does is declared in a YAML file you own.