If you spend most of your day typing things like npm run build && npm run test && npm run deploy into a terminal, you are leaving a lot of leverage on the table. I lived that way for years, and the day I started writing real tasks.json and launch.json files in Antigravity was the day my workflow finally felt like a workflow. A single Cmd+Shift+B now triggers save, lint, build, test, and a Problems-panel summary; an AI agent reading my repository can execute deploy:staging on request and report back whether it succeeded.
Documentation for these two files is scattered across VS Code, Microsoft, and a handful of community wikis, and very little of it explains how Antigravity—with its agent surface and integrated AI—changes the game. In this article I want to walk you through the configurations I actually run in indie projects, explain the design choices, and share the dead ends I have hit. Copying the snippets is fine, but the takeaway I really hope to leave you with is the why, so you can adapt these ideas to whatever stack you live in.
tasks.json is a vocabulary for your project
tasks.json lives at .vscode/tasks.json (or .antigravity/tasks.json) and declares commands the editor can invoke. On the surface that sounds like a thin wrapper around shell scripts or package.json scripts, but Antigravity tasks bring four superpowers that are easy to underestimate.
- Their output flows through a problem matcher, which converts errors and warnings into rows in the Problems panel.
- Tasks can chain other tasks via
dependsOn, declared serially or in parallel. - AI agents can call them by name, capturing stdout and exit codes back into the conversation.
- They can be bound to keys—Cmd+Shift+B by default, or any custom shortcut you like.
I think of this as turning shell incantations into editor vocabulary. The more projects you juggle, the more this matters: switching context becomes nearly free because every project speaks the same set of verbs.
Minimal example: bind build to Cmd+Shift+B
Let us start with a minimal task for a Next.js project. The goal is to make Cmd+Shift+B run npm run build.
// .vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "npm",
"script": "build",
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "silent", // do not steal focus on success
"panel": "dedicated", // a dedicated panel per task
"clear": true // wipe previous output on each run
},
"problemMatcher": ["$tsc"] // route TypeScript errors to the Problems panel
}
]
}When you hit Cmd+Shift+B the build runs silently, and the Problems panel only opens if something fails. I prefer reveal: "silent" for tasks that should pass; the always-on terminal panel I used to keep open just trains me to ignore it. The built-in $tsc matcher does the heavy lifting of converting compiler diagnostics into clickable rows. Without it, you are back to scrolling raw output, which is fine for the first couple of errors but ruins your flow once you have ten.
Chain "prepare → run → tear down" with dependsOn
When you have a sequence to run—generate Prisma client, then run unit tests—dependsOn plus dependsOrder keeps everything declarative.
{
"version": "2.0.0",
"tasks": [
{
"label": "prisma:generate",
"type": "shell",
"command": "npx prisma generate",
"presentation": { "reveal": "silent", "panel": "shared" }
},
{
"label": "test:unit",
"type": "npm",
"script": "test:unit",
"presentation": { "reveal": "always", "panel": "dedicated" },
"problemMatcher": []
},
{
"label": "test (with prisma)",
"dependsOn": ["prisma:generate", "test:unit"],
"dependsOrder": "sequence",
"group": {
"kind": "test",
"isDefault": true
},
"problemMatcher": []
}
]
}dependsOrder: "sequence" runs the dependencies one after another. Setting parallel lets them run concurrently, which is great for warming caches but dangerous when one task races with another. By marking group.kind: "test" as isDefault, the chain becomes the default test task—a single Cmd+Shift+P → Tasks: Run Test Task fires the whole pipeline.
You might wonder if regenerating Prisma every time is wasteful. In my own work, the single most common cause of "passes in CI, fails locally" was forgetting to regenerate the Prisma client after a schema change. Forcing it through the dependency makes that bug class disappear, which is worth the few seconds of overhead.
Hand-rolling problem matchers turns logs into navigation
Built-in matchers ($tsc, $eslint-stylish, $go, and friends) cover the common cases. When your tooling is not in the list, writing a custom matcher feels intimidating, but it is usually one or two regular expressions away.
Pipe Vitest output into the Problems panel
Run Vitest with --reporter=verbose and the failing lines look like FAIL src/foo.test.ts > should bar. Here is a matcher that catches them.
{
"label": "test:vitest",
"type": "shell",
"command": "npx vitest run --reporter=verbose",
"problemMatcher": {
"owner": "vitest",
"fileLocation": ["relative", "${workspaceFolder}"],
"pattern": [
{
"regexp": "^\\s*FAIL\\s+(\\S+)\\s+>\\s+(.+)$",
"file": 1,
"message": 2
}
],
"severity": "error"
}
}Now failed tests appear in the Problems panel with file paths, and clicking jumps you to the relevant file. The honest part of this work is that regular expressions are slippery; my advice is to debug them in isolation. Replace the real command with bash -c "echo 'FAIL src/foo.test.ts > should bar'" and confirm the matcher catches your fake line, then put the real command back. Mixing real test runtimes with regex iteration burns time you will not get back.
Multi-line patterns
When errors span more than one line, switch pattern to an array. The last entry consumes the line, and earlier entries accumulate context.
"pattern": [
{
"regexp": "^\\s*FAIL\\s+(\\S+)$",
"file": 1
},
{
"regexp": "^\\s*Error: (.+)$",
"message": 1
}
]Once you have a working multi-line matcher, copying it into other repos is trivial. It is the kind of investment that pays off forever, especially if you maintain in-house tooling that no public matcher will ever cover.
launch.json is your "how to run and debug" script
launch.json describes how the debugger starts. I find it helpful to think of it as a script for "what happens when I press F5." It is similar to the VS Code version, with the bonus that AI agents in Antigravity can invoke a launch configuration by name.
Minimal Node + TypeScript setup
Here is what I use for a Node.js service running through tsx.
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Run server (tsx watch)",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": ["tsx", "watch", "${workspaceFolder}/src/server.ts"],
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env.development",
"skipFiles": ["<node_internals>/**", "node_modules/**"]
}
]
}The envFile line is the most important one to internalize. I once spent the better part of an evening wondering why my code could not see DATABASE_URL, and the answer was that I had forgotten this single line. If your code touches process.env.X, either envFile or env must be present.
skipFiles controls which files Step Over treats as part of "the runtime." Leaving <node_internals>/** excluded keeps you out of low-level Node guts. I sometimes drop node_modules/** from the list when I genuinely need to step into a buggy dependency, then put it back when the investigation is done.
compounds for spinning up multiple processes
Most apps need both a backend and a frontend running. A compounds entry bundles several configurations into a single launch.
{
"version": "0.2.0",
"configurations": [
{
"name": "Backend",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": ["tsx", "watch", "${workspaceFolder}/api/server.ts"],
"envFile": "${workspaceFolder}/.env.development"
},
{
"name": "Frontend",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": ["next", "dev"],
"cwd": "${workspaceFolder}/web"
}
],
"compounds": [
{
"name": "Full Stack Dev",
"configurations": ["Backend", "Frontend"],
"stopAll": true
}
]
}Always set stopAll: true. Forgetting it is how you end up with orphan Node processes hogging port 3000 because you stopped the frontend but not the backend. Before I switched to compounds I used to keep two terminals open and start each side separately—it worked, but it cost me a lot of mental energy that I could spend on the actual code.
Calling tasks from an AI agent
This is where Antigravity stands apart from a plain editor. Any task in tasks.json can be invoked by an agent. When the agent says "Run task: build," Antigravity runs it, captures stdout and the exit code, and feeds them back into the conversation so the agent can decide what to do next.
A "fix → build → fix again" loop
A pattern I lean on heavily is asking the agent to fix TypeScript errors until the build succeeds. For that loop to be safe, the task itself has to be designed for unattended execution.
{
"label": "build:strict",
"type": "npm",
"script": "build",
"presentation": { "reveal": "silent" },
"problemMatcher": ["$tsc"],
"options": {
"env": {
"CI": "true"
}
}
}Setting CI=true keeps interactive prompts—"do you want to upgrade dependencies?"—from hanging the build. If your agent triggers a build that quietly waits on stdin, the loop never terminates and you only notice when you check on it half an hour later. I have lost too many afternoons to that exact failure mode.
The instruction I give the agent stays simple:
Run the build:strict task.
If it fails, read the errors, fix them, and run the task again.
After three attempts, summarize the situation and stop.
The "stop after three attempts" guardrail is non-negotiable. Without it the agent tends to keep "fixing" further and further from the original error, and you wake up to a codebase that compiles but no longer does what it used to.
Force quality gates before deploying
Every deploy task in my projects depends on lint and tests, no exceptions.
{
"label": "deploy:cf-pages",
"type": "shell",
"command": "wrangler pages deploy out --project-name=${input:projectName}",
"dependsOn": ["lint", "test:unit", "build"],
"dependsOrder": "sequence",
"presentation": { "reveal": "always", "panel": "dedicated" }
}"inputs": [
{
"id": "projectName",
"type": "promptString",
"description": "Cloudflare Pages project name"
}
]The ${input:projectName} field forces a runtime prompt, which is my insurance policy against the classic "deployed staging code to production" disaster. I have done that exactly twice in my career, both times in a hurry, and both times I wished I had a confirmation step. Ten seconds of typing is cheaper than ten minutes of rolling back.
For payment-critical services I sometimes use pickString instead, listing the explicit environments and forcing me to choose. It is a small ceremony that pays for itself the first time it saves you.
Common pitfalls and how to escape them
The framework above is the trunk. Here are the branches—the situations where I, or someone I have helped, have spent disproportionate time confused.
Pitfall 1: Cmd+Shift+B runs the wrong task
If two tasks have group.isDefault: true, Antigravity either prompts you to pick one or quietly chooses the most recently saved one. The fix is to keep isDefault on exactly one task per group, or to invoke Tasks: Run Build Task explicitly so the choice is conscious.
Pitfall 2: Hollow breakpoints in launch.json
A hollow red circle means Antigravity could not match the breakpoint to source maps. The reasons cluster around three issues:
tsconfig.jsonis missing"sourceMap": true.- You launched with plain
nodeagainst.tsfiles instead oftsx/ts-node. outFilesdoes not match the actual build output path.
If you are using tsx watch, sourcemaps are resolved internally, so omitting outFiles actually makes things more reliable. If you compile via tsc and run the resulting dist/, then you must point outFiles at the build directory: "outFiles": ["${workspaceFolder}/dist/**/*.js"].
Pitfall 3: Environment variables seem ignored
When you set both env and envFile in the same configuration, env wins on conflicts. I have caught myself overriding values from .env without realizing it.
{
"envFile": "${workspaceFolder}/.env",
"env": {
// Reserve this block for transient overrides only
"DEBUG": "app:*"
}
}Treat envFile as the canonical source of truth and reserve env for one-off overrides. It keeps your config readable and your future self thankful.
Pitfall 4: Tasks cannot find binaries on PATH
Run command: "fly deploy" and you might be greeted by command not found. GUI-launched Antigravity does not always inherit your login shell's PATH.
"options": {
"shell": {
"executable": "/bin/zsh",
"args": ["-l", "-c"] // -l opens a login shell
}
}Either force a login shell as above, or call binaries by absolute path (/usr/local/bin/fly deploy). The -l approach is friendlier but slower; for tasks I run dozens of times a day, I prefer the absolute-path version.
Pitfall 5: Parallel tasks produce unreadable output
Set dependsOrder: "parallel" and you may end up with interleaved logs that are impossible to skim. Give each task its own panel:
"presentation": {
"panel": "dedicated",
"group": "build-parallel" // tasks in the same group share a window with tabs
}A dedicated panel per task is one of those tweaks that does not look like much in a diff, but transforms parallel debugging from "where is the failure?" to "click the red tab."
Beyond Node: launch profiles for Python, Go, and the browser
The Node example covers the most common case, but Antigravity's debugger handles other languages just as gracefully when you keep the structure of your launch.json consistent.
Python with debugpy
When I prototype data tools or experiment with LLM evaluation scripts, my first move is to set up a Python launch configuration. The debugpy extension is the canonical choice in Antigravity.
{
"name": "Run pytest (single file)",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"args": ["${file}", "-vv", "--tb=short"],
"console": "integratedTerminal",
"justMyCode": false,
"envFile": "${workspaceFolder}/.env.test"
}justMyCode: false is the line I always tweak first. The default of true means the debugger silently steps over third-party code, which is fine until you are tracking a bug into pandas or a vendored client. Disabling it costs a tiny amount of speed but unlocks the rest of the call stack when you actually need it.
Go services
Go's debugger plays well with launch configurations once the path conventions are right.
{
"name": "Run API (delve)",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/api",
"envFile": "${workspaceFolder}/.env.development",
"args": ["--port", "8080"]
}The program field points to the package directory, not a single file. New Go developers in particular trip over this because the rest of the editor encourages "open the file you want to run." For Go, you debug a package.
Chrome and Edge for frontend
Frontend debugging in Antigravity hooks into a Chromium devtools protocol session, so you can hit breakpoints inside React components running in a real browser.
{
"name": "Debug Frontend (Chrome)",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/web/src",
"sourceMapPathOverrides": {
"webpack:///./src/*": "${webRoot}/*",
"webpack:///*": "*"
}
}The sourceMapPathOverrides block looks intimidating, but it solves a single problem: bundlers rewrite import paths during builds, and the debugger needs to translate those back into source paths. If breakpoints land in the wrong file, this is the section to inspect first.
Tasks as the boundary between editor and CI
One of the quieter wins of investing in tasks.json is that the same vocabulary describes both your local environment and your CI pipeline. When lint, test:unit, and build exist as named tasks, your CI YAML can call them by name, which means there is exactly one definition of "what does it mean to build this project?" floating around.
# .github/workflows/ci.yml
- run: npm ci
- run: npx run-task lint
- run: npx run-task test:unit
- run: npx run-task buildThat is a slight oversimplification—run-task is shorthand for whatever runner you wire in—but the principle holds. I have seen too many repos where local commands and CI commands drift apart for months, and one day you ship a change that broke locally but not in CI (or worse, vice versa). Tasks are the contract that keeps both sides honest.
If you go down this path, audit your tasks for hidden side effects. A task that opens a browser tab during local development is great until CI tries to run it on a headless agent and waits for a window that never appears. Wrap behaviors like that behind environment checks (if (process.env.CI) return;) and document them at the top of the task definition.
Migrating from package.json scripts
If your project already has a sprawling scripts block in package.json, it is tempting to leave it untouched and treat tasks.json as a parallel system. I have tried that twice and regretted it both times. The two systems start to drift, the team is not sure which one is canonical, and the AI agent ends up calling whichever it sees first.
A migration that has worked well for me is to keep package.json scripts as low-level primitives—plain shell commands, no chaining—and elevate every chained or interactive workflow into tasks.json. So package.json keeps "build": "next build" as a one-liner, and tasks.json owns "build, then run lighthouse, then write a report." The result is that anyone running npm run build gets exactly what they expect, while editor users (and agents) get the smarter pipeline.
This convention also helps when teammates copy commands from a README into their terminal. If you put the smart logic in tasks.json only, README snippets stay simple and stay correct.
Monorepo strategies
For monorepos the calculus changes because each package may want its own tasks.json while the workspace root keeps a higher-level set. Antigravity supports this naturally: tasks defined at the workspace root show up alongside tasks defined per package, and the picker labels them with the package name.
What I do is keep two layers:
- Root tasks.json owns workflows that touch every package:
lint:all,test:all,release:all. They typically usedependsOnto call into per-package tasks. - Package tasks.json owns workflows specific to that package:
dev,build,test:unit. They never know about other packages.
This split matches the directory structure and keeps responsibilities clean. The first time you onboard a teammate to a monorepo with this layout, the speed at which they get productive is striking.
Observability and telemetry inside tasks
Once tasks become the backbone of your day, you want to know how often they run, how long they take, and which ones fail. Antigravity does not bake in metrics, but you can wrap a task in a tiny shell wrapper that emits OpenTelemetry events to a local collector.
{
"label": "build:traced",
"type": "shell",
"command": "./scripts/with-tracing.sh build",
"presentation": { "reveal": "silent" },
"problemMatcher": ["$tsc"]
}#!/usr/bin/env bash
# scripts/with-tracing.sh
TASK_NAME="$1"
START=$(date +%s%3N)
shift
"$@"
EXIT=$?
END=$(date +%s%3N)
curl -s -X POST http://localhost:4318/v1/traces -H 'Content-Type: application/json' -d "{ "task": "$TASK_NAME", "duration_ms": $((END-START)), "exit_code": $EXIT }" > /dev/null
exit $EXITThis is dramatically simpler than full OTel instrumentation, and it gives you enough signal to catch regressions like "build went from 8 seconds to 60 seconds last week." If you only do this for one task, do it for build—everyone is sensitive to build time, and you will find regressions you would otherwise dismiss as "vibes."
Security: do not let tasks run anything they receive
Tasks that take user input via ${input:...} are a small attack surface. If your task does something like git checkout ${input:branch} and the input is unconstrained, a hostile prompt response could execute arbitrary shell text. The fix is either to validate the input with pickString (which restricts choices to a known list) or to sanitize the value inside a wrapper script.
"inputs": [
{
"id": "branch",
"type": "pickString",
"options": ["main", "release/*", "dev"],
"description": "Branch to check out"
}
]For most personal projects this is overkill, but if your tasks ever drive a deploy or touch a production database, treat task inputs with the same suspicion you would treat any other unsanitized command-line argument.
Where to put the configs and how to share them
.vscode/ versus .antigravity/ is a real question. My own convention is:
- Commit
.vscode/tasks.jsonand.vscode/launch.jsonto Git when the configs are meant for the whole team. - Keep machine-specific overrides (API keys, personal preferences) in
.vscode/*.local.jsonand add them to.gitignore. - Move long-running, AI-only configurations into
.antigravity/tasks.jsonso they do not pollute the human task picker.
That last point matters more than it sounds. When humans and agents share one tasks.json, the picker fills up with tasks that take ten minutes to run, which makes the human-friendly verbs harder to find. Splitting them keeps both audiences happy.
What two weeks of daily use felt like
After two weeks of running this setup on a real personal project, the things I noticed were:
- I almost never type repeat commands into a terminal anymore.
- Failing tasks—lint and tests in particular—accumulate in the Problems panel, so the next action is always one click away rather than a scroll up the terminal.
- When an agent loops on a build, I stop micromanaging because the loop has hard guardrails.
- New projects start at a higher baseline because I now bring a polished
tasks.jsontemplate with me.
The downside is that with too many tasks the picker becomes a needle-in-a-haystack experience. My fix is to bind the most-used tasks to keyboard shortcuts in keybindings.json:
// keybindings.json
[
{ "key": "cmd+shift+t", "command": "workbench.action.tasks.runTask", "args": "test (with prisma)" },
{ "key": "cmd+shift+d", "command": "workbench.action.tasks.runTask", "args": "deploy:cf-pages" }
]A field-tested tasks.json starter you can adopt today
If you want a single file you can drop into a fresh project and tweak from there, this is the closest thing I have to a personal canon. It assumes a Node + TypeScript stack with Vitest and Cloudflare Workers, but the structure transfers cleanly to other ecosystems.
// .vscode/tasks.json
{
"version": "2.0.0",
"inputs": [
{
"id": "deployTarget",
"type": "pickString",
"options": ["staging", "production"],
"description": "Choose deploy target"
}
],
"tasks": [
{
"label": "lint",
"type": "npm",
"script": "lint",
"presentation": { "reveal": "silent", "panel": "shared" },
"problemMatcher": ["$eslint-stylish"]
},
{
"label": "test:unit",
"type": "npm",
"script": "test:unit",
"presentation": { "reveal": "always", "panel": "dedicated" },
"problemMatcher": []
},
{
"label": "test:e2e",
"type": "shell",
"command": "npx playwright test",
"presentation": { "reveal": "always", "panel": "dedicated" },
"problemMatcher": []
},
{
"label": "build",
"type": "npm",
"script": "build",
"group": { "kind": "build", "isDefault": true },
"presentation": { "reveal": "silent" },
"problemMatcher": ["$tsc"]
},
{
"label": "verify",
"dependsOn": ["lint", "test:unit", "build"],
"dependsOrder": "sequence",
"group": { "kind": "test", "isDefault": true }
},
{
"label": "deploy",
"type": "shell",
"command": "npx wrangler deploy --env ${input:deployTarget}",
"dependsOn": ["verify"],
"dependsOrder": "sequence",
"presentation": { "reveal": "always", "panel": "dedicated" }
}
]
}A few things to highlight in this layout. First, every higher-level task (verify, deploy) is just an orchestrator—it does no real work itself, only chains other tasks. That keeps each leaf task small enough to read in one screen and easy to test in isolation. Second, deploy depends on verify, which depends on lint, test:unit, and build. The result is that nobody can deploy without passing the gates, including your future self at 2am.
Third, inputs is at the top of the file rather than mixed into the tasks. I find this aids navigation; if I want to add another deploy target tomorrow, I edit one place. Habits like this seem trivial when the file has six tasks, but pay off when the file has thirty.
Related articles
If you want to keep going down this path, these articles dig into adjacent topics:
- Mastering Antigravity custom commands
- Setting up reproducible devcontainers in Antigravity
- Parallel workspaces with Antigravity and git worktree
tasks.json and launch.json are unglamorous files, but they earn their keep faster than almost any other change you can make to your setup. Pick the single command you have typed most often this week, turn it into a task tomorrow morning, and stay inside the editor for the rest of the day. Six months from now, the friction you will not be feeling is the closest thing to a free upgrade an indie developer ever gets.