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

Can a compiler change Bazel's action graph after execution starts?
It cannot add arbitrary actions, outputs, or graph edges. Supported tools may report the inputs they actually used.
load analyze execute
Analysis
Create actions and declare outputs before tools run
Commands, graph edges, declared files
known graph before tools run
bazel aquery '//app:server'
Execution
Run tools using the declared action graph
Some tools report the inputs they used
cache keys remote workers
Discovered inputs still affect dependency checks and cache keys
Why it matters
One known plan enables inspection and distribution
Bazel can inspect and schedule it
  • 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
What does not fit
Run a tool, then create new actions or outputs
Too late for the normal action graph
  • 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
ctx.actions.map_directory() can create limited actions from a declared tree artifact — see 4.12.1 Dynamic Actions with ctx.actions.map_directory().
Tools may report effective inputs through supported discovery. They cannot freely add actions, outputs, or graph edges after execution starts.

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

think

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

key takeaway

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

extra

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

During analysis, File objects in Starlark are graph nodes that wire providers together, not open files whose contents a rule can read.
Because Bazel internals have become more flexible, ordinary rules can now run tools during analysis to discover additional dependency edges.

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

0 of 3 answered

Footnotes

  1. Extension Overview — analysis generates the action graph and requires outputs to be listed before commands execute 1 2 3

  2. Bazel Glossary — definitions of action graph and analysis phase 1 2

  3. Rules — implementation functions run in analysis, File objects are not readable file contents, and actions must know inputs and outputs in analysis 1 2 3 4 5

  4. Frequently Asked Questions — no tool execution or arbitrary file I/O during loading and analysis 1 2 3

  5. Build programs with Bazelquery sits on loading, aquery is a post-analysis action-graph query, and analysis determines the work to do 1 2

  6. Challenges of Writing Rules — the rules API still reflects the historical phase split and remote execution requires inputs ahead of time 1 2

  7. 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

  8. Incremental builds for Haskell with Bazel — explicit haskell_module targets and Gazelle-generated declarations as a Bazel-friendly pattern

  9. Bazel Dynamic Actionsmap_directory() as a constrained execution-time escape hatch 1 2

  10. actions — the map_directory() API requires input/output directories and callback structure to be declared up front 1 2