CI/CD

Unable to download artifact: Artifact not found

The download step could not find what an earlier job uploaded. Name mismatches, job ordering, expiry and the v3 to v4 change that broke every matrix upload.

easy fix6 min read

the ci/cd error
Error: Unable to download artifact(s): Artifact not found for name: build-output

Error: Create Artifact Container failed: The artifact name build-output is not valid

ERROR: Downloading artifacts from coordinator... not found  id=12345 responseStatus=404 Not Found

Error: Conflict: an artifact with this name already exists on the workflow run

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Compare the upload name against the download name, exactly

    grep -n -A3 "upload-artifact\|download-artifact" .github/workflows/*.yml

    The name is a literal string match and is the cause most of the time. An expression such as build-${{ matrix.os }} produces a different name per matrix leg, and a download naming the bare prefix finds nothing.

  2. 2

    Confirm the producing job ran and actually uploaded something

    echo "check the upload step's log for the file count it reported"

    upload-artifact warns rather than failing when its path matches no files, so a job can succeed having uploaded nothing. Set if-no-files-found to error to turn that into a real failure.

  3. 3

    Check the jobs are ordered, not parallel

    grep -n "needs:\|stage:" .github/workflows/*.yml .gitlab-ci.yml 2>/dev/null | head

    Without needs in GitHub Actions or a later stage in GitLab, the consuming job may start before the producer finishes. It then downloads nothing and the failure looks intermittent.

All 9 sections

The consuming job asked for an artifact that is not there. Four causes, in the order they occur.

1. The name does not match

The name is a literal string. The most common failure is an expression producing a different one per job:

- uses: actions/upload-artifact@v4
  with:
    name: build-${{ matrix.os }}      # build-ubuntu-latest, build-macos-latest
    path: dist/

- uses: actions/download-artifact@v4
  with:
    name: build                        # matches nothing

Either name them consistently, or download everything:

- uses: actions/download-artifact@v4
  with:
    pattern: build-*
    merge-multiple: true
    path: dist/

pattern and merge-multiple were added in v4 precisely for the matrix case.

2. v4 changed the rules

This broke a great many pipelines and is worth knowing in full:

v3v4
Same name from several jobsMerged into one artifactConflict error
Cross-workflow downloadPossibleNeeds run-id and a token
ImmutabilityMutableImmutable once uploaded
SpeedSlowerMuch faster
Error: Conflict: an artifact with this name already exists on the workflow run

In v3 every matrix leg uploading build merged silently. In v4 the second one fails. Give each a unique name and merge on download:

  build:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/upload-artifact@v4
        with:
          name: build-${{ matrix.os }}
          path: dist/

  publish:
    needs: build
    steps:
      - uses: actions/download-artifact@v4
        with:
          pattern: build-*
          merge-multiple: true
          path: dist/

Do not mix versions: a v4 download cannot see a v3 upload, and the reverse also fails.

3. The upload silently matched nothing

- uses: actions/upload-artifact@v4
  with:
    name: build
    path: dist/

If dist/ does not exist, this warns and succeeds. The job is green and no artifact exists, and the failure surfaces later in a different job, which is a confusing place to debug it.

    if-no-files-found: error

Worth setting on every upload. It converts a silent miss into a failure at the point where the problem actually is.

Check the path too: it is relative to the workspace, and a build that writes to packages/app/dist is not matched by dist/.

4. Job ordering

  publish:
    needs: build          # without this, they run in parallel

Without needs, jobs run concurrently and the consumer may start before the producer finishes. That produces an intermittent failure that passes on a slow build and fails on a fast one.

In GitLab, stage order handles it:

stages: [build, deploy]

build:
  stage: build
  artifacts:
    paths: [dist/]
    expire_in: 1 week

deploy:
  stage: deploy
  dependencies: [build]      # only fetch this job's artifacts

GitLab passes artifacts from all earlier stages by default. dependencies narrows it, and an empty list disables the download entirely:

  dependencies: []

That is worth setting on jobs that need no artifacts, since a large artifact downloaded into every job costs real time.

Expiry

GitLab artifacts expire, and the default is often shorter than people assume:

artifacts:
  paths: [dist/]
  expire_in: 1 week
ERROR: Downloading artifacts from coordinator... not found  responseStatus=404

A 404 on a job from last month is usually expiry rather than a fault. expire_in: never keeps them, at a storage cost. Use keep_latest_artifact in project settings to retain the most recent per ref.

GitHub Actions artifacts default to 90 days, configurable per upload:

    retention-days: 7

Shorter retention is worth setting for large build outputs, since artifact storage counts against your account quota.

Across workflows

In v4 this needs explicit parameters:

- uses: actions/download-artifact@v4
  with:
    name: build
    run-id: ${{ github.event.workflow_run.id }}
    github-token: ${{ secrets.GITHUB_TOKEN }}
    repository: ${{ github.repository }}

Without run-id and a token, the action only looks within the current run. This is the standard pattern for a workflow_run workflow consuming output from the workflow that triggered it.

Artifacts are not a cache

Worth stating because it causes both errors and cost:

ArtifactsCache
PurposePass output between jobs, keep build resultsSpeed up repeated work
MissingAn error you must handleA miss, the job continues
ScopeOne workflow runAcross runs, keyed

Using artifacts for node_modules is slow and expensive. Use the cache action, which is designed to tolerate a miss.

A checklist

  1. Compare upload and download names character for character.
  2. Matrix jobs → unique names, then pattern plus merge-multiple on download.
  3. Do not mix upload-artifact v3 with download-artifact v4.
  4. Set if-no-files-found: error so an empty upload fails loudly.
  5. Confirm the path is relative to the workspace and actually contains files.
  6. GitHub → needs:. GitLab → a later stage.
  7. GitLab → dependencies: [] on jobs that need nothing, to save time.
  8. 404 on an older run → expiry. Check expire_in and retention-days.

Frequently Asked Questions

Why did upload-artifact v4 break my matrix build?

Because v4 removed the implicit merging that v3 did. In v3, several jobs uploading under the same name were combined into one artifact; in v4 names must be unique within a run and the second upload fails with a conflict. Give each matrix leg a distinct name such as build-${{ matrix.os }}, then download with pattern: build-* and merge-multiple: true, which reassembles them into one directory. The versions are also incompatible in both directions, so upload and download must match.

Why does my upload succeed but the download find nothing?

Because upload-artifact only warns when its path matches no files; the step and the job both go green having uploaded nothing. The failure then surfaces in a different job, which is a misleading place to start debugging. Set if-no-files-found: error on every upload so the failure happens where the problem is. Also verify the path: it resolves relative to the workspace, so a monorepo writing to packages/app/dist is not matched by dist/.

Why is the failure intermittent?

Almost certainly missing job ordering. Without needs: in GitHub Actions, jobs run in parallel, so whether the consumer finds the artifact depends on which job happens to finish first. A slow build masks it and a fast one exposes it. In GitLab, jobs within a stage run in parallel and only later stages see earlier artifacts, so a consumer placed in the same stage as its producer has the same race.

What is the difference between artifacts and cache?

Artifacts pass build output between jobs and are meant to be retrievable afterwards; a missing artifact is an error your pipeline has to handle. Cache exists to speed up repeated work and is keyed, shared across runs, and safe to miss, since a cache miss simply means the job does more work. Using artifacts for dependency directories such as node_modules is slower and consumes storage quota, and using cache to pass build output between jobs is unreliable because a miss is silent.

Why do older artifacts return 404?

They expired. GitLab's expire_in is commonly set to days or weeks and defaults to a limited period, and GitHub Actions retains artifacts for 90 days by default. A download from a job run last month is therefore likely to be genuine expiry rather than a fault. Set expire_in: never or a longer retention-days for artifacts you need to keep, bearing in mind that storage counts against your account's quota and large build outputs add up quickly.

Reference and practice

Learn the underlying concept

Other CI/CD errors