VS Code Tasks

VS Code Tasks turn repeated shell commands into one-click operations with problem matchers that pipe errors directly into the Problems tab. Here's how to set them up.

March 30, 20266 min read1 / 2

Every project has commands you run over and over: start the dev server, run the compiler, run the linter, run tests. Tasks automate those and bring their output into VS Code — so instead of reading error text in a terminal, you click a file path and jump straight to the problem.

The Anatomy of a Task

Tasks live in .vscode/tasks.json. Open the Command Palette → "Tasks: Configure Task" → "Create tasks.json from template" to get started.

A minimal task looks like this:

JSON
{ "version": "2.0.0", "tasks": [ { "label": "Start Dev Server", "type": "shell", "command": "npm run dev", "group": "build", "isBackground": true, "problemMatcher": [] } ] }

Key fields:

FieldWhat it does
labelThe name shown in the task picker
typeshell for terminal commands (most common)
commandThe actual shell command to run
groupbuild or test — affects the default keybinding
isBackgroundtrue for long-running processes like dev servers
problemMatcherHow to parse output into Problems tab entries

Running Tasks

Plain text
Cmd/Ctrl + Shift + P → "Tasks: Run Task" → pick from the list Cmd/Ctrl + Shift + B → runs the default build task Cmd/Ctrl + Shift + T → runs the default test task

Or open the terminal menu → "Run Task."

You can set a task as the default build task by adding "group": { "kind": "build", "isDefault": true }.

Problem Matchers: Turning Output into Clickable Errors

A problem matcher is a pattern that tells VS Code how to read error output. When it matches a line, VS Code creates an entry in the Problems tab — and you can click it to jump to that exact file and line.

You almost never need to write one. VS Code ships with matchers for the most common tools:

JSON
{ "problemMatcher": "$tsc" // TypeScript compiler } { "problemMatcher": "$eslint-stylish" // ESLint } { "problemMatcher": "$jshint" // JSHint } { "problemMatcher": ["$tsc", "$eslint-stylish"] // Multiple matchers }

If you're using a popular build tool or linter, there's almost certainly a pre-built matcher for it. Use one of those. Writing a custom matcher (parsing regex for file, line, column, severity, message) is tedious and rarely necessary.

What a custom matcher looks like (just so you know what you're not doing):

JSON
{ "problemMatcher": { "owner": "myTool", "fileLocation": "relative", "pattern": { "regexp": "^(.+):(\\d+):(\\d+):\\s+(warning|error):\\s+(.+)$", "file": 1, "line": 2, "column": 3, "severity": 4, "message": 5 } } }

Write one if you have a bespoke test runner or build tool with no existing matcher. Otherwise, grab one off the shelf.

Presentation Options

Control how the task uses terminal space:

JSON
{ "label": "TypeScript Watch", "command": "npx tsc --watch", "isBackground": true, "presentation": { "reveal": "silent", // don't focus the terminal when it starts "panel": "dedicated", // use its own terminal panel "showReuseMessage": false }, "problemMatcher": "$tsc-watch" }

reveal options:

  • "always" — bring the terminal into focus when the task runs (default)
  • "silent" — run in the background, don't steal focus
  • "never" — hide the terminal entirely

panel options:

  • "shared" — use the same terminal as other tasks
  • "dedicated" — always use its own terminal panel
  • "new" — create a new terminal every run

For long-running background tasks (dev server, TypeScript watcher), use "reveal": "silent" + "panel": "dedicated". They stay running in their own panel, out of your way, but available when you need to see them.

Compound Tasks: Running Multiple Things at Once

Run multiple tasks with a single command:

JSON
{ "label": "Start Everything", "dependsOn": ["Start API Server", "Start Frontend"], "dependsOrder": "parallel" }

dependsOrder options:

  • "parallel" — start all at the same time
  • "sequence" — run in order (wait for each to finish before starting the next)

Use "sequence" when order matters — like wiping a database before seeding it:

JSON
{ "label": "Reset and Seed DB", "dependsOn": ["Drop DB", "Run Migrations", "Seed Data"], "dependsOrder": "sequence" }

Use "parallel" when the tasks are independent — starting a frontend and backend at the same time.

A Practical tasks.json

JSON
{ "version": "2.0.0", "tasks": [ { "label": "TypeScript: Watch", "type": "shell", "command": "npx tsc --watch", "isBackground": true, "problemMatcher": "$tsc-watch", "presentation": { "reveal": "silent", "panel": "dedicated" }, "group": { "kind": "build", "isDefault": true } }, { "label": "Dev Server", "type": "shell", "command": "npm run dev", "isBackground": true, "problemMatcher": [], "presentation": { "reveal": "silent", "panel": "dedicated" } }, { "label": "Test: Watch", "type": "shell", "command": "npm run test:watch", "isBackground": true, "problemMatcher": "$jest", "presentation": { "reveal": "silent", "panel": "dedicated" }, "group": { "kind": "test", "isDefault": true } }, { "label": "Start All", "dependsOn": ["TypeScript: Watch", "Dev Server"], "dependsOrder": "parallel" } ] }

The Hidden Benefit: Clickable File Paths

Even without a problem matcher, anything in your terminal that looks like a file path with a line number — src/components/UserCard.tsx:42:8 — is Cmd/Ctrl+clickable. VS Code detects that pattern automatically and makes it a link.

Problem matchers are better (they appear in the Problems tab, you can filter by file, severity, etc.), but the raw clickable path is a quick win when you just need to jump somewhere fast.

Checking the box on setup

If you find yourself running the same 2–3 commands every time you open a project, that's the list of tasks worth setting up. The overhead is 10 minutes once, and then it's a menu pick — or a single keyboard shortcut — for the lifetime of the project.

Enjoyed this? Get more like it.

Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.