2.2.3 Static Action Graph
A static action graph means that once Bazel finishes the analysis work described in 2.2.1 Loading, Analysis & Execution, it already knows the ordinary build plan: which actions exist, which artifacts they read, and which artifacts they are expected to write. Execution may skip some of that work because of cache hits, but it is not supposed to discover the normal graph by reading generated files on the fly.1,2
- aquery reads the post-analysis graph without running compilers
- Actions and declared outputs are known before execution starts
- Supported input discovery, such as C/C++ depfiles, is included in dependency checks and cache correctness
- Remote execution receives actions through Bazel's supported protocols
- A compiler result says to register another compile or link action
- A tool produces output files that the rule never declared
- Rule logic that needs artifact bytes to decide sibling actions
- Fix: model the structure earlier or use an API designed for it
Analysis writes the plan
Loading creates the target graph from BUILD and .bzl declarations. Analysis then turns that target graph into an action graph by running rule implementation functions and registering actions.1,2,3 Those implementation functions do not run compilers, unzip archives, or inspect file contents. They describe future work.
That description has to be concrete enough for Bazel to trust it. The rule contract is explicit here: if your rule runs unzip, you still have to tell Bazel which files you expect before unzip executes.3 File objects in analysis are therefore graph nodes, not open files you can read. They exist so rule code can wire inputs, outputs, and providers together before any tool starts.3
Decide: A generator action will write a manifest, and the manifest's contents decide how many files should come out of the next step. Can the rule implementation read that manifest during analysis and declare exactly those outputs?
Reveal
No. The rule would be trying to learn the build graph after the phase that must declare it. During analysis, the manifest is a File node, not bytes that Starlark can open.
The repair depends on what later consumers need. If downstream targets need named files, model that list earlier and declare those outputs up front. If only a tool needs "whatever this generator produced," declare a bounded shape such as a directory artifact and validate the manifest inside an execution action. What you cannot do is let ordinary execution discover new graph edges after Bazel has already planned scheduling, caching, and remote execution.
Why Bazel wants that boundary
Knowing the graph early buys Bazel both observability and scheduling freedom. The graph-inspection tools are the user-visible proof:
bazel query 'deps(//app:server)' # loading-phase target graph
bazel aquery '//app:server' # post-analysis action graph
Those commands work at different layers, but neither needs to run your compiler just to discover the structure of the build.4,5 The same up-front description lets Bazel compute action fingerprints, plan parallel work, and hand actions to remote execution or caches without a second "discover inputs later" protocol in the middle.5,6
What ordinary rules cannot do
This is the practical restriction behind the slogan. During loading and analysis, tools cannot run and rules cannot perform arbitrary file I/O beyond evaluating BUILD and .bzl files.4 A rule therefore cannot compile one file, read the generated metadata, and then decide which sibling actions to create. If some check depends on artifact contents, that check must itself be an execution-phase action, not analysis logic.3,4
That constraint is stricter than "Bazel likes declarative style." It is part of how Bazel keeps incremental correctness and remote execution tractable. The rules API still reflects that historical phase split even though Bazel internals have become more flexible over time.6
Where this shows up in real languages
Haskell is a clean example because the compiler naturally exposes a module dependency graph that is finer-grained than a typical haskell_library target. Bazel will not compile first and ask questions later. If you want module-level incrementality, you usually have to surface that graph earlier with explicit haskell_module targets or generate those declarations with tooling such as Gazelle.7,8
That same pressure appears in OCaml and C++20 modules: the difficult cases are languages where "compile this file" reveals dependency edges Bazel would have preferred to know during analysis. In ordinary Starlark rules, the answer is not "let execution discover the graph." The answer is usually "model the graph earlier" or "use a specialized mechanism built for that case."7
If Bazel cannot know an input, output, or dependency edge until a tool has already started running, ordinary analysis-time rules are already too late. Static action graph means Bazel wants the build plan first and tool execution second. When that feels awkward, the fix is usually to expose the graph earlier, not to let execution improvise it.1,3,7
The boundary is getting escape hatches
Newer APIs such as ctx.actions.map_directory() show that Bazel is willing to relax the old boundary in tightly scoped ways. In Bazel 9+, that API can defer limited action creation until a tree artifact's directory contents exist, but the scope is still declared up front: the input directories, output directories, tools, and callback shape are fixed before execution starts.9,10
That is not the everyday model to rely on at Level 2. It is better understood as an experimental exception that proves the rule. 4.12 Experimental Rule Patterns revisits it.9,10
Check your understanding · 3 questions
1.Why must a rule declare all expected output files from an unzip action before that action runs, even though the file list is only known after unzipping?
Select one answer
2.True or false: constraints of the static action graph.
Choose True or False for each sentence
File objects in Starlark are graph nodes that wire providers together, not open files whose contents a rule can read.3.Which pair of commands demonstrates that Bazel can inspect its build plan at two different graph layers without running any compile actions?
Select one answer
Footnotes
-
Extension Overview — analysis generates the action graph and requires outputs to be listed before commands execute ↩1 ↩2 ↩3
-
Bazel Glossary — definitions of action graph and analysis phase ↩1 ↩2
-
Rules — implementation functions run in analysis,
Fileobjects are not readable file contents, and actions must know inputs and outputs in analysis ↩1 ↩2 ↩3 ↩4 ↩5 -
Frequently Asked Questions — no tool execution or arbitrary file I/O during loading and analysis ↩1 ↩2 ↩3
-
Build programs with Bazel —
querysits on loading,aqueryis a post-analysis action-graph query, and analysis determines the work to do ↩1 ↩2 -
Challenges of Writing Rules — the rules API still reflects the historical phase split and remote execution requires inputs ahead of time ↩1 ↩2
-
Haskell Builds at Scale: Comparing Bazel and Buck2 — static action graph limits compiler-discovered dependency graphs and motivates special handling for Haskell, OCaml, and C++20 modules ↩1 ↩2 ↩3
-
Incremental builds for Haskell with Bazel — explicit
haskell_moduletargets and Gazelle-generated declarations as a Bazel-friendly pattern ↩ -
Bazel Dynamic Actions —
map_directory()as a constrained execution-time escape hatch ↩1 ↩2 -
actions — the
map_directory()API requires input/output directories and callback structure to be declared up front ↩1 ↩2