Migrate to RunxBuild today!! Deploy More while Spending Less

Calculate your savings
unxBuild

GitHub Actions Cross Repository: Reusable Workflows vs Dispatch vs Checkout

Sean

Platform Writer

Sep 22, 2026
8 min read

GitHub Actions has three distinct ways to work across repositories, and they solve different problems. A reusable workflow, called with workflow_call, shares one workflow’s steps so multiple repos run identical logic without copying YAML. repository_dispatch sends a custom event from one repo’s workflow to trigger a workflow in another, useful for kicking off a downstream build. Checking out another repository inside a job, using actions/checkout with a token, pulls in code or config that the current job needs to read but does not own. Picking the wrong one usually means fighting permissions instead of writing logic.

GitHub Actions Cross Repository: Reusable Workflows vs Dispatch vs Checkout

Every cross-repo question on GitHub Actions eventually becomes a token-scoping question, because the default GITHUB_TOKEN only ever sees the repository the workflow is running in.

This post covers the three patterns, real YAML for each, and the choice between a personal access token and a GitHub App installation token for the cases that need one.

Table of contents

Which pattern for which need

Start from the goal, not the mechanism. Want several repositories to run the same CI logic without copy-pasting a workflow file into each one: a reusable workflow. Want repo A to kick off a build in repo B when something happens in A: repository_dispatch. Want a job in repo A to read files, run scripts, or build an artifact from repo B: checkout with a token. The three are not alternatives to each other; most cross-repo setups end up using two of them together.

A quick way to tell which pattern applies: does the work need to happen in the other repository’s own checkout, with its own runner and its own job graph. If yes, that is a reusable workflow or a dispatch, not a checkout. Does the current job only need to read a file or a script that lives in the other repository. That is a checkout with a token, and nothing about the other repository’s own workflows needs to run at all.

  • same logic run identically across repos, no repo-specific state: a reusable workflow
  • an event in repo A should kick off a full run in repo B: repository_dispatch
  • a job in repo A only needs to read files from repo B: checkout with a token

Reusable workflows with workflow_call

A reusable workflow is a normal workflow file with on: workflow_call instead of on: push, kept in a repository the caller can reach. The caller’s job points uses: at that file and passes inputs and, if needed, secrets.

# .github/workflows/build.yml in the owning repo
on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: '20'
    secrets:
      NPM_TOKEN:
        required: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci
      - run: npm run build
# in a different repo, calling the workflow above
jobs:
  call-build:
    uses: org/shared-workflows/.github/workflows/build.yml@main
    with:
      node-version: '22'
    secrets: inherit

secrets: inherit passes every secret the caller has to the callee; list secrets individually instead if the callee should only see some of them. The called workflow must live in a public repo, or the caller must have access to the private repo it lives in.

Get an input wrong and the failure is at parse time, before any step runs. The caller’s job fails immediately with an error naming the missing or mismatched input, which is easier to debug than a job that runs partway and fails on a step that assumed the input existed.

repository_dispatch: trigger another repo

repository_dispatch fires when something outside the normal push or pull_request events happens, and the trigger is a POST to the GitHub API carrying a custom event_type and an optional payload, as GitHub’s event reference describes.

curl -X POST \
  -H "Authorization: Bearer $GH_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  https://api.github.com/repos/OWNER/downstream-repo/dispatches \
  -d '{"event_type":"upstream-release","client_payload":{"version":"1.4.0"}}'
# downstream workflow, listening
on:
  repository_dispatch:
    types: [upstream-release]

jobs:
  handle:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Upstream shipped ${{ github.event.client_payload.version }}"

GH_TOKEN needs at least contents: write or the fine-grained repository dispatches permission on the target repo; the default GITHUB_TOKEN cannot post to another repository, so this always needs a PAT or an app token.

Two failure modes are common here. A token without at least contents: write, or the dispatches permission on a fine-grained PAT, gets a 404 Not Found back from the API rather than a 403, which reads as though the target repository does not exist and is the most confusing error in this whole pattern. The other is a typo in event_type: the API call succeeds with a 204, nothing errors, and the downstream workflow simply never runs, because its on: repository_dispatch: types list does not include whatever string was actually sent.

Checking out another repo with a token

Reading another repository’s contents inside a job, without triggering anything, is actions/checkout pointed at a different repository and given a token that can see it.

- uses: actions/checkout@v4
  with:
    repository: org/shared-config
    token: ${{ secrets.CROSS_REPO_PAT }}
    path: shared-config

The default GITHUB_TOKEN can only check out the repository the workflow already runs in, even for a public sibling repo in the same org, unless that org has explicitly allowed broader access. Anything else needs a token supplied as a secret.

A missing or wrong-scoped token here fails at the checkout step itself, with an error like remote: Repository not found even though the repository clearly exists, since Git cannot distinguish a private repository from one that does not exist without valid credentials. The path input matters too: without it, the other repository’s files land on top of the current checkout’s root and can silently overwrite files with the same name, so giving the second checkout its own subdirectory is worth doing even when nothing looks like it would collide today.

PAT vs GitHub App token scoping

A classic personal access token is scoped to everything the issuing user can touch and does not expire unless you set that; a fine-grained PAT can be scoped to specific repositories and specific permissions, which is the one worth using for a single cross-repo job.

A GitHub App installation token is scoped to the app’s declared permissions and only the repositories the app was installed on, and it expires automatically after an hour. It takes more setup, generating a private key and a short script to mint the token, but it is the option that does not depend on one person’s account and does not need manual rotation.

A rotated or revoked PAT breaks every workflow using it at once, often silently until the next run fails. That is the practical argument for the App token even on a small project: the private key rotates independently of any person leaving, and an audit of what the App can touch is one settings page rather than a guess about what a departed teammate’s token could reach.

Private repo access settings

An org’s Settings, Actions, General page controls whether workflows in one repository can access other private repositories in the same org at all, independent of any token. Some orgs turn this off by default, in which case even a correctly scoped PAT gets a permission error until an admin allows it.

It is worth checking this setting first when a cross-repo checkout fails with an authentication error that looks like a token problem but is actually an org policy.

The error this produces, Resource not accessible by integration on a 403, looks identical whether the cause is a missing token scope or an org policy blocking the access entirely, which is why it is worth checking the org setting before spending time re-scoping a token that was already correct. A second, quieter version of the same policy applies to Actions reading packages or artifacts across repos in the same org, controlled from a separate settings page, and it trips up teams who only checked the repository-access one.

A monorepo vs multirepo aside

None of this is needed inside a single monorepo, where a workflow can read any path in the same checkout with no cross-repo token at all. The three patterns above exist specifically for the multirepo case, and a project chasing token scoping across ten repositories is sometimes a project that would rather be one repository with path filters.

A concrete version of this: a monorepo with a frontend and a backend directory can use one workflow with a paths filter, path: frontend/** triggering the frontend job and path: backend/** triggering the backend one, achieving the same selective-build behavior that repository_dispatch exists to fake across repository boundaries. The cost moves in the other direction too. A monorepo’s CI has to understand which paths changed, and a change touching a shared library at the root can still trigger every job regardless of the filters, which is its own kind of coordination problem.

How this fits the rest of the stack

Each of these three patterns needs the right token in the right place before the YAML matters at all, and getting that wrong is most of the debugging time in a cross-repo setup. Once the build actually runs, deploying the result straight from GitHub removes a separate release step, and the RunxBuild hosting calculator totals the service and database cost for whatever the pipeline is shipping.

Useful related references:

FAQ

Can a GitHub Actions workflow trigger a workflow in another repository?

Yes, with repository_dispatch. A workflow in the source repository sends a POST request to the target repository’s dispatches API endpoint with a custom event_type, and a workflow in the target repository listening for that type starts. It needs a token with write access to the target repo, since the default GITHUB_TOKEN cannot reach another repository.

What is the difference between a reusable workflow and a composite action?

A reusable workflow, called with workflow_call, can define entire jobs, run on its own runner, and accept secrets, which suits sharing a full CI pipeline. A composite action bundles a sequence of steps to run inside an existing job, which suits sharing a smaller piece of logic like a setup routine. Reusable workflows are the right choice when whole repos should run identical pipelines.

Should I use a personal access token or a GitHub App for cross-repo access?

A fine-grained personal access token is faster to set up and fine for a small number of repositories tied to one maintainer. A GitHub App installation token is scoped to exactly the repositories the app is installed on, expires automatically, and does not depend on one person’s account, which matters more as the number of repos or the team grows.

Why does actions/checkout fail on a private repo in the same organization?

The default GITHUB_TOKEN only has access to the repository the workflow is running in, even for a sibling private repo in the same org, unless the org has explicitly enabled broader repository access under its Actions settings. Passing a scoped PAT or app token to the checkout step’s token input is the usual fix.

#github actions cross repository#reusable workflows#repository dispatch#github app tokens#monorepo vs multirepo