GitHub Reusable Workflows with Typed Inputs and Outputs

Build a reusable GitHub workflow with workflow_call, typed inputs and outputs, then call it at job level with explicit permissions and contracts.

Lesson 24 of 30Intermediate10 min read

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.

Pin shared code and preserve permission boundaries

A same-repository relative reference uses the workflow from the same commit as the caller, which makes a local demonstration easy to review together. A cross-repository reference includes an owner, repository, path and ref. Pin external shared code to an reviewed full commit identifier when you need a stable execution target; a mutable branch or tag can change without editing the caller. Establish an update process rather than leaving a security-relevant pin frozen forever.

A called workflow cannot elevate the caller's token permissions beyond what the call chain allows. Set a minimal permission contract and verify the repository access required to use private shared workflows. Be especially careful when the caller processes untrusted contributions: code reuse does not change the source's trust level. Treat modifications to the shared implementation as automation changes with potentially many consumers. Test normal inputs, invalid inputs and outputs before rolling a new version into production callers.

Sources and prerequisites

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.

Example 1

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.

YAML
# .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"
Output
docs checked

GitHub 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.

YAML
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"
Output
received: tests checked

The 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.

Multiple choicemedium

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.

  1. Which trigger declares a workflow callable through another workflow's job?

    • issue_comment alone
    • A comment named reusable
    • workflow_call
  2. 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