GitHub Actions "The expression is too long (max 21000 characters)"
Every individual GitHub Actions expression is parsed into an AST and is capped at 21000 characters. A giant inlined fromJSON blob, a long concatenated string, or a deeply nested toJSON usually trips it.
What this error means
The workflow fails at parse time on a single key that contains one large \${{ }} expression, most often a matrix built from an inlined JSON literal or a long interpolated run command.
Invalid workflow file: .github/workflows/ci.yml#L22
The expression is too long. Max allowed length: 21000Diagnose it: print the context before you change anything
Most workflow-expression bugs are not syntax errors, they are an expression reading something that is empty. GitHub resolves a missing property to an empty string instead of failing the run, so a wrong reference looks like a logic bug rather than a mistake. Dump the contexts first and you will usually see the answer immediately.
- name: Dump contexts
run: |
echo '--- github ---' ; echo '${{ toJSON(github) }}'
echo '--- needs ---' ; echo '${{ toJSON(needs) }}'
echo '--- steps ---' ; echo '${{ toJSON(steps) }}'
echo '--- matrix ---' ; echo '${{ toJSON(matrix) }}'
echo '--- inputs ---' ; echo '${{ toJSON(inputs) }}'Check the context is allowed where you used it
Contexts are not available everywhere. The same expression can be valid in a step if and invalid in a job if, which is why an expression that works in one workflow fails when moved.
| Where you wrote it | Contexts available there |
|---|---|
run-name | github, inputs, vars |
concurrency | github, inputs, vars |
Top-level env | github, secrets, inputs, vars |
jobs.<id>.if | github, needs, vars, inputs |
jobs.<id>.steps.if | github, needs, strategy, matrix, job, runner, env, vars, steps, inputs |
jobs.<id>.outputs | Full access, including secrets |
Reusable workflow outputs | github, jobs, vars, inputs |
Common causes
Huge inline JSON passed to fromJSON
A matrix or value built from a large JSON literal embedded directly in the YAML inflates a single expression past the ceiling.
Long concatenated/interpolated string
Building a big command or payload with many \${{ }} concatenations inside one expression hits the limit.
How to fix it
Move the large value out of the expression
- Generate the JSON in a prior step and pass it through GITHUB_OUTPUT, then reference the output once.
- Split a long interpolation into multiple steps or multiple smaller expressions.
- For matrices, produce the array in a setup job and consume it with fromJSON of a single short output.
jobs:
setup:
runs-on: ubuntu-latest
outputs:
items: ${{ steps.gen.outputs.items }}
steps:
- id: gen
run: echo "items=$(cat matrix.json)" >> "${GITHUB_OUTPUT}"
build:
needs: setup
strategy:
matrix:
item: ${{ fromJSON(needs.setup.outputs.items) }}Catch it before it reaches CI
Every failure in this cluster is statically detectable. actionlint parses workflow expressions, checks context availability against the same rules above, and validates needs references, so these bugs never need to cost you a run.
# one-off
docker run --rm -v "$(pwd):/repo" --workdir /repo rhysd/actionlint:latest -color
# as a job, before anything expensive runs
- uses: actions/checkout@v4
- run: |
bash <(curl -s https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
./actionlint -colorHow to prevent it
- Keep generated data in step outputs or files, not inlined into one expression.
- Reference outputs by name instead of re-building large strings inline.