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.

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.
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/ directoryA 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.
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
- Another repository: owner, repository, the workflows path, the file name, then a ref.
- This repository: the same-repository shorthand, no ref.
- 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.
deploy:
uses: octo-org/shared/.github/workflows/deploy.yml@v3
with:
environment: ${{ inputs.environment }}
secrets: inheritPin 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.
scan:
uses: octo-org/shared/.github/workflows/scan.yml@8f4b7c2e0d1a9f3b6c5e2d4a7b8c9e0f1a2b3c4d # v3.2.1Catch 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."
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 refThe 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.
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.ymlThe 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 is | What it names | Message on a missing ref | Checked by |
|---|---|---|---|
jobs.<job_id>.uses | a reusable workflow | Invalid workflow reference | validateWorkflowUsesFormat |
steps[*].uses | an action | Expected format ...@{ref} | validateStepUsesFormat |
jobs.<job_id>.uses | a same-repository workflow | none, a ref is refused here | validateWorkflowUsesFormat |
steps[*].uses | a local action, ./path | none, a ref is not used | validateStepUsesFormat |
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?
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?
How do I call a reusable workflow in the same repository?
$/ 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?
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.