Bazel subrules are a lesser-known mechanism for splitting rule functionality into smaller, reusable building blocks. They are designed to improve rule architecture by encapsulating implicit dependencies, toolchains, and action logic — without passing the entire ctx (“god object”) around.
In the past, we achieved similar reuse through plain Starlark helper functions. While that works, it often requires threading ctx through multiple layers, which reduces encapsulation and makes refactoring harder.
Subrules
A subrule is similar to a rule in that it can create actions during analysis. However, it operates under strict encapsulation constraints and exposes only a reduced context API.
Let’s start with a simple example.
def _hello_subrule_impl(ctx, name):
hello_file = ctx.actions.declare_file(ctx.label.name + name + ".txt")
return hello_file
hello_subrule = subrule(
implementation = _hello_subrule_impl,
)
def _hello_impl(ctx):
hello_file = hello_subrule(name = "Example")
ctx.actions.write(
output = hello_file,
content = "Hello, world!",
)
return [
DefaultInfo(files = depset([hello_file])),
]
hello = rule(
implementation = _hello_impl,
subrules = [hello_subrule],
)
- We define
hello_subruleusingsubrule()and provide its implementation. - The implementation function receives a restricted
ctx(aSubruleContext) and any additional parameters. - When defining the
hellorule, we must declaresubrules = [hello_subrule]. - Inside the rule implementation, we call the subrule like a normal function:
hello_file = hello_subrule(name = "Example")
We do not call it via ctx. The ctx argument is automatically injected.
Subrules may return arbitrary values — not just providers.
Important: Subrules Must Be Declared
If you call a subrule but forget to list it in the subrules = [...] parameter of the rule (or another subrule), Bazel raises a runtime error during analysis.
What You Can and Cannot Do
Subrules can create actions just like rules, but they are intentionally constrained for better encapsulation.
All Attributes Must Be Private
Subrules can declare only implicit dependencies, and their attribute names must begin with an underscore.
This results in an error:
hello_subrule = subrule(
implementation = _hello_subrule_impl,
attrs = {
"compiler": attr.label(),
},
)
Error in subrule: illegal attribute name 'compiler': subrules may only define private attributes (whose names begin with '_').
Attributes Must Be label or label_list
Subrule attributes may only be attr.label() or attr.label_list().
Other attribute types (e.g., attr.string(), attr.bool()) are not allowed:
attrs = {
"_compiler": attr.string(),
}
Produces:
Error in subrule: bad type for attribute '_compiler': subrule attributes may only be label or lists of labels.
Private attributes Require Defaults
Because subrule attributes are private implicit dependencies and cannot be set by users of the rule, they must define a default value (or a late-bound default label).
They must be spelled out as implementation function arguments.
The Context Is Restricted
Subrules receive a SubruleContext, not a full RuleContext.
Attempting to access ctx.attr will fail:
def _hello_subrule_impl(ctx, name):
print(ctx.attr.surname)
return None
Error: 'subrule_ctx' value has no field or method 'attr'
Available Fields on subrule context:
In the current API, the available members are:
actionsfragmentslabeltoolchains
Toolchains and Execution Groups
Subrules can declare:
subrule(
implementation = _implementation,
toolchains = [...],
exec_group = ...,
subrules = [...],
)
This allows:
- Toolchain requirements per subrule
- Encapsulation of execution groups
Only one exec group per subrule is supported.
Nested Subrules
A subrule may declare and call other subrules by using the subrules = [...] parameter.
Why Subrules Matter
Subrules address several long-standing architectural issues in Starlark rule design:
- Avoid passing
ctxeverywhere - Encapsulate implicit dependencies
- Encapsulate toolchain resolution
- Improve composability
- Make helper-function patterns more structured
Takeaways
Subrules are a powerful architectural improvement for composing complex Bazel rules, just remember:
- You must declare subrules using
subrules = [...]. - You call a subrule like a function — not through
ctx. ctxis automatically injected.- Attributes must be private and label-based.
- The context is intentionally restricted.
- Subrules operate only at analysis time.
They are not replacements for existing mechanisms, but they are a much cleaner way to encapsulate reusable rule logic compared to traditional helper functions.
As adoption increases, they will likely become a default way of composing complex rules.