3.7.1 Bazel's Output Boundary

recommended

Bazel is excellent at two jobs: inspecting the graph implied by a build request, and materializing the requested outputs under Bazel's output tree. When a workflow needs to mutate the checkout, compare two revisions, publish to an external system, or sweep across targets that are not naturally one dependency closure, you have stepped outside Bazel's model.1,2 Seeing that boundary early matters for a maintainer: it tells you when to stop fighting BUILD files and start designing a workflow around Bazel instead of inside it.1

Which work belongs in Bazel — and what belongs in CI or a script?
Bazel handles one declared build request. A wider workflow uses its outputs and continues outside the build graph.
The problem A workflow must build or test, then publish results, compare revisions, update source files, or combine several Bazel requests. Should all of that become Bazel actions?
INSIDE BAZEL
One graph-shaped request
Declared inputs and outputs
Inspect
Read packages, targets, configurations, and actions with query, cquery, or aquery.
Produce
Build or test the requested closure and materialize its declared artifacts, logs, and status.
The request stays within one reachable graph snapshot.
THE BOUNDARY
bazel-out/
Declared outputs cross from Bazel's build model into the surrounding workflow.
bazel-bin
bazel run is one clear crossing: build the executable, then launch it outside the action graph.
OUTSIDE THE GRAPH
Orchestration spans the wider workflow
Beyond one graph snapshot
Consume outputs, then coordinate what comes next
A task runner or CI can combine Bazel requests with repository state, other revisions, external systems, and results from many targets.
source updates revision comparison publishing many requests
These workflows can use Bazel outputs without pretending the whole workflow is one build action.
Inside: inspect and produce. At the boundary: declared outputs. Outside: orchestrate beyond one graph snapshot.

What Bazel actually owns

The mental model from 2.2 Three Phases of a Build still holds. Loading and analysis determine the reachable target and action graphs. Execution runs the actions needed for the requested outputs. Those actions run relative to execroot, and the actual build outputs land under bazel-out. The familiar bazel-bin, bazel-out, and bazel-testlogs entries in the workspace are convenience symlinks into that tree, not special locations Bazel reasons about directly.2,3

That is why Bazel reduces to two capabilities: graph inspection (query, cquery, aquery) and output production (build, test). Even tests fit the same model: Bazel is still producing files and status for the requested targets, not becoming a general-purpose workflow engine.1,2

Why the boundary is deliberate

Bazel's incrementality depends on controlling the full input-to-output contract. It wants declared inputs, declared outputs, and an action graph fixed before ordinary execution starts. The output-directory docs make that concrete: actions execute in a workspace-shaped execroot, while actual build outputs live under bazel-out for a specific configuration.3 Work that writes back into the source tree, talks to external services, or needs a view wider than one requested target closure breaks that contract.1,2

This is also where 3.5 Developer Experience & Local Tooling stops being the right lens. That section is about making Bazel easier to drive locally: IDE sync, tool distribution, wrappers, and CLI extensions. Here the question is narrower and more structural: does the workflow belong in Bazel's output boundary at all? If not, the right design is usually "Bazel as one step in a larger task", not "one more clever target".1,2

Boundary smells

A workflow usually belongs outside the graph when one of these is true:

  • The result is not "a declared file under bazel-out" but "the checkout changed", "an artifact was published", or "a deployment happened".1,2,4
  • The step needs information Bazel intentionally does not model as one build request, such as "the entire repository" or "the outputs from base commit versus this commit".1
  • You are tempted to invent a giant collector target just so another tool can consume many unrelated outputs. That often destroys incrementality by forcing whole-repo loading or analysis for a task that should have been orchestration.1
  • The workflow has multiple phases with different tools or flags, where Bazel produces intermediate outputs but some later step aggregates, uploads, compares, or rewrites them.1,2

3.7.2 Workflows Outside Bazel takes these smells and turns them into concrete patterns: whole-repo archives, compare-against-base checks, Gazelle, coverage, and query-driven target collection. 3.7.3 Aspect Extension Language (AXL) shows one structured answer when shell scripts and Makefiles stop scaling.1,2

bazel run is an explicit boundary crossing

Leaving the graph does not mean abandoning Bazel. The official "custom verbs" tutorial shows a cleaner pattern: keep the pure build output as one target, then generate a second executable target that performs the side effect when you invoke it with bazel run.4

sphinx_site(
    name = "docs",
    srcs = ["index.md", "providers.md"],
)
# Also creates :docs.publish

bazel build :docs stays inside Bazel's contract: produce the site under the output tree. bazel run :docs.publish is different on purpose: it launches a script that publishes that already-built output.4 The same pattern appears in golden-file acceptance targets such as .accept.4 The important distinction is not "Bazel versus not Bazel". It is "ordinary build action versus explicit task that uses Bazel outputs". The workflow-orchestration notes genrule shows the inside-the-boundary side — declared inputs, declared outputs, no source-tree writes — and the paired notes.publish sh_binary is the outside-the-boundary side: a launcher that takes that artifact through runfiles and writes it under BUILD_WORKSPACE_DIRECTORY/out/published/.

extra

bazel-bin is a view, not the boundary itself

In the workspace root, bazel-bin points at the most recently written binary directory for the current configuration, bazel-testlogs points at the test log directory, and bazel-out points at the broader output path. Bazel creates these symlinks only as a convenience for humans, and only when the workspace root is writable. Internally it works from outputBase/execroot/....3 When you need to reason about "inside Bazel" precisely, think in terms of declared outputs under the output tree, not whatever happens to be easy to cd into.

key takeaway

Bazel owns graph inspection and declared outputs under the output tree. If a workflow needs source-tree writes, cross-revision comparison, whole-repo aggregation, or external side effects, keep Bazel as the build engine and move the orchestration around it. Use the boundary deliberately: build and test stay inside it, bazel run can cross it explicitly, and 3.7.2 Workflows Outside Bazel covers the recurring cases.

Check your understanding · 3 questions

1.Alex Eagle describes Bazel as having two capabilities. What are they, and what falls outside them?

Select one answer

2.True or false about Bazel's output boundary:

Choose True or False for each sentence

bazel-bin is a stable API path that build tooling should use to locate artifacts.
A 'bazel run :target.publish' target that publishes already-built outputs is an appropriate way to cross the output boundary intentionally.
A giant collector target that depends on every package is a good way to build the entire repo for release archives.

3.Which of the following is a 'boundary smell' indicating a workflow belongs outside the Bazel build graph?

Select one answer

0 of 3 answered

Footnotes

  1. The 'outside of Bazel' pattern — graph inspection plus output-tree framing, collector-target anti-pattern, whole-repo archive pattern, compare-against-base pattern, and Gazelle as a pre-build step 1 2 3 4 5 6 7 8 9 10

  2. Sponsored Lightning Talk: Beyond Make Serve: Starlarkification for Tasks - Alex Eagle, Aspect Build — Bazel's "two jobs" framing, the task-runner gap, and examples of source-tree writes, repo-wide tasks, and CI wrapping around Bazel 1 2 3 4 5 6 7 8

  3. Output Directory Layoutexecroot, bazel-out, and the workspace symlinks Bazel exposes for outputs and test logs 1 2 3

  4. Using Macros to Create Custom Verbs.publish / .accept launcher-target pattern for explicit side effects via bazel run 1 2 3 4