# GitHub Actions unexpected value steps, and the file it usually came from

> github actions unexpected value steps is reported with a column. Column 1 means a whole file from another CI system landed in the workflows directory.

Source: https://latchkey.dev/learn/github-actions/gha-unexpected-value-steps-key-in-ci  
Updated: 2026-09-21

GitHub Actions unexpected value steps is the validator saying the mapping it was reading has no `steps` key in it, and the column it reports tells you which mapping that was. At column 1 the file is being read as a workflow root, which nearly always means the file was written for a different CI system and ended up in the workflows directory.

## What this error means

Validation fails on a `steps:` line, and the useful information is the position rather than the key. Read the column first. Column 1 means `steps` was at the top level of the file, where the workflow root allows only nine keys and this is not one of them; that finding usually arrives beside several others and a complaint that `jobs` is missing, which together describe a file that is not a GitHub workflow at all. A column deeper in means `steps` was inside something, and the two somethings that reject it are a job that has already been read as a reusable-workflow caller, and the `jobs` mapping itself when a job identifier was left out.

```Actions log, quoted from Linetec-Services-LLC/Generate-Weekly-PDFs-DSR-Resiliency#12
Invalid workflow file: .github/workflows/azure-pipelines.yml#L1
(Line: 8, Col: 1): Unexpected value 'trigger', (Line: 18, Col: 1): Unexpected value 'pr',
(Line: 20, Col: 1): Unexpected value 'pool', (Line: 23, Col: 1): Unexpected value 'variables',
(Line: 34, Col: 1): Unexpected value 'steps', (Line: 5, Col: 1): Required property is missing: jobs
```

## Common causes

### The file belongs to another CI system

An Azure Pipelines, GitLab or CircleCI definition copied or generated into `.github/workflows/`. Every file in that directory is validated as a GitHub workflow, so a foreign one trips several top-level keys at once and then reports no jobs. The quoted report is precisely this, and the fix was to move the file rather than to edit it.

### The job calls a reusable workflow and cannot also have steps

The version that appears in a workflow that was fine yesterday. Somebody added a step to a job that has `uses:`, and the schema has no shape that allows both. In our experience the intent is almost always a setup or notification step that belongs in a separate job.

### A job identifier is missing, so the mapping shape is wrong

Writing `steps:` directly under `jobs:` produces a job called `steps` rather than a rejected key, and the complaints then land on the step entries and the missing `runs-on`. It is worth knowing so you can recognise when this is not your problem.

### A de-indent pulled the block out of its job

The explanation most guidance leads with, and the least common of the four in practice. It is also the easiest to confirm, because the reported column is 1 while the rest of the file uses nested columns, and the rest of the job is still intact above it.

## How to fix it

### Read the whole finding list, not the first line

1. Collect every `Unexpected value` in the report along with the missing-property complaints.
2. If several top-level keys are rejected and `jobs` is missing, the file is not a GitHub workflow; stop editing it.
3. If only `steps` is rejected, take its column and match it against the table above.

### Move foreign CI files out of the workflows directory

A definition for another system belongs wherever that system reads it, not under `.github/workflows/`. Move it, and if the pipeline is genuinely wanted on GitHub, write a workflow for it rather than translating the file in place.

```Terminal
git mv .github/workflows/azure-pipelines.yml azure-pipelines.yml
```

### Give the extra work its own job

A caller job cannot carry steps, so put them in a normal job and order the two with `needs`. This also makes the dependency explicit, which interleaved steps never did.

```.github/workflows/ci.yml (illustrative)
notify:
    needs: build
    if: always()
    runs-on: ubuntu-latest
    steps:
      - run: ./scripts/notify.sh "${{ needs.build.result }}"
```

### Put a job identifier between jobs and steps

1. Confirm there is a name under `jobs:` and that `runs-on` and `steps` are indented inside it.
2. Remember that `jobs` takes any identifier, so a mistake there is accepted rather than rejected.
3. Re-read the report afterwards: a different set of findings means you have moved on to the next problem.

## How to prevent it

- Keep `.github/workflows/` for GitHub workflows only, and move other systems' definitions out on day one of a migration.
- Treat a caller job as a job with no body, so nobody tries to add steps to it.
- Attach the workflow schema in your editor, which catches the shape while you type.
- Read the whole finding list in review, because a set of rejections often says more than any single one.

## One pass, several findings, and what they add up to

Validation does not stop at the first problem. It reads the file through, records every key it could not place, and reports them together, which is why the quoted failure above carries five rejections and a missing-property complaint in one go. Read as a set rather than one at a time, that list is a diagnosis: `trigger`, `pr`, `pool`, `variables` and `steps` are all top-level keys in Azure Pipelines, and none of them exist in a GitHub workflow.

The final finding is the one that names the real problem. `Required property is missing: jobs` says the file has no jobs at all, which no workflow with a genuine indentation slip would ever produce. A file that trips every top-level key and then has no jobs is a file from somewhere else.

| Column of the reported `steps` | Mapping being read | Most likely cause |
| --- | --- | --- |
| 1 | The workflow root | A file written for another CI system, or a workflow with no `jobs:` line |
| Same as job identifiers | The `jobs` mapping | A job identifier was omitted, so `steps` was read as a job name |
| Same as a job's other keys | A job already narrowed to the caller shape | The job has a `uses:` key |

> The workflow root allows `on`, `name`, `description`, `run-name`, `defaults`, `env`, `permissions`, `concurrency` and `jobs`. Anything else at column 1 is rejected, so the same report shape appears for any foreign top-level key.

## Why a caller job refuses steps too

A job in the published schema is a choice between two shapes: the ordinary job, which requires `runs-on` and allows `steps`, and the caller job, which requires `uses` and allows neither. The reader narrows between them as it goes, so a job that has already shown a `uses:` key is committed to the caller shape by the time it reaches `steps`.

This is the version of the error that appears in an otherwise healthy workflow, and it usually arrives when somebody adds a setup or notification step to a job that calls a reusable workflow. There is no way to interleave steps around a `uses:` job, and the message is the schema saying so. The work has to move into the called workflow, or into a separate job.

```.github/workflows/ci.yml (illustrative)
prepare:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

  build:
    needs: prepare
    uses: ./.github/workflows/build.yml
```

## A missing job identifier turns steps into a job name

The `jobs` mapping does not have a fixed set of keys. It accepts any identifier and expects a job underneath each one, which means a `steps:` written directly under `jobs:` is not rejected as a key at all; it is accepted as a job called `steps`, and the sequence of steps beneath it is then read as that job's body.

That is why this case produces different wording from the other two. The complaints land on the step entries and on the missing `runs-on`, not on the word `steps`. A report naming `steps` at the same column as your job identifiers means something narrower: the mapping being read was `jobs` and something about the identifier itself did not work.

```.github/workflows/ci.yml (illustrative)
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
```

## The same rule one level down, at the step

Steps are narrowed the same way and it is worth recognising because the message looks identical. A step is either a `run` step, which allows `shell` and `working-directory`, or a `uses` step, which allows `with`. Mixing them gets an unexpected-value complaint on whichever key came second.

The most common instance of it has its own page, because `working-directory` on a `uses` step is widely believed to be accepted and quietly ignored when in fact the file is rejected before anything runs: [working-directory on a uses step](/learn/github-actions/github-actions-working-directory-ignored-by-uses-step).

## What is published, and what is not

The message that reaches the Actions tab is written by GitHub's pre-run validation service, which is closed, so nothing here describes what that service runs. The template `Unexpected value '{0}'` is one line in the runner's generated string resources, and the key lists it is applied against are in the workflow schema the runner ships. Those are what this page rests on.

The reports agree with them, which is the reason to trust the account. A file of Azure Pipelines YAML rejecting exactly its own top-level keys, and a caller job rejecting exactly the keys the caller shape omits, are what the published schema predicts in both cases.

## Why there is no recorded run on this page

There is no run to record. The file is rejected before any job is created, so nothing is scheduled, nothing is dispatched and no runner is involved; a reproduction on our infrastructure would produce an identical rejection with our repository name on it and no machine in the picture at all. What would be genuinely useful, a file that trips several findings at once, already exists in the public report quoted above, and it is more instructive than anything we would write on purpose because nobody set out to produce it.

## FAQ

### Why is steps rejected at the top level of my workflow file?

Because the workflow root allows only nine keys: `on`, `name`, `description`, `run-name`, `defaults`, `env`, `permissions`, `concurrency` and `jobs`. Steps live inside a job, and a job lives inside `jobs`. A `steps:` at column 1 is outside all of that.

### Can I add a step to a job that uses a reusable workflow?

No. The published schema gives a job two shapes, and the one that requires `uses` does not allow `steps`. Move the work into the called workflow, or into a separate job ordered with `needs`.

### My whole file is rejected key by key. What does that mean?

Usually that the file was written for another CI system and copied into `.github/workflows/`. Validation reports every unplaceable key from one pass, so a foreign definition trips several at once and then reports that `jobs` is missing, which no ordinary workflow mistake produces.

### What happens if I put steps directly under jobs?

It is accepted as a job named `steps`, because `jobs` takes any identifier rather than a fixed key list. The complaints then land on the step entries and on the missing `runs-on`, not on the word `steps`, so that report looks different from this one.

## References

- [actions/runner: workflow-v1.0.json, the workflow root, jobs, job and steps definitions](https://github.com/actions/runner/blob/main/src/Sdk/WorkflowParser/workflow-v1.0.json)
- [actions/runner: TemplateStrings.g.cs, the Unexpected value template](https://github.com/actions/runner/blob/main/src/Sdk/Resources/TemplateStrings.g.cs)
- [actions/runner: TemplateReader.cs, the mapping reader that reports an unplaceable key](https://github.com/actions/runner/blob/main/src/Sdk/WorkflowParser/ObjectTemplating/TemplateReader.cs)
- [Linetec-Services-LLC/Generate-Weekly-PDFs-DSR-Resiliency#12: the multi-finding report quoted here](https://github.com/Linetec-Services-LLC/Generate-Weekly-PDFs-DSR-Resiliency/pull/12)

---

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
