# Unable to get ACTIONS_ID_TOKEN_REQUEST_URL in GitHub Actions

> Unable to get ACTIONS_ID_TOKEN_REQUEST_URL is thrown by the toolkit before any network call, because the runner was never given the variable to read.

Source: https://latchkey.dev/learn/github-actions/github-actions-oidc-id-token-request-url-missing  
Updated: 2026-09-20

Unable to get ACTIONS_ID_TOKEN_REQUEST_URL is thrown by `@actions/core` when an environment variable it expects is absent, before any request is made to GitHub or to a cloud provider. The runner never received the variable, so nothing on the machine can produce it.

## What this error means

A cloud login, an attestation step or any action that mints an OIDC token fails within a second of starting. No provider is contacted, so there is no cloud-side error to look at and no audit entry anywhere. The line in the log is rarely the bare sentence: it usually arrives with one or two prefixes bolted on by the code that asked for the token, and those prefixes are the most useful part.

```Actions log, quoted from astropy/extension-helpers#156
Error: Unhandled error: Error: Error message: Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable
```

## Common causes

### The job did not request id-token: write

The dominant cause. GitHub injects the two variables only for jobs that hold the `id-token` permission at write, so without it the environment is simply missing them. There is no warning at startup and no mention in the job summary beyond the permissions list itself.

### The permission was declared somewhere it does not reach the job

Written at the workflow level in a file where a job also has its own `permissions` block, the job block replaces the workflow one rather than merging with it, so the job silently loses `id-token`. This is the version where the file plainly contains the right line and the job still has nothing.

### A reusable workflow was called without the permission at the caller

The called workflow cannot grant itself more than the caller has. A job inside it that needs OIDC gets nothing unless the caller's job declared `id-token: write`, and the file that fails is not the file that has to change.

### An organization or enterprise policy caps what the token may hold

Worth ruling out when the workflow is provably correct. A restrictive default or an explicit policy can withhold the scope, and the symptom is identical to forgetting the line. The run's own permissions listing is where the request and the grant can be compared.

## How to fix it

### Declare the permission on the job that mints the token

Put it on the job rather than the workflow, so it applies where it is needed and nothing else gains it. GitHub notes that this setting "only enables fetching and setting the OIDC token; it does not grant write access to other resources", so it is not a broad grant.

```.github/workflows/deploy.yml (illustrative)
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v5
```

### Remember that a job block replaces the workflow block

If the job has its own `permissions`, every scope it needs has to be listed there, including `contents: read` for checkout. Adding `id-token: write` at the workflow level will not reach a job that overrides it.

### Declare it at the caller for a reusable workflow

GitHub is explicit about this for workflows outside your organization: set `id-token` to write "explicitly at the caller workflow or job level", which "ensures the OIDC token is only available to intended caller workflows". Change the calling file, not the called one.

```.github/workflows/release.yml (illustrative)
jobs:
  call-deploy:
    permissions:
      id-token: write
      contents: read
    uses: ./.github/workflows/deploy.yml
```

### Confirm the grant rather than the request

Open the failing job and read the permissions it was actually given. If the workflow asks for `id-token: write` and the job did not receive it, no edit to the file will help and the next question is an organization policy.

## How to prevent it

- Keep `id-token: write` on the smallest job that needs it, so a refactor that splits jobs makes the loss visible.
- When a job declares any `permissions`, list every scope it needs rather than relying on a workflow-level block.
- Set the permission at the caller whenever a reusable workflow performs the login.
- Prefer OIDC over stored cloud credentials, since the failure mode here is a missing line rather than a leaked key.

## Where the sentence comes from, and what wraps it

The string is thrown in `OidcClient.getIDTokenUrl()` in `@actions/core`, in a two line branch: read `process.env.ACTIONS_ID_TOKEN_REQUEST_URL`, and if it is falsy, throw. That is the entire test. The variable is injected into the job environment by the Actions service, so its absence is decided before the runner starts your step.

The wrapper matters as much as the message. `getIDToken()` calls that function inside a `try`, and its `catch` rethrows with a fixed prefix: the new error's message is `Error message: ` followed by the original. So the bare sentence essentially never appears alone in a log. Whatever sits in front of it was added by the code that asked for the token, which makes the prefix a reliable way to identify the caller.

## Two variables, read in a fixed order

The OIDC handshake needs both an endpoint and a bearer token for that endpoint, and the toolkit reads them at different moments. When the permission is absent, neither is present, and yet only one message can be first.

| Variable | Which toolkit step reads it | When it is reached |
| --- | --- | --- |
| `ACTIONS_ID_TOKEN_REQUEST_URL` | `getIDTokenUrl()`, the first call inside `getIDToken()` | Always first, so this is the message you see |
| `ACTIONS_ID_TOKEN_REQUEST_TOKEN` | `getRequestToken()`, when the HTTP client is constructed | Only after a URL was found, so it is rare in practice |

> This is why searching for the token variable's message turns up so little. `Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable` exists in the same file and is thrown by the same kind of branch, but the URL is read first and the two variables appear and disappear together. Seeing the token message instead usually means something set the URL by hand.

## What the prefix tells you about the caller

Because `getIDToken()` always adds `Error message: `, anything in front of that came from your step. A line reading `Failed to get OIDC token: Error message: Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable` was produced by an action that caught the toolkit error and added its own description. A line with a doubled `Error: Unhandled error: Error:` in front came from an action that let the rejection escape to its top-level handler.

Neither prefix changes the cause, and neither is worth debugging on its own. What they are good for is identifying which step asked, which is genuinely useful when several steps in a job could have been the one that wanted a token.

## Why there is no recorded run on this page

A recorded run here would be a video of an absence. The failing branch reads one environment variable, finds nothing, and throws, all before a socket is opened, so the log holds exactly one line and that line is already quoted above from a public issue. The part worth establishing is which of the two variables is read first and what the wrapper adds, and neither of those is observable from a log at all: they are facts about the toolkit, which we read in its source instead.

## FAQ

### Why does my log say "Error message:" twice over?

Because two layers added text. `getIDToken()` catches the original throw and rethrows with a fixed `Error message: ` prefix, and whatever called it usually adds a description of its own. The repetition is normal and carries no extra meaning beyond telling you which step asked for the token.

### Do I need id-token: write for a job that only reads?

You need it for any job that mints an OIDC token, whatever it does afterwards. The name is misleading: GitHub documents that the setting only enables fetching and setting the OIDC token and does not grant write access to other resources. A read-only deployment that authenticates over OIDC still requires it.

### Why is the other variable never the one that fails?

Because of the order. `getIDToken()` resolves the URL before it builds the HTTP client that reads the bearer token, so the URL branch is always reached first. The two variables are injected together and withheld together, which means the second branch is effectively unreachable in the ordinary case.

### Can a step set the variable itself as a workaround?

No, and there is nothing to set it to. The endpoint and its bearer token are issued by the Actions service for a job that was authorized to have them; they are not values the runner can compute. A job that could grant itself an OIDC token would defeat the point of the permission.

## References

- [actions/toolkit: packages/core/src/oidc-utils.ts, the branch that throws](https://github.com/actions/toolkit/blob/main/packages/core/src/oidc-utils.ts)
- [GitHub Actions: OIDC reference, the permission and the two environment variables](https://docs.github.com/en/actions/reference/security/oidc)
- [astropy/extension-helpers#156: the wrapped message in a real run](https://github.com/astropy/extension-helpers/issues/156)

---

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
