4.6.5 Execution Groups & Auto Exec Groups
recommended4.6.4 Execution Configuration for Tools (cfg = "exec") ends with a clean picture: one tool attribute, cfg = "exec", ctx.actions.run(executable = ...), and Bazel picks a single execution platform for the whole target — both the tool build and the action land there. That picture breaks the moment a rule needs to compile on remote Linux, sign on a local Mac, and still look like one target from the outside. Execution groups are the mechanism that lifts the "one execution platform per target" assumption: each group is a named bundle of toolchains and exec_compatible_with constraints, and each action picks the group it belongs to.1
Two Mechanisms, One Goal
Bazel exposes two ways to get per-action execution platforms inside one target.
Manual exec_groups is the original API and the most flexible. The rule author lists named groups in rule(exec_groups = {...}), attaches each to specific toolchain types and platform constraints, and pairs each tool attribute and each action with the group it belongs to.1
Automatic Execution Groups (AEGs), available since Bazel 7, fold the common case into the toolchain system: instead of declaring groups, each toolchain type registered on the rule becomes its own implicit exec group, and Bazel picks an execution platform per toolchain rather than one platform that has to satisfy all toolchains at once.2
AEGs are still opt-in. As of Bazel 9, --incompatible_auto_exec_groups defaults to false, so existing rulesets continue to use the legacy single-platform-per-target model unless they enable AEGs globally with that flag or per rule with the _use_auto_exec_groups attribute.2 In practice, both APIs coexist in the same rulesets and the same builds, and the question is when each is the right tool.
Manual exec_groups: A Named Bundle
A manual exec group is declared inside rule() and looks like a small slice of the toolchain plumbing from 4.6.2 Defining, Registering & Accessing Toolchains, scoped to one named bucket of actions:1
my_rule = rule(
_impl,
exec_groups = {
"link": exec_group(
exec_compatible_with = ["@platforms//os:linux"],
toolchains = ["//foo:toolchain_type"],
),
"test": exec_group(
toolchains = ["//foo_tools:toolchain_type"],
),
},
attrs = {
"_compiler": attr.label(
executable = True,
cfg = config.exec("link"),
),
},
)
Three pieces are doing work here. The exec_group() constructor declares the toolchain types and constraints that group needs.3 The tool attribute uses config.exec("link") instead of the bare cfg = "exec" from 4.6.4 Execution Configuration for Tools (cfg = "exec"), so the _compiler is built for the same execution platform Bazel selects for the link group rather than for the rule's default group.4 The actions then route themselves with the exec_group parameter on ctx.actions.run (or ctx.actions.run_shell. ctx.actions.map_directory() has the same routing knobs for its generated actions):5
def _impl(ctx):
ctx.actions.run(
inputs = [ctx.executable._compiler, ctx.files.srcs[0]],
executable = ctx.executable._compiler,
exec_group = "link",
# ...
)
The pairing matters in both directions: the tool's cfg and the action's exec_group should reference the same group name so the binary that lands in the action's inputs is built for the platform the action will actually run on.6
Reading Resolved Toolchains Per Group
After analysis, each group's resolved toolchains are reachable from the rule context the same way ctx.toolchains exposes the rule-level resolution from 4.6.2 Defining, Registering & Accessing Toolchains, but indexed by group name:1
def _impl(ctx):
foo_info = ctx.exec_groups["link"].toolchains["//foo:toolchain_type"].fooinfo
ctx.actions.run(
inputs = [foo_info.tool, ctx.files.srcs[0]],
executable = foo_info.tool,
exec_group = "link",
# ...
)
The ctx.exec_groups collection is the rule-side view of ExecGroupCollection. Each entry is an ExecGroupContext with a toolchains field for that group's resolved ToolchainInfo providers.7,8 The official guidance is explicit that an action which uses a tool from a group but forgets to declare exec_group = "..." on the action itself is a latent bug — it may not fail immediately, but the action will run on the rule's default execution platform while consuming a tool built for a different one.1
Predefined Execution Groups
Bazel predefines a few execution groups for specific rule families, without every rule declaring them in exec_groups = {...}:
test— the test runner action's group, present on Starlark test rules and on native test rules. Test rules pick it up automatically.1cpp_link— the C++ linking action's group, used by the native C++ rules and reachable throughexec_propertieskeys likecpp_link.mem.1
The test group is what makes the test runner platform configurable independently of the actions that build the test binary — useful when the test must run on a specific OS or hardware while the build itself can stay anywhere.1
Automatic Execution Groups
The motivation for AEGs is the most common multi-toolchain mistake. A rule that declares two toolchain types,
my_rule = rule(
_impl,
toolchains = ["//tools:toolchain_type_1", "//tools:toolchain_type_2"],
)
forces Bazel — under the legacy model — to find one execution platform that satisfies both toolchain types at once.9 When no such platform exists, the build fails even though each toolchain individually has a valid execution platform somewhere in the registered set.
AEGs flip the default: instead of one platform per rule, Bazel runs toolchain resolution once per toolchain type and assigns each action to the platform that hosts the toolchain that action uses.9 The action implementation declares which toolchain it is invoking via the toolchain parameter on ctx.actions.run:9
def _impl(ctx):
ctx.actions.run(
mnemonic = "FirstAction",
executable = ctx.toolchains["//tools:toolchain_type_1"].tool,
toolchain = "//tools:toolchain_type_1",
)
ctx.actions.run(
mnemonic = "SecondAction",
executable = ctx.toolchains["//tools:toolchain_type_2"].tool,
toolchain = "//tools:toolchain_type_2",
)
Each action runs on the execution platform of its declared toolchain. If two toolchains happen to resolve to the same platform on a given host, the actions still co-locate. If they don't, they don't have to.
ctx.actions.run documents the explicit precedence when both parameters are set: exec_group wins, and Bazel raises an error if the named exec group does not resolve to the same toolchain.5 In other words, manual groups remain the override — AEGs are the default routing for the toolchain-driven case.
Enabling and Migrating
AEGs are gated behind a migration flag rather than turned on by default. As of Bazel 9, --incompatible_auto_exec_groups is false by default, so a rule built today still gets the legacy single-platform behavior unless the project flips the flag or the rule itself opts in:2
- Project-wide: set
--incompatible_auto_exec_groups=true(typically in.bazelrc).2 - Per rule: declare
"_use_auto_exec_groups": attr.bool(default = True)on the rule. This overrides the incompatible flag and lets a single ruleset migrate independently.2
The migration error messages in auto-exec-groups are worth recognizing because they show up the moment AEGs are enabled on a rule whose actions don't yet declare a toolchain. Two are common:10
- "Couldn't identify if tools are from implicit dependencies or a toolchain. Please set the toolchain parameter." — the action uses tools and Bazel cannot tell whether they came from a toolchain. Add
toolchain = "//pkg:toolchain_type"for toolchain tools, ortoolchain = Nonefor plain implicit deps.10 - "Action declared for non-existent toolchain '[toolchain_type]'." — the action sets
toolchain = "..."for a type the rule does not list intoolchains = [...]. Either register that toolchain on the rule or settoolchain = None.10
These errors are the visible surface of the migration: Bazel asks the rule to be explicit about which toolchain each action uses so it can route the action to the right platform.
When Manual Exec Groups Are Still Needed
The auto-exec-groups documentation calls out the one case where AEGs are not enough: when a single action invokes tools from two or more toolchains and therefore must run on one platform that satisfies both at the same time.11 In that case the rule still has to declare a manual group that lists both toolchain types, and the action still has to opt into it:11
my_rule = rule(
_impl,
exec_groups = {
"two_toolchains": exec_group(
toolchains = ["//tools:toolchain_type_1", "//tools:toolchain_type_2"],
),
},
)
def _impl(ctx):
ctx.actions.run(
executable = ctx.toolchains["//tools:toolchain_type_1"].tool,
tools = [ctx.toolchains["//tools:toolchain_type_2"].tool],
exec_group = "two_toolchains",
)
Manual groups are also the right answer when the constraint isn't a toolchain at all — for example, "this action must run on a platform with a particular hardware feature, regardless of which toolchain it uses." exec_compatible_with on the group expresses that directly, and AEGs have nothing equivalent because they key off toolchain types. A mac_only group is the canonical small example of that pattern: a tool attribute under cfg = config.exec(exec_group = "mac_only") plus exec_compatible_with = ["@platforms//os:macos"] on the group definition pins one action to macOS while the rest of the rule resolves freely.6
exec_properties Travel With The Group
Once a rule has multiple execution groups, the exec_properties attribute that already exists on every rule starts taking group-prefixed keys. Setting link.mem on a target overrides only the link group's mem value. Bare mem covers the default group:12
my_rule(
name = "my_target",
exec_properties = {
"mem": "12g",
"link.mem": "16g",
},
)
The same prefixing rule applies to exec_properties defined on platform() targets, with one extra freedom: platforms may set keys for arbitrary group names, while targets reject keys for unknown groups.12 Target-level values take precedence over inherited platform values when both name the same key.12 In practice, this is the seam 6.3 Remote Execution Infrastructure relies on to size a remote action — for example, asking the executor for more memory specifically for the C++ link step instead of for every action in the build.
A parallel attribute, exec_group_compatible_with = {"<group>": [<constraints>]}, lets target authors layer additional execution-platform constraints onto specific groups without touching the rule definition.1 It's the BUILD-side complement to the rule-side exec_compatible_with on exec_group().
Where This Sits In The Stack
Execution groups complete the per-rule platform story that 4.6.4 Execution Configuration for Tools (cfg = "exec") starts:
cfg = "exec"routes one tool through the rule's default execution platform.4- Toolchain resolution from 4.6.3 Toolchain Resolution lets Bazel pick the toolchain that fits the rule's target/execution platform pair.
- Manual
exec_groupsand AEGs split that single execution platform into named or per-toolchain buckets when one platform is not enough.
They also feed forward into how actions get scheduled. 2.5 Execution Strategies explained that strategy chooses where an already-planned action runs without changing what it does — execution groups are the rule-author lever that decides which "where" a given action even asks for, before the strategy layer picks local, sandbox, worker, or remote. 6.3 Remote Execution Infrastructure then uses exec_properties carried by those groups to allocate cluster resources per action class.
The action contract those actions still have to honor — declared inputs, declared outputs, no hidden state — is the same one from 4.4.3 Action Execution Contract. Execution groups change the platform an action runs on. They do not weaken hermeticity, and they do not let an action read inputs it didn't declare.
The mini-ruleset puts the manual path in one place: glyph_report declares the
glyph_report exec group,
and its implementation reads that group's toolchain and routes the action back
to the same group.
Build //examples/basic:module_report --output_groups=glyph_report to exercise
the consumer target declared in
examples/basic/BUILD.bazel.
Execution groups break the "one execution platform per rule" assumption: each named group bundles its own toolchains and exec_compatible_with constraints, tool attributes opt in via cfg = config.exec(exec_group = "..."), and actions opt in via ctx.actions.run(exec_group = "...").
Automatic Execution Groups (Bazel 7+, opt-in via --incompatible_auto_exec_groups or _use_auto_exec_groups) handle the common case automatically by giving each toolchain type its own implicit group. Manual groups remain necessary when a single action needs multiple toolchains on one platform, or when a group needs custom exec_compatible_with constraints beyond what toolchains imply.
Check your understanding · 4 questions
1.On Bazel 7+ with --incompatible_auto_exec_groups=true (still off by default in Bazel 9), what changes about a rule's behavior?
Select one answer
2.A rule has --incompatible_auto_exec_groups enabled. Which case still requires the rule author to declare a manual exec_group?
Select one answer
3.True or false: statements about manual exec_groups.
Choose True or False for each sentence
cfg = config.exec(exec_group = "link") is built for the execution platform Bazel selects for the link exec group, not for the rule's default execution platform.exec_group = "..." on an action that uses a group-specific tool is a latent platform bug, not a guaranteed analysis error.cpp_link exec group available even if it does not register any C++ toolchain or build any C++ targets.toolchain/exec_group pair is accepted silently.4.Match each Starlark API surface to what it does for execution groups.
Drag each answer onto the matching prompt, or click an answer and then click a prompt
exec_group(toolchains = [...], exec_compatible_with = [...])cfg = config.exec(exec_group = "link") on a tool attributectx.actions.run(..., exec_group = "link")ctx.exec_groups["link"].toolchains["//pkg:type"]exec_properties = {"link.mem": "16g"} on a targetFootnotes
-
Execution Groups — declaring
exec_groupsinrule(),exec_groupparameter onctx.actions.runandctx.actions.run_shell, accessing per-group resolved toolchains, defaulttestandcpp_linkgroups, latent-bug warning when a tool from a group is used without settingexec_groupon the action, andexec_group_compatible_withon targets. ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
Automatic Execution Groups (AEGs) — AEGs select an execution platform per toolchain type, are fully supported from Bazel 7, are gated by
--incompatible_auto_exec_groups, and can be enabled per rule with_use_auto_exec_groups. Verified againstbazelisk help build:--incompatible_auto_exec_groupsdefaults tofalsein Bazel 9.0.0. ↩1 ↩2 ↩3 ↩4 ↩5 -
.bzl files —
exec_group()Starlark constructor:toolchainsandexec_compatible_withparameters.rule(exec_groups = {...})parameter. ↩ -
config —
config.exec(exec_group = ...)selects the execution platform of the named exec group. Withoutexec_group, the rule's default execution platform is used. ↩1 ↩2 -
actions —
run(),run_shell(), andmap_directory()exec_group/toolchainparameters route created actions to execution platforms. When bothtoolchainandexec_groupare set,exec_groupwins and Bazel raises an error if the group doesn't specify the same toolchain. ↩1 ↩2 -
Action configuration in Bazel — Execution Groups walkthrough: defining a
mac_onlygroup withexec_compatible_withon macOS, pairing it with a tool attribute viacfg = config.exec(exec_group = "mac_only"), and routing actions withctx.actions.run(exec_group = "mac_only"). Also notes that the action'sexec_groupmust match the tool'scfggroup. ↩1 ↩2 -
ExecGroupCollection — exposes the exec groups available to a rule. ↩
-
ExecGroupContext — per-group struct with a
toolchainsfield for accessing that group's resolved toolchains. ↩ -
Automatic Execution Groups (AEGs) — pre-AEG behavior required one execution platform satisfying all toolchain types on the rule. AEGs select per-toolchain platforms, with the action declaring its toolchain via the
toolchainparameter onctx.actions.run. ↩1 ↩2 ↩3 -
Automatic Execution Groups (AEGs) — migration error messages: "Couldn't identify if tools are from implicit dependencies or a toolchain. Please set the toolchain parameter." and "Action declared for non-existent toolchain '[toolchain_type]'." ↩1 ↩2 ↩3
-
Automatic Execution Groups (AEGs) — "Custom exec_groups are needed only in case where multiple toolchains need to execute on a single execution platform". Example with a
two_toolchainsgroup listing both toolchain types and an action settingexec_group = "two_toolchains". ↩1 ↩2 -
Execution Groups —
exec_propertiesintegration: group-prefixed keys likelink.memoverride the same key inside that group. Platform-levelexec_propertiesmay set keys for arbitrary groups, target-level values take precedence over inherited platform values. ↩1 ↩2 ↩3