# GitHub Actions Required property is missing: jobs

> Required property is missing: jobs means the workflow root has no jobs key. Here is what breaks the root mapping and how to read the line.

Source: https://latchkey.dev/learn/github-actions/gha-jobs-section-required  
Updated: 2026-09-20

Required property is missing: jobs is the workflow parser telling you that the root of the file has no jobs key where it expects one. The key is usually present in the file and has simply stopped being at the root, which is why the message reads as absurd the first time you see it next to a file that obviously has jobs in it.

## What this error means

The workflow does not run and GitHub shows an invalid-file annotation against the commit. The reported position is normally the very start of the file, because a missing property is reported against the mapping that should have contained it rather than against any line you wrote. That makes the message unhelpful on its own: it tells you what is absent and nothing about where it went. Often there are other lines above it in the same annotation, one per key that ended up somewhere unexpected, and those lines are the useful ones. If the annotation contains nothing but this single line, the file has genuinely lost its jobs key, either because it was never added or because the block that held it was moved under another key.

```Actions annotation, the last line, quoted from just-another-job-application-tracker#208
Invalid workflow file: .github/workflows/deploy.yml#L1
(Line: 1, Col: 3): Required property is missing: jobs
```

## Common causes

### Stray indentation at the root of the file

One leading space on the first line is enough. Everything below it is then nested inside a document whose root mapping has no keys the schema recognizes, and the required-property check fires against a root that looks empty even though the file is full.

### The header of the file was lost in a merge or a squash

The name, trigger and jobs keys live in the first few lines, and a badly resolved conflict or a rewritten branch can take all of them at once while leaving the job bodies behind. The annotation then reports every job id as an unexpected root key, with the missing-property line last.

### The jobs block ended up under another key

Usually under the trigger block, and usually from an automatic reindent. The key is still spelled correctly and is still in the file, which is why searching for the word finds nothing wrong and reading the column number does.

### The file is not a workflow at all

A linter configuration, an action manifest or a Dependabot file that has been saved into the workflows directory is read as a workflow and rejected. The message is the same, and the fix is to move the file out rather than to add a jobs key to it.

## How to fix it

### Check column one before anything else

1. Open the file and confirm that the first character of the first key is at column one.
2. Confirm the same for name, on and jobs, which all belong at the same level.
3. If the annotation reported unexpected values as well, look at what those keys are: job ids there mean the wrapper is gone.

### Restore the root keys

Put the header back and leave the job bodies alone. The corrected version of the illustrative file is below, with every root key flush left and the job indented under jobs.

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

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

### Give each job a valid identifier

Once the key is back at the root, every entry under it is a job id, and the ids have rules. The documentation gives them: the identifier "must start with a letter or `_` and contain only alphanumeric characters, `-`, or `_`". The schema does not enforce that rule. The loose key under `jobs` is typed only as a non-empty string, so a bad id is read without complaint and is rejected later, in the converter, by `IdBuilder.TryAddKnownId`.

```.github/workflows/deploy.yml (illustrative)
jobs:
  build_and_test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: npm ci && npm test
```

> A bad identifier therefore reads nothing like this page. It arrives as "The identifier 'build job' is invalid. IDs may only contain alphanumeric characters, '_', and '-'. IDs must start with a letter or '_' and must be less than 100 characters." A job that is rejected once it does parse, for a key that belongs to the other kind of job, is a third message again: [GitHub Actions Unexpected Value](/learn/github-actions/github-actions-unexpected-value-line-number).

### Keep non-workflow files out of the workflows directory

Everything in that directory with a YAML extension is read as a workflow. Configuration for a linter or a bot belongs one level up, in the repository metadata directory itself, where nothing tries to schedule it.

```Terminal
git mv .github/workflows/dependabot.yml .github/dependabot.yml
```

## How to prevent it

- Keep a workflow linter in the pre-commit hook, so an unindented root never reaches a push.
- Review the first ten lines of any workflow that comes out of a squash or a conflict resolution.
- Store bot and linter configuration outside the workflows directory.
- Turn on a YAML mode in the editor that shows the indentation guide for column one.

## A minimal workflow that produces it

This file is written for this page and has never been run. It has a job in it. What it does not have is a job at the root, because a single leading space on the first line pushed the entire document one level down, so the root mapping the parser sees has no keys it recognizes at all.

A YAML parser is content with this. The indentation is internally consistent and the document is well formed. Only the schema check notices, and what it notices is the absence at the root rather than the space on line one.

```.github/workflows/deploy.yml, one leading space (illustrative)
name: deploy
 on: push
 jobs:
   build:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v7
       - run: make
```

## Where the message comes from and what it checks

The root of a workflow is a mapping with a fixed set of properties, and exactly one of them is marked required. The schema file that says so is published in actions/runner, and the editor tooling ships its own copy, so the definition you can check locally is the one GitHub publishes.

The check itself runs at the end of reading a mapping, after every key in it has been read. For each property the schema marks required, the reader asks whether that key was among the ones it saw, and reports against the mapping if it was not. That is why the position in the annotation is the start of the mapping rather than a line you could point at.

```actions/runner, ObjectTemplating/TemplateReader.cs
// TemplateReader.cs, after every key of the mapping has been read
if (property.Value.Required)
{
    if (!keys.Contains(property.Key))
    {
        m_context.Error(mapping, $"Required property is missing: {property.Key}");
    }
}
```

## What the workflow root accepts

Nine keys, one of which is not optional. Nothing else may appear at the root, so a key that is not in this list produces an unexpected-value line instead, and a block of them usually means the root is not where you think it is.

Worth noting what is not marked required: the trigger. A file with jobs and no triggers fails a different way, and a file with triggers and no jobs fails this way, so the two cases are worth keeping apart when you are reading an annotation that contains both.

| Root key | Required by the schema |
| --- | --- |
| jobs | yes |
| on | no |
| name | no |
| run-name | no |
| description | no |
| env | no |
| defaults | no |
| permissions | no |
| concurrency | no |

> Read from the `workflow-root` definition in `workflow-v1.0.json` in actions/runner, which lists these nine properties and marks only `jobs` as required.

## The three edits that cause it

In the reports where the cause is stated, it is nearly always one of three things, and none of them is a missing job. In paruff/fawkes#1617 the author traced it precisely: "line 1 had a leading space, which broke the YAML root mapping so GitHub's parser couldn't find the `jobs` key." That is the illustrative file above.

The second is a lost wrapper. In just-another-job-application-tracker#208 a squash dropped the header of the file and the three job ids were left at the root, each one reported as an unexpected value above the missing-property line quoted at the top of this page.

The third is a job accidentally indented under the trigger block, which happens when an editor reindents a region or a merge resolves in favor of the wrong side. The parser then sees a trigger mapping with a strange key in it and a root with no jobs.

> All three leave the word `jobs` somewhere in the file, which is what makes the message read as wrong. Search for it by column rather than by name.

## Why there is no recorded run on this page

A workflow that fails this check never produces a run, so there is no log to capture and no job identifier to link to. The failure is decided when GitHub reads the file, before anything is scheduled. That also means no runner behavior could avoid it and none could repair it, so this page makes no such claim. The annotation is quoted from a public repository and both workflows here are illustrative.

## FAQ

### Why does GitHub say jobs is missing when my file clearly has jobs?

Because it is missing from the root mapping, not from the file. A leading space, a lost header or an over-indented block leaves the key one level down, where the schema does not look. The check runs against the root after every key in it has been read, and reports the absence against the mapping rather than against any line.

### Is the on key required in a workflow file?

Not by this check. The workflow root definition marks only jobs as required, so a file with jobs and no triggers fails elsewhere rather than here. If your annotation names jobs, adding or moving a trigger will not clear it.

### Why is the reported position line 1, column 1?

Because a missing property has no position of its own. The reader reports it against the mapping that should have contained it, and the workflow root mapping starts at the top of the file. Treat the position as naming the mapping, not as pointing at a mistake on that line.

### What happens if the workflows directory contains a non-workflow YAML file?

It is read as a workflow and fails this check, since nothing in a linter or bot configuration is a jobs key. Moving the file out of the directory is the fix; adding a stub job to satisfy the parser creates a workflow that runs for no reason.

## References

- [actions/runner: workflow-v1.0.json, the workflow-root definition](https://github.com/actions/runner/blob/main/src/Sdk/WorkflowParser/workflow-v1.0.json)
- [actions/runner: TemplateReader.cs, the required-property check](https://github.com/actions/runner/blob/main/src/Sdk/WorkflowParser/ObjectTemplating/TemplateReader.cs)
- [GitHub Actions: workflow syntax, jobs and jobs.<job_id>](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobs)
- [paruff/fawkes#1617: a leading space on line 1 that broke the root mapping](https://github.com/paruff/fawkes/issues/1617)

---

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
