Unable to get ACTIONS_ID_TOKEN_REQUEST_URL in GitHub Actions
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.
Error: Unhandled error: Error: Error message: Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variableWhere 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.
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.
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v5Remember 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.
jobs:
call-deploy:
permissions:
id-token: write
contents: read
uses: ./.github/workflows/deploy.ymlConfirm 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.
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 |
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.
How to prevent it
- Keep
id-token: writeon 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.
Frequently asked questions
Why does my log say "Error message:" twice over?
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?
Why is the other variable never the one that fails?
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.