GitHub Actions / Reusable automation and scoped authority / Lesson 11
Reusable workflows and composite actions
Choose the reuse boundary that preserves execution, input, output, and authority contracts.
On this page
Lesson goals
- Distinguish job-level workflow calls from step-level composite actions.
- Review typed inputs, secret passing, outputs, and dependency identity.
Reuse the right unit
A reusable workflow packages one or more jobs and is invoked through jobs.<id>.uses. A composite action packages steps and runs as a step within a caller’s job. Choose a reusable workflow when runner selection, multiple jobs, or a consistent cross-job policy belongs to the shared unit. Choose a composite when a repeatable sequence should fit inside an existing job. Parcel’s shared fixture normalization can be a composite; a multi-platform candidate validation graph fits a reusable workflow. Reuse should clarify ownership rather than hide an unreviewable chain of execution.
Define a small typed workflow interface
A reusable workflow declares workflow_call inputs, secrets, and outputs under the supported schema. Inputs have declared types, including boolean, number, and string, and callers supply matching values. Keep the interface narrow: accepting an arbitrary shell program as an input defeats much of the review value. The fragment below declares a string fixture mode and maps a produced job output to the workflow interface. It lacks the implementing jobs, so it is an interface sketch rather than a complete runnable workflow.
on:
workflow_call:
inputs:
fixture-mode:
required: true
type: string
outputs:
candidate:
description: Synthetic candidate label
value: ${{ jobs.identify.outputs.candidate }}
An implementing identify job and its candidate output would be required. A string type alone does not validate allowed fixture names or authorize a target. No shared workflow is created or called.
Calls use a different job form
A caller job using a reusable workflow is not an ordinary runs-on job with steps appended afterward. Only the documented caller keywords are supported. Follow the call with another needs job when further work is required. A local same-repository workflow reference uses its supported relative path form, while cross-repository calls need an explicit ref. Operational dependencies should resolve to reviewed immutable commits rather than an unreviewed moving branch. A pin establishes identity, not safety; the pinned implementation and its nested dependencies still require review.
Pass authority deliberately across reuse
Named secrets make the interface visible; secrets: inherit broadens implicit coupling within its supported organization or enterprise scope. Prefer an explicit contract when only one secret is needed. Environment secrets are not passed through workflow_call as an environment field; a called job’s environment can select its own environment secrets under documented behavior. Token permissions cannot simply be elevated by a nested workflow beyond the caller’s permitted authority. Review the whole chain, including checkout behavior, because a called workflow executes in the caller’s context rather than automatically checking out its own repository.
A composite is an action contract
A composite metadata file declares runs.using: composite and individual steps, with explicit shell for run steps. It does not create service containers or independent jobs. Inputs must be passed and validated; secret context is not automatically a generic secret interface, so needed sensitive values require careful explicit passing under caller policy. The complete local model below checks a narrow fixture-mode interface. It is not an Actions metadata validator and does not execute a composite. It demonstrates rejecting unsupported values before they become script or path inputs.
def validate_mode(value):
if not isinstance(value, str) or value not in {"plain", "unicode"}:
raise ValueError("invalid fixture mode")
return value
for value in ["plain", True]:
try:
print(validate_mode(value))
except ValueError as error:
print(error)
plain
invalid fixture mode
The boolean is rejected even though it could be converted to text. This model does not reproduce GitHub’s type checker or establish workflow-call permissions.
Common errors and fixes
A reusable workflow is placed under a step uses field.
- Why it happens
- Job-level workflow reuse was confused with action invocation.
- How to fix it
- Call it through the supported job uses form.
- Prevent it next time
- Document the shared unit’s invocation and execution boundary.
A called workflow unexpectedly uses an environment secret.
- Why it happens
- Environment selection and named secret passing were conflated.
- How to fix it
- Review the called job’s environment and secret precedence.
- Prevent it next time
- Make authority and environment ownership explicit in the interface.
A moving shared branch changes release behavior without local review.
- Why it happens
- The dependency reference is mutable.
- How to fix it
- Adopt a reviewed immutable identity and controlled update process.
- Prevent it next time
- Review nested actions and workflows as part of the dependency change.
Check your understanding
1. A shared sequence needs two operating systems and a downstream aggregation job. Which reuse form fits?
Show worked solution
A reusable workflow can express multiple jobs and their runner choices. A composite is a sequence inside one caller job and cannot provide that graph. Keep typed inputs and outputs narrow so callers can reason about the shared evidence.
2. Can a workflow input named script safely accept arbitrary contributor commands because the workflow itself is pinned?
Show worked solution
No. The pin identifies the wrapper, while arbitrary executable input can still change behavior and exploit its authority. Prefer a small validated mode or reviewed configuration input, and keep untrusted execution away from privileged jobs.
Key takeaways
- Choose reuse by execution boundary, not only reduced text.
- A reusable interface carries data and authority contracts.