The Aspect CLI ships with built-in tasks (Documentation Index
Fetch the complete documentation index at: https://docs.aspect.build/llms.txt
Use this file to discover all available pages before exploring further.
build, test, format, lint, etc.) and lets you define your own in AXL. This page covers the essentials: running tasks, understanding how the CLI discovers extensions, and writing a simple custom task.
After installing, run aspect help to see what’s available:
build and test are loaded from the @aspect extension library. Custom tasks you define locally appear alongside them.
How tasks are registered
A task becomes anaspect <name> CLI command through one of three paths.
1. Auto-discovery in .aspect/
The CLI scans every .axl file inside an .aspect/ directory and registers any task(...) it finds at the module’s top level. This is the happy path for project-local tasks. The scan starts in your current working directory and walks up to the workspace root, so subdirectories can scope tasks to themselves:
app1, the CLI loads tasks from app1/.aspect/, then walks up and loads tasks from the root .aspect/. Both mycmd and subcmd are visible. From the repo root, only mycmd is.
config.axl and version.axl are reserved filenames inside .aspect/. The task auto-discovery scan skips them — they’re loaded for their own purpose (CLI / task configuration and version pinning, respectively). Tasks defined directly in those files won’t appear as CLI commands; use a separately-named .axl file or the programmatic path below.2. External AXL modules via MODULE.aspect
Tasks shipped by an external AXL module are pulled in by declaring the module in MODULE.aspect at the repo root. With auto_use_tasks = True, every task the module exports becomes a CLI command without a load() in your own files; otherwise you load() the symbols you want. See How to use external AXL modules for the full mechanics.
3. Programmatic registration in config.axl
config.axl itself isn’t scanned for top-level tasks, but it can register them programmatically — useful when the task value comes from a helper or an alias rather than a static task(...) literal. The canonical example is format.alias(), which produces a task value you register with ctx.tasks.add(...):
.aspect/config.axl
aspect buildifier is a real CLI command alongside the built-ins. See aspect buildifier for the complete alias.
Write your first extension
Custom tasks are Starlark functions registered with thetask() built-in. Here’s a minimal example that builds targets and prints the output file paths:
Follow these steps to create a custom mycmd task that wraps Bazel’s build functionality:
-
Create a
.aspectdirectory at the root of your project: -
Create a file named
mycmd.axlwithin the.aspectdirectory: -
Add the following Starlark code to
mycmd.axl: -
Verify the new task appears:
-
Test the new AXL task by running:
Task arguments
Theargs dict in task() maps argument names to arg specs. Argument names become kebab-case CLI flags (output_dir → --output-dir). Values merge from defaults → config.axl overrides → CLI flags (CLI wins).
| Constructor | CLI form | Type in ctx.args |
|---|---|---|
args.string(default="") | --flag=value | str |
args.boolean(default=False) | --flag / --flag=false | bool |
args.int(default=0) | --flag=42 | int |
args.string_list(default=[]) | --flag=a --flag=b | list[str] |
args.positional(default=[]) | trailing positional words | list[str] |
args.custom(type, default=None) | config.axl only — not exposed on CLI | type |
values=["a","b","c"] on args.string to restrict to a fixed set — the CLI validates the input and shows choices in --help. Any scalar arg accepts short="x" for a single-character alias (e.g., -v).
Here’s a task that generates code from Bazel targets using typed arguments:
task() built-in also accepts summary (one-liner in aspect help), description (extended --help text), and name (override the command name derived from the variable name). Tasks are named from their variable name converted to kebab-case: my_cmd → my-cmd.
Running processes
Usectx.std.process.command(binary) to run any tool. .spawn() is non-blocking and returns a handle; .wait() blocks until the process exits.
.stdout("piped"):
.current_dir(path) sets the working directory. .env(name, value) injects an environment variable. Methods chain fluently — each returns the same Command.
File and environment access
Filesystem —ctx.std.fs reads and writes files:
ctx.std.env inspects the runtime environment:
| Method | Returns |
|---|---|
ctx.std.env.var("NAME") | Env var value, empty string if unset |
ctx.std.env.current_dir() | Current working directory |
ctx.std.env.aspect_root_dir() | Workspace root (where MODULE.bazel lives) |
ctx.std.env.git_root_dir() | Git repository root |
ctx.std.env.temp_dir() | System temp directory |
ctx.std.env.var() works the same in both TaskContext (task implementations) and ConfigContext (config.axl):
Rich return values
Tasks return anint exit code or a TaskConclusion for a message alongside the exit code. The message appears in the terminal and in CI platform annotations.
Cleanup with defer
ctx.defer(callable, *args) registers a cleanup function that runs after the task returns, in LIFO order, even on failure or cancellation. Errors in deferred calls are logged but do not change the exit code. The pattern is borrowed from Go’s defer.
ctx.defer for any resource that must be cleaned up regardless of how the task exits: file handles, temp files, running subprocesses.
Querying the build graph
ctx.bazel.query() exposes Bazel’s query engine. Set an expression with .raw(), then .eval() to get back an iterable TargetSet:
deps(), rdeps(), kind(), attr(), somepath(), set operations, and so on:
ctx.bazel.info() returns a dict of every key Bazel exposes (output_base, execution_root, workspace, release, etc.):
Task identity and phases
ctx.task describes the currently-running task. Use it to log identifiers that show up in CI annotations, or to break long-running work into named phases that surface in status displays:
| Field | Description |
|---|---|
ctx.task.name | Task name (e.g. "build", "my-cmd") |
ctx.task.key | Human-readable invocation key (e.g. "fluffy-parakeet") |
ctx.task.id | UUID v4 unique per invocation |
ctx.task.elapsed_ms | Wall time in milliseconds since the task spawned |
ctx.task.phase(name, description="", emoji="", display_name="") marks the start of a new phase. The currently-active phase closes automatically when the next one starts or when the task returns.
