Selecting what runs
lattice run always starts from the full task graph for the tasks you name —
see Task graph for how that graph is built. This
page covers the flags that change what part of it actually executes, and how
it executes: --filter, --dry-run, --continue, --concurrency, naming
several tasks at once, and --sequentially.
Filtering to one workspace
-f/--filter <PATTERN> keeps only the workspaces whose name contains
PATTERN, before the task graph is built:
lattice run build --filter core
It matches against the workspace’s name field, not its path, and it’s a
substring match, not a glob — --filter co matches a workspace named core
just as well as one named docs-core. Only one --filter is accepted per
run; passing it twice is a clap error, not two patterns:
error: the argument '--filter <PATTERN>' cannot be used multiple times
If nothing matches, that’s a clean no-op, not a failure — lattice run build --filter nonexistent prints lattice: no workspaces matched filter 'nonexistent'. and exits 0.
Filtering does not pull in dependencies
This is the part worth reading carefully. Filtering removes non-matching
workspaces from the set before the dependency graph is built, so an edge
into a filtered-out workspace is never created — it isn’t run first, and it
isn’t an error either. Take this repo’s own lattice.json, where
lattice-runner depends on dagger, lattice-cache, lattice-output,
lattice-config, and lattice-workspace:
$ lattice run build --dry-run --filter lattice-runner
❖ lattice dry run · build
→ lattice-runner:build cargo build
Every one of lattice-runner’s dependencies is gone from the graph, and
lattice-runner:build runs on its own — against whatever those dependencies
last produced, cached or not. Compare the unfiltered graph:
$ lattice run build --dry-run
❖ lattice dry run · build
→ web:build npm run build
→ lattice-output:build cargo build
→ lattice-config:build cargo build
→ lattice-cache:build cargo build
→ lattice-workspace:build cargo build
→ dagger:build cargo build
→ lattice-runner:build cargo build
→ lattice:build cargo build
Use --filter to re-run one workspace’s task in isolation once you know its
dependencies are already up to date; don’t reach for it as a shortcut for “and
everything it needs,” because that isn’t what it does. If you want the
dependency closure too, drop --filter and let the full graph run — a cache
hit makes the already-current dependencies cheap, not repeated work.
Previewing with --dry-run
--dry-run prints the resolved task graph in topological order and exits
without running or caching anything:
$ lattice run lint check build --sequentially --dry-run
❖ lattice dry run · lint (phase)
→ lattice:lint cargo lint
→ lattice-runner:lint cargo lint
...
❖ lattice dry run · check (phase)
→ lattice:check cargo check
...
❖ lattice dry run · build (phase)
→ web:build npm run build
...
Each line is workspace:task followed by the exact resolved shell command for
that node — the same command the runner would hand to sh -c (or cmd /C on
Windows). Reading top to bottom shows the order the scheduler would honor,
though independent branches can still run concurrently in a real run; nothing
is guaranteed to happen at the same wall-clock moment just because two lines
are adjacent. Without --sequentially, stacked tasks print as one merged
graph under a single banner; with it, each task gets its own banner and its
own topological order, one “(phase)” per named task, in the order you listed
them. --dry-run composes with --filter: the graph it prints is the same
narrowed graph a real run would execute.
Keeping going after a failure with --continue
By default, one task failing stops the run: nothing still queued starts, and
anything already running is left to finish or fail on its own, but no new work
begins. --continue changes that to: keep starting everything that doesn’t
depend, transitively, on the task that failed.
lattice run build --continue
A task that depends — directly or transitively — on a failed task is never
started; it’s marked skipped, with dependency failed as the reason, and
counted separately from tasks that actually failed. Everything outside that
downstream slice runs exactly as it would have without --continue. Either
way, lattice run exits non-zero if anything failed; --continue only
changes how much of the graph you get to see fail (or pass) in one invocation
instead of finding out about the second problem on your next run.
Capping parallelism with --concurrency
--concurrency N caps how many tasks the scheduler runs at once. Without it,
the cap is the number of logical CPUs available on the machine:
lattice run test --concurrency 4
The cap applies across the whole run, not per workspace — with --concurrency 1, two tasks that don’t depend on each other still run one at a time, in
whichever order the scheduler picks them up, never interleaved. It does not
change the graph itself: dependency order is still honored, and a task still
only starts once every task it depends on has finished (or restored from
cache). --concurrency 0 is treated the same as not passing the flag at all.
Naming several tasks in one run
lattice run takes one or more task names:
lattice run lint test build
By default these are merged into a single combined graph before anything
runs. A dependency shared by more than one of the named tasks — test
depending on build, say — appears once in that graph and runs once, and
independent tasks like lint parallelize alongside the rest instead of
waiting their turn. This is why the earlier lattice-runner example only
prints one build node even though several stacked roots could in principle
need it.
Running each task to completion with -s/--sequentially
-s/--sequentially turns that merging off. Instead of one combined graph,
each named task gets its own graph, and each graph runs to completion before
the next one starts:
lattice run lint test build --sequentially
For lint test build, that means: build and run every lint node to
completion, then build and run everything test needs (which pulls in
build again, from cache if nothing changed), then finally everything
build needs on its own. Reach for --sequentially when a later task’s
result should never race a concurrent instance of an earlier one — for
example, clean before build, where clean running concurrently with a
build task in another workspace would be a bug in your task definitions
that stacking would otherwise mask, not fix.
By default, a failure during one phase stops the run before the next phase
starts, the same fail-fast behavior described above. Combine it with
--continue and that stops being true across phases too — a failed phase is
recorded, but --sequentially moves on to the next phase regardless:
$ lattice run lint build --sequentially --continue --no-cache -l
lattice: running `lint` across 1 workspace(s)
app:lint: FAILED
lattice: 1 tasks, 0 cached, 1 failed, 0.00s
lattice: running `build` across 1 workspace(s)
app:build: done (0.00s)
lattice: 1 tasks, 0 cached, 0 failed, 0.00s
build here has no dependency on lint, so nothing marks it as downstream of
the failure — within each phase, --continue still only skips tasks that
actually depend on the one that failed. The run exits non-zero once every
phase has run, because at least one of them failed.