# Making download-artifact from another workflow run actually work

> A download-artifact from another workflow run needs github-token, not run-id. Without the token the action ignores run-id and searches your own run.

Source: https://latchkey.dev/learn/github-actions/gha-artifact-download-from-other-run  
Updated: 2026-09-20

A download-artifact from another workflow run is switched on by `github-token`, not by `run-id`. The `run-id` input already has a default, so setting it on its own changes nothing at all and the action keeps searching the run it is executing in.

## What this error means

You added `run-id` to a download step, pointed it at a run you can see in the browser with the artifact plainly attached to it, and the step still says the artifact is not there. Or you added the token as well and now the step fails with a message about a resource not being accessible, which names no permission and no repository. Both of these look like the artifact is missing. Neither of them is about the artifact. The first is the action never having left your own run, and the second is it having left and been refused at the door.

```Composite: the prefix is a literal in actions/download-artifact src/download-artifact.ts (v8.0.1); the rest is the REST API message, so the whole line is in no source file
Error: Unable to download artifact(s): Resource not accessible by integration
```

## Common causes

### You set run-id and left the token out

This is the great majority. The workflow looks correct, the run id is correct, and the input is silently unused because `findBy` was never built. The step then searches the current run, finds nothing, and reports it in words that say nothing about which run it looked in.

### The job does not grant actions: read

A workflow with an explicit `permissions:` block that omits `actions`, or a repository whose default workflow token permissions have been set to read contents only, leaves the token unable to list artifacts. The API answers 403 and the action reports a resource that is not accessible.

### The source run is in another repository

The default job token cannot cross a repository boundary regardless of the permissions block, because its audience is the repository that issued it. You need a token that was issued with both repositories in scope, and the `repository` input set to the source.

### The artifact is genuinely gone

Worth ruling out once the token is right. Artifacts expire, and a run old enough to be worth reaching across to is old enough to have lost them. The listing API will still show the entry with an expired flag on it, which is a quick way to separate this from a permission problem.

## How to fix it

### Add the token, then the permission, then the run id

1. Set `github-token` on the step; nothing else about cross-run downloading takes effect until you do.
2. Add `actions: read` to the job `permissions:` block, alongside whatever it already declares.
3. Set `run-id` to the source run, usually `${{ github.event.workflow_run.id }}`.
4. Leave `repository` alone unless the source run is in a different repository.

### Check what the token can see before blaming the artifact

One call settles whether this is a permission problem or a missing artifact. Run it with the same credential the workflow uses. If it lists the artifact, the workflow has a permissions problem; if it returns an empty list, you are looking at the wrong run.

```Terminal
gh api "repos/OWNER/REPO/actions/runs/RUN_ID/artifacts?name=build-output" \
  --jq '.total_count, (.artifacts[] | {name, expired, expires_at})'
```

### Use artifact-ids when the name is ambiguous

If the source run uploaded several artifacts whose names differ only by a matrix value, the name filter is a blunt instrument and the action returns the newest match. Listing the run and downloading by id is exact, and the ids are stable, which makes a failure easier to read later.

```.github/workflows/deploy.yml (illustrative)
- uses: actions/download-artifact@v8
        with:
          artifact-ids: ${{ needs.collect.outputs.artifact_id }}
          run-id: ${{ github.event.workflow_run.id }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
```

### Or avoid the cross-run download entirely

Reaching into another run is the right answer for a `workflow_run` chain and the wrong one for most other cases. If the producing job and the consuming job can live in the same run, put them there with a `needs:` between them and the token question disappears along with the permission block.

## How to prevent it

- Treat `github-token` as the required input for any cross-run download, and `run-id` as the detail.
- Declare `actions: read` explicitly in any job that downloads across runs.
- Prefer one run with `needs:` over two runs and a token wherever the graph allows it.
- Check `expired` in the artifact listing before you debug a credential.

## The branch that decides which lookup runs

This is the whole switch, quoted from the action. `options.findBy` is built inside an `if (inputs.token)`, and everything cross-run hangs off `findBy`. Set `run-id` and `repository` without a token and they are parsed, validated, and then dropped on the floor.

That is easy to miss because `run-id` is not obviously optional. Its default in the action metadata is `${{ github.run_id }}`, so it is always populated, and overriding a value that already had a value feels like it should do something.

```actions/download-artifact, src/download-artifact.ts (v8.0.1)
const options: FindOptions = {}
if (inputs.token) {
  const [repositoryOwner, repositoryName] = inputs.repository.split('/')
  if (!repositoryOwner || !repositoryName) {
    throw new Error(
      `Invalid repository: '${inputs.repository}'. Must be in format owner/repo`
    )
  }

  options.findBy = {
    token: inputs.token,
    workflowRunId: inputs.runID,
    repositoryName,
    repositoryOwner
  }
}
```

## Two lookups, one message

With no token the artifact client uses backend identifiers taken from the runtime token the runner already holds, and asks the artifact service for the artifacts of the current run. There is no parameter in that call for a different run, which is why `run-id` has nowhere to go.

With a token it calls the public REST endpoint instead, `GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts`, with your name filter on the end. That call can name any run in any repository the token can read.

The catch is that both paths raise the identical error when they come back empty, down to the wording and the link at the end of it. So the not-found message cannot tell you which lookup ran. That is deliberate in the toolkit and it is the single reason this failure is hard: the log line you get for looking in the wrong place is the same line you get for looking in the right place and finding nothing.

| Inputs you set | Which lookup runs | What it can see |
| --- | --- | --- |
| `name` only | internal, via the runtime token | artifacts of the current run |
| `name` and `run-id` | internal, run-id unused | artifacts of the current run |
| `name`, `run-id`, `github-token` | public REST, run-scoped | that run, if the token can read it |
| plus `repository` | public REST, other repository | that run there, if the token reaches it |

> If the message you actually have is the not-found one rather than the permission one, the page that owns it is [GitHub Actions artifact not found for name](/learn/github-actions/github-actions-failed-to-download-artifact-not-found).

## What the token has to be allowed to do

The default job token can read the Actions API of its own repository, but only if the job grants it. `actions: read` is the permission that covers listing and downloading artifacts, and a workflow with a restrictive `permissions:` block, or a repository whose default workflow permissions have been narrowed, will not have it.

The refusal that follows is a plain 403 from the REST API with a message that names nothing useful. The action wraps it in its own prefix and hands it to `setFailed`, which is why the quoted line at the top of this page reads as one sentence made of two halves. The action contributes `Unable to download artifact(s): ` and the API contributes the rest, so grepping any source in either repository for the whole line finds nothing. This is worth stating plainly because an unfindable string looks exactly like an invented one.

For a different repository the default token is not enough at all, whatever permissions you give it, because its scope stops at the repository it was issued for. That needs a personal access token or a GitHub App installation that covers both repositories.

```.github/workflows/deploy.yml (illustrative)
permissions:
  contents: read
  actions: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v8
        with:
          name: build-output
          run-id: ${{ github.event.workflow_run.id }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
```

## Why there is no recorded run on this page

Recording this one honestly would mean publishing two workflow runs in two different workflows and a token with the scope that makes the interesting half work. The line that distinguishes the two code paths is the permission refusal, and a refusal is only meaningful next to the credential that was refused, which is the one thing we cannot show you. A redacted screenshot of a 403 proves nothing that the sentence above does not already say.

The mechanism is also entirely decided before any work happens. One `if` on a string input chooses the lookup, and the lookup either has a parameter for another run or does not. There is no timing in it, nothing transient, and nothing a runner could observe mid-job and put right.

## FAQ

### Why does run-id do nothing in actions/download-artifact?

Because the action only builds its cross-run lookup options inside a check on `github-token`. With no token it uses the run-scoped internal client, which has no parameter for a different run, so `run-id` is parsed and then unused. Add the token and the same `run-id` starts working.

### What permission does download-artifact need for another run?

`actions: read` on the job, plus a token that covers the source repository. The default job token has the right audience for its own repository but only the permissions the workflow grants it, and a narrowed `permissions:` block is the usual reason a correct `run-id` still gets refused.

### Can I download an artifact from a different repository?

Yes, with `repository` set to the source and a token issued for both. The default `GITHUB_TOKEN` cannot do it at any permission level, because its scope is the repository that issued it. Use a fine-grained personal access token or a GitHub App installed on both.

### Why does the error not say which run it searched?

Because both lookups raise the same error text from the same place in the artifact toolkit. The internal and public code paths each throw an identical not-found message, so the wording carries no information about scope. That is why the diagnosis has to start from whether the token is set.

## References

- [actions/download-artifact src/download-artifact.ts: the token check that builds findBy](https://github.com/actions/download-artifact/blob/main/src/download-artifact.ts)
- [actions/download-artifact action.yml: the defaults on run-id and repository](https://github.com/actions/download-artifact/blob/main/action.yml)
- [actions/download-artifact#489: the 403 message does not name the permission it wanted](https://github.com/actions/download-artifact/issues/489)
- [REST API: list workflow run artifacts, the endpoint the token path calls](https://docs.github.com/en/rest/actions/artifacts#list-workflow-run-artifacts)
- [Permissions for GITHUB_TOKEN, including the actions scope](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions)

---

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
