# GitHub Actions Unexpected Value at a workflow line

> GitHub Actions Unexpected Value names a key or a value the workflow schema did not expect. Read the line, then the branch that rejected it.

Source: https://latchkey.dev/learn/github-actions/github-actions-unexpected-value-line-number  
Updated: 2026-09-20

A GitHub Actions Unexpected Value annotation means your YAML parsed and then failed the workflow schema, at the line and column the message gives you. The parser prints the same six words for two different findings, and which one you have is decided by whether the quoted text is a key you wrote on the left of a colon or a value you wrote on the right.

## What this error means

The file is valid YAML. Nothing is wrong with the indentation as far as any YAML linter is concerned, and the editor shows no red. GitHub still refuses to queue the run, and the annotation on the commit lists one or more lines, each with a line number, a column number, and a quoted piece of your own file. The quoted piece is the diagnosis. If it is a key, the schema has no property by that name in the position you put it. If it is a value, the schema has a fixed set of values for that key and yours is not one of them. Several of these usually arrive together, because one structural mistake pushes a whole block into a place where none of its keys belong, and the parser reports every key in the block rather than stopping at the first. The last line of the annotation is often the one that explains the rest.

```Actions annotation, quoted from just-another-job-application-tracker#208
Invalid workflow file: .github/workflows/deploy.yml#L1
(Line: 1, Col: 3): Unexpected value 'build'
(Line: 69, Col: 3): Unexpected value 'deploy'
(Line: 215, Col: 3): Unexpected value 'composer-validate'
(Line: 1, Col: 3): Required property is missing: jobs
```

## Common causes

### A key the schema has no property for

Usually a typo, and usually a singular where the schema wants a plural: `step` for `steps`, `run-on` for `runs-on`, `output` for `outputs`. The reader has no fuzzy matching, so a key one letter out is as unknown as a key you invented. The quoted text in the message is the key exactly as you wrote it, which is the fastest way to see the typo you have been reading past.

### A block indented under the wrong parent

This is the one that produces a page of errors from a single edit. Every key in the block is now being checked against the wrong definition, so every key that does not happen to exist there is reported. The annotation quoted above is this case: the `jobs:` wrapper was lost in a squash, so three job ids became top-level keys and the root mapping lost its only required property.

### A value outside the fixed set for that key

Some keys take free text and some take a closed list. `secrets:` on a calling job takes a mapping or the single word `inherit`, and nothing else; `permissions:` takes a mapping or `read-all` or `write-all`. A near miss such as `inherits` or a bare `read` is reported as an unexpected value rather than as a bad option, because to the reader they are the same finding.

### The parser narrowed to the other shape first

The key is real, but an earlier key in the same block eliminated the definition it belongs to. In our experience this is the least obvious of the four, because the error points at the line you wrote last while the cause is a line above it that the message never names.

## How to fix it

### Read the column number, not just the line

1. Take the column from the annotation and count characters on that line.
2. If the column lands on text before a colon, you have the key branch: the name is wrong or it is in the wrong place.
3. If it lands after a colon, you have the value branch: the key is fine and the value is not on the list.
4. Work through the reported lines from the bottom up, because the last one is usually the structural cause of the others.

### Fix the key, or move the block that carries it

For a typo, correct the spelling. For a misindented block, put it back under the parent that owns it rather than renaming keys to suit where it landed. The corrected version of the illustrative file is below: `steps` under the job, and `permissions` as a mapping.

```.github/workflows/deploy.yml, corrected (illustrative)
name: deploy
on: push

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: make
```

### When the key is real, reorder or split the job

A job that calls a reusable workflow may only carry the keys in the middle and right columns of the table above. If one of the left-column keys is there because you needed it, that work belongs in a separate job rather than on the caller. Setting `env:` on a caller is the common one, and there is nowhere to put it: a called workflow does not inherit the caller environment.

```.github/workflows/release.yml (illustrative)
jobs:
  deploy:
    uses: ./.github/workflows/deploy-impl.yml
    with:
      target: production
    secrets: inherit
```

> A `secrets:` key indented into `with:` instead of sitting beside it clears this schema and is rejected later, by the reusable workflow check: [Invalid input, secrets is not defined in the referenced workflow](/learn/github-actions/gha-reusable-secret-not-declared).

### Validate before you push

The schema in `actions/runner` is published beside the parser that loads it, and the editor tooling ships its own copy, so a local check is reading a published definition rather than an approximation. Either the VS Code extension or a command-line linter will show you the annotation on the line you are editing instead of after a push.

```Terminal
actionlint .github/workflows/deploy.yml
```

## How to prevent it

- Copy the shape of a job from a file that already runs, rather than from memory.
- Keep reusable-workflow callers in their own jobs, so no left-column key ever lands on one.
- Read annotations from the bottom up, since the structural error is usually reported last.
- Run a workflow linter in a pre-commit hook, where the cost of being wrong is seconds.

## A minimal workflow that produces it

This file is written for this page and has never been run. It carries one mistake of each kind, so the annotation comes back with two lines rather than one. `step` on the left of a colon is not a property of a job, and `read` on the right of `permissions:` is not one of the two shorthand strings the schema accepts there.

Both lines say the same thing and mean different things. The first is a key the schema cannot place. The second is a key the schema knows perfectly well, holding a value it will not take.

```.github/workflows/deploy.yml (illustrative)
name: deploy
on: push

permissions: read

jobs:
  build:
    runs-on: ubuntu-latest
    step:
      - uses: actions/checkout@v7
      - run: make
```

## One message, two branches of the reader

The parser is open source twice over. `actions/runner` carries the C# copy GitHub publishes, and `actions/languageservices` carries the TypeScript copy that the VS Code extension runs. Both spell the message from the same template, `TemplateStrings.resx` in the runner giving it as a format string with one slot.

The key branch fires at the end of the loop that reads a mapping. For each key in turn the reader asks the schema whether any candidate definition has a property by that name, then whether the mapping accepts loose keys. If neither is true there is nowhere to put the key, and the reader reports it and skips whatever was under it. That last step matters when you are reading an annotation: a rejected key takes its entire value with it, so one bad key can silence every error inside the block it introduced.

The value branch fires while reading a scalar. The reader collects every scalar definition the schema allows in that position and looks for one that matches the literal. If the literal is not a string it converts it and tries once more. Only after both attempts does it report, which is why a number where a string was wanted usually passes and a misspelled keyword does not.

```actions/languageservices, workflow-parser/src/templates/template-reader.ts
// key branch, template-reader.ts: no property and no loose key type
this._context.error(nextKey, `Unexpected value '${nextKey.value}'`);

// value branch, template-reader.ts: no scalar definition matched the literal
this._context.error(literal, `Unexpected value '${literal.toString()}'`);
```

## Why a real key is sometimes rejected anyway

The third case is the one that sends people in circles, because the key is spelled right and documented and still comes back unexpected. It happens where the schema offers a choice. A job is defined as one of two shapes, and so is a step, and the reader narrows to one of them while it is reading, using your own keys as the evidence.

The narrowing rule is in `TemplateSchema.TryMatchKey`. When a key exists in some candidate definitions and not others, the ones that do not have it are removed from the list. The first key unique to one shape therefore decides the shape, and every later key belonging only to the other shape is reported as unexpected. Order in the file is what settles it, so moving two lines can change which half of the job gets the error.

For a job the split is below. A caller job that sets `env:` before it sets `uses:` has already been read as a normal job by the time `uses` arrives, which is how a correct reusable-workflow call earns three rejections in a row.

| Only on a normal job | Only on a call job | On both |
| --- | --- | --- |
| runs-on, steps | uses | name, needs |
| env, environment | with | if, permissions |
| container, services | secrets | concurrency, strategy |
| outputs, defaults |  |  |
| timeout-minutes |  |  |
| continue-on-error |  |  |

> The columns come from the `job-factory` and `workflow-job` definitions in `workflow-v1.0.json` in actions/runner, which is the schema the reader loads. Two rarer normal-job keys are left out of the left column for space, `cancel-timeout-minutes` and `snapshot`; the middle and right columns are complete.

## Why there is no recorded run on this page

The pages in this library that carry a log carry it because we ran the job and kept the output. This failure never reaches a runner. The schema check happens when GitHub reads the file, before a job is queued and before any machine is assigned, so there is no run to record and nothing on a runner that could have behaved differently. The annotation above is quoted from a public repository rather than produced here, and the workflows on this page are illustrative.

## FAQ

### What does Unexpected value mean in a GitHub Actions workflow?

That the file is valid YAML but does not fit the workflow schema at the position given. The quoted text is either a key with no property to match it, or a value outside the fixed set the key accepts. The message is spelled from one format string in the runner, so both findings read identically and the column number is what tells them apart.

### Why do I get several Unexpected value lines from one mistake?

Because a block in the wrong place has every one of its keys checked against the wrong definition. In just-another-job-application-tracker#208 a lost `jobs:` wrapper produced three of them, one per job id, followed by a missing-property line for `jobs` itself. Fixing the wrapper cleared all four.

### Why is uses rejected on a job when the documentation shows it?

Because the reader had already decided your job was a normal job. It narrows the two job shapes down using your keys in file order, so a key such as `env:` or `timeout-minutes:` appearing before `uses:` removes the reusable-workflow shape from the candidates, and `uses`, `with` and `secrets` are then unknown keys.

### Does the line number in the annotation point at the real problem?

It points at the token the reader rejected, which is not always where the mistake is. A misindented block is reported at each of its keys, not at the indentation that moved it. Treat the line as where the parser gave up and look one level out for what moved it there.

## References

- [actions/languageservices: workflow-parser template-reader.ts, both Unexpected value branches](https://github.com/actions/languageservices/blob/main/workflow-parser/src/templates/template-reader.ts)
- [actions/runner: workflow-v1.0.json, the job and step one-of definitions](https://github.com/actions/runner/blob/main/src/Sdk/WorkflowParser/workflow-v1.0.json)
- [just-another-job-application-tracker#208: a lost jobs wrapper and the annotation it produced](https://github.com/godie/just-another-job-application-tracker/issues/208)
- [GitHub Actions: workflow syntax reference](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
