▸ Agent Skills
6 min read

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 after other (other is a dependency, “upstream”).
  • before = [ "other" ] — run this task before other (other is “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:

SuffixSatisfied whenFailure propagates?
@startedthe target has begun executingyes
@readya process passes its readiness probe; for oneshot tasks this means successyes
@succeededthe target exits with code 0 (or is skipped)yes
@completedthe target finishes, regardless of exit codeno (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:

ModeRuns
singleonly the named task
before (default)the task and everything upstream of it (its dependencies)
afterthe task and everything downstream of it (tasks that depend on it)
allthe 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:enterShell runs before the shell is entered (devenv shell) and before processes start (devenv up).
  • devenv:enterTest runs before tests execute (devenv test). It depends on devenv: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 of tasks."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. Write name\0base64(value)\0 pairs 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/after with 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:

  1. First run any tasks that have before = [ "devenv:processes:web-server" ]
  2. 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