Copying a CI job into five repositories makes the first setup easy and later fixes difficult. A reusable workflow provides a shared job-level interface: callers supply declared inputs, the workflow performs its work and declared outputs return results. Good reuse makes expectations visible instead of hiding them in copied YAML. This guide builds a two-file demonstration with no external actions or secrets. It also explains where a reusable workflow differs from a composite action and why access and permissions must be considered at the call boundary.
Choose the appropriate reuse unit
A reusable workflow is a workflow file with workflow_call as a trigger. A caller invokes it through a job's uses field, rather than inserting it as a step inside an existing job. It can define jobs and their runners. A composite action, by comparison, packages steps used within a caller's job. Choose based on whether you need to share job orchestration or a sequence of steps, not merely on which example looks shorter.
Keep reusable workflow files directly in .github/workflows. Give the shared workflow one clear responsibility and an interface a caller can explain. Avoid a single enormous workflow with dozens of loosely related mode switches. When two callers need fundamentally different trust or dependency assumptions, separate interfaces may be easier to maintain. Reuse is valuable when a fix can be reviewed once and adopted predictably; it is less valuable when callers cannot tell which hidden behavior they are invoking.
Declare inputs, secrets and validation
workflow_call declares input names, supported types and required or default behavior. The caller supplies values through with at the calling job. Type checking provides a useful boundary, but a string type does not mean every possible string is an acceptable target. The demonstration explicitly allows only docs or tests before printing a result. Make semantic validation part of the shared workflow so each caller does not need to reconstruct it independently.
Secrets use their own declaration and passing mechanism. Pass only the credentials the shared operation needs; do not default to broad secrets inheritance simply because it is convenient. Nested calls do not automatically make every caller secret available at every level. Also consider environment-level secrets separately when a called job selects an environment. Document which credential source the job actually uses. A reusable interface should make access predictable enough that a reviewer can trace it across the call chain.
Return outputs through the complete chain
A step creates its output through GITHUB_OUTPUT. The called job maps that step output to a job output, and workflow_call maps the job output to a workflow output. The caller then reads the reusable job's result through needs in a dependent job. Leaving out one mapping breaks the interface even when logs show that a value was printed successfully. Logs are diagnostic text; they are not declared outputs.
Use outputs for small coordination values, not complete archives or secret payloads. Specify the meaning of each value and whether failure prevents its production. If you expand the workflow into a matrix, understand its documented output-selection behavior rather than assuming every matrix result becomes a list automatically. For multiple reports, upload explicit artifacts or combine results deliberately. Interface changes should be reviewed as compatibility changes because several repositories may depend on their names and formats.
Sources and prerequisites
Begin with Actions, teams and security. Read GitHub's reuse guide, reuse reference, workflow syntax and output guide.
GitHub Reusable Workflows with Typed Inputs and Outputs Summary
- Use job-level workflow calls for shared orchestration.
- Declare, validate and document inputs, outputs and secret requirements.
- Review shared-code updates and preserve minimal permissions across calls.
GitHub Reusable Workflows with Typed Inputs and Outputs Examples
Each example shows the code, the output it produces and what each part does.
Two complete files for a reusable report workflow
The fence contains two YAML documents separated by ---. Save each document to the filename in its comment; do not save both as one workflow. Dispatch the caller manually. Its expected final output is shown below. These files demonstrate interface wiring without checking out or testing an application.
The shared workflow validates the selected target and publishes a small result. The caller invokes it at job level and prints the declared workflow output. No secret is required, so neither file declares or inherits one.
# .github/workflows/shared-report.yml
name: Shared report
on:
workflow_call:
inputs:
target:
required: true
type: string
outputs:
result:
description: Validated demonstration result
value: ${{ jobs.report.outputs.result }}
permissions: {}
jobs:
report:
runs-on: ubuntu-latest
outputs:
result: ${{ steps.make.outputs.result }}
steps:
- id: make
env:
TARGET: ${{ inputs.target }}
run: |
case "$TARGET" in docs|tests) ;; *) exit 1 ;; esac
printf 'result=%s checked\n' "$TARGET" >> "$GITHUB_OUTPUT"
---
# .github/workflows/call-report.yml
name: Call report
on: workflow_dispatch
permissions: {}
jobs:
shared:
uses: ./.github/workflows/shared-report.yml
with:
target: docs
show:
needs: shared
runs-on: ubuntu-latest
steps:
- env:
RESULT: ${{ needs.shared.outputs.result }}
run: printf '%s\n' "$RESULT"docs checkedGitHub Reusable Workflows with Typed Inputs and Outputs in Real Projects
Call a reusable workflow for a test report
Keep shared-report.yml from the main example and replace the caller with this complete version. It calls the same interface for tests and consumes the same output name. Predict the final line, then try an unsupported target in a separate run and observe the validation failure.
name: Call test report
on: workflow_dispatch
permissions: {}
jobs:
shared:
uses: ./.github/workflows/shared-report.yml
with:
target: tests
show:
needs: shared
runs-on: ubuntu-latest
steps:
- env:
RESULT: ${{ needs.shared.outputs.result }}
run: |
printf 'received: %s\n' "$RESULT"received: tests checkedThe interface remains unchanged while the input value changes. A target such as deploy fails the explicit case validation; it does not become a shell command. For a production test workflow, replace the demonstration result with real verified work and document what a successful result means.
GitHub Reusable Workflows with Typed Inputs and Outputs FAQ
Why can I not call a reusable workflow inside steps?
A reusable workflow is invoked at job level through uses and defines its own jobs or runners. If you want to package steps that run inside an existing job, consider a composite action instead. Keep the distinction visible in the design. Moving the same path under steps does not convert a workflow into an action, and adding runs-on to a calling reusable-workflow job is not the normal call structure.
Why is the caller's output empty even though the shared job printed it?
Printing to a log does not publish an interface output. Check that the producing step writes GITHUB_OUTPUT, the called job maps that named step output, workflow_call declares its mapping and the caller reads it through a needs dependency. Verify spelling at each level. The example intentionally shows the complete chain so you can diagnose the missing connection instead of trying unrelated environment-variable changes.
Should I use secrets: inherit for every shared workflow?
Pass only the secrets required by the operation when practical. Broad inheritance can make the shared implementation's access less obvious and complicate review, particularly with nested calls. Declare the contract and document any environment-specific credential source. A workflow that needs no secret, like this demonstration, should not receive one just for convenience. Review the trust level of both the caller's data and the called implementation before adding credentials.
How do I update a pinned cross-repository workflow?
Review a new implementation commit, run caller compatibility checks and deliberately change the full commit reference. Include any input, output or permission changes in that review. A pin gives a stable target but still needs maintenance. Keep a rollback reference and test representative callers before a broad rollout. Same-repository relative calls follow the caller's commit, while cross-repository pins need an explicit adoption step.
GitHub Reusable Workflows with Typed Inputs and Outputs Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
With the practice caller's target set to tests, what should its show job print?
GitHub Reusable Workflows with Typed Inputs and Outputs Quiz
2 questions to check what you learned.
Which trigger declares a workflow callable through another workflow's job?
- issue_comment alone
- A comment named reusable
- workflow_call
Which property of a called workflow's token permissions is essential?
- They cannot elevate beyond the caller's allowed permissions
- They always gain repository administration
- They are determined only by the workflow filename