4.2.1 Rule Function
A rule function is the declaration of a new target kind. It gives Bazel a callable symbol for BUILD files, names the implementation function Bazel will run during analysis, declares the attributes users may set, and defines the boundary for outputs and providers.1 The implementation does not execute the compiler, formatter, or generator itself. It describes the actions Bazel may run later, if the requested outputs need them.2
The Smallest Useful Rule
A Starlark rule starts in a .bzl file with two parts: a private implementation function and a global value assigned from rule().3
def _mini_message_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(
output = out,
content = ctx.attr.message + "\n",
)
return [DefaultInfo(files = depset([out]))]
mini_message = rule(
implementation = _mini_message_impl,
attrs = {
"message": attr.string(
default = "hello",
doc = "Text written into the generated file.",
),
},
doc = "Writes a small text file.",
)
The name mini_message is the rule kind users load and call from BUILD files. Each call instantiates a target with a normal name plus the rule's declared attributes.4
load("//tools/messages:defs.bzl", "mini_message")
mini_message(
name = "greeting",
message = "hello architect",
)
Even this tiny rule shows the whole skeleton: rule() defines the public shape, _mini_message_impl(ctx) receives the analyzed target, ctx.actions.declare_file() names a generated output, ctx.actions.write() registers the generating action, and DefaultInfo(files = ...) tells Bazel which file belongs to the target's default build result.5 When a shell one-liner is enough, stay with 3.4.1 Genrule (The Escape Hatch). Reach for a custom rule when you need providers, toolchains, or a reusable target kind.
rule() Defines The Target Contract
The rule() call is not a helper function that runs when the target builds. It creates a rule symbol, and Bazel requires that symbol to be stored in a global variable in a .bzl file.6 The global variable's name becomes the rule's public name, which is why production rulesets treat these exported rule symbols as API.
The most important parameters for a first rule are:
| Parameter | What it decides |
|---|---|
implementation | Which analysis-phase function handles each target of this rule kind.7 |
attrs | Which rule-specific attributes the BUILD author may set, and how Bazel validates them.8 |
doc | Documentation text tools such as Stardoc can extract from the rule declaration.9 |
executable / test | Whether targets of this rule can be used with bazel run or bazel test. Those contracts continue in 4.2.6 Executable & Test Rules.10 |
Other rule() parameters matter later: toolchains and exec_groups connect a rule to platform-aware tool selection, build_setting turns a rule into a typed configuration flag, and provides advertises the providers an implementation promises to return.11 This article keeps those as boundary markers. Toolchain access belongs in 4.6.1 Platform Model for Rule Authors and 4.6.2 Defining, Registering & Accessing Toolchains. Build settings belong in 4.7.1 Build Settings for Rule Authors.
Normal build rules are also a different extension point from repository rules and module extensions. A build rule creates targets inside a package and runs in analysis. A repository rule materializes an external repository on demand during loading, so Bazel can then load its packages. 4.9 Repository Rules covers that mechanism. A module extension aggregates MODULE.bazel tags and usually calls repository rules, covered in 4.10 Authoring Module Extensions.
Attributes Are The BUILD-File API
Attributes are the public inputs to the rule. They can be simple values, such as attr.string(), attr.int(), and attr.bool(), or dependency attributes such as attr.label() and attr.label_list().12 Dependency attributes add edges to the target graph: a label string in BUILD becomes a Target object in ctx.attr during analysis.13
Use the attribute schema to say what the rule actually accepts:
my_report = rule(
implementation = _my_report_impl,
attrs = {
"src": attr.label(
allow_single_file = [".json"],
mandatory = True,
doc = "Input JSON file.",
),
"deps": attr.label_list(
providers = [ReportInfo],
doc = "Reports to merge before rendering.",
),
"_renderer": attr.label(
default = Label("//tools/report:renderer"),
executable = True,
cfg = "exec",
),
},
)
Here src accepts one .json file, deps requires each dependency to expose ReportInfo, and _renderer is a private implicit dependency. Private attributes start with _, cannot be set at the call site, and must have defaults. They are commonly used for implementation tools.14 Because _renderer is executable, cfg = "exec" makes the tool build for the execution platform rather than the target platform.15 Full toolchain resolution is usually the better production answer once the tool varies by platform or ruleset setup, but a private exec-configured tool is the simplest first boundary.
The providers parameter on label attributes is an early design tool, not just validation polish. If a dependency must supply ReportInfo, encode that in attr.label_list(providers = [ReportInfo]) instead of checking later with a vague fail(). The attribute now states the dependency contract where users and documentation tools can see it.16
ctx Is The Analysis Handle
The implementation function takes exactly one parameter, conventionally named ctx.17 ctx lives only for that implementation call and exposes the target's label, attribute values, configuration, dependencies' providers, output declarations, and action APIs.18
The accessors differ by what you declared:
| Declaration shape | Analysis access |
|---|---|
attr.string() | ctx.attr.message as a string.19 |
attr.label() | ctx.attr.dep as a Target or None.20 |
attr.label_list() | ctx.attr.deps as a list of Target objects.21 |
attr.label(allow_single_file = True) | ctx.file.src as one File or None.22 |
attr.label_list(allow_files = ...) | ctx.files.srcs as a list of default output Files.23 |
attr.label(executable = True, cfg = "exec") | ctx.executable._tool as an executable File or None.24 |
attr.output() | ctx.outputs.out as a predeclared output File.25 |
This is the first big difference from macros. A macro sees raw loading-phase values and stamps out other targets. A rule implementation sees configured attributes and analyzed dependency providers, so it can create a new target contract instead of just reshaping declarations. That is the rule side of the decision from 4.1.1 Macro vs Rule Decision Framework, grounded in the core/ruleset split introduced in P.2.3 Core vs Rulesets.
Outputs Must Cross An Action Boundary
Generated files are represented by File objects. During analysis, the implementation may declare output files or directories, but it cannot read or write their contents directly.26 Every generated file must be produced by exactly one action. A file declared with no generating action is an error, and files an action writes outside its declared outputs are not available to consumers.27
There are two common output shapes:
# Derived output: label is internal to the rule implementation.
out = ctx.actions.declare_file(ctx.label.name + ".txt")
# Predeclared output: user chooses a label through attr.output().
out = ctx.outputs.out
Use ctx.actions.declare_file() when the rule can derive a stable output name from the target. Use attr.output() when the user needs to choose the output label in BUILD. The deprecated outputs parameter on rule() still appears in old code, but current API docs direct new rules toward attr.output() or output groups instead.28
Actions themselves are the next item, 4.2.2 Actions. For this article, keep one invariant: declaring an output only creates a File object. Registering an action is what tells Bazel how that file will be produced.
Providers Are The Return Boundary
A rule implementation returns a list of providers.29 DefaultInfo is the universal provider every rule author should understand: its files field is the depset of default outputs built when the target is requested, and its executable and runfiles fields define the basic runtime contract for runnable targets.30
For a first rule, return DefaultInfo(files = depset([out])) whenever the rule produces a normal file. Without an explicit DefaultInfo(files = ...), Bazel falls back to predeclared outputs, but rules that register actions should still expose useful default outputs so bazel build //pkg:target exercises the rule directly.31
Custom providers come later in 4.2.7 Custom Provider Declaration and 4.2.8 Provider-as-Interface Pattern. The early design rule is simple: return DefaultInfo for files Bazel and users should build by default. Add a custom provider only when another rule needs semantic information that file paths alone cannot express.
For a complete target kind built from this skeleton, inspect the mini-ruleset's
glyph_library:
its attrs enter through ctx, its implementation declares actions and outputs,
and the rule returns both DefaultInfo and a domain provider.
When you want to isolate one design step at a time, compare the tagged versions
of rules_go_simple: v1 starts with one executable rule, v2 adds a library
and provider flow, and later tags add runfiles, execution-time tooling,
toolchains, and a module extension.32 This is a teaching progression, not a
production Go ruleset API.
A custom rule is a target contract, not a command wrapper.
rule() names the implementation and declares the BUILD-file API. The implementation receives ctx, reads configured attributes and providers, declares outputs, registers actions, and returns providers. Keep the first version small: typed attrs, one clear output path, one generating action, and an explicit DefaultInfo.
Check your understanding · 4 questions
1.What does assigning mini_message = rule(...) in a .bzl file create?
Select one answer
2.Which statements describe a rule implementation function?
Select all that apply
3.Match each rule-authoring declaration to the corresponding ctx access path.
Drag each answer onto the matching prompt, or click an answer and then click a prompt
attr.label(allow_single_file = True)attr.label_list(allow_files = ...)attr.label(executable = True, cfg = "exec")attr.output()4.True or false: rule implementation boundaries
Choose True or False for each sentence
Footnotes
-
Rules — rules define actions, outputs, providers, attributes, and implementation functions. ↩
-
Rules — implementation functions register actions during analysis instead of running commands directly. ↩
-
Rules Tutorial — empty rule pattern with
_foo_binary_impl(ctx)andfoo_binary = rule(...). ↩ -
Rules — target instantiation from a loaded rule symbol in a BUILD file. ↩
-
Rules Tutorial —
declare_file,actions.write, andDefaultInfo(files = depset([out]))progression. ↩ -
.bzl files —
rule()creates a rule value that must be assigned to a global variable. ↩ -
.bzl files —
rule(implementation = ...)parameter and analysis-phase implementation contract. ↩ -
.bzl files —
attrsparameter for rule-specific attributes and implicit common attributes. ↩ -
Writing Bazel rules: simple binary rule —
doconrule()and attributes feeds structured documentation. ↩ -
Rules — executable and test rule declaration with
executable = Trueortest = True. ↩ -
.bzl files —
rule()parameters includingtoolchains,exec_groups,build_setting, andprovides. ↩ -
attr — attribute schema functions and common scalar/dependency attribute types. ↩
-
Rules — dependency attributes form graph edges and become
Targetobjects in analysis. ↩ -
Rules — private attributes and implicit dependencies for implementation tools. ↩
-
attr —
executable = Truerequires an explicitcfg, with"exec"for build-time tools. ↩ -
attr —
providerson label attributes enforces dependency provider requirements. ↩ -
Rules — implementation functions take exactly one
ctxparameter and return providers. ↩ -
ctx — rule context exposes attributes, label, configuration, outputs, actions, and dependency providers. ↩
-
Rules — label attributes appear as a
TargetorNoneinctx.attr. ↩ -
attr —
label_listcorresponds to a list ofTargetobjects inctx.attr. ↩ -
Rules —
ctx.fileexposes a singleFilefor attributes withallow_single_file. ↩ -
Rules —
ctx.filesexposes default output files from label/list attributes. ↩ -
ctx —
ctx.executableexposes executable files from label attributes markedexecutable = True. ↩ -
attr —
attr.output()creates output labels available throughctx.outputs. ↩ -
Rules —
Fileobjects cannot be used for analysis-phase I/O and are passed to action APIs. ↩ -
Rules — generated files and actions: one generating action, declared inputs/outputs, and pruned unrequested outputs. ↩
-
.bzl files — deprecated
outputsparameter and migration towardattr.output()orOutputGroupInfo. ↩ -
DefaultInfo —
files,runfiles, andexecutableconstructor parameters. ↩ -
Rules — default outputs, pruning behavior, and why action-producing rules should expose default outputs. ↩
-
rules_go_simple — teaching ruleset for Go — the upstream
v1throughv6guide separates rule, provider, runfiles, execution, toolchain, and module-extension mechanics into inspectable stages. ↩