4.8.7 shadowed_action — Action Composition
extrashadowed_action is an advanced action-composition parameter for the rare case where a new action should run beside an existing one and reuse that action's inputs and environment. This separate companion action is often called a sidecar. Instead of rebuilding a long input list by hand, the new ctx.actions.run() action can add the shadowed action's inputs and environment to its own declared inputs and environment.1 That makes it useful for aspect-style augmentation, but only when your extension path already has a real Action object to shadow.
shadowed_action avoids reconstructing the original action's input envelope by hand.
The sidecar action declares its own executable, arguments, outputs, and exposure path.
What Gets Reused
The parameter lives on the action-creating APIs, not on the aspect API itself. For ctx.actions.run(), shadowed_action = some_action means Bazel runs the new action with the shadowed action's inputs and environment added to the new action's own inputs and environment. If both actions set the same environment variable, the new action's env value wins.2
That is narrower than "copy the action." The new action still declares its own outputs, executable, arguments, mnemonic, progress message, tools, execution group, toolchain, and execution requirements through the normal ctx.actions.run() parameters.3 It does not mutate the original action and it does not make the new outputs part of the target's default outputs. If users or external tools should build the sidecar artifact directly, put it in OutputGroupInfo so callers can request it with --output_groups. If another rule or aspect should consume it, expose it through a provider.
ctx.actions.run_shell() also has a shadowed_action parameter, but the documented contract is different: it adds the shadowed action's discovered inputs to the shell action's inputs. The current API reference does not promise environment inheritance for the shell form.4 Prefer run() when composing around a real tool. Use run_shell() only when the shell is deliberately part of the rule contract.
Why Aspects Care
Aspects already operate as a shadow graph over the target dependency graph: an aspect can propagate along selected attributes, visit targets, return providers, and generate actions without changing the target's rule definition.5 That is exactly the setting where a sidecar action may be attractive. A compiler action might already know a complicated source set, generated headers, include-scanning state, or explicit environment. A separate augmentation action may only want to add a scanner, instrumentation step, or report output while reusing that input envelope.
The important word is sidecar. A shadowing action is not a patch applied to the original action. It is another action with its own output:
def _augment_impl(target, ctx):
# original_action must come from a supported API that actually exposes
# Action objects to this extension path.
original_action = _pick_action_to_shadow(target)
report = ctx.actions.declare_file(ctx.label.name + ".analysis.txt")
ctx.actions.run(
executable = ctx.executable._analyzer,
outputs = [report],
inputs = [ctx.file._policy],
arguments = [
"--target", str(target.label),
"--report", report.path,
],
env = {"ANALYZER_MODE": "sidecar"},
mnemonic = "SidecarAnalyze",
shadowed_action = original_action,
)
return [OutputGroupInfo(sidecar_analysis = depset([report]))]
The explicit inputs = [ctx.file._policy] remains local to the sidecar action. The shadowed action contributes the original action's input envelope, so the analyzer can see the same files without the aspect reconstructing the rule author's private input logic.6
The Access Boundary
The hard part is not spelling the parameter. The hard part is getting an Action object responsibly.
The public Action object is primarily an inspection surface: the API reference says it represents an action created during rule analysis, is visible for testing, and is normally not something rule implementations need to access directly.7 ctx.created_actions() is similarly restricted to rules with _skylark_testable = True and is documented as a helper for tests of rule-implementation helper functions, not as a general production introspection API.8
The smallest runnable probe makes
that restriction visible on Bazel 9.1.0: bazel build //:shadowed_action_probe obtains an action through the test-only
ctx.created_actions().by_file path, then its sidecar reads the original
action's source input and inherited environment. The
shadowed_action.bzl
file is evidence for ctx.actions.run() composition, not a recipe for obtaining
production actions from an arbitrary target or aspect.
That boundary matters for design. If an aspect can solve the problem with providers, do that. Providers are the stable inter-rule communication channel. Aspects can read providers from visited targets and return their own providers as they propagate.9 If the result is an optional artifact, return OutputGroupInfo. Reach for shadowed_action only when you genuinely need to reuse another action's inputs and environment, and your ruleset has a supported way to obtain the action object.
How To Review A Use
Treat every shadowed_action use as an advanced rule-authoring escape hatch. Review the new action as a full action from 4.2.2 Actions, not as a harmless annotation.
Check these points before keeping it:
- The new action declares its own outputs, mnemonic, executable, arguments, and any extra inputs it adds.
- The author can explain where the shadowed
Actionobject comes from and why a provider would not be enough. - The new action's
envoverrides are deliberate, because they can replace variables inherited from the shadowed action. - The action does not imply it inherits scheduling requirements or output visibility unless those are explicitly declared on the new action.
- The generated output is exposed through an output group when users or external tools need to request it, or through a provider when another rule or aspect should consume it.
aquery is the right family of tool when you need to inspect the resulting action graph. It exposes actions, artifacts, inputs, outputs, mnemonics, and command-line details after analysis, which is the level where a shadowing action becomes visible as its own action.10
shadowed_action composes a new action with an existing action's input envelope. It is not a shortcut for arbitrary target introspection, and it is not a way to mutate the original action.
Use it only when action-level composition is the actual requirement. For ordinary cross-target metadata, providers and output groups remain the clearer aspect contract.
Check your understanding · 4 questions
1.What does shadowed_action add to a new ctx.actions.run() action?
Select one answer
2.An aspect author wants to pass shadowed_action = compiler_action for a production cc_library. What is the main engineering difficulty?
Select one answer
3.Which parts must the sidecar action still declare for itself?
Select all that apply
4.True or false: common misconceptions about shadowed_action.
Choose True or False for each sentence
shadowed_action mutates the original action.ctx.actions.run_shell() documents a narrower shadowing contract than ctx.actions.run().shadowed_action is a general replacement for provider-based communication between rules.Footnotes
-
actions —
ctx.actions.run()documentsshadowed_actionas adding the shadowed action's inputs and environment to the new action. ↩ -
actions —
run()environment behavior forshadowed_action, including new-actionenvoverriding shadowed variables. ↩ -
actions —
run()parameters for outputs, executable, tools, arguments, mnemonic, execution requirements, exec group, toolchain, and resource estimation. ↩ -
actions —
run_shell()documentsshadowed_actionas adding the shadowed action's discovered inputs. ↩ -
Aspects — aspects create a shadow graph, propagate along selected attributes, return providers, and may generate actions. ↩
-
actions —
inputsremains an explicit parameter on the newrun()action whileshadowed_actioncontributes the referenced action's inputs and environment. ↩ -
Action —
Actionis visible for testing and represents an action created during rule analysis. ↩ -
ctx —
ctx.created_actions()is available only for_skylark_testablerules and is intended for testing rule-implementation helper functions. ↩ -
Aspects — aspect implementations can inspect target providers, return providers, and consume aspect-propagated provider data from dependencies. ↩
-
Action Graph Query (aquery) —
aqueryexposes actions, artifacts, inputs, outputs, mnemonics, and command-line details from the post-analysis action graph. ↩