4.12.3 Subrules
extraSubrules are an experimental way to split a rule implementation into reusable, encapsulated analysis-time building blocks. They are for shared rule logic that needs its own private tools or dependencies, but should still be called from an ordinary rule implementation rather than exposed as a separate BUILD target.1
Composition Instead Of Inheritance
The previous item, 4.12.2 Rule Extension API: parent + ctx.super(), is about IS-A composition: a child rule extends a parent rule and calls ctx.super() to reuse the parent's behavior. A subrule is the complementary HAS-A shape. A rule can have a linting step, a validation step, a compile helper, or a metadata-producing helper without becoming a subclass of another rule.
That distinction matters when the shared part is smaller than a rule. A binary rule is a useful mental model: compile and validate sources, reuse library logic, and link the result. Some of those pieces can also be useful inside a library rule, so putting them behind reusable subrules avoids copying Starlark helper code across related rule implementations.2
What A Subrule Encapsulates
A subrule is created with the global subrule() function. The current .bzl API reference documents implementation, attrs, toolchains, fragments, and nested subrules parameters.3 Unlike a rule, it does not define public BUILD-file attributes. Its attrs are private label or label-list attributes, and Bazel resolves those implicit dependencies for the subrule implementation.4
That is the encapsulation point. If an Android lint helper needs a lint wrapper executable, the outer java_library-shaped rule should not have to declare _android_lint_wrapper just so a helper function can find it. A subrule packages its implementation function, implicit dependencies, and private attributes. Only the subrule can access those private dependencies.5
The subrule implementation receives a deliberately small context, not the outer rule's full ctx. It can receive arbitrary parameters from the caller, receive injected tools from its private attributes, declare actions, call other subrules, and work with providers.6 That makes the call feel like a normal helper function, but with Bazel-enforced access to the analysis-time pieces the helper is allowed to use.
How A Rule Uses One
The outer rule declares which subrules it may call by passing them through rule(subrules = [...]). The same API reference also exposes subrules on aspects and on subrule() itself, so a subrule can be composed from smaller subrules when the decomposition is useful.7
In shape, the relationship looks like this:
def _lint_impl(ctx, srcs, _lint_tool):
out = ctx.actions.declare_file(ctx.label.name + ".lint.txt")
ctx.actions.run(
executable = _lint_tool,
inputs = depset(srcs),
outputs = [out],
)
return out
_lint = subrule(
implementation = _lint_impl,
attrs = {
"_lint_tool": attr.label(
default = "//tools:lint",
executable = True,
cfg = "exec",
),
},
)
def _library_impl(ctx):
lint_result = _lint(srcs = ctx.files.srcs)
# Return DefaultInfo and any providers the rule exposes.
my_library = rule(
implementation = _library_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
},
subrules = [_lint],
)
Treat this as an example of the current API, not as code to copy unchanged. Subrules are explicitly experimental and version-sensitive. Check current code against the Bazel version and API docs you support.8 The smallest
runnable probe pins Bazel 9.1.0:
bazel build //:subrule_copy calls a declared subrule whose private executable
copies the caller's source. Read its
subrule.bzl as evidence
for the encapsulated private-tool path, not as a compatibility promise for a
different Bazel release.
When This Is Better Than A Helper Function
A plain Starlark helper function is still enough when all the helper needs is ordinary data passed by the caller. For example, formatting an output filename or converting provider fields into command-line arguments does not need a subrule.
Subrules become interesting when the helper needs rule-like capabilities of its own: a private executable, a private label dependency, toolchain access, a configuration fragment, a nested helper, or the ability to declare actions while being isolated from the outer rule's public attributes. The design goal is to enforce rule-architecture guidelines through API structure rather than relying only on documentation.9
That enforcement helps large rulesets. If every helper receives the full ctx, helper code can accidentally depend on unrelated public attributes, features, or output conventions. A subrule receives a smaller context and explicit parameters, so the caller has to choose what crosses the boundary.
What It Does Not Solve
Subrules do not make the build graph dynamic. The consuming rule still participates in loading and analysis like any other rule, and its public attributes still define the BUILD-file API. If the problem is "create actions after seeing the contents of a tree artifact," that belongs to ctx.actions.map_directory() in 4.12.1 Dynamic Actions with ctx.actions.map_directory(), not subrules.
They also do not replace provider design. A subrule can construct or consume providers, but the rule still returns the public provider contract seen by downstream targets. Use the provider-as-interface habits from 4.2.8 Provider-as-Interface Pattern when the data crosses target boundaries. Use subrules when you are organizing implementation logic inside one rule or rule family.
Finally, subrules are not the stable default for everyday rule authoring. The API is experimental work being pressure-tested on internal and baseline rulesets, with Java rules as an early playground.10 Treat them as an advanced design tool for ruleset authors who are already comfortable with actions, providers, private attrs, and toolchains.
Use a subrule when a reusable piece of rule implementation needs its own private analysis-time dependencies or toolchain context. Use ctx.super() when one rule extends another rule's public contract. Use an ordinary helper function when all you need is pure Starlark reuse.
Check your understanding · 3 questions
1.When is a subrule a better fit than an ordinary Starlark helper function?
Select one answer
2.Which statements capture the responsibility split in subrule composition?
Select all that apply
3.True or false: subrule responsibilities and calling rules.
Choose True or False for each sentence
rule(subrules = [...]).Footnotes
-
.bzl files - current global API reference documents experimental
subrule()construction andsubrulesdeclarations on rules and aspects. ↩ -
Bazel Rules - subrules section introduces reusable pieces such as compile/validate and linking logic that can be shared across rule implementations. ↩
-
.bzl files -
subrule()parameters in the current global.bzlAPI reference. ↩ -
.bzl files - subrule
attrsare private label or label-list attributes whose resolved values are passed to the implementation. ↩ -
Bazel Rules - Android lint subrule example with private attributes and implicit dependencies visible only to the subrule. ↩
-
Bazel Rules - subrule implementation receives a small context, arbitrary parameters, and injected tools. It can declare actions, call subrules, and work with providers. ↩
-
.bzl files -
subrulesparameter onrule(),aspect(), andsubrule(). ↩ -
Bazel Rules - roadmap notes describe the API as experimental and version-sensitive. ↩
-
Bazel Rules - motivation for replacing unenforced coding guidelines with an API that encourages encapsulated structure. ↩
-
Bazel Rules - pressure-testing and Java-rules playground for subrule adoption. ↩