GitLab CI Error Guide: 'Invalid CI config' .gitlab-ci.yml YAML Validation Errors
Fix GitLab's 'Invalid CI config' and 'jobs config should contain at least one visible job': YAML syntax, indentation, includes, anchors, and rules in .gitlab-ci.yml.
- #gitlab-cicd
- #troubleshooting
- #errors
- #yaml
Overview
GitLab parses .gitlab-ci.yml in two passes before a single job runs: first it loads the file as raw YAML, then it validates the result against the CI/CD schema — stages, jobs, keywords, includes, and rules. If either pass fails, the pipeline never starts and the whole config is rejected as invalid. Nothing queues, and the merge request shows a broken pipeline status instead of a failed job.
You will see this on a push, in the Pipeline Editor, or from the CI Lint tool:
Unable to create pipeline
jobs config should contain at least one visible job
The message is generated by the same validator the CI Lint tool uses, so the wording is identical whether it comes from a push, the API, or glab. A YAML-level failure looks different — it reports a line and column instead of a schema rule:
Status: syntax is incorrect
Error: (<unknown>): did not find expected key while parsing a block mapping at line 14 column 3
The distinction matters: a YAML error means the file did not parse at all (indentation, a missing colon, a tab), while a schema error means it parsed fine but broke a CI rule (a job with no script, an undefined stage, a typo’d keyword). Read the message first to know which pass failed.
Symptoms
- A push is rejected with
Invalid CI configand no pipeline is created. - The CI Lint tool or Pipeline Editor shows
syntax is incorrectwith a line/column. jobs config should contain at least one visible jobeven though the file has jobs.- A keyword error like
jobs:build config should implement a script: or a trigger: keyword. - An
include:resolves to a 404 ordoes not have valid YAML syntax.
glab ci lint
✓ Validating...
✗ Invalid configuration
jobs config should contain at least one visible job
yamllint .gitlab-ci.yml
.gitlab-ci.yml
14:3 error syntax error: did not find expected key (syntax)
Common Root Causes
1. Raw YAML syntax error (tabs, indentation, missing colon)
The file does not parse as YAML at all — a tab character, a missing colon after a key, an inconsistent indent, or an unquoted value with a special character (:, @, *). GitLab reports a line and column, not a CI rule.
yamllint -d relaxed .gitlab-ci.yml
14:3 error syntax error: did not find expected key while parsing a block mapping (syntax)
(<unknown>): did not find expected key while parsing a block mapping at line 14 column 3
A tab where YAML expects spaces, or a script block under-indented by one space, produces exactly this. Run grep -nP '\t' .gitlab-ci.yml to catch hidden tabs.
2. A job with no script (no visible job)
Every visible job needs a script: (or trigger:/run:). A job that only has, say, image: or variables: is not runnable, and if it is your only job, the validator reports there are zero visible jobs.
build:
stage: build
image: node:20
# no script — not a runnable job
jobs:build config should implement a script: or a trigger: keyword
jobs config should contain at least one visible job
Template jobs meant to be reused must be hidden with a leading dot (.build_template:); a non-hidden job still needs a script.
3. Invalid or misspelled keyword
A typo’d keyword (scripts: instead of script:, stages: inside a job instead of stage:, only: instead of only:) is not recognized by the schema. GitLab rejects unknown keys rather than ignoring them.
test:
stage: test
scripts: # typo: should be "script"
- pytest
jobs:test config contains unknown keys: scripts
jobs:test config should implement a script: or a trigger: keyword
Globally misspelled keys behave the same way: before-script (hyphen) or imagee produce config contains unknown keys.
4. Broken include (file not found, 404, wrong ref)
An include: that points at a missing local file, an unreachable remote URL, a non-existent project ref, or a template that does not exist fails before merging — and a syntax error inside the included file surfaces against the included path.
include:
- local: 'templates/deploy.yml'
- project: 'group/ci-templates'
ref: 'main'
file: '/jobs/build.yml'
Included file `templates/deploy.yml` does not have valid YAML syntax!
Project `group/ci-templates` reference `main` does not have a file named `/jobs/build.yml`.
A remote include returning HTTP 404 shows couldn't download the file. Confirm the ref exists and the path is repo-absolute (leading / for project: includes).
5. Duplicate job names, reserved names, or an undefined stage
Two jobs with the same name, a job named after a reserved keyword (stages, variables, image, before_script), or a job whose stage: is not listed in the top-level stages: all fail schema validation.
stages: [build, test]
deploy:
stage: deploy # not declared in stages:
script: ["./deploy.sh"]
deploy job: chosen stage deploy does not exist; available stages are .pre, build, test, .post
jobs:variables config should implement a script: or a trigger: keyword
Either add the stage to stages: or rename the job so it does not collide with a reserved keyword.
6. Bad rules/workflow expression or invalid variable expansion
A malformed rules: entry (missing if, an unbalanced $CI_COMMIT_BRANCH == with no value, mixing rules: with only:/except: on the same job), or a workflow:rules that excludes every pipeline, makes the job or whole config invalid.
deploy:
script: ["./deploy.sh"]
only: [main]
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
jobs:deploy config key may not be used with `rules`: only
jobs:deploy rules:if invalid expression syntax
Use rules: or only/except, never both on one job, and quote the full expression so $VAR == "value" parses.
Diagnostic Workflow
Step 1: Validate the raw YAML first
yamllint .gitlab-ci.yml
grep -nP '\t' .gitlab-ci.yml # hidden tabs
If yamllint reports a line/column, fix the syntax before looking at CI rules — the schema validator never even ran.
Step 2: Run the GitLab CI Lint tool
glab ci lint
# or in the UI: Project > Build > Pipeline Editor (Validate tab),
# or visit /-/ci/lint
The CI Lint tool merges includes and anchors and reports the exact schema error with the offending job name.
Step 3: Lint via the API for CI/CD or scripts
curl -s --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.com/api/v4/projects/$PROJECT_ID/ci/lint" \
--data-urlencode "content=$(cat .gitlab-ci.yml)" | jq
A failing response returns {"valid":false,"errors":["..."],"warnings":[]}, so you can gate commits on it programmatically.
Step 4: Expand includes and anchors
# UI: Pipeline Editor > "Full configuration" / "View merged YAML"
glab ci lint --include
The merged view shows what include:, extends:, !reference, and &anchor/*alias actually resolved to — most “phantom” errors live in an included file, not the one you edited.
Step 5: Bisect by reducing the file
git stash # or comment out jobs/includes one at a time
glab ci lint
Strip back to one minimal valid job, confirm it lints, then add jobs/includes back until the error returns — that isolates the broken block.
Example Root Cause Analysis
A push to main is rejected with Invalid CI config and no pipeline appears. The CI Lint tool reports:
jobs config should contain at least one visible job
The file clearly has jobs, so this is confusing. Raw YAML parses fine — yamllint is clean. Running the API lint gives more detail:
curl -s --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.com/api/v4/projects/42/ci/lint" \
--data-urlencode "content=$(cat .gitlab-ci.yml)" | jq '.errors'
[
"jobs:build config should implement a script: or a trigger: keyword"
]
Looking at the merged config, the only job is build, and a recent refactor moved its commands into a YAML anchor but never aliased it back:
.build_steps: &build_steps
- npm ci
- npm run build
build:
stage: build
image: node:20
# script: *build_steps <- the alias was deleted by mistake
build has no script, so it is not a visible job; the template anchor .build_steps is hidden and does not count either — leaving zero visible jobs.
Fix: restore the alias so the job has a script again.
build:
stage: build
image: node:20
script: *build_steps
glab ci lint
✓ Validating...
✓ Configuration is valid
The pipeline creates normally. The deeper fix is to lint .gitlab-ci.yml in a pre-commit hook so an empty-script job is caught before it reaches main.
Prevention Best Practices
- Lint before you push: run
glab ci lintor the API/ci/lintendpoint in a pre-commit hook so an invalid config never lands on a branch. - Run
yamllintfirst to separate raw-YAML errors (line/column) from CI schema errors (job/keyword) — they need different fixes. - Enable editor YAML formatting and reject tabs; the most common syntax error is a tab or a one-space indent in a
script:block. - Keep reusable jobs hidden with a leading dot and pull them in with
extends:or!reference:, so a half-edited template can never become your only “visible” job. - Pin
include:refs to a tag or SHA and validate the merged config in CI, so a moved or deleted template file fails loudly instead of 404-ing silently. - For triage, the free incident assistant can turn a CI Lint error block into a likely-cause-and-fix suggestion. More patterns live in the GitLab CI/CD guides.
Quick Command Reference
# Validate raw YAML first (line/column errors)
yamllint .gitlab-ci.yml
grep -nP '\t' .gitlab-ci.yml # hidden tabs
# Lint with glab (merges includes/anchors)
glab ci lint
glab ci lint --include # show merged config
# Lint via the GitLab CI Lint API
curl -s --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.com/api/v4/projects/$PROJECT_ID/ci/lint" \
--data-urlencode "content=$(cat .gitlab-ci.yml)" | jq
# UI tools
# Project > Build > Pipeline Editor (Validate + View merged YAML)
# /-/ci/lint
# Bisect a broken file
git stash && glab ci lint # or comment jobs/includes out one by one
Conclusion
Invalid CI config means GitLab rejected .gitlab-ci.yml in either the YAML pass or the schema pass before any job ran. The usual root causes:
- A raw YAML syntax error — a tab, bad indentation, or a missing colon (line/column message).
- A job with no
script:/trigger:, so there is no visible job to run. - An invalid or misspelled keyword like
scripts:orstages:inside a job. - A broken
include:— a missing local file, 404 remote, or wrong project ref. - Duplicate job names, a reserved keyword used as a job name, or a stage not in
stages:. - A bad
rules:/workflow:expression orrules:mixed withonly/except.
Read the message to tell a YAML error from a schema error, lint with the CI Lint tool or API before pushing, and view the merged config so you fix the right file. The durable fix is linting in CI so an invalid config never reaches a protected branch.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.