GitHub Actions Artifacts
How to move files between jobs and keep build output after a run. upload-artifact v4 behaviour, retention, the immutability change, and when to use a cache instead.
Each job runs on its own runner, so a file produced in one job does not exist in the next. Artifacts are the mechanism for moving files between jobs and for keeping build output after the run finishes.
Verified against actions/upload-artifact@v4 and actions/download-artifact@v4 as of September 2026.
Upload and Download
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- run: ./deploy.sh dist/
needs: build is required. Without it the deploy job starts immediately, the artifact does not exist yet, and the download fails.
The v4 Change That Breaks Old Workflows
Version 4 changed artifact behaviour in a way that silently breaks patterns which worked in v3, and this is the single most important thing to know about artifacts today.
Artifacts are immutable. In v3, several jobs could upload to the same artifact name and the contents merged. In v4 the first upload creates the artifact and any subsequent upload with the same name fails. This breaks the common matrix pattern:
# Fails on the second matrix job in v4.
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
The fix is a unique name per job, then a merge if you want one download:
- uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.os }}
path: dist/
and in a later job, either download them all at once with a pattern:
- uses: actions/download-artifact@v4
with:
pattern: build-*
merge-multiple: true
path: dist/
or combine them into a single artifact with actions/upload-artifact/merge@v4.
v3 and v4 cannot see each other's artifacts. A v4 download cannot find an artifact uploaded by v3 and vice versa. Upgrade upload and download together, or you get a confusing "artifact not found" on an artifact you can see in the UI.
If you are hitting that error now, the artifact not found page walks through the causes in order.
Paths
path accepts globs and multiple lines:
with:
name: reports
path: |
coverage/
junit.xml
!coverage/tmp/**
The directory structure inside the artifact is relative to the least common ancestor of everything matched. This catches people out: uploading both coverage/ and junit.xml produces an archive with coverage/ and junit.xml at the root, but uploading only coverage/lcov.info produces an archive containing just lcov.info, with the coverage/ directory gone. If you need the structure preserved, upload the parent directory.
An upload that matches nothing succeeds with a warning by default, which means a typo in the path produces a green build and an empty artifact. Set if-no-files-found: error on anything you depend on later.
Retention and Cost
retention-days defaults to 90 for public repositories and follows the repository or organisation setting otherwise, with a maximum of 90 (400 for enterprise). Artifact storage is billed on private repositories, and the bill is a function of size multiplied by days.
The practical consequence is that a workflow uploading a 500 MB build on every push to every branch, kept for the default period, is the most likely source of an unexpected Actions bill. Two habits prevent it:
Set a short retention for anything that is only needed within the run. A dist/ artifact whose only consumer is the next job needs retention-days: 1, not 90.
Upload only what you need. Uploading node_modules or a whole workspace directory happens more often than you would expect, usually from a path: . that was never narrowed.
Artifacts are also zipped during upload, so a directory of many small files compresses well while an already-compressed archive does not.
Artifacts or Cache?
They look similar and solve different problems.
Use a cache for inputs that speed up a job and can be regenerated: dependency downloads, compiler caches. A cache miss is a slow build, not a broken one. Caches are keyed and shared across runs.
Use an artifact for outputs you need to keep or hand to another job: a compiled bundle, a test report, a container image tarball. An artifact is scoped to one workflow run and is retrievable from the run's page in the UI.
The distinguishing question is whether losing it breaks correctness or only speed. If a missing copy means the next job cannot proceed, it is an artifact.
Downloading Outside the Workflow
Every run's page has an Artifacts section with download links, which is how a colleague gets the built binary without running anything. Artifacts are also reachable through the REST API, which is the supported way for an external system to collect build output.
One limitation to plan around: artifacts from a run cannot be downloaded through the UI until the whole run completes. A long-running workflow does not let you grab an early job's output while later jobs are still going.
Frequently Asked Questions
Why does my download say the artifact does not exist?
Four causes, in order of frequency: the producing job has not finished because needs: is missing, the names do not match exactly including case, the upload matched no files and produced nothing, or the upload used v3 while the download uses v4. The run's Artifacts section tells you whether it was created at all, which separates the upload problem from the download problem.
Can I upload an artifact from a failed job?
Yes, with if: always() on the upload step. This is the normal way to keep test reports and screenshots from a failing run, and without it the artifact is skipped along with everything else after the failure.
Do artifacts work across workflows?
Not with download-artifact directly, which is scoped to the current run. Fetching an artifact produced by a different workflow run requires the REST API, or a third-party action that wraps it.
How large can an artifact be?
Individual artifacts can be very large, but you are constrained by the repository's storage allowance and by upload time. In practice anything over a few hundred megabytes on every push is worth questioning.
Is an artifact a good way to pass a small value between jobs?
No. Use a job output for values, which is covered in the workflow syntax lesson. An artifact for a single string means an upload, a download and an unzip to move a few bytes.