Skip to content
Latchkey LogoLatchkey home

GitHub Actions invalid workflow reference on a called workflow

GitHub Actions invalid workflow reference is the message when a job-level uses is not in one of the three accepted forms. The commonest reason is the one the message names, no version specified; the second is an expression, because this is the one keyword where a value cannot be computed.

Two uses keywords validated by two different rules, and the message each one emits
A job-level uses and a step-level uses are checked by different code and fail with different messages. Reading which one you got tells you which keyword is wrong.

What this error means

The run never starts. There is no job, no log and nothing to open, because the file was rejected during validation. The message quotes your value back and then says what is wrong with it in a short phrase, which is usually enough on its own. The workflow editor and the VS Code extension flag it as you type, attaching the diagnostic to the uses value itself rather than to the job, so most never reach a push. Two mistakes account for almost all of them: a reference that stops at the file name, and one whose ref is built from an expression.

actions/languageservices, languageservice/src/validate.ts
message: `Invalid workflow reference '${token.value}': ${reason}`
code: "invalid-workflow-uses-format"

reason, for a cross-repository reference:
  no version specified
  too many '@' in workflow reference
  references to workflows must be rooted in '.github/workflows'

reason, for a ./ or $/ reference:
  cannot specify version when calling local workflows
  cannot specify version when calling self repository workflows
  workflows must be defined at the top level of the .github/workflows/ directory

A minimal workflow that produces it

This file is written for this page and has never been run. Both jobs look ordinary and neither is accepted. The first names a workflow file and stops there, the way you would name a path. The second builds the ref from an input, the natural thing to try with a branch per environment.

For the first job the message reads Invalid workflow reference, the value, then no version specified. The second is rejected earlier, because an expression never reaches the reference check.

.github/workflows/deploy.yml (illustrative)
name: deploy
on:
  workflow_dispatch:
    inputs:
      environment:
        type: string
        default: staging

jobs:
  build:
    uses: octo-org/shared/.github/workflows/build.yml

  deploy:
    uses: octo-org/shared/.github/workflows/deploy.yml@${{ inputs.environment }}

Common causes

The reference stops at the file name

The common one, because a workflow path looks complete without a ref. Everything before the at sign is a location and everything after it is a version, and a cross-repository reference needs both. The message names the missing half and prints the value it received.

The ref is built from an expression

The keyword accepts no context and no expression, which makes it the only place in a workflow where a value cannot be computed. The intent is usually reasonable, one caller and several versions of a shared pipeline, so vary an input instead.

A same-repository reference carries a ref

The mirror image, and the one people hit after being told to pin everything. Both same-repository forms resolve to the caller commit, so a ref on one is refused rather than ignored.

The ref is fully qualified

Prefixes such as the heads and tags namespaces are not allowed, even though that is what the same value looks like in the context that triggered the run. The keyword takes a short name.

The path is not under the workflows directory

A called workflow lives directly in the workflows directory: "Subdirectories of the workflows directory are not supported." A shared file kept in a folder of its own produces a reference that looks well formed and names a location that cannot hold one.

How to fix it

Pick the form before you pick the ref

  1. Another repository: owner, repository, the workflows path, the file name, then a ref.
  2. This repository: the same-repository shorthand, no ref.
  3. Then check the message names a job rather than a step, because the step keyword has its own error.

Vary an input, not the reference

The fix for every version of the dynamic reference. Pin the call and pass the thing that changes as an input, which the called workflow declares and type checks.

.github/workflows/deploy.yml (illustrative)
  deploy:
    uses: octo-org/shared/.github/workflows/deploy.yml@v3
    with:
      environment: ${{ inputs.environment }}
    secrets: inherit

Pin third-party calls to a SHA

A called workflow runs with access to the secrets you pass it, so the reference is a trust boundary rather than a version number. The docs call the commit SHA "the safest option for stability and security", and nobody can change it by moving a tag.

.github/workflows/deploy.yml (illustrative)
  scan:
    uses: octo-org/shared/.github/workflows/scan.yml@8f4b7c2e0d1a9f3b6c5e2d4a7b8c9e0f1a2b3c4d # v3.2.1

Catch it in the editor rather than on push

This whole class of error is reported as you type, by the same validator the web editor runs, so the five second version of this fix is to open the file in an editor that knows the schema. Dependabot then keeps the pinned refs moving without anyone editing a reference by hand.

Three accepted forms, and which one needs a ref

The documentation lists three, and the third is the one most people have never met: "$/.github/workflows/{filename} for a reusable workflow in the same repository. This is the recommended syntax for referencing a reusable workflow in the same repository. This syntax is not available in GitHub Enterprise Server."

The ref rule follows from the form. A cross-repository reference must carry one: "When you reference a reusable workflow with {owner}/{repo} and @{ref}, the {ref} can be a SHA, a release tag, or a branch name. If a release tag and a branch have the same name, the release tag takes precedence over the branch name. Using the commit SHA is the safest option for stability and security."

The two same-repository forms must not carry one and do not need one: "the called workflow is from the same commit as the caller workflow. A $/ reference must not include an @{ref} suffix, and $/ is not available in GitHub Enterprise Server. Ref prefixes such as refs/heads and refs/tags are not allowed. You cannot use contexts or expressions in this keyword."

The three accepted forms (illustrative)
uses: octo-org/shared/.github/workflows/build.yml@v3   # cross-repository, ref required
uses: $/.github/workflows/lint.yml                     # same repository, recommended, no ref
uses: ./.github/workflows/lint.yml                     # same repository, older form, no ref

The corrected file

The corrected version fixes both jobs, each differently. The first gains the ref it was missing. The second stops computing a ref and passes the value it was really varying, the environment, as an input to a workflow pinned to one commit. That input is the one the caller declares, so the samples compose.

Read them as a pair: the first is rejected with no run created, the second validates and starts. The third job shows the same-repository shorthand, which needs no ref at all.

.github/workflows/deploy.yml, corrected (illustrative)
jobs:
  build:
    uses: octo-org/shared/.github/workflows/build.yml@v3

  deploy:
    uses: octo-org/shared/.github/workflows/deploy.yml@8f4b7c2e0d1a9f3b6c5e2d4a7b8c9e0f1a2b3c4d
    with:
      environment: ${{ inputs.environment }}

  lint:
    uses: $/.github/workflows/lint.yml

The step-level message is a different message

There is a near neighbor easy to mistake for this one. A uses under steps: names an action, not a workflow, and is checked by different code with a different message, which begins Expected format and then prints {owner}/{repo}[/path]@{ref} and the value it received. The runner spells the same check {org}/{repo}[/path]@ref.

So the message tells you which keyword to open: an invalid workflow reference is a job-level uses calling a workflow, and an expected-format error is a step-level uses calling an action. ManuLinares/setup-c3#1 is the step-level one in the wild.

It refuses the swap both ways, one sentence each. A workflow reference under steps: is caught by the step check: "Reusable workflows should be referenced at the top-level jobs.<job_id>.uses key, not within steps". An action reference on a job fails the workflows-path test of the job check: "references to workflows must be rooted in '.github/workflows'".

Where the uses isWhat it namesMessage on a missing refChecked by
jobs.<job_id>.usesa reusable workflowInvalid workflow referencevalidateWorkflowUsesFormat
steps[*].usesan actionExpected format ...@{ref}validateStepUsesFormat
jobs.<job_id>.usesa same-repository workflownone, a ref is refused herevalidateWorkflowUsesFormat
steps[*].usesa local action, ./pathnone, a ref is not usedvalidateStepUsesFormat

Why there is no recorded run on this page

The file is rejected during validation, so no run is created and there is no job log. Nothing is transient and a runner has nothing to repair: the reference either matches one of the three accepted forms or it does not. The message at the top is quoted from the validator that builds it, because the rendered line appears in an annotation rather than in any log a job could produce.

How to prevent it

  • Treat the reference as a trust boundary and pin anything you do not control to a SHA.
  • Use the same-repository shorthand inside one repository, where it cannot drift and needs no ref.
  • Never publish a tag and a branch with the same name, because the tag wins and nothing says so.
  • Vary inputs rather than references, so the set of workflows a caller can reach stays enumerable.

Frequently asked questions

What does "Invalid workflow reference: no version specified" mean?
That a job-level uses names a workflow in another repository and stops at the file name. Everything before the at sign is a location and everything after it is a version, and a cross-repository reference needs both. The message quotes your value back so you can see which half is missing.
Can I use a variable in a reusable workflow uses?
No. The documentation says "You cannot use contexts or expressions in this keyword", and the reference is resolved while the file is validated, before any context exists. Pin the call and pass the value that varies as an input instead.
How do I call a reusable workflow in the same repository?
With one of the two same-repository forms and no ref at all. The docs call the $/ form "the recommended syntax for referencing a reusable workflow in the same repository", and the older relative form is still accepted. Neither takes a ref, and $/ is not available in GitHub Enterprise Server.
Why do I get Expected format instead of Invalid workflow reference?
Because the value is under steps: rather than on the job, so the step check reports it and the job check never sees it. Decide which you meant: a workflow moves up to the job, where the rules on this page apply; an action stays put and is simply missing its ref.

Related guides

References

Pinning the ref is one decision. What it runs on is the other. Latchkey is $0.0025/min at 2 vCPU. Start free → 30-day trial · No credit card