Chaining workflows with workflow_run Jump to heading

A single workflow file that builds, tests, scans, deploys and posts results grows unreadable, and it forces every step to share one trigger and one set of permissions. Splitting it into separate workflows is cleaner, but then something has to start the second one when the first finishes. workflow_run does that: it triggers a workflow when another named workflow completes, gives it the triggering run’s details, and lets it download that run’s artefacts. It is also the standard way to do privileged work after unprivileged CI on fork pull requests. It has quirks — it runs on the default branch’s workflow definition, its context describes the other run rather than a push, and conclusions must be checked explicitly. This page covers them, within CI/CD pipeline trigger mapping.

When to use this approach Jump to heading

  • A follow-up job — deploy, publish, comment, notify — should run only after CI succeeds.
  • Different stages need different permissions or secrets, and you want them in separate workflows.
  • Fork pull requests need a privileged follow-up, as in running CI for fork pull requests safely.
  • A tag or commit created by one workflow should start another, which the default token cannot do directly.

Step 1 — Trigger on another workflow’s completion Jump to heading

Name the upstream workflow by its name: field, not its file name, and filter on completion. Then check the conclusion yourself: completed includes failures and cancellations.

# .github/workflows/deploy-staging.yml
on:
  workflow_run:
    workflows: ["CI"]                 # the upstream workflow's name:
    types: [completed]
    branches: [main]                  # only runs of CI on main
jobs:
  deploy:
    if: github.event.workflow_run.conclusion == 'success'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { ref: "${{ github.event.workflow_run.head_sha }}" }
      - run: ./scripts/deploy.sh staging
A two-stage chainA push to main starts the CI workflow, which builds, tests and uploads an artefact. When CI completes, the forge starts the deploy workflow with details of the CI run. The deploy workflow checks the conclusion, downloads the artefact by run ID and deploys exactly the commit CI tested.push to mainCI workflowforgedeploy workflowpush eventbuild, test, uploadcompleted: successworkflow_run eventdownload artefact by run iddeploy the head_sha from the event — main may have moved since CI started

Checking out head_sha rather than the branch matters: by the time the chained workflow starts, main may have moved, and you want to deploy the commit that was actually tested.

Step 2 — Pass data through artefacts Jump to heading

The chained workflow receives the upstream run’s ID, which it uses to download that run’s artefacts. That is the clean way to pass build outputs, test reports or version numbers between stages.

# In CI: upload what the next stage needs
      - run: make build && echo "${GITHUB_SHA}" > dist/SOURCE_SHA
      - uses: actions/upload-artifact@v4
        with: { name: dist, path: dist/ }
# In the chained workflow: download from the triggering run
      - uses: actions/download-artifact@v4
        with:
          name: dist
          run-id: ${{ github.event.workflow_run.id }}
          github-token: ${{ github.token }}
      - run: test "$(cat dist/SOURCE_SHA)" = "${{ github.event.workflow_run.head_sha }}"

Deploying the artefact CI built, rather than rebuilding, guarantees that what ships is what was tested.

Step 3 — Understand which definition and permissions apply Jump to heading

A workflow_run workflow always runs the version of its file on the default branch, with the base repository’s permissions and secrets — even when the upstream run was for a fork pull request. That is what makes it useful for privileged follow-ups, and what makes it dangerous if it executes anything from the upstream run.

What the chained workflow inheritsThe chained workflow's own file comes from the default branch and it receives full repository permissions and secrets. The upstream run's commit, branch and artefacts may come from a fork. Anything from the upstream side must be treated as data, never executed.TrustedPossibly untrustedworkflow filedefault branch—secrets, tokenrepository's—head_sha, branch—may be a forkartefacts—produced by fork codenever run scripts from the upstream checkout or artefact in a privileged chain
# Guard privileged chains so they only act on runs from this repository
    if: >
      github.event.workflow_run.conclusion == 'success' &&
      github.event.workflow_run.head_repository.full_name == github.repository

Step 4 — Avoid accidental loops and fan-out Jump to heading

A chained workflow can itself be an upstream for another chain. Keep chains short and acyclic: CI → deploy-staging → smoke-test is fine; a smoke-test workflow that re-triggers CI is a loop. GitHub limits chains to a few levels deep, but hitting that limit is a sign the design is wrong.

# List workflow_run relationships in a repository to spot cycles
grep -l 'workflow_run' .github/workflows/*.yml | while read -r f; do
  printf '%s <- %s\n' "$(sed -n 's/^name: *//p' "$f")" "$(sed -n '/workflow_run/,/types/p' "$f" | sed -n 's/.*workflows: *//p')"
done

Step 5 — Debug a chain that did not start Jump to heading

The most common reasons a chained workflow does not run are mismatched names, the file not being on the default branch, and branch filters that do not match.

Why didn't the chained workflow start?If the workflows list does not exactly match the upstream workflow's name field, nothing triggers. If the chained workflow file exists only on a feature branch, it is not loaded. If the branches filter excludes the upstream run's branch, the event is ignored. If the job ran but skipped, the conclusion check excluded it.What happened when CI finished?no run at allName or branchmatch name:, check filterfile only on branchNot loadedmerge to defaultrun skippedConclusion checkCI failed or cancelledworkflow_run matches the name: field — renaming CI silently breaks every chain
gh run list --workflow deploy-staging.yml --limit 5 --json event,conclusion,headSha
grep -n '^name:' .github/workflows/ci.yml          # must equal the value in workflows: [...]

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Can a chained workflow report status back on the pull request? Jump to heading

Yes, through the API: create a check run or commit status on head_sha with a token that has the right permission. Without that, the chained workflow’s result is visible only in the Actions tab, not on the pull request.

Is workflow_run better than needs within one workflow? Jump to heading

For stages with the same trigger and permissions, needs between jobs in one workflow is simpler and shows as one run. Use workflow_run when stages need different triggers, permissions or definitions.

What about GitLab? Jump to heading

GitLab uses stages and needs within one pipeline, and multi-project or parent-child pipelines with trigger: for chaining. The privilege concerns are similar: child pipelines inherit the parent’s context, so be careful about running untrusted code in them.