4.6.4 Execution Configuration for Tools (cfg = "exec")
A rule that drives a build will eventually depend on something its actions need to run: a compiler, a code generator, a stamping tool, a protoc plugin. The exec transition is how a rule says "this dep is a build-time tool, not a library that ships with the final artifact." Setting cfg = "exec" on a dependency attribute switches that edge from the target configuration to an execution configuration so the tool is built for the machine that runs Bazel actions, not for the platform the final binary is destined for.1
Two Configurations Show Up In One Rule
Bazel keeps the target/execution split from 4.6.1 Platform Model for Rule Authors in scope around every rule.2 The target platform describes what the final artifact is built for. The execution platform describes where actions actually run. They are often the same machine, but they don't have to be — bazel build on a Mac driving remote execution on Linux is a routine setup.3
The mismatch matters as soon as a rule registers an action that invokes a tool. The user asked to build "this Android binary" (target = Android arm64), but the code generator runs as a normal x86-64 Linux process on the build host. Building the code generator for Android would just produce something that can't run on the executor. Building it for the executor is what you actually want. The exec transition is Bazel's standard answer to that mismatch.4
Marking An Attribute As A Tool
The exec transition attaches to a dependency attribute. The canonical shape — straight from the rules reference — is a private label attribute that points at a tool and is marked both executable and exec-configured:5
example_library = rule(
implementation = _example_library_impl,
attrs = {
"srcs": attr.label_list(allow_files = [".proto"]),
"_compiler": attr.label(
default = "//tools:example_compiler",
allow_single_file = True,
executable = True,
cfg = "exec",
),
},
)
Inside the implementation, ctx.executable._compiler resolves to the compiler built for the execution platform, ready to hand to ctx.actions.run(executable = ...). The same shape is the implicit-dependency pattern the rule guide recommends for build-time helpers, and the same cfg = "exec" clause appears throughout production rule code — it's the production-action contract spelled out in 4.4.3 Action Execution Contract, viewed from the configuration side.
If an attribute has executable = True, the rules docs require cfg to be set explicitly.6 That rule exists precisely to guard against accidentally building a tool for the wrong configuration: an executable that ends up in the target configuration when it should run as a build tool is one of the easiest mistakes to make, and Bazel forces the rule author to pick a side.
The rules_glyph mini ruleset uses the same attribute shape inside its toolchain implementation: compiler = attr.label(executable = True, cfg = "exec") and worker = attr.label(executable = True, cfg = "exec") wrap tools that its consuming rules will run during actions. Glyph uses toolchains because the compiler is a reusable tool family, not one private helper. The cfg decision is the same, but the public boundary moves to 4.6.2 Defining, Registering & Accessing Toolchains.
What The Transition Actually Does
When Bazel walks into a cfg = "exec" edge, it flips that subtree from the target configuration into an exec configuration: the tool dependency, its libraries, and its own tool deps all get configured for the execution platform.7 Bazel selects an execution platform for the consuming rule's actions, and the exec-configured tool is built for that same side. When the rule also declares toolchains, toolchain resolution participates in choosing that execution platform. For a fixed private helper, the important invariant is simpler: the tool binary and the action that invokes it agree on where they run.8
If rule implementation or test code needs to distinguish this state explicitly, ctx.configuration.is_tool_configuration() reports whether the current target is being analyzed in the tool/exec configuration.9 Treat it as an inspection hook, not a reason to hide tool-vs-target behavior that should be expressed through attributes, toolchains, or exec groups.
This is the simple fixed-helper path. You add cfg = "exec" to one tool attribute, ctx.actions.run(executable = ...), and Bazel decides on one execution platform for the rule and routes both the tool build and the action to it.10 That's why the conspect calls this the simplest place where the target-vs-execution split becomes concrete — there is no per-action wiring, no custom transition function, no exec_group argument anywhere.
For a production-shaped boundary case, rules_py keeps a focused regression workspace where a PEP 517 frontend must run as an execution tool while a native wheel's C/C++ toolchain follows the transitioned target platform.11 It is executable evidence for keeping "tool that runs now" separate from "artifact being produced". The case exercises private ruleset machinery, so downstream projects should learn from the platform split rather than load its private symbols.
The Three Values Of cfg On A Dependency
A dependency attribute generally takes one of three cfg shapes:12
cfg value | Meaning |
|---|---|
"exec" | Tool that runs during the build. Bazel transitions the dep into an exec configuration. |
"target" | Stays in the target configuration. Library, runtime data, test fixture — anything that ships or runs alongside the final artifact. |
| omitted | Same as inheriting the parent's configuration. Allowed only when executable = False. |
Setting cfg = "target" does not actually change anything: it is purely a convenience value that lets rule designers make their intentions explicit — use it when it improves readability, not because Bazel demands it.13 The asymmetry is real: omitting cfg is fine for plain sources and libraries. Flipping to exec is a deliberate decision tied to "this thing must execute on the build machine."
The cfg slot also accepts a Starlark transition object, which is the entry point to custom configuration flips covered in 4.7.2 Starlark Transitions.14 The exec transition is not a custom transition — it is one of Bazel's standard built-in transitions, and that is why it is the smallest tool-routing transition to understand before reaching for custom configuration changes.
A Historical Note: cfg = "host"
Reading old rule code, you'll run into cfg = "host" on tool attributes. Bazel used to model only the host platform (where Bazel itself runs) and conflate it with where actions execute, which broke down as soon as remote execution put actions on a different machine than the CLI.15 The 6.0 release retired the cfg = "host" spelling in favor of the explicit execution-platform model. Rule code written for current Bazel should use cfg = "exec". cfg = "host" appears only in legacy rulesets and old documentation.16
Where The Exec Transition Sits
The exec transition is the smallest building block in a stack of related mechanisms:
- Implicit tool deps with
cfg = "exec"route one tool through the rule's default execution platform. This article. - Toolchain resolution (4.6.3 Toolchain Resolution) generalizes the same idea: instead of hardcoding a
_compilerlabel, the rule asks for a toolchain type and Bazel picks a registered toolchain that fits the target + execution platform pair. Inside toolchain rules,cfg = "exec"on a tool attribute means "the execution platform of the rule that consumes the toolchain," not the toolchain rule itself.17 - Execution groups (4.6.5 Execution Groups & Auto Exec Groups) extend the model when a single rule needs different execution platforms for different actions — one tool on macOS, another on remote Linux. Each exec group has its own toolchain dependencies and
exec_compatible_withconstraints. Tool attributes and actions opt into that group withcfg = config.exec(exec_group = "...")andctx.actions.run(exec_group = "...").18 - Custom Starlark transitions (4.7.2 Starlark Transitions) are the escape hatch for everything else — flipping
--compilation_mode, instrumenting a subtree, switching CPU per artifact. They make the build graph larger and harder to reason about. Reach for them only when exec / toolchain / exec-group can't express what you need.19
Most production rules will only ever need the first two. The exec transition is the answer to "my rule needs to invoke a tool during the build," and most of the time, that is the whole question.
cfg = "exec" on a dependency attribute marks that dep as a build-time tool: Bazel builds it for the execution platform (where actions run) rather than the target platform (where the final artifact runs).
It is the smallest, default tool-routing transition — not a custom Starlark transition. Set executable = True and cfg = "exec" on private _tool attrs, and pass ctx.executable._tool to ctx.actions.run to invoke them.
Check your understanding · 4 questions
1.A Starlark rule has a private attribute pointing at a code generator that its actions invoke. What does adding cfg = "exec" to that attribute do?
Select one answer
2.Why does Bazel require cfg to be set explicitly whenever a dependency attribute has executable = True?
Select one answer
3.True or false — common claims about cfg = "exec" and the exec transition.
Choose True or False for each sentence
cfg = "target" and omitting cfg on a non-executable attribute produce the same build behavior. The explicit form is a readability hint.cfg = "exec" is a user-defined Starlark transition that must be allowlisted with _allowlist_function_transition before a rule can use it.cfg = "host" is the modern spelling and should be preferred over cfg = "exec" on current Bazel.cfg = "exec" edge is taken, the tool's own transitive dependencies are also built for the execution platform, not the target platform.4.Match each scenario to the cfg value a well-written rule should put on the dependency attribute.
Drag each answer onto the matching prompt, or click an answer and then click a prompt
ctx.actions.run during the build.Footnotes
-
Rules — when a dep is a build tool, the attribute should transition into an exec configuration so the tool builds for the execution platform. ↩
-
Toolchains — target platform vs execution platform as the two platforms Bazel reasons about per rule. ↩
-
Action configuration in Bazel — host platform vs execution platform distinction, motivated by remote execution moving actions off the host. ↩
-
Writing Bazel rules: platforms and toolchains — host vs execution vs target platforms and what each one means for the build. ↩
-
Rules — private executable tool attributes use
default,executable = True, andcfg = "exec". ↩ -
Rules —
cfgmust be set explicitly whenexecutable = True, to guard against tools accidentally built for the wrong configuration. ↩ -
Rules —
cfg = "exec"on a dep attribute causes the tool and all of its own dependencies to be built for the execution platform. ↩ -
Writing Bazel rules: moving logic to execution —
_builderimplicit dep withcfg = "exec"is built for the execution platform even when cross-compiling for a different target platform. ↩ -
configuration —
is_tool_configuration()returns whether the current target is being analyzed in the tool configuration. ↩ -
Action configuration in Bazel — simplest rule shape: a tool attribute with
cfg = "exec",ctx.actions.run(executable = ...), and Bazel picks one execution platform for both the tool build and the action. ↩ -
rules_py repository map —
pep517-frontend-exec-groupverifies that the build frontend executes on the appropriate execution platform while native wheel compilation selects the transitioned target toolchain. ↩ -
Rules —
cfgselects between same-configuration deps and exec-configured tools on each dependency attribute. ↩ -
Rules —
cfg = "target"is a readability hint with no behavior change. Set it only when it clarifies intent. ↩ -
Rules —
cfgalso accepts a user-defined transition object for custom configuration changes. ↩ -
Action configuration in Bazel — Bazel previously assumed the execution platform equaled the host platform, which broke remote execution scenarios. ↩
-
Rules — Bazel versions before 6.0 used a distinct "host" configuration. Current Bazel uses the explicit execution-platform model. ↩
-
Action configuration in Bazel — inside a toolchain rule,
cfg = "exec"means the execution platform of the rule consuming the toolchain, not the toolchain rule itself. ↩ -
Action configuration in Bazel — exec groups attach distinct constraints to different actions within one rule and pair with
cfg = config.exec(exec_group = ...)tool attributes. ↩ -
Rules —
cfg = my_transitioninvokes a user-defined transition, trading flexibility for graph size and analyzability. ↩