Skip to content
| Marketplace
Sign in
Visual Studio Code>Other>Local ActionsNew to Visual Studio Code? Get it now.
Local Actions

Local Actions

Santosh Jallapuram

|
2 installs
| (0) | Free
Define and run local dev automation pipelines with a Taskfile-style YAML.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

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).

Run panel


⚠️ 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 tree 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:

  1. 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.
  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

One Intentional Difference From CI Tools

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.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
© 2026 Microsoft