# GitHub Actions Contexts: Where Each One Is Actually Available

> Which GitHub Actions context is available at which workflow key, why an unavailable one silently becomes an empty string, and the scoping mistakes that produce it.

Source: https://latchkey.dev/learn/github-actions/github-actions-contexts-reference  
Updated: 2026-08-20

Contexts are not available everywhere, and referencing one where it is not available does not fail the run. It resolves to an empty string, so the workflow continues with a silently wrong value.

Almost every confusing GitHub Actions expression bug comes down to one rule: **a context that is not available at that key resolves to an empty string rather than raising an error**. Your `if:` condition does not fail, it just evaluates against nothing, and the job silently runs or silently skips.

That is why the same expression can work in a step and break when moved to the job level. This page is the availability table plus the specific mistakes it explains.

## Availability by workflow key

| Key | Contexts available |
| --- | --- |
| `run-name` | `github`, `inputs`, `vars` |
| `concurrency` | `github`, `inputs`, `vars` |
| `env` (top level) | `github`, `secrets`, `inputs`, `vars` |
| `jobs.<id>.if` | `github`, `needs`, `vars`, `inputs` |
| `jobs.<id>.runs-on` | `github`, `needs`, `strategy`, `matrix`, `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` |

> The trap worth memorising: `steps` and `matrix` are available in a **step** `if` and not in a **job** `if`. Moving a working condition up one level breaks it silently.

## The twelve contexts

| Context | What it holds |
| --- | --- |
| `github` | Event payload, ref, actor, repository, run metadata |
| `env` | Variables set at workflow, job, or step level |
| `vars` | Configuration variables from repo, org, or environment |
| `job` | Status and services of the current job |
| `jobs` | Reusable workflow job outputs only |
| `steps` | Outputs and status of completed steps that have an `id` |
| `runner` | OS, arch, temp paths, and tool cache of the runner |
| `secrets` | Secret values available to the job |
| `strategy` | Matrix strategy metadata such as `job-index` |
| `matrix` | The current matrix combination |
| `needs` | Outputs and results of jobs this one depends on |
| `inputs` | Inputs of a `workflow_call` or `workflow_dispatch` workflow |

## The four mistakes this table explains

- **`steps` in a job-level `if`.** Not available. The condition evaluates against an empty string and the job runs or skips unconditionally.
- **`secrets` in `runs-on` or a job `if`.** Not available. Move the check into a step, or surface a non-secret flag through `outputs`.
- **`inputs` in a workflow with no `workflow_call` or `workflow_dispatch` block.** The context does not exist, producing `Unrecognized named-value: inputs`.
- **`env` in a job-level `if`.** Not available at that key. Use `vars` for configuration, or move the condition into a step.

## Confirm what you actually have

```.github/workflows/ci.yml
- 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) }}'
```

> An empty `{}` is the finding: the context is not populated at that point, which is a different problem from the value being wrong and needs a different fix.

## Catch it before it costs a run

```Terminal
docker run --rm -v "$(pwd):/repo" --workdir /repo rhysd/actionlint:latest -color
```

> `actionlint` checks context availability against these same rules statically, so every mistake on this page is detectable without running a workflow.

## Diagnose 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.

```.github/workflows/ci.yml
- 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) }}'
```

> An empty `{}` or a blank line is the finding. It means the context is not populated at that point, which is a different problem from the value being wrong, and it needs a different fix.

## 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` |

> The most common trap in this table: `steps` and `matrix` are available in a **step** `if` but not in a **job** `if`. Moving a condition up a level silently breaks it.

## 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.

```Terminal
# 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 -color
```

## FAQ

### Why is my GitHub Actions context empty?

Because it is not available at that workflow key. GitHub resolves an unavailable context to an empty string rather than failing, so the expression evaluates against nothing and the job silently behaves as if the condition were false.

### Why can I use steps.x in one if and not another?

The `steps` context is available in `jobs.<id>.steps.if` but not in `jobs.<id>.if`. A condition that works inside a step breaks silently when moved to the job level.

### What causes "Unrecognized named-value: inputs"?

Referencing the `inputs` context in a workflow that declares no `workflow_call` or `workflow_dispatch` inputs. The context does not exist there at all, which is one of the few cases GitHub reports as an error rather than an empty string.

### Can I use secrets in runs-on or a job-level if?

No. `secrets` is not available at either key. Move the check into a step, or expose a non-secret boolean through a job output and branch on that instead.

---

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
