GitHub Actions reusable workflow input type mismatch
A GitHub Actions reusable workflow input type mismatch is the caller and the called workflow disagreeing about what a value is. workflow_call inputs are declared with a type and checked against it, so a quoted string sent to a boolean input, or an expression the typed key cannot accept, stops the run before any job starts.

What this error means
One of two things happens, and they look nothing alike. The loud one is a validation failure in the caller: the run never starts, and the annotation names the input and the file it was not found in, or names the value it could not use. The quiet one is worse, because the run is green. A boolean input arrives holding text, the called workflow writes if: inputs.flag against it, and the condition is true whether the text says true or false, so a deploy that was supposed to be gated runs on every call. The tell for the quiet version is that toggling the caller changes nothing at all: the job runs identically with the flag set and cleared, and the only way to see it is to print the value rather than branch on it.
Invalid input, disable_phpcpd is not defined in the referenced workflowA minimal pair that produces it
These two files are written for this page and have never been run. The called workflow declares a boolean, the caller passes the quoted text true, and the condition inside the called workflow reads the input directly. Nothing is red. The deploy runs whatever the caller passes, because a non-empty string is truthy and the text false is a non-empty string.
# .github/workflows/deploy.yml (called)
on:
workflow_call:
inputs:
publish:
type: boolean
required: true
jobs:
ship:
if: ${{ inputs.publish }}
runs-on: ubuntu-latest
steps:
- run: ./publish.sh
# .github/workflows/ci.yml (caller)
jobs:
call:
uses: ./.github/workflows/deploy.yml
with:
publish: 'true'Common causes
A quoted literal sent to a boolean or number input
YAML quoting decides the type before Actions sees it. publish: true is a boolean, publish: 'true' is a string, and the declared type is checked against what YAML produced. This is the version that passes validation in some shapes and then behaves as text inside the called workflow.
The value came from an expression, so it is text
Every step output is a string by definition, and a job output built from one is a string as well. Feeding it straight into a typed key gives you either a rejection or a truthy string, depending on where the mismatch lands. The cast has to be explicit because nothing casts it for you.
The with block reached for a context it is not allowed
Only six contexts are available in jobs.<job_id>.with.<with_id>. Referencing env, secrets or steps there produces an unrecognized name, and the typed key then rejects the whole value. Secrets have their own key, jobs.<job_id>.secrets, and that one does accept the secrets context.
The key is not declared in the called workflow at all
A rename on one side, a copied caller, or an input removed from a shared workflow. The parser is explicit about it, naming the key and the file: this is the one failure in the set that tells you precisely what to change.
How to fix it
Pass literals unquoted and let YAML carry the type
- Write
trueandfalsewithout quotes for a boolean input, and bare digits for a number. - Keep quotes for strings, where they are harmless and sometimes necessary.
- Check the called workflow declares the type you think it does before changing the caller.
call:
uses: ./.github/workflows/deploy.yml
with:
publish: true
retries: 3
environment: 'staging'Cast expression results with fromJSON
When the value comes from a step or a job output, wrap it. The cast belongs in the caller, at the moment the text becomes a typed input, and it keeps the called workflow honest about what it declared.
with:
publish: ${{ fromJSON(needs.decide.outputs.publish) }}
retries: ${{ fromJSON(needs.decide.outputs.retries) }}Compare explicitly inside the called workflow
A condition that reads a bare input is only safe if the input really is a boolean. Comparing against the value you expect makes the job behave the same way whether the input arrived typed or as text, which is a useful property while a shared workflow has callers you do not control.
jobs:
ship:
if: ${{ inputs.publish == true || inputs.publish == 'true' }}
runs-on: ubuntu-latest
steps:
- run: ./publish.shMove secrets to the key that accepts them
A secret passed through with: is both a type problem and a disclosure problem, because inputs are not masked the way secrets are. Declare it under on.workflow_call.secrets and pass it with jobs.<job_id>.secrets, or use secrets: inherit to pass the caller secrets through, which works for a called workflow in the same repository and for one in the same organization or enterprise.
call:
uses: ./.github/workflows/deploy.yml
with:
publish: true
secrets:
registry-token: ${{ secrets.REGISTRY_TOKEN }}What workflow_call promises about types
The type is not optional and the set is small. The workflow syntax reference for on.workflow_call.inputs.<input_id>.type says it is "Required if input is defined for the on.workflow_call keyword", and that "The value of this parameter is a string specifying the data type of the input. This must be one of: boolean, number, or string."
The obligation on the caller is stated just as plainly under jobs.<job_id>.with.<with_id>: "The identifier must match the name of an input defined by on.workflow_call.inputs.<inputs_id> in the called workflow. The data type of the value must match the type defined by on.workflow_call.inputs.<input_id>.type in the called workflow." A key the called workflow does not declare is rejected with the line at the top of this page, which is the exact text the workflow parser emits.
The with block has a context list of its own
A caller that passes a literal is easy. A caller that passes an expression has a second constraint, and it is the one that produces the confusing annotations. The context availability table lists jobs.<job_id>.with.<with_id> as accepting exactly six: github, needs, strategy, matrix, inputs and vars. No env, no secrets, no steps, no job, no runner.
Use one of the missing ones and you get two complaints on the same line, because the name is rejected and then the value that contained it is rejected too. actions/runner#1492 shows the pairing on a boolean-typed key, continue-on-error rather than a with: input, and the two complaints are the same pair: "Unrecognized named-value: 'env'. Located at position 10 within expression: contains(env.fullySupportedScalaVersions, matrix.scalaVersion)" followed by "Unexpected value '${{ contains(env.fullySupportedScalaVersions, matrix.scalaVersion) }}'". The second half is the typed key refusing what the first half left behind.
Cast the value where it becomes text
Step outputs are the usual source of a boolean that is not one. The expressions reference states it as a rule: "steps.<step_id>.outputs.<output_name> evaluates as a string." So a flag computed in a step arrives at the caller as text, and a typed key will not take it as a boolean.
fromJSON is the documented way across. It "returns a JSON object or JSON data type for value", which turns the text true into a real boolean and the text 3 into a real number. Put the cast in the caller, where the type is being crossed, rather than inside the called workflow where the declaration has already promised a type.
jobs:
decide:
runs-on: ubuntu-latest
outputs:
publish: ${{ steps.flags.outputs.publish }}
steps:
- id: flags
run: echo "publish=true" >> "$GITHUB_OUTPUT"
call:
needs: decide
uses: ./.github/workflows/deploy.yml
with:
publish: ${{ fromJSON(needs.decide.outputs.publish) }}Why there is no recorded run on this page
Both halves are decided by the workflow parser: the loud one before the run starts, the quiet one when the expression is evaluated. Neither is something a script on a runner can produce, and neither is transient, so there is nothing to detect, retry or repair. The annotation at the top is quoted from a public issue and the two files above are illustrative.
How to prevent it
- Declare every
workflow_callinput with the type it really is, and give it a default where one makes sense. - Pass boolean and number literals unquoted, and cast anything that came from an expression with
fromJSON. - Keep secrets in the secrets key, where the secrets context is available and the value is masked.
- Version a shared workflow by tag, so a removed input cannot break every caller on the same afternoon.
Frequently asked questions
Why does my reusable workflow treat a boolean input as always true?
if: inputs.flag is true for the text false as well. Pass the literal unquoted from the caller, or compare explicitly inside the called workflow. Printing the value once is faster than reasoning about it, because the condition itself gives you no signal.How do I pass a step output to a boolean workflow_call input?
fromJSON(needs.decide.outputs.publish) in the caller. The function returns a real JSON data type, so the text true becomes a boolean and the text 3 becomes a number.What does "Invalid input, X is not defined in the referenced workflow" mean?
on.workflow_call.inputs. It is usually a rename on one side or a copied caller, and it is the one message in this family that names exactly what to change. catalyst/catalyst-moodle-workflows#159 is a public report of it after an input was dropped from a shared workflow.Can I use secrets or env in the with block of a reusable workflow?
jobs.<job_id>.with.<with_id> as accepting github, needs, strategy, matrix, inputs and vars, and nothing else. A secret belongs in jobs.<job_id>.secrets, which does accept the secrets context, and an environment variable has to be lifted into one of the six first.