Tasks
Added in 1.2
Tasks allow you to form dependencies between code, executed in parallel.
Defining tasks
{ pkgs, ... }:
{ tasks."myapp:hello" = { exec = ''echo "Hello, world!"''; };}
devenv.nix
$ devenv tasks run myapp:helloRunning tasks myapp:helloSucceeded myapp:hello 9ms1 Succeeded 10.14ms
Terminal window
Added in 1.7
You can also run all tasks in a namespace by providing just the namespace prefix:
$ devenv tasks run myappRunning tasks myapp:hello myapp:build myapp:testSucceeded myapp:hello 9msSucceeded myapp:build 120msSucceeded myapp:test 350ms3 Succeeded 479.14ms
Terminal window
Dependencies between tasks
Tasks form a dependency graph (a DAG). Declare an edge between two tasks
with before or after:
after = [ "other" ]— run this task afterother(otheris a dependency, “upstream”).before = [ "other" ]— run this task beforeother(otheris “downstream” and depends on this one).
Processes are tasks too (see Processes as tasks),
so the same before/after edges connect tasks and processes
interchangeably — see process dependencies
for process-focused examples.
before and after describe the same edge from opposite ends, so you
can declare a dependency from whichever side is more convenient. These
are equivalent:
{ # declared from the dependent task tasks."myapp:build".after = [ "myapp:generate" ];
# ...is the same edge as declaring it from the dependency tasks."myapp:generate".before = [ "myapp:build" ];}
devenv.nix
Dependency states
A dependency waits for its target to reach a particular state before it
is considered satisfied. Append an @ suffix to choose the state
explicitly:
| Suffix | Satisfied when | Failure propagates? |
|---|---|---|
@started | the target has begun executing | yes |
@ready | a process passes its readiness probe; for oneshot tasks this means success | yes |
@succeeded | the target exits with code 0 (or is skipped) | yes |
@completed | the target finishes, regardless of exit code | no (soft dependency) |
When no suffix is given the default is @ready for processes and
@succeeded for oneshot tasks.
A common use is running a setup task once a service is ready:
{ tasks."myapp:configure" = { exec = "create-buckets"; after = [ "devenv:processes:garage@ready" ]; };}
devenv.nix
Execution modes
When you run a task, devenv schedules a subgraph around it rather than
only that one task. --mode controls how much of the graph is included:
| Mode | Runs |
|---|---|
single | only the named task |
before (default) | the task and everything upstream of it (its dependencies) |
after | the task and everything downstream of it (tasks that depend on it) |
all | the entire connected graph, both upstream and downstream |
$ devenv tasks run myapp:build # before mode (default): build + its dependencies$ devenv tasks run myapp:build --mode single # just build$ devenv tasks run myapp:build --mode all # build, its dependencies, and its dependents
Terminal window
devenv up starts processes in before mode, while devenv test runs
in all mode. This difference matters for setup tasks attached to
processes — see Processes as tasks.
enterShell / enterTest
devenv:enterShell and devenv:enterTest are built-in lifecycle events
that run setup tasks at specific points:
devenv:enterShellruns before the shell is entered (devenv shell) and before processes start (devenv up).devenv:enterTestruns before tests execute (devenv test). It depends ondevenv:enterShell, so all shell setup tasks run first automatically.
To hook into these events, use before to declare that your task should
run before the event completes:
{ pkgs, lib, config, ... }:
{ tasks = { "bash:hello" = { exec = "echo 'Hello world from bash!'"; before = [ "devenv:enterShell" ]; };
"myapp:test-setup" = { exec = "echo 'Preparing test fixtures...'"; before = [ "devenv:enterTest" ]; }; };}
devenv.nix
$ devenv shell...Running tasks devenv:enterShellSucceeded devenv:git-hooks:install 25msSucceeded bash:hello 9msSucceeded devenv:enterShell 13ms3 Succeeded 28.14ms
Terminal window
Many devenv modules automatically hook into these events. For example,
enabling git hooks registers devenv:git-hooks:install as a dependency
of devenv:enterShell.
Using your favourite language
Tasks can also use another package for execution, for example when entering the shell:
{ pkgs, lib, config, ... }:
{ tasks = { "python:hello" = { exec = '' print("Hello world from Python!") ''; package = config.languages.python.package; }; };}
devenv.nix
Avoiding running expensive exec via status check
If you define a status command, it will be executed first and if it
returns 0, exec will be skipped.
{ pkgs, lib, config, ... }:
{ tasks = { "myapp:migrations" = { exec = "db-migrate"; status = "db-needs-migrations"; }; };}
devenv.nix
Tasks using the status attribute will also cache their outputs. When a
task is skipped because its status command returns success, the output
from the most recent successful run will be restored and passed to
dependent tasks.
Executing tasks only when files have been modified
You can specify a list of files to monitor with execIfModified. The
task will only run if any of these files have been modified since the
last successful run. This attribute supports glob patterns, allowing you
to monitor multiple files matching specific patterns.
{ pkgs, lib, config, ... }:
{ tasks = { "myapp:build" = { exec = "npm run build"; execIfModified = [ "src/**/*.ts" # All TypeScript files in src directory "*.json" # All JSON files in the current directory "package.json" # Specific file "src" # Entire directory ]; # Optionally run the build in a specific directory cwd = "./frontend"; }; };}
devenv.nix
This is particularly useful for tasks that depend on specific files and don’t need to run if those files haven’t changed.
The system tracks both file modification times and content hashes to detect actual changes. If a file’s timestamp changes but its content remains the same (which can happen when touching a file or when saving without making changes), the task will be skipped.
When a task is skipped due to no file changes, any previous outputs from that task are preserved and passed to dependent tasks, making the caching more efficient.
Inputs / Outputs
Tasks support passing inputs and produce outputs, both as JSON objects:
$DEVENV_TASK_INPUT: JSON object oftasks."myapp:mytask".input.$DEVENV_TASKS_OUTPUTS: JSON object with dependent tasks as keys and their outputs as values.$DEVENV_TASK_OUTPUT_FILE: a writable file with tasks’ outputs in JSON.$DEVENV_TASK_EXPORTS_FILE: a writable file where tasks can export environment variables. Writename\0base64(value)\0pairs to this file and they will be set in the environment of dependent tasks.
{ pkgs, lib, config, ... }:
{ tasks = { "myapp:mytask" = { exec = '' echo $DEVENV_TASK_INPUT > $DEVENV_ROOT/input.json echo '{ "output": 1 }' > $DEVENV_TASK_OUTPUT_FILE echo $DEVENV_TASKS_OUTPUTS > $DEVENV_ROOT/outputs.json ''; input = { value = 1; }; }; };}
devenv.nix
Shell messages
Tasks can display messages to the user when entering the shell by
writing a devenv.messages array to $DEVENV_TASK_OUTPUT_FILE. This is
useful for showing informational output like trace URLs or setup status
after initialization.
{ pkgs, lib, config, ... }:
{ tasks = { "myapp:info" = { exec = '' echo '{"devenv":{"messages":["Setup complete. Dashboard: http://localhost:3000"]}}' > "$DEVENV_TASK_OUTPUT_FILE" ''; before = [ "devenv:enterShell" ]; }; };}
devenv.nix
Messages are printed after the shell environment is loaded, so they remain visible in the interactive session.
Passing inputs from the CLI
You can override or add inputs when running tasks from the command line
using --input and --input-json:
$ devenv tasks run myapp:mytask --input value=42 --input name=hello
Terminal window
Values are automatically parsed as JSON when valid, otherwise treated as
strings. For example, --input count=3 sets a number,
--input flag=true sets a boolean, and --input name=hello sets a
string.
You can also pass a full JSON object:
$ devenv tasks run myapp:mytask --input-json '{"value": 42, "name": "hello"}'
Terminal window
Both flags can be combined. --input-json is applied first, then
individual --input values are merged on top (CLI wins on conflict with
Nix-defined inputs).
Processes as tasks
Added in 1.4
All processes defined in processes are automatically available as
tasks with the devenv:processes: prefix. This allows you to:
- Run individual processes as tasks
- Define dependencies between tasks and processes
- Use task features like
before/afterwith processes
{ pkgs, ... }:
{ # Define a process processes.web-server = { exec = "python -m http.server 8080"; };
# Define a task that runs before the process tasks."app:setup-data" = { exec = "echo 'Setting up data...'"; before = [ "devenv:processes:web-server" ]; };}
devenv.nix
When you run devenv tasks run devenv:processes:web-server, it will:
- First run any tasks that have
before = [ "devenv:processes:web-server" ] - Then start the process itself
This is particularly useful for:
- Running setup tasks before starting a process
- Creating complex startup sequences
- Checking that a single process reaches its ready state without starting all of them
!!! note “Processes only live for the duration of the run”
`devenv tasks run` stops every process it started once the task graph finishes,whether the process is a root or was pulled in as a dependency of another task.Use [`devenv up`](devenv-processes-2ae92f11.md) when you want a process to keep running.
You can also run tasks after a process finishes by depending on its
@completed state (see Dependency states). The
default suffix for a process dependency is @ready, which fires as soon
as the process is healthy, so use @completed to wait for it to exit
instead:
{ pkgs, ... }:
{ # Define an application server process processes.app-server = { exec = "node server.js"; };
# Define a task that runs after the server stops tasks."app:cleanup" = { exec = '' echo "Server stopped, cleaning up..." rm -f ./server.pid rm -rf ./tmp/cache/* ''; after = [ "devenv:processes:app-server@completed" ]; };}
devenv.nix
This ensures that cleanup tasks like removing PID files or clearing caches are executed when the application server stops.
Git Integration
Added in 1.10
Tasks can reference the git repository root path using
${config.git.root}, which is particularly useful in monorepo
environments:
{ config, ... }:
{ tasks."build:frontend" = { exec = "npm run build"; cwd = "${config.git.root}/frontend"; };
tasks."test:backend" = { exec = "cargo test"; cwd = "${config.git.root}/backend"; };}
devenv.nix
This allows tasks to reference paths relative to the repository root
regardless of where the devenv.nix file is located within the
repository.
SDK using Task Server Protocol
See Task Server Protocol for a proposal how defining tasks in your favorite language would look like.
Last updated Oct 08, 2026