Local Actions
Define your project's dev pipeline once, in a local.yml next to your
code, and run any part of it from a native VS Code sidebar — with a
live, GitHub Actions-style run panel and dependency graph, no CI, no
Docker, no leaving the editor.
Placeholder screenshots below (resources/screenshots/*.png, rendered
from the .svg sources also in that folder) sketch the actual UI layout
and colors from the real theme, but aren't captures of the running
extension - swap them for real screenshots/GIFs whenever it's
convenient (same filenames, just replace the files - the Marketplace
doesn't allow SVG images in a README, so keep any replacements as PNG
or GIF).

⚠️ Caution
run: executes arbitrary shell commands, with no sandboxing - same
risk class as any task runner (npm run, make, a Taskfile.yml). A
local.yml travels with a repo, so cloning an untrusted repo and
clicking a task is a real execution vector. Treat local.yml the same
way you'd treat any other executable script you didn't write.
- This is a personal weekend project, still young. It has unit test
coverage over its core business logic (parsing, the execution graph,
templating, validation - see Testing), but the VS Code
integration itself (tree view, run panel, webview) is still verified
manually, not automated. Expect rough edges; see
Known Rough Edges for the ones already found.
- Not intended for a mixed-OS team - see Non Goals.
Why this exists
I built this on a weekend, for one very specific itch: every project I
work on ends up with the same 5–10 commands I run constantly - reset a
local DB, build a client app, start a dev server, kill stray processes,
restart the local test runner - and none of the existing options felt
right for that specific problem:
- Plain
npm run scripts / a Makefile have no dependency graph and
no visual feedback - you're staring at raw terminal scrollback, and
ordering ("build the UI, then start the API") is something you have
to remember to do by hand every time.
- Taskfile.dev actually gets the core model right - one
task
concept, deps:, - but it's CLI-only. You still leave your editor,
open a terminal, and remember (or task --list) what to type.
- VS Code's own
tasks.json technically has dependency ordering
(dependsOn), but the authoring experience is clunky, there's no real
visual run panel, and no dependency graph - you're reading interleaved
raw terminal output for however many tasks fired.
- A real GitHub Actions workflow, run "in your head" - which is what
a lot of teams actually do: manually chain the same commands a CI
workflow would run, because pushing to a branch just to see if
build+test+lint passes is a slow, wasteful feedback loop. GitHub
Actions'
jobs/steps/needs shape is genuinely good for describing
this kind of pipeline - the problem is it's bound to GitHub's runners,
not something you can point at your own machine.
act runs real GitHub Actions
workflows locally in Docker - excellent if CI parity is the actual
goal, but that's a different, heavier problem than "let me run my own
build/test/lint chain locally with a nice UI." Requiring Docker and
emulating GitHub's runner semantics is pure overhead for that.
So: reuse the parts of GitHub Actions' authoring model that are genuinely
good (a pipeline shape most developers already half-know), borrow
Taskfile.dev's one-concept simplicity (a task, not a job+step+
stage hierarchy), and pair both with the thing neither has - a native,
visual, in-editor run experience. That's local.yml.
If none of that friction sounds familiar - if npm run build && npm run test already covers your day - you probably don't need this, and that's
fine. See Comparison with Taskfile.dev
for an honest breakdown of where the established tools are still ahead.
What it looks like
| Sidebar tree (workspace + global tasks) |
Dependency graph |
 |
 |
- Sidebar — just two headings: Global, for tasks from your user
profile (
git pull, clean commands, whatever isn't project-specific),
and Local, for your workspace's own local.yml. With the common
case of one local.yml per workspace, Local is that file's row -
runnable directly, no extra nesting - and its tasks sit right under it.
(A workspace with more than one local.yml gets one row per file
underneath Local instead, labeled by folder.) Tasks are always
listed A-Z, regardless of their order in local.yml. Every task is
independently runnable, whether or not anything else depends on it. A
workflow file's own name: isn't shown anywhere; an optional desc:
on a task shows up as that task's tooltip - see
Schema Reference.
- Run panel — one shared tab, GitHub Actions-styled: a task list on
the left (live status icons), collapsible per-command logs on the
right, Run/Stop/Restart, and Log/Graph/Help view toggles.
- Dependency graph — the same run panel, rendered as a DAG instead of
a log: tasks layered by dependency depth, colored by live status.
Tasks in the same row have no dependency on each other - visually, at
a glance, which parts of your pipeline are independent.
Features
- One
task concept - composable via deps: and independently
runnable from the tree, no separate "job" vs "reusable step" split to
learn (see Philosophy).
- Live, collapsible, GitHub Actions-style output, streamed per
command as it runs.
- Dependency graph view, colored by live status, right in the run
panel.
- Long-running processes survive closing the run panel tab -
dotnet watch, npm run dev, vite, docker compose up, etc. are never
silently killed just because you closed that editor tab, the way a CI
runner would kill them at the end of a job (see
One Intentional Difference).
They are stopped when you close the VS Code window itself, or click
Stop explicitly - "survives a closed tab" isn't "survives forever."
- Env var layering (workflow → task → cmd) plus
${{ root }} /
${{ env.NAME }} templating - root is the workflow file's own
directory, always available for free.
- Per-command shell selection (
pwsh default, cmd opt-in) - not a
global "everyone's on bash" assumption.
- Clickable links in run logs, opened externally.
- A shared, single run tab, reused across every workflow/task you
open - not a new editor tab per task.
- Pre-run validation and clear, immediate errors on an unresolvable
${{ }} template - instead of a silent no-op or a delayed, cryptic
failure (see Validation).
- A "Help" tab in the run panel with a quick
local.yml schema
reference, so you don't have to leave the editor to look it up.
- Global Actions tier (
~/.local-actions/global.yml) for commands
that aren't tied to any one repo.
- An empty-state prompt when a workspace has no
local.yml yet - one
click creates a starter file rather than staring at a blank sidebar.
- Auto-reload on
local.yml changes.
- Self-healing on Windows quirks it actually hit during development -
e.g. a stale
PATH missing pwsh's Windows-Store alias folder, or
PowerShell not propagating a failing native command's exit code by
default (both fixed transparently, see
Known Rough Edges).
Using the Extension
Not on the Marketplace yet (see Publishing), so for now:
Install it. Either:
- Ask whoever built it for the
.vsix file, then in VS Code:
Extensions view → ... menu → Install from VSIX..., or from a
terminal: code --install-extension local-actions-x.y.z.vsix.
- Or build your own - see Developing Local Actions
below.
Write a local.yml at the root of whichever project you want to
automate (see Schema Reference for the full
shape):
tasks:
hello:
cmds:
- echo Hello World
Or use step 1's empty-state prompt / the Local Actions: Initialize
local.yml command, which creates exactly this for you.
Open the sidebar. Click the Local Actions icon in the Activity
Bar - every local.yml in your open workspace shows up there, each
expandable into its tasks. If you also have
~/.local-actions/global.yml (see Global Actions),
its tasks appear under a Global group above your workspace's own.
Click a task row to run it directly. Running one task also runs
whatever it transitively deps: on first (same as task build on
the Taskfile.dev CLI) - just that subset, not the whole file. Want to
look without running? Use the row's inline hover 👁 icon instead.
Clicking the "Local" row (or a workflow row, if you have more than
one local.yml) only opens the panel - it never runs anything.
Running a whole file executes every task in it, which can include
commands you only want to trigger deliberately (a DB reset, a
destructive cleanup) - that always requires the explicit inline ▶
icon or the panel's own Run button, never an accidental click.
Read the output in the run panel: a task list on the left, live
collapsible per-command logs on the right. Switch to the Graph
tab to see the same run as a dependency DAG instead, or Help for
a quick local.yml schema reference. Stop and Restart work from
either the tree's inline icons or the panel's buttons.
Everything reuses one shared tab - clicking a different
workflow/task re-points that same tab rather than opening a new one,
and a running task keeps going in the background if you close the tab
(not if you close VS Code - see the Caution section above).
That's the whole loop: edit local.yml → click a task → hit Run →
watch it execute locally, instantly - no commit, no push, no waiting
on CI.
Schema Reference
A top-level name: is accepted but never shown anywhere - there are only
ever two scopes, Global and Local (see What it looks like),
so a per-file display name is redundant. Leave it out.
Tasks and Cmds
env:
BUILD_CONFIGURATION: Release
tasks:
setup:
cmds:
- echo Checking out...
- echo Restoring dependencies...
build:
desc: Builds the client app # optional - shown as this task's tooltip in the tree, nowhere else
deps: [setup] # single string or array - other tasks that must run first
env:
BUILD_CONFIGURATION: Debug # overrides the workflow-level value for every cmd in this task
cmds:
- echo Compiling... # plain string - shorthand for { run: <string> }
- name: Package artifacts # object form - for per-cmd overrides
cwd: ${{ root }}/dist
run: echo "Packaging from $(Get-Location)..."
lint:
deps: setup
cmds:
- echo Linting...
- name: Windows-only check
shell: cmd
run: echo %PATH%
test:
deps: [build, lint]
cmds:
- echo Running tests...
Every task above is both composable (test depends on build+lint)
and independently runnable from the tree - clicking build alone runs
its full transitive deps: chain first (setup → build), same as
task build would on the Taskfile.dev CLI.
Environment Variables
env: can be set at the workflow, task, or cmd level (cmd-level only via
the object form of a cmds: entry). Each level merges on top of (and can
override) the one above it, and the process's own environment (PATH,
etc.) is always inherited underneath all of them.
Working Directory & Templating
By default every cmd runs in the workflow file's own directory (the
"root"). cwd: can be set at the task level (every cmd in that task) or
the cmd level (overrides the task), always resolved relative to the root.
cwd: (and run:/env:) can reference the workflow root and other env
vars via ${{ }} - the only way to parameterize cwd: at all, since
unlike run: it's never handed to a shell, so native $env:NAME syntax
doesn't work there:
env:
SCRIPTS_DIR: ${{ root }}/scripts
tasks:
reset:
cmds:
- name: Reset DB
cwd: ${{ env.SCRIPTS_DIR }}
run: ./db-reset.ps1
Cmd Shells
tasks:
demo:
cmds:
- Get-ChildItem # plain string - runs in pwsh (the default)
- name: Windows-only cmd
shell: cmd
run: echo %PATH%
shell defaults to pwsh (PowerShell 7+), which is genuinely
cross-platform - local.yml runs the same way on Windows, macOS, and
Linux as long as pwsh is installed. shell: cmd only makes sense on
Windows (cmd.exe doesn't exist elsewhere); using it on macOS/Linux is
flagged as a validation error before it ever runs (see
Validation) rather than failing with a bare ENOENT.
This is about the extension running correctly on whichever single OS
you're on - not about mixing OSes within one team's local.yml, which
stays out of scope (see Non Goals).
uses: (placeholder only)
uses: is parsed but never executed - logged as a skipped placeholder.
There's no marketplace action support and none is planned; write the
equivalent as a run: cmd instead.
Validation
Before a workflow runs, WorkflowValidator checks it for common
mistakes and logs each as [warning]/[error] in the run panel -
currently: a workflow with no tasks, a task with neither cmds: nor
deps:, a cmd with neither run: nor uses:, and shell: cmd on a
non-Windows machine. A typo'd deps: entry is handled separately, by
the execution graph itself - the bad reference is dropped and reported
(No task found with name 'X' - ignoring this dependency.) rather than
crashing the run.
These are currently informational, not a hard gate - most of them
(missing run:, shell: cmd on the wrong OS) already fail cleanly at
the point they'd actually run, via the same runtime guards. The point is
surfacing them clearly and immediately rather than as a delayed, cryptic
failure a few cmds later.
Any ${{ }} expression that can't be resolved - an unknown env var, or
an unrecognized built-in - throws immediately with the exact expression
and surrounding text, instead of silently becoming a literal string that
only fails later as a confusing ENOENT from whatever tried to use it
as a path.
Global Actions
Some commands aren't really project-specific - git pull, a repo clean
command, whatever you run the same way across every repo you touch.
~/.local-actions/global.yml is a normal workflow file (same schema,
usually just tasks: with no cross-task deps:) read from your user
profile rather than a workspace. Its tasks show up under a Global
group at the top of the tree, in every workspace.
# ~/.local-actions/global.yml
tasks:
git-pull:
cmds:
- git pull --rebase
git-clean:
cmds:
- git clean -fdx
Philosophy
One Task, Not Three Concepts
Earlier drafts of local.yml copied GitHub Actions' jobs → steps
shape directly, then had to bolt on a third concept ("stages") just to
get reusability and standalone-run - because a job couldn't be nested
inside another job, and a step had no identity of its own to reference.
That was a sign the two-level model was missing a concept, not that it
needed a third one added.
A task doesn't have that gap: it composes (deps:), it runs standalone,
and it holds a flat cmds: list - one concept, no branching decision
about which kind of thing you're declaring. This mirrors Taskfile.dev's
own model, which has had one task concept from the start.
Familiar Enough, Not Compatible
env:/cwd: layering, run:/shell: per command, and a live
GitHub-Actions-style run panel are kept because they're genuinely good
ideas - not because of any compatibility goal with GHA or Taskfile.dev.
local.yml is its own schema that happens to borrow good ideas from both.
GitHub Actions (and most CI task runners) can terminate long-running
processes because CI runs are expected to finish. Local Actions will
not just because you closed a tab - dotnet watch, npm run dev,
vite, docker compose up, and similar keep running in the background
when you close the run panel tab; reopening the task's panel reattaches
to it, logs and all (see Architecture). They're stopped
only by an explicit action: clicking Stop, or closing the VS Code window
itself (which shuts the extension down and stops everything it started -
see the Caution section above). "Survives a closed tab" is the whole
guarantee; it isn't a persistent background daemon that outlives VS Code.
Non Goals
Local Actions intentionally does not try to be a local GitHub Actions
runner, or GHA-syntax compatible:
- Marketplace actions (
uses: actions/checkout@v4, setup-node, etc.)
- Matrix builds
- Services / containers / Docker
- Composite actions / reusable workflows
- Artifacts / caching
- GitHub Secrets API / GitHub context (
github.*, secrets.*)
- Per-task
runs-on / multi-OS runner selection - every team member
using a given local.yml is assumed to run the same OS, so there's no
per-task OS matrix (see Cmd Shells instead). The
extension itself still runs correctly on Windows, macOS, or Linux -
that's a different thing from supporting a mixed-OS team.
If your goal is genuinely running real GitHub Actions workflows locally
for CI parity testing, use act instead
- it's a mature, Docker-based tool built specifically for that.
Known Rough Edges
- No
runs-on - removed from the schema entirely rather than left in
as a do-nothing field (see Non Goals).
uses: is a placeholder - parsed, never executed.
- Windows-specific quirks already hit and fixed: a long-running app
like VS Code can have a stale
PATH missing pwsh's Windows-Store
alias folder - self-healed by appending it defensively before every
spawn. PowerShell also doesn't propagate a failing native command's
exit code to its own process exit code by default (pwsh -Command "yarn build" would exit 0 even if yarn build failed) - fixed by
appending exit-code propagation to every pwsh-invoked command.
- An unresolved
${{ }} template used to fail silently - it stayed
as a literal string that then got used as a real (nonexistent) path,
surfacing only later as a bare ENOENT. Now throws immediately with
the exact expression and surrounding text (see
Validation).
- A typo'd
deps: entry used to crash the whole run - WorkflowExecutor
threw synchronously and uncaught, leaving the run stuck "running"
forever with no way to stop it short of closing VS Code. The bad
reference is now dropped and reported instead.
- Editing
local.yml used to require restarting VS Code to take effect
in an already-opened run panel - a RunSession cached its parsed
workflow the first time it was opened and never reloaded it, no matter
how many times the file changed on disk afterward (the tree itself
refreshed fine via WorkflowWatcher - only the run panel's own copy
was stale). Now reloaded every time a session is opened while idle.
- No parallel execution yet - independent tasks (visible as same-row
siblings in the Graph view) still execute in strict topological order,
one at a time. See Planned Features.
- Test coverage stops at
core/ - the parser, execution graph,
templating, and validation logic are unit tested (see
Testing); the VS Code integration (tree view, run panel,
webview) is not, and is where most bugs have actually been found so
far.
Architecture
local.yml
│
▼
Workflow Loader
│
▼
Workflow Parser
│
▼
Workflow Model
│
▼
Execution Graph (topological order + cycle detection)
│
▼
Workflow Executor
│
▼
Task Executor
│
▼
Cmd Executor
│
▼
Process Runner (spawns pwsh or cmd per cmd's `shell:`)
On the VS Code side:
Activity Bar
│
▼
Workflow Tree View ──(click / ▶ / ■)──▶ Run Panel (single shared editor tab)
│ │
▼ ▼
WorkflowWatcher (**/local.yml) RunSession (executor + logs) ──▶ live log/status stream
- The sidebar is a native
TreeView: one row per discovered local.yml
workflow, expandable into every task it declares. A top-level
Global group lists tasks from ~/.local-actions/global.yml.
- There is exactly one run panel (a webview editor tab), reused and
re-pointed at whichever workflow/task you last opened - not one tab
per task.
- Each workflow/task run is a
RunSession: the executor, its running
flag, and everything logged so far - kept alive independent of the
view. Closing the run panel tab only detaches the view; it never stops
the session. Sessions are only stopped when the extension itself
deactivates (VS Code closing).
- A
WorkflowWatcher watches every local.yml in the workspace and
auto-refreshes the tree on change.
Developing Local Actions
This section is for working on the extension itself - if you just want
to use it, see Using the Extension above.
Project Structure
src/
core/
execution/ # ExecutionGraph, ExecutionGraphBuilder, ExecutionNode
loader/ # WorkflowLoader (reads local.yml)
models/ # Workflow/Task/Cmd/Shell/TaskStatus + Messages
parser/ # WorkflowParser, TaskParser, CmdParser
runner/ # ProcessRunner, CmdExecutor, TaskExecutor, WorkflowExecutor, TemplateResolver
validation/ # WorkflowValidator
vscode/
dashboard/ # WorkflowTreeProvider (sidebar), RunPanel (shared editor tab + RunSession)
workspace/ # WorkspaceService, WorkflowWatcher
webview/ # RunView.tsx (React) + DependencyGraph.tsx + Help.tsx + shared theme.css
extension.ts
See Architecture above for how these fit together.
Clone & Run
git clone <this repo>
cd local-actions
yarn install
Press F5 in VS Code to launch an Extension Development Host - this
repo's own .vscode/launch.json opens it against Samples/HelloWorld,
which doubles as a feature showcase / manual test suite covering every
schema feature (env layering, deps:, ${{ }} templating, shell:,
real .ps1 script invocation, intentional failure cases, etc.).
yarn watch runs esbuild in watch mode; reload the Extension
Development Host window (Ctrl+R inside it, or stop/restart the debug
session) to pick up changes - contribution points in package.json
(commands, views, menus) only get re-read on a full restart, not a
simple watch rebuild.
Testing
yarn test # runs the suite once (vitest run)
yarn test:watch # watch mode
Unit tests (vitest) cover the pure core/ business logic - the layer
that's actually been the source of every real bug found so far
(WorkflowParser/TaskParser/CmdParser, ExecutionGraphBuilder,
TemplateResolver, WorkflowValidator). Test files live next to the
code they cover, under __tests__/.
Not covered: anything importing vscode (RunPanel, WorkflowTreeProvider,
WorkspaceService) or the webview (RunView.tsx, DependencyGraph.tsx) -
those still rely on manually running the extension via F5 and clicking
through Samples/HelloWorld. See Known Rough Edges.
Building & Packaging
yarn build # bundles extension.js + the webview via esbuild
npx @vscode/vsce package --no-dependencies # produces a local .vsix
The .vsix can be installed into any VS Code the same way described in
Using the Extension - useful for trying a build
outside the Extension Development Host, or handing it to someone else
without publishing anywhere. .vscodeignore controls what actually goes
into it (source, tests, and dev tooling are excluded; only dist/,
resources/, and the manifest/docs ship).
Publishing
Not currently published to the VS Code Marketplace (see
Comparison with Taskfile.dev and the
the Caution section for why that's a deliberate pause, not an
oversight). The packaging mechanics are in place, though -
license/icon/repository are set in package.json, and
vsce package already produces a clean .vsix with no warnings. If/when
publishing for real: register a publisher at
marketplace.visualstudio.com/manage,
generate a Personal Access Token, then vsce publish (or vsce login <publisher> first). Bump version in package.json first - it doesn't
happen automatically.
Planned Features
- [x] Pre-run validation + clear template resolution errors (see
Validation)
- [x] Unit tests for the core parsing/execution/templating logic (see
Testing)
- [ ] Parallel tasks (run independent tasks concurrently, not just in
topological order - visually previewable today in the Graph view)
- [ ]
if / timeout-minutes / continue-on-error
- [ ] Persisted run history across window reloads
- [ ] Sub-folder
package.json script discovery/autocomplete when
authoring local.yml (the mechanism - cwd: + run: yarn <script> - already works today)
- [ ] Edit a workflow from the tree via a pencil icon
- [ ] Automated coverage for the VS Code integration (tree view, run
panel, webview) - currently manual only, see
Known Rough Edges
Explicitly not planned: marketplace actions, matrix strategy,
containers/services, composite actions, reusable workflows,
artifacts/cache, GitHub context - see Non Goals.
Comparison with Taskfile.dev
Taskfile.dev is the closest sibling to this
project - both are local task runners built around one task concept
with deps:.
|
Taskfile.dev |
Local Actions |
| Format |
Its own Taskfile.yml schema |
Its own local.yml schema - borrows ideas from GHA and Taskfile.dev, compatible with neither |
| Dependency model |
Task deps: (parallel by default) |
Task deps: graph, topological order (sequential today - see Planned Features) |
| Env vars |
env: + dotenv: file support, global/task-level |
env: at workflow/task/cmd level; no .env file loading yet |
| Working directory |
dir: per task |
cwd: per task/cmd, resolved against the workflow root |
| Variables/templating |
Rich - vars, templating ({{.VAR}}), OS-specific commands, for: loops |
${{ root }} / ${{ env.NAME }} only - no loops/conditionals yet |
| Shell selection |
Global set/shopt, mostly assumes POSIX sh |
Explicit per-cmd shell: pwsh \| cmd, validated against the current OS |
| UI |
CLI only |
Native VS Code sidebar + GitHub Actions-style run panel + dependency graph view |
| Cross-platform |
First-class, designed for mixed-OS teams |
The extension runs correctly on Windows/macOS/Linux, but assumes one local.yml is authored for a single-OS team - no per-task OS matrix (see Non Goals) |
Where Taskfile.dev is ahead: richer variables/templating, .env
file support, cross-platform portability, and years of real-world
maturity.
Where Local Actions differentiates: the VS Code-native visual run
experience - sidebar, live collapsible logs, and a dependency graph you
never get on a CLI.
Honest take: if you don't need the graph visualization or the
in-editor run panel, Taskfile.dev (or just, or plain npm scripts)
will serve a pure command-runner need with less code and more maturity.
Local Actions earns its keep specifically through the VS Code
integration - if that isn't valuable to your workflow, there's less
reason to choose it over an established tool.
History
local.yml originally copied GitHub Actions' jobs: → steps: shape
directly (with a stages: block bolted on afterward for reusable/
standalone step groups). That model was replaced with the current flat
tasks:/cmds: shape - see Philosophy
for why. There's no back-compat shim for the old shape.