Skip to content
Latchkey LogoLatchkey home

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.

The three workflow_call input types against what a caller may pass into each of them
Three declared types, one list of contexts the with block accepts, and one cast for everything that arrives as text.

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.

Annotation, quoted from catalyst/catalyst-moodle-workflows#159
Invalid input, disable_phpcpd is not defined in the referenced workflow

A 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.

Two files, illustrative
# .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

  1. Write true and false without quotes for a boolean input, and bare digits for a number.
  2. Keep quotes for strings, where they are harmless and sometimes necessary.
  3. Check the called workflow declares the type you think it does before changing the caller.
.github/workflows/ci.yml (illustrative)
  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.

.github/workflows/ci.yml (illustrative)
    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.

.github/workflows/deploy.yml (illustrative)
jobs:
  ship:
    if: ${{ inputs.publish == true || inputs.publish == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - run: ./publish.sh

Move 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.

.github/workflows/ci.yml (illustrative)
  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.

.github/workflows/ci.yml, corrected (illustrative)
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_call input 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?
Because the value arrived as text and a non-empty string is truthy, so 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?
Cast it. The expressions reference says a step output "evaluates as a string", so surface it as a job output and wrap it in 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?
The caller passed a key the called workflow never declared under 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?
No. The context availability table lists 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.

Related guides

References

A gate that never closed has been billing on every call. Latchkey bills at $0.0025/min at 2 vCPU. Start free → 30-day trial · No credit card