Skip to content
Latchkey LogoLatchkey home

Invalid input, secrets is not defined in the referenced workflow

Invalid input, secrets is not defined in the referenced workflow is the input check rejecting a key named secrets that it found where input names are read, which is what a secrets: line does when it is indented one level too deep and lands inside with:. There is a second message one word away, Invalid secret, and that word is the fastest diagnosis on the page: it names which of the two checks gave up, and therefore which file you should have open.

The two placements of a secrets key on a calling job, and the message each one produces
The grid on the right is the table reproduced in the body of this page. The annotation is quoted in full from pytorch/torchchat#1351.

What this error means

The run is rejected before any job starts. The Actions tab holds a run with no jobs and one annotation against a line in the calling workflow, carrying a line and a column number, and the pull request shows a check that never reports rather than one that went red. What makes this one confusing is the wording: the annotation says Invalid input while the key on the line it points at is called secrets, so the usual next move is to go hunting for a secret that does not exist. There is none to find. Another job in the same file, calling the same workflow, will often validate perfectly, because its secrets: line is one indent further left.

Actions annotation, quoted from pytorch/torchchat#1351
Invalid workflow file: .github/workflows/run-readme-periodic.yml#L50
The workflow is not valid. .github/workflows/run-readme-periodic.yml (Line: 50, Col: 16): Invalid input, secrets is not defined in the referenced workflow.

The word after Invalid names the check, not the key

The sentence comes from a single format string in WorkflowTemplateConverter.cs in actions/runner, and the kind of parameter is a slot in it. Exactly two calls fill that slot. The pass over the caller's with block passes the literal input, and the pass over the caller's secrets map passes a constant whose value is secret. Nothing else calls it. So Invalid input can only have come from the pass that compares your with keys against the inputs the called workflow declares, whatever the offending key happens to be named.

That is the whole diagnosis of the annotation at the top of this page. A key called secrets was found among the values the caller supplied as inputs, the called workflow declares no input by that name, and the check reported it. The spelling is right and the key is real; it is in the wrong block.

MessageCheck that emitted itWhere the key was writtenWhat to change
Invalid input, secrets is not defined in the referenced workflow.inputInside with:Move secrets: out to the job
Invalid input, NAME is not defined in the referenced workflow.inputInside with:Match a name in the callee on.workflow_call.inputs
Invalid secret, NAME is not defined in the referenced workflow.secretUnder secrets: on the jobDeclare it in on.workflow_call.secrets, or stop sending it
Secret NAME is required, but not provided while calling.secretNowhere, nothing was sentPass it from the caller

Common causes

The secrets key is indented into the with block

The cause in the quoted annotation, and in our experience the usual one when the name in the message is secrets. One extra level of indentation moves the key out of the job and into the input map, where it is read as an input name. The value and the spelling are untouched, so the line is correct in isolation and reviewers read straight past it.

Another scalar job key was pulled in by the same edit

Reindenting a block moves everything under it. if: and name: carry scalars and clear the schema the way secrets: inherit does, so either landing inside with: produces this message with its own name in it. Keys carrying a mapping, such as permissions: or concurrency:, never reach this check, because with accepts only scalar values.

The caller passes an input the called workflow never declared

The same message from the same check, with nothing to do with secrets: the key really is an input and no declaration matches it. A rename on either side does it, and catalyst/catalyst-moodle-workflows#159 is a public report of an input dropped from a shared workflow taking a caller in another repository down with it.

The other direction, which is a different string

A caller that passes secrets by name rather than inheriting is checked against on.workflow_call.secrets in the called workflow, and an unmatched name is reported as Invalid secret, NAME is not defined in the referenced workflow. One word from the message on this page, and a fix in the other file.

How to fix it

Read the column number, then move the key

  1. Take the line and column from the annotation. The column lands on the value, not the key, so on secrets: inherit it points at the first letter of inherit.
  2. Look at what that line is nested under. secrets: belongs on the job, indented the same as uses: and with:, never among the entries inside it.
  3. Move it out one level and leave the value alone. Nothing else about the call changes, and nothing has to be declared.
  4. Check the other calling jobs in the file before you push, since a reindent rarely touches only one.
.github/workflows/periodic.yml, corrected (illustrative)
jobs:
  test-quantization-any:
    uses: ./.github/workflows/linux-job.yml
    secrets: inherit
    with:
      runner: linux.g5.4xlarge.nvidia.gpu
      gpu-arch-type: cuda

Treat the with block as input names and nothing else

Everything else a calling job may carry sits beside with:, not in it, and GitHub publishes the closed set: "When you call a reusable workflow, you can only use the following keywords in the job containing the call". Those keys are name, uses, with, secrets, strategy, needs, if, concurrency and permissions, the same nine properties the workflow schema gives a calling job. Anything on that list is a sibling of uses:; anything that is neither on it nor a declared input has no place on that job.

.github/workflows/release.yml (illustrative)
jobs:
  release:
    name: Release
    needs: [build]
    if: github.ref == 'refs/heads/main'
    permissions:
      contents: read
    uses: ./.github/workflows/deploy.yml
    with:
      target: production
    secrets: inherit

Read the declared inputs before adding a key to with

When the key really is an input, the legal names are in the called workflow under on.workflow_call.inputs, at the ref your uses: line pins. The syntax reference puts the obligation on the caller: "The identifier must match the name of an input defined by on.workflow_call.inputs.<inputs_id> in the called workflow." Read that file at that ref rather than a README, since an input removed from a shared workflow breaks callers at their next run, not at the change.

.github/workflows/linux-job.yml, the called workflow (illustrative)
on:
  workflow_call:
    inputs:
      runner:
        type: string
        default: ubuntu-latest
      secrets-env:
        description: 'Names of the secrets to expose to the script'
        type: string
        required: false

For the Invalid secret form, fix the declaration or stop sending it

A named secrets: map is the strict form, and the docs state it plainly: "Any secrets that you pass must match the names defined in the called workflow." Either add the declaration to the called workflow or drop the name from the caller. Switching that job to secrets: inherit silences it by skipping the check rather than satisfying it, and hands the callee everything the caller can read, so treat that as a decision about trust rather than a repair.

Two files, illustrative
# The called workflow declares what it accepts.
on:
  workflow_call:
    secrets:
      DEPLOY_TOKEN:
        description: 'Token used by ./deploy.sh to publish the release'
        required: true

# The caller passes it by name.
jobs:
  release:
    uses: ./.github/workflows/deploy.yml
    secrets:
      DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

One public file with both spellings in it

The repository the annotation comes from is worth opening, because it got the same line right and wrong in the same file. At the commit in effect when that run failed, .github/workflows/run-readme-periodic.yml held three jobs, all calling pytorch/test-infra/.github/workflows/linux_job.yml@main. In test-readme, secrets: inherit is line 14, a sibling of uses: on line 13 and with: on line 15. In test-quantization-any, with: opens on line 48 and secrets: inherit is line 50, indented into it between runner: and gpu-arch-type:. Line 50, column 16 is where the annotation points, and column 16 is the first character of inherit.

It is an easy edit to make, because the neighbors look right. That called workflow declares an input named secrets-env, and the job that validates passes it inside with:. A line reading secrets: near a line reading secrets-env: does not look out of place, and nothing in the caller can tell you it is.

.github/workflows/periodic.yml (illustrative)
jobs:
  # Correct: secrets is a job key, a sibling of uses and with.
  test-readme:
    uses: ./.github/workflows/linux-job.yml
    secrets: inherit
    with:
      runner: linux.g5.4xlarge.nvidia.gpu
      secrets-env: "HF_TOKEN_PERIODIC"

  # Rejected: secrets is now an input name, and no such input is declared.
  test-quantization-any:
    uses: ./.github/workflows/linux-job.yml
    with:
      runner: linux.g5.4xlarge.nvidia.gpu
      secrets: inherit
      gpu-arch-type: cuda

Why nothing catches it until both files are read

The workflow schema in actions/runner defines the with block as a loose mapping: workflow-job-with sets the key type to a non-empty string and the value type to a scalar, and lists no permitted names, because the names come from whatever workflow you are calling. secrets: inherit inside with: is a non-empty key with a scalar value, which is everything the schema asks, so the reader that checks your file has no reason to stop on it.

The comparison that catches it needs the called workflow in hand, at whatever ref your uses: line resolves to, so anything reading only the file you edited has nothing to compare against. The scalar rule also explains a difference people trip on: permissions: pulled into with: never reaches this check, because it carries a mapping where the schema wants a scalar.

A correct secrets: inherit never reaches secret validation

Worth knowing, because it rules out a whole file at a glance. The function that checks secrets returns before doing anything when the call inherits, so neither secret message can come from a job that writes secrets: inherit in the right place. If a run was rejected and the annotation points at a line reading secrets: inherit, the problem is where the line is, not what it says.

It also means inheriting cannot settle a declaration argument. A caller that inherits is never told that it is missing a declared secret, or sending an undeclared one, because both checks sit behind that return.

actions/runner, Conversion/WorkflowTemplateConverter.cs
// if the secrets are inherited from the caller, we do not have any workflowJob.SecretValues (i.e. explicit mapping)
// Inherited org/repo/env secrets will be stored in context variables and will be validated there
if (workflowJob.InheritSecrets)
{
    return;
}

Why there is no recorded run on this page

Nothing here happens on a runner. GitHub compares the caller against the called workflow while it assembles the run graph, and the run behind the quoted annotation holds no jobs at all, so there is no log to capture and no machine whose behavior could have differed. A recording would be a picture of an empty run. The file that produced it is public and cited here by line, which is the part worth reading; the workflows printed here are illustrative and have never been run.

How to prevent it

  • Keep secrets: on the same indent as uses: and with:, and reread it after any edit that moves a block.
  • Open the called workflow at the ref your uses: line pins before adding a key to with:.
  • Read Invalid input and Invalid secret as different findings, because they point at different files.
  • Review a reindent one calling job at a time, since one file can hold a correct call and a rejected one.

Frequently asked questions

Why does the error say Invalid input when the key is called secrets?
Because the word names the check rather than the key. One format string in actions/runner produces both messages, and the slot is filled by whichever pass called it: the pass over with fills in input, the pass over the secrets map fills in secret. Invalid input beside a key named secrets means the key was found among the inputs, which is to say inside with:.
Can secrets: inherit on its own cause this error?
No. Secret validation returns immediately when a call inherits, so a job that writes secrets: inherit beside uses: is never compared against the callee's declarations at all. If a run was rejected and the annotation points at a line reading secrets: inherit, the line is in the wrong place, not the wrong shape.
What is the difference between Invalid input and Invalid secret here?
One word, and it tells you which block to look at. Invalid input, NAME means the caller supplied NAME inside with: and the callee declares no input by that name. Invalid secret, NAME means it supplied NAME under a named secrets: map and the callee declares no secret by that name. The rest of the sentence is identical, which is why the two get read for each other.
Why did one job in my workflow validate and another not?
Because each calling job is checked on its own against the workflow it calls. A reindent that moved one job's secrets: line leaves its neighbors untouched, so one file can hold a correct call and a rejected one. That is the run quoted here: three jobs calling the same reusable workflow, one of them with the key a level too deep.

Related guides

References

Reading two files to move one indent is the cheap part. Latchkey is for everything after it. Start free → 30-day trial · No credit card