5.2.1 bazel query — Static Graph Analysis

bazel query answers questions about the dependency graph declared in BUILD files. It is the fastest member of the query family because it works after loading, before Bazel resolves build options, platforms, transitions, or select() branches.1 Use it when the question is structural: “what can this target reach?”, “what reaches this library?”, or “why is there an edge between these two areas?”

That boundary matters. query reports the unconfigured graph, not the exact graph for one build. When the answer depends on a platform or flag, move to 5.2.2 bazel cquery — Configured Graph. When it depends on compiler invocations or artifacts, move to 5.2.3 bazel aquery — Action Graph. This is the deeper version of the three-layer map introduced in 2.1.5 Inspecting the Graph — Query Preview.

Why does //app:runner reach //lib:core?
bazel query walks the declared, unconfigured graph. The expression selects nodes. The output format only changes their representation.
UNCONFIGURED TARGET GRAPH
deps() follows arrows → rdeps() walks them ← only inside its chosen universe
//app:runner
//app/data:repo
//service:menu
//config:app_config
//lib:core
DECLARED ROUTE A
//app:runner
//app/data:repo
//config:app_config
//lib:core
DECLARED ROUTE B
//app:runner
//service:menu
//lib:core
SOMEPATH · ONE WITNESS
Proves that at least one route exists
//app:runner → //app/data:repo → //config:app_config → //lib:core
Bazel may return a different witness. It is not promised to be shortest or unique.
ALLPATHS · COMPLETE SUBGRAPH
Keeps every node that lies on any route
ROUTE A //app:runner → //app/data:repo → //config:app_config → //lib:core
ROUTE B //app:runner → //service:menu → //lib:core
This is a subgraph, not a promised list of paths. For readable graph output, narrow first: --output=graph
Navigate first with deps, rdeps, and path functions. Filter next with kind() or filter(). Then choose output for the next reader.

Start With a Set, Then Transform It

A query expression evaluates to a set of targets. A target pattern creates the initial set. Functions navigate or filter it. Set operators combine results. This gives investigations a useful rhythm:

# A package subtree: where should Bazel look?
bazel query //app/...

# A transitive closure: what can this target reach?
bazel query 'deps(//app:runner)'

# A filtered closure: which source files can it reach?
bazel query 'kind("source file", deps(//app:runner))'

Quote the whole expression. Parentheses, *, and set operators otherwise risk interpretation by the shell instead of the query parser.2 The basic-query snippet is the running example for the four core graph questions below: dependencies, reverse dependencies, one path, and all paths. It continues the first deps() walk from 1.1.8 Basic Query.

Target patterns and query functions solve different problems. //app/... selects targets by package location. deps(//app:runner) follows graph edges, so it may leave //app and include source files, generated targets, implicit dependencies, and external repositories. The target-pattern syntax itself is introduced in 0.2.3 Target Patterns.

Four Graph Questions Worth Memorizing

What does this target depend on?

bazel query 'deps(//app:runner)'

deps() returns the target plus its transitive dependency closure. Add a depth to stop after a fixed number of edges: deps(//app:runner, 1) is a compact view of immediate dependencies.1

By default the result can include implicit and toolchain-related edges. Keep them when diagnosing Bazel's complete declared graph. Add --noimplicit_deps when you deliberately want a simpler view of dependencies written by repository authors.2 Say which view you used when sharing results—a “surprising” edge may simply be an implicit dependency hidden by the second command.

What depends on this target?

bazel query 'rdeps(//..., //lib:core)'

rdeps(universe, targets) walks edges in reverse. The first argument is not decoration: Bazel first computes the transitive dependency closure of that expression, then searches for reverse dependencies inside that graph. Here it starts with deps(//...). The result includes //lib:core and consumers such as //service:menu and //app:runner. The universe does not mean “only labels matching its root pattern”. It means the dependency closure of those roots.1

Choose universe roots whose dependency closure covers the consumers relevant to the decision. Repository-wide roots are appropriate for a breaking public API change. Service roots can focus a local impact question, but their dependencies remain part of the searchable graph. A third argument limits reverse depth. With depth 1, the result includes the seed //lib:core at depth 0 and its direct consumers, including //service:menu, at depth 1.

Why does A depend on B?

bazel query 'somepath(//app:runner, //lib:core)'

somepath() returns one dependency path from the first set to the second, or an empty result if none exists. It is the quickest explanation for an unexpected transitive dependency. The path is a witness, not necessarily the shortest or only path.1

When removing a dependency, one witness is not proof that all routes are gone:

bazel query 'allpaths(//app:runner, //lib:core)'

allpaths() returns every node lying on any path between the two sets.1 On a highly connected graph that subgraph can be large, so begin with somepath() and expand only when the investigation needs completeness.

In the running example, //app:runner reaches //lib:core through both the configuration branch and the service branch. somepath() chooses one witness. allpaths() keeps the nodes from both routes. The query does not promise which witness somepath() prints, so the useful comparison is one route versus the complete path subgraph, not a fixed label sequence.

think

Trace: You remove the edge shown by somepath(), but the dependency remains. What did the first query establish—and what did it not establish?

Reveal

It proved that at least one path existed. It did not prove that this was the only path. Rerun somepath() or inspect allpaths().

Narrow Before You Render

Filters are most valuable when they express the question, not merely when they reduce terminal output:

# Filegroups that may be affected by a library change
bazel query \
  'kind("filegroup rule", rdeps(//..., //lib:core))'

# Dependencies whose labels are in external repositories
bazel query 'filter("^@", deps(//app:runner))'

kind(pattern, expression) matches a regular expression against a target's kind. Kind strings include text such as rule, so anchor patterns when an exact classification matters. filter(pattern, expression) matches labels instead.1 This distinction prevents a common error: using filter() to search rule kinds or kind() to search package names.

Set operations make comparisons explicit:

bazel query \
  'deps(//app:runner) except deps(//app:runner, 1)'

The query language supports union, intersect, and except (also +, ^, and -). All three operators have equal precedence, so use parentheses rather than relying on visual intuition in a mixed expression.1

Pick Output for the Next Reader

The expression decides which graph nodes are in the answer. --output decides how Bazel represents them.

NeedOutputWhy
Read or pipe labelslabelOne label per line. The default
See target kindslabel_kindAdds rule/file classification
Inspect loaded rule valuesbuildReconstructs BUILD-like rule calls
Visualize a small subgraphgraphEmits Graphviz DOT
Feed durable automationproto, streamed_proto, or streamed_jsonprotoStructured output avoids parsing display text

--output=build is particularly useful when macros obscure a target's origin: the reconstructed rule includes expanded attributes and generator metadata or a macro call trace where available.3 It still shows the loading-phase view. its BUILD-like appearance does not mean that select() has been resolved. See 4.3.2 Macro Expansion Inspection for the focused macro-debugging workflow.

Graph output should be the last step, after narrowing:

bazel query \
  'allpaths(//app:runner, //lib:core)' \
  --output=graph | dot -Tsvg > dependency-paths.svg

A DOT rendering of deps(//...) is usually an unreadable hairball. A path, depth bound, kind filter, or scoped universe turns the same renderer into an explanation.2

For scripts, prefer a structured format over scraping build or Graphviz text. For very long generated expressions, --query_file avoids shell command- length limits.2 Repeatedly launching one query per target also scales poorly. combine work into a larger query when possible, and use a purpose-built graph extraction mechanism when a tool needs rich data for thousands of targets.2

Read Empty and Partial Results Carefully

An empty result can mean “there is no path in this unconfigured graph,” “the dependency closure of the reverse-dependency universe roots excluded the consumer,” or “your filter removed the matching nodes.” Reduce a complex expression from the inside out: first inspect the seed pattern, then the closure, then each filter.

In a large repository, --keep_going can preserve results from unaffected packages after loading errors. Those results are partial evidence, not a clean bill of health. Retain the command's exit status and diagnostics when using the output in automation.1

Finally, treat configuration-sensitive answers as a handoff, not a query trick. Because query sees all declared select() branches, an optional dependency may appear even when the active build would not choose it.4 Re-run the same structural question with 5.2.2 bazel cquery — Configured Graph when the decision asks what a specific build configuration actually uses.

key takeaway

Use bazel query for the declared, unconfigured target graph. Seed a bounded set, navigate it with deps, rdeps, somepath, or allpaths, narrow before rendering, and choose output for a human or a machine. Always state the universe, including that it means the dependency closure of its roots, plus the implicit-edge policy and graph layer behind the answer. Switch to cquery when configuration can change that answer.

Check your understanding · 4 questions

1.A dependency appears in bazel query even though the active platform's select() branch does not use it. What is the best next step?

Select one answer

2.You run rdeps(//service/..., //lib:core), but a known consumer under //app is absent. What should you check first?

Select one answer

3.After somepath() reveals one route from //app:runner to //lib:core, which conclusions and actions are justified?

Select all that apply

4.Match each investigation need to the query or output choice that directly addresses it:

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
Keep only filegroup rules from a result set
Keep only labels in external repositories
Inspect loaded rule attributes and macro provenance
Feed results to durable automation
0 of 4 answered

Footnotes

  1. Bazel Query Language reference — graph semantics, functions, set operators, ordering, and output formats 1 2 3 4 5 6 7 8

  2. Bazel Query Deep Dive — practical scoping, quoting, output, robustness, and tooling guidance 1 2 3 4 5

  3. Legacy macro debugging — expanded rule inspection and macro provenance with query output

  4. Configured Query reference — loading-phase query versus configuration-resolved target graphs