Chapter 16Lesson 01~170 minutes

Reusable Workflows, workflow_call, Inputs, Secrets, Outputs, and Nesting: Core Concepts and Mental Model

Chapter 15 separated execution topology inside one job. Chapter 16 separates automation ownership: a caller workflow can delegate a whole job graph to a reusable workflow, but the interface, source revision, permissions, secrets, outputs, runner access and nested call chain remain explicit state. Reuse is safe only when the contract is versioned and the caller still owns trust.

workflow_callCaller contractTyped inputsExplicit secretsOutputs

Learning objectives

  • Explain a reusable workflow as a versioned job-graph contract rather than a text include.
  • Distinguish caller event/SHA, called workflow path/ref/SHA, runner state and nested workflow state.
  • Define typed inputs, named secrets, workflow outputs and permission ceilings before using them.
  • Predict which caller state propagates automatically and which state must be passed explicitly.
  • Inspect the call chain without exposing credentials or conflating reuse with deployment authorization.

1. The practical problem: copy-and-paste CI drifts faster than the application

Teams often start by duplicating a working test job into several workflow files. The copies look harmless until one caller updates a runtime, another adds a permission, a third fixes a security problem and a fourth keeps the old behavior. At that point the organization has several pipelines that claim to implement the same control but no single interface or version proves it.

A reusable workflow solves a different problem from a YAML anchor or a composite action. It lets one workflow delegate an entire job or job graph to another workflow file. The caller chooses the reusable workflow identity and supplies the declared interface; the called workflow owns its internal jobs and steps. That separation is useful only when the boundary stays explicit.

2. Mental model: caller state → contract → called jobs → outputs → caller decision

Start with the caller's event and exact source revision. The caller job points to a reusable workflow path and, for cross-repository reuse, a ref. GitHub validates typed inputs and secret names, applies the caller's token-permission ceiling and runner-access context, then runs the called workflow's jobs. The called workflow can expose declared outputs. Back in the caller, downstream jobs consume those outputs through needs. A nested reusable workflow repeats the same boundary one hop deeper.

Reusable workflow trust and data-flow boundary
flowchart TD
  A[Caller event + repository + exact SHA] --> B[Caller job uses reusable workflow]
  B --> C[Workflow path + immutable ref identity]
  B --> D[Typed inputs + named secrets + permissions]
  C --> E[Called workflow jobs]
  D --> E
  E --> F[Runner from caller access context]
  E --> G[Declared workflow outputs]
  E --> H{Nested reusable call?}
  H -->|Yes| I[Re-pass inputs secrets permissions]
  I --> J[Nested jobs cannot elevate permission]
  G --> K[Caller needs..outputs]
  J --> K
  K --> L[Evidence / next decision]

The important arrows are authorization boundaries, not decorative flow. A secret available to workflow A does not magically appear in workflow C through B. A token permission denied by A cannot be invented by B or C. A caller-level env value is not an API parameter. Reuse is therefore explicit dataflow plus explicit authority.

3. State ledger before writing YAML

Layer State to record Why it matters
Caller event/revision event, repository, ref, GITHUB_SHA, run ID/attempt proves what source and trigger created the call
Caller configuration caller workflow path plus call job ID proves which contract invocation was evaluated
Called workflow identity repository, .github/workflows/… path, ref and resolved SHA proves which reusable code ran
Interface input names/types/defaults, secret names, output names separates stable contract from implementation
Trust/token caller permissions and each downstream reduction nested workflows cannot elevate the caller token
Runner hosted/self-hosted selection available from caller context called repository does not donate a hosted runner to caller
Artifacts/cache explicitly separate from workflow outputs large files belong in artifacts; small scalar control data can be outputs
External/deployment none unless a called job explicitly mutates a target workflow reuse is not deployment approval or rollback
Governance access policy, pinned refs, ownership and review process central reuse creates a supply-chain dependency

4. What makes a workflow reusable?

The workflow file must live under .github/workflows and include workflow_call as a trigger. A reusable workflow can also have other triggers, but the reusable interface is defined under on.workflow_call. Inputs are typed as boolean, number or string. Secrets are named separately because they carry a different sensitivity model. Workflow outputs map internal job outputs to a caller-visible interface.

name: reusable-contract
on:
  workflow_call:
    inputs:
      runtime:
        description: Runtime family the contract supports
        required: true
        type: string
      strict:
        description: Enable the strict policy branch
        required: false
        default: true
        type: boolean
    secrets:
      demo_secret:
        description: Synthetic lab-only secret
        required: true
    outputs:
      result:
        description: Non-sensitive contract result
        value: ${{ jobs.check.outputs.result }}

permissions:
  contents: read

jobs:
  check:
    runs-on: ubuntu-24.04
    outputs:
      result: ${{ steps.result.outputs.value }}
    steps:
      - id: result
        shell: bash
        env:
          RUNTIME: ${{ inputs.runtime }}
          STRICT: ${{ inputs.strict }}
          DEMO_SECRET: ${{ secrets.demo_secret }}
        run: |
          test -n "$DEMO_SECRET"
          printf 'value=runtime-%s-strict-%s
' "$RUNTIME" "$STRICT" >> "$GITHUB_OUTPUT"

The secret is tested for presence but never printed, copied to an output or written to an artifact. The output is intentionally non-sensitive. That distinction is part of the interface contract.

5. A reusable workflow call is a job, not a step

An ordinary job declares runs-on and steps. A caller job instead declares uses at the job level. The caller can add only the supported call-job keys such as with, secrets, needs, if, strategy, concurrency and permissions. You do not mix steps or runs-on into that call job; the called workflow owns its internal jobs and runner selection.

jobs:
  ci:
    permissions:
      contents: read
    uses: ./.github/workflows/reusable-contract.yml
    with:
      runtime: '3.13'
      strict: true
    secrets:
      demo_secret: ${{ secrets.CH16_FAKE_SECRET }}

  report:
    needs: ci
    permissions: {}
    runs-on: ubuntu-24.04
    steps:
      - run: echo "contract_result=${{ needs.ci.outputs.result }}"

6. Same-repository identity versus a central repository

A same-repository call such as ./.github/workflows/reusable-contract.yml uses the reusable workflow from the same commit as the caller. That is convenient for a disposable learning repository because one source SHA identifies both files. A cross-repository call has the form owner/repo/.github/workflows/file.yml@ref. The ref can be a SHA, tag or branch; a full reviewed commit SHA is the safest production reference because it cannot move later.

If a tag and branch share a name, GitHub gives the tag precedence. Do not rely on that subtlety as a versioning strategy. Treat a central workflow repository exactly like an executable dependency: review changes, record the resolved commit and update callers intentionally.

7. What crosses the boundary automatically?

State Automatic? Correct interface
Caller github context associated with caller use safe caller identity fields; never dump the whole context
GITHUB_TOKEN available but bounded by caller permissions caller sets least privilege; called/nested workflows may only reduce
Workflow-level caller env no pass an input, use vars, or return an output
Named Actions secrets no pass explicitly with secrets: or deliberately use inherit
Secret to a nested workflow no transitive forwarding the intermediate workflow passes it again
Called workflow output only if declared map job output → workflow output → caller needs
Runner access evaluated from caller context do not assume the called repo grants extra hosted/self-hosted capacity

8. Permission ceiling and caller-owned trust

A called workflow does not create a new authority domain. If the caller job has only contents: read, a called or nested workflow cannot obtain issues: write just by declaring it. This non-escalation property is central to platform workflow design: callers decide the maximum authority they delegate.

At the same time, a central reusable workflow is executable code chosen by the caller. Pinning its commit does not make unsafe code safe; it makes the reviewed code identity stable. Review both the interface and implementation before granting secrets or write permissions.

9. Read-only inspection before mutation

echo "repository=$GITHUB_REPOSITORY"
echo "sha=$GITHUB_SHA"
echo "run=$GITHUB_RUN_ID attempt=$GITHUB_RUN_ATTEMPT"
echo "workflow=$GITHUB_WORKFLOW"
echo "ref=$GITHUB_REF"
# In a same-repository lab, the reusable file selected with ./ is from this same commit.
printf '%s
' 'called_path=.github/workflows/reusable-contract.yml'

These fields are safe bounded evidence. Do not dump toJSON(github) indiscriminately because the context includes token-bearing and event-derived fields that may be sensitive or attacker-controlled.

10. Security rules for reusable workflow contracts

Prefer explicit secret names over secrets: inherit so reviewers can see the capability being delegated. Never return a secret as a workflow output. Avoid mutable cross-repository refs for privileged workflows. Keep deployment credentials out of generic build contracts. If a called job uses an environment, treat that environment's protections and secrets as a separate authorization layer rather than as a substitute for caller-to-workflow secret passing.

Knowledge check

Why is a reusable workflow closer to an API than to copy-pasted YAML?

Can workflow B raise GITHUB_TOKEN permissions above workflow A in an A → B → C chain?

Does caller workflow-level env automatically appear inside the called workflow?

Why is ./.github/workflows/ci.yml useful in the same repository?

Where does a reusable workflow call appear syntactically?

Next lesson

Build the contract and watch one secret cross two explicit hops

Lesson 2 creates two callers, one central reusable workflow and one nested leaf workflow in a disposable repository.

Official references and version notes

Version and compatibility note

Version-sensitive behavior was rechecked on 2026-09-09 for GitHub.com. A reusable workflow is called at the job level, not from a step. Current GitHub.com limits allow up to 10 connected workflow levels and 50 unique reusable workflows in one top-level workflow tree. Supported caller-job surfaces are name, uses, with, secrets, strategy, needs, if, concurrency and permissions. Nested GITHUB_TOKEN permissions can stay the same or become more restrictive, never more permissive. Secrets are passed only to the directly called workflow unless forwarded again. Workflow-level caller env values do not cross the boundary automatically. Same-repository ./.github/workflows/file.yml calls use the same commit as the caller; cross-repository production calls should use a reviewed full commit SHA instead of a mutable branch or tag. Re-running all jobs against a non-SHA ref resolves that ref again, while re-running failed/specific jobs uses the called workflow commit from the first attempt.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.