4.2.2 Actions
An action is the part of a custom rule that says, "when this output is needed, run this executable with these inputs and expect these outputs." The rule implementation still runs during analysis, so it does not compile, copy, read, or write files directly. It declares File objects and registers actions in Bazel's action graph. Bazel executes only the necessary actions later, during execution.1,2
Starlark describes the output. It does not open or write the file.
The Rule Author's Boundary
The implementation function receives ctx, reads attributes and dependency providers, declares outputs, and calls ctx.actions methods.3 That is planning work. The external tool, shell command, file write, or template expansion happens later if some requested output depends on it.4
This is the bridge from 2.2 Three Phases of a Build into rule authoring. Level 2 introduced the loading, analysis, and execution split. Here you are using that split deliberately. The rule author defines the action graph during analysis, while Bazel decides at execution time which actions are already cached, which can run in parallel, and which need to run at all.5
The smallest useful mental model is:
declared inputs + command/tool + declared outputs = action
Bazel can schedule, cache, sandbox, and inspect an action because that contract is explicit. If the tool reads a file that was not declared as an input, the rule has hidden information from Bazel. If the tool writes an output that was not declared, downstream targets cannot depend on it through the normal graph.
Declare Outputs First
Generated files are represented by File objects during analysis. A File is not an open file handle and cannot be read or written by Starlark rule code. It is a handle passed to action-creating APIs.6
For derived outputs, use ctx.actions.declare_file() or ctx.actions.declare_directory():
def _copy_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".txt")
# Next: register exactly one action that produces out.
declare_file() creates a generated file under the current package's output tree. Declaring a file is separate from creating the action that emits it: you pass the returned File object to an action construction function afterward.7
Tree artifacts use ctx.actions.declare_directory(). They are still declared outputs, but their contents are not directly visible to ordinary Starlark during analysis. Action command lines can expand their contents with Args.add_all() when that is the right shape.8 Dynamic action generation from tree contents is a later, experimental topic in 4.12 Experimental Rule Patterns.
Predeclared outputs are different. If the rule has output attributes such as attr.output, Bazel creates output labels during loading, and the implementation reads their File objects from ctx.outputs instead of calling declare_file().9
Register An Action
The most common action API is ctx.actions.run(): invoke a tool directly with declared inputs, declared outputs, arguments, and usually a stable mnemonic.10
def _copy_impl(ctx):
src = ctx.file.src
out = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.run(
outputs = [out],
inputs = [src],
executable = ctx.executable._copier,
arguments = [src.path, out.path],
mnemonic = "CopyText",
)
return [DefaultInfo(files = depset([out]))]
copy_file = rule(
implementation = _copy_impl,
attrs = {
"src": attr.label(allow_single_file = True),
"_copier": attr.label(
default = "//tools:copier",
executable = True,
cfg = "exec",
),
},
)
The private _copier attribute models a build-time tool. Marking it executable = True makes it available through ctx.executable._copier, and cfg = "exec" builds it for the execution platform rather than the target platform.11 Full toolchain-based selection comes later in 4.6.2 Defining, Registering & Accessing Toolchains. For a first custom rule, a private executable attr is often enough.
Return the output through DefaultInfo if it is part of the target's normal build result. Registering an action teaches Bazel how the file can be produced, but DefaultInfo(files = depset([out])) teaches Bazel that this file is the target's default output when someone builds or depends on the target.12
Pick The Narrowest Action API
ctx.actions.run() is the default for real tools. It avoids shell quoting, keeps the executable explicit, and makes the rule easier to move across platforms. A common progression makes this concrete: early rule prototypes use run_shell() for brevity, then move more logic into a builder binary invoked with ctx.actions.run() because direct tool execution is less error-prone and less OS-specific.13
Use ctx.actions.run_shell() when the shell is part of the rule's intended contract, not just because it is convenient. The API runs a shell command and receives outputs, inputs, tools, arguments, and environment parameters much like run().14 Shell actions are useful for small glue, but they make quoting, host tools, platform differences, and environment inheritance part of the design problem.15 The next item, 4.2.3 Args & Command Lines, goes deeper on command-line construction and why Args is usually better than hand-built strings.
Some action APIs do not run an external executable:
| API | Use when |
|---|---|
ctx.actions.write() | The file content is known from analysis-time data, such as a small launcher or metadata file.16 |
ctx.actions.expand_template() | You have a UTF-8 template file and a substitution dictionary. The stable surrounding text belongs in the template.17 |
ctx.actions.symlink() | The rule's public output should be a symlink to another file, directory, or explicit target path.18 |
ctx.actions.do_nothing() | You need a graph node with inputs but no command or outputs. This is an escape hatch, not normal generation.19 |
The official Rules Tutorial demonstrates the difference between declaring a file and writing it: after declare_file(), a rule must register an action such as ctx.actions.write() or Bazel has no generating action for the output.20
Inputs Are Part Of Correctness
Action inputs include source files, generated files from dependencies, tools, and sometimes transitive depsets of files.21 From Bazel's perspective, compilers, standard libraries, code generators, and helper scripts are inputs too, not background facts about the machine.22
That is why rules usually separate user inputs from tools:
attrs = {
"srcs": attr.label_list(allow_files = [".proto"]),
"_compiler": attr.label(
default = "//tools:proto_compiler",
executable = True,
cfg = "exec",
),
}
The user's srcs describe what the target processes. The private _compiler describes how the rule implementation performs that processing. Both affect the action. If either the source file or compiler changes, Bazel has enough information to invalidate the old result.
For large transitive inputs, keep the shape graph-friendly. ctx.actions.run(inputs = ...) accepts a list or depset, and ctx.actions.args() can defer expansion of depsets until the action command line is actually needed.23 That connects back to 4.1.5 depset vs list: do not flatten transitive data just to feed an action API that already understands depsets.
Make Actions Inspectable
Choose mnemonic values as if someone will debug them in logs, profiles, and aquery. A mnemonic is the one-word action type name, such as CppCompile, GoLink, or CopyText.24 A stable progress_message can make long builds readable, and the official API recommends pattern substitutions such as %{label}, %{input}, and %{output} instead of static strings where possible.25
This is the authoring side of 5.2.3 bazel aquery — Action Graph. Later, bazel aquery lets you inspect the post-analysis action graph: command lines, inputs, outputs, mnemonics, and related action details. The quality of that debugging experience depends partly on how clearly your rule declared its actions.
When Starlark analysis or action construction becomes memory-heavy, 5.6.6 Starlark Memory Profiling shows how to profile that cost.
Rule tests can also inspect created actions when the rule opts into Starlark testability. The Action API reference documents fields such as inputs, outputs, argv, args, env, mnemonic, content, and substitutions.26 Use argv for analysis-time assertions over ordinary command lines. args preserves more accurate Args data, including expanded output directories, but the frozen Args objects are not themselves readable during analysis.26 That testing surface belongs in 4.5 Rule Testing & Documentation, but the rule authoring habit starts here: make each action's contract crisp enough to verify.
The mini-ruleset's complete
GlyphCompile and GlyphValidate actions
show the same contract with a toolchain executable, structured args, declared
inputs and outputs, stable mnemonics, and separate compile and validation work.
For a production code-generation case, rules_proto documents how protoc inputs,
plugins, declared outputs, and output mappings become one action contract in
docs/CORE_RULES.md.
Use it to inspect a concrete protoc action shape. Its output-mapping API is a
ruleset contract, not a universal Bazel action feature.27
An action is not "some code the rule runs." It is a declared execution contract: inputs, tools, arguments, environment, outputs, and identity.
Write Starlark rule code to describe that contract during analysis. Put file-reading, parsing, compiling, linking, formatting, and expensive logic in execution-phase tools registered by actions.
Check your understanding · 4 questions
1.What does a rule implementation do when it creates an action?
Select one answer
2.When should a rule author prefer ctx.actions.run() over ctx.actions.run_shell()?
Select one answer
3.Match each action API to the situation where it is the narrowest fit.
Drag each answer onto the matching prompt, or click an answer and then click a prompt
ctx.actions.write()ctx.actions.expand_template()ctx.actions.symlink()ctx.actions.do_nothing()4.True or false: output declaration and output exposure in custom rules.
Choose True or False for each sentence
ctx.actions.declare_file() creates a File handle, not the file's contents.DefaultInfo(files = depset([out])) exposes an output as part of the target's normal build result.Footnotes
-
Rules — rule implementations register actions during analysis instead of running commands directly. ↩
-
actions —
ctx.actionsmodule provides functions that create actions. ↩ -
ctx —
ctxexposes attributes, dependencies, outputs, and action declaration APIs to the implementation function. ↩ -
Rules Tutorial —
ctx.actions.write()registers an action. The output is only produced when requested. ↩ -
Writing Bazel rules: simple binary rule — loading, analysis, and execution phases from a rule author's perspective. ↩
-
File —
Fileis an analysis-phase representation, not an open file handle. ↩ -
actions —
declare_file()declares an output, and a separate action must emit it. ↩ -
actions —
declare_directory()creates a tree artifact whose contents can be expanded in action commands withArgs.add_all(). ↩ -
Rules — predeclared outputs from output attributes are available through
ctx.outputs. ↩ -
Rules — private executable attributes and
cfg = "exec"for implicit tool dependencies. ↩ -
Rules Tutorial — returning
DefaultInfo(files = depset([out]))makes a generated file the target's output. ↩ -
Writing Bazel rules: moving logic to execution — preference for
ctx.actions.run()over shell actions and moving complexity into builder tools. ↩ -
actions —
run_shell()executes a shell command with declared inputs, outputs, tools, arguments, and environment. ↩ -
Writing Bazel rules: simple binary rule — portability, quoting, host tools, and environment drawbacks of shell-based actions. ↩
-
actions —
write()creates a file write action from analysis-time content. ↩ -
actions —
expand_template()creates a template expansion action with substitutions. ↩ -
actions —
symlink()creates a symlink output to a file, directory, or target path. ↩ -
actions —
do_nothing()creates an empty action for extra-action graph uses. ↩ -
Rules Tutorial — declared files without generating actions produce an error. ↩
-
Rules —
Fileobjects are passed to action-creating functions, and generated files must be outputs of exactly one action. ↩ -
Rules — rule writers must treat tools and libraries needed by actions as inputs. ↩
-
actions — action
inputscan be lists or depsets, andargumentscan includeactions.args()objects. ↩ -
actions —
progress_messagesupports efficient%{label},%{input}, and%{output}substitutions. ↩ -
Action — action introspection fields for testing rule implementations. ↩1 ↩2
-
rules_proto repository map —
docs/CORE_RULES.mdis the supported orientation route, with implementation details underpkg/protoc/reserved for escalation. ↩