Every GitLab job runs in a fresh environment. That clean isolation is great for reliability, but it creates a problem: files produced by one job do not automatically exist in the next job. If your build job creates a compiled app, your test job cannot see it unless you explicitly preserve and pass it forward. That is exactly what artifacts are for.
Artifacts are one of the most important GitLab CI/CD concepts because they let you move meaningful outputs through a pipeline and make them downloadable later from the GitLab UI.
An artifact is a file or directory saved from a job after the job finishes. GitLab stores those files for a configurable amount of time so they can be:
Typical artifacts include:
dist/ folders from frontend buildsA useful mental model is this:
If a later job truly needs a file created earlier, artifacts are usually the right answer.
artifacts: paths:The most common way to create artifacts is with artifacts: paths:.
build-app:
stage: build
image: node:20
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/After the job succeeds, GitLab saves the dist/ directory as a job artifact.
paths meansEach path is relative to the project directory in the job workspace. You can store files or folders.
artifacts:
paths:
- dist/
- coverage/
- reports/junit.xmlThat tells GitLab to archive multiple outputs from the same job.
Suppose your build step compiles a Node.js app into dist/. If you want a later packaging or deployment job to use that compiled output, you should not rebuild it unless you have to. Artifacts allow the downstream job to use the exact build result produced earlier.
artifacts: expire_in: for Automatic CleanupArtifacts take storage space, so GitLab lets you define how long they should be kept.
build-app:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 weekAfter one week, GitLab automatically removes the stored artifact.
1 hour for temporary build outputs1 day for frequent branch pipelines1 week for normal CI outputs1 month or longer for release bundles and reports you want to retainChoose the shortest retention that still supports your team workflow. A quick feature-branch build probably does not need to live forever. A release package may need a much longer lifetime.
Without cleanup, artifact storage can grow quickly. Large projects build many branches, many merge requests, and many reports. expire_in keeps storage predictable and prevents old, unused pipeline files from piling up.
One of the easiest benefits of artifacts is visibility. When a job stores artifacts, GitLab exposes them in the UI.
From the pipeline page or job page, you can typically:
This is especially useful when you want to:
For many teams, artifacts become the bridge between automation and human review.
By default, later jobs in later stages can download artifacts from earlier jobs. That makes a simple build-then-test or build-then-deploy pipeline straightforward.
stages:
- build
- deploy
build-app:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
deploy-app:
stage: deploy
script:
- test -d dist
- ./deploy.shIn this example, deploy-app can use the dist/ directory created by build-app because it is an artifact from an earlier stage.
That behavior is convenient, but in larger pipelines you often want tighter control over exactly which artifacts get downloaded.
dependencies vs needsThere are two related ideas here:
dependencies controls which earlier job artifacts a job downloadsneeds creates an execution dependency and can also fetch artifacts sooner in DAG pipelinesdependenciesdependencies is useful when you want to limit artifact downloads to specific previous jobs.
package-app:
stage: deploy
script:
- tar -czf release.tar.gz dist
dependencies:
- build-appThis tells GitLab to download artifacts only from build-app, not from every earlier job that might have published artifacts.
Why does that matter? Because downloading unnecessary artifacts wastes time and bandwidth. In a large pipeline, selective downloads can make jobs faster and cleaner.
needsneeds is about the job graph. It allows a job to start as soon as its required upstream jobs finish, instead of waiting for the whole previous stage.
package-app:
stage: deploy
needs:
- job: build-app
artifacts: true
script:
- tar -czf release.tar.gz distHere, package-app explicitly needs build-app and requests its artifacts. This is powerful because the job can start earlier in a DAG pipeline and still receive the required files.
dependencies when you mainly want to control which earlier artifacts are downloaded.needs when you want both faster scheduling and explicit dependency relationships.In newer pipelines, needs is often the more flexible pattern because it improves both clarity and speed.
artifacts: reports: for Structured GitLab ReportsNot all artifacts are just downloadable archives. GitLab understands certain artifact types as reports and uses them to power UI features.
unit-tests:
stage: test
script:
- npm ci
- npm test -- --reporters=jest-junit
artifacts:
reports:
junit: junit.xmlIn this case, junit.xml is not only stored as an artifact. GitLab also interprets it as a JUnit test report and can show test results in the pipeline and merge request UI.
artifacts:
reports:
junit: junit.xmlUse this for unit test or integration test output in JUnit XML format.
Coverage reporting often uses a specific format produced by your test tool.
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xmlThis helps GitLab visualize code coverage information.
Security scanners can produce reports that GitLab understands.
artifacts:
reports:
sast: gl-sast-report.jsonThe same idea applies to dependency scanning and other security-focused report types in GitLab.
A normal artifact is something you download. A report artifact is something GitLab can parse and surface directly in the product experience. That makes pipelines much more useful because results are visible where developers already work.
artifacts: when:By default, artifacts are usually saved on successful jobs. Sometimes you want different behavior.
artifacts:
when: always
paths:
- logs/Common values are:
on_successon_failurealwayson_successSave artifacts only if the job succeeds. This is the normal choice for build outputs and deployment bundles.
on_failureSave artifacts only if the job fails. This is great for debugging data.
integration-tests:
stage: test
script:
- ./run-integration-tests.sh
artifacts:
when: on_failure
paths:
- logs/
- screenshots/If the test run fails, you can collect screenshots, logs, and traces for investigation.
alwaysSave artifacts regardless of result. This is useful when diagnostic output is valuable even on passing runs.
artifacts: untracked:GitLab can also save untracked files automatically.
build-debug:
stage: build
script:
- ./generate-debug-files.sh
artifacts:
untracked: trueThis captures files created during the job that are not tracked by Git.
untracked: true is convenient, but it can also collect more than you expect. Large temporary files, generated directories, or accidental outputs may get stored. For predictable pipelines, paths: is usually clearer. Use untracked when you genuinely want all generated untracked files and understand the storage impact.
Imagine a frontend app built with Node.js.
stages:
- build
- deploy
build-web:
stage: build
image: node:20
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
deploy-web:
stage: deploy
image: alpine:3.20
dependencies:
- build-web
script:
- test -d dist
- ./scripts/deploy-dist.shHere, the build output in dist/ is preserved and then consumed by deployment. This ensures the deployment job uses the exact files generated during the verified build.
Now imagine a test job that produces both human-readable output and a machine-readable JUnit report.
unit-tests:
stage: test
image: node:20
script:
- npm ci
- npm test -- --reporters=jest-junit
artifacts:
reports:
junit: junit.xml
paths:
- junit.xml
expire_in: 3 daysThe file is stored as a normal artifact and also recognized as a report. That means:
Suppose you want to package a Node.js application into a tarball for release.
stages:
- build
- package
- deploy
build-app:
stage: build
image: node:20
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
create-package:
stage: package
image: alpine:3.20
needs:
- job: build-app
artifacts: true
script:
- tar -czf release.tar.gz dist
artifacts:
paths:
- release.tar.gz
expire_in: 1 month
deploy-prod:
stage: deploy
image: alpine:3.20
dependencies:
- create-package
script:
- ls -lh release.tar.gz
- ./deploy-release.sh release.tar.gzThis pattern is common in production pipelines. Build first, package once, then deploy the packaged result.
Artifacts are powerful, but they should be intentional.
Do not store entire workspaces unless you have a strong reason. Store only the files later jobs or humans actually need.
A missing expire_in is not always wrong, but clear retention is healthier for storage management.
If GitLab can interpret a report, let it. Structured visibility is better than forcing people to download raw files.
A downstream job should ideally consume the tested build artifact rather than repeating the build. That improves consistency.
Expecting files to survive automatically between jobs Each job is isolated. Without artifacts, generated files disappear.
Using cache instead of artifacts for pipeline outputs Cache is not a correctness mechanism. Artifacts are.
Storing too many large files forever This creates unnecessary storage cost.
Not narrowing downloads in large pipelines
Use dependencies or needs to avoid pulling unrelated artifacts.
Forgetting report syntax
A file under paths: alone does not automatically become a GitLab test or coverage report.
Artifacts make pipelines practical. They preserve outputs, move files between isolated jobs, expose useful downloads in the GitLab UI, and power rich reporting features. Once you start treating artifacts as deliberate pipeline handoffs, your CI/CD design becomes more reliable and much easier to reason about.
<MCQExercise exercise={{ id: 'gitlab-artifacts-q1', title: 'Question 1: Purpose of Artifacts', description: 'What is the primary reason to use artifacts in GitLab CI/CD?', options: [ { id: 'a', text: 'To store reusable dependency caches across many unrelated pipelines' }, { id: 'b', text: 'To save files from a job so later jobs or users can access them' }, { id: 'c', text: 'To choose which runner executes a job' }, { id: 'd', text: 'To define the order of stages in the pipeline' }, ], correctId: 'b', explanation: 'Artifacts preserve files created by a job, such as build outputs, reports, or packages, so they can be downloaded later or consumed by downstream jobs in the pipeline.', wrongExplanations: { a: 'Reusable dependency storage is the job of cache, not artifacts.', c: 'Runner selection is controlled by settings like tags, not artifacts.', d: 'Stage order is controlled by the stages keyword, not by artifacts.', }, }} />
<MCQExercise exercise={{ id: 'gitlab-artifacts-q2', title: 'Question 2: Expiration', description: 'What does artifacts: expire_in: 1 week do?', options: [ { id: 'a', text: 'It retries the job for one week if it fails' }, { id: 'b', text: 'It keeps the artifact stored for one week before GitLab cleans it up' }, { id: 'c', text: 'It forces the artifact to download within one week or the pipeline fails' }, { id: 'd', text: 'It makes the job run only once per week' }, ], correctId: 'b', explanation: 'The expire_in setting controls artifact retention time. After the specified period, GitLab can remove the stored artifact automatically to free space.', wrongExplanations: { a: 'Expire_in affects artifact retention, not job retries.', c: 'It does not impose a deadline on users or downstream jobs. It only sets how long GitLab keeps the artifact.', d: 'Job schedules are configured separately. Expire_in does not control pipeline frequency.', }, }} />
<MCQExercise exercise={{ id: 'gitlab-artifacts-q3', title: 'Question 3: Reports', description: 'Why would you use artifacts: reports: junit: junit.xml in a test job?', options: [ { id: 'a', text: 'To tell GitLab that junit.xml is a structured test report it can display in the UI' }, { id: 'b', text: 'To convert the whole job into a manual approval step' }, { id: 'c', text: 'To stop the test job from producing normal console output' }, { id: 'd', text: 'To rename the job automatically to junit' }, ], correctId: 'a', explanation: 'The reports syntax tells GitLab that the artifact file is a recognized report format. GitLab can then parse it and show test results directly in pipeline and merge request views.', wrongExplanations: { b: 'Manual jobs are configured with when: manual, not with artifacts reports.', c: 'Console output behavior is unrelated to report artifacts.', d: 'Reports do not rename jobs. They describe artifact content.', }, }} />
Declare them as artifacts in the producing job with artifacts: paths:, and by default every job in a later stage downloads them automatically. To control that precisely, use needs: to name the specific jobs whose artifacts you want, or dependencies: to restrict it, dependencies: [] downloads nothing at all, which is worth setting on jobs that do not need them since it speeds them up noticeably.
Until their expire_in elapses, defaulting to 30 days on GitLab.com unless the instance or project overrides it. Set expire_in explicitly on every artifact: an hour is plenty for files only passed between jobs in the same pipeline, while release builds might justify weeks. Artifacts consume project storage and are a common cause of unexpectedly hitting a storage quota, since a pipeline running dozens of times a day accumulates quickly. The latest artifacts for the most recent successful pipeline on a ref are kept regardless unless you disable that behaviour.
It tells GitLab that an artifact is a structured report of a known type, JUnit test results, code coverage, SAST findings, a dotenv file, so the platform can parse it and surface the contents in the merge request rather than leaving it as a file to download. A JUnit report turns into a test summary showing exactly which tests newly failed, which is far more useful than reading a log. Report artifacts are subject to their own retention rules and, for some types, size limits.
Usually the paths are wrong relative to the project root, which is where they are evaluated from, not from wherever the script happened to cd. The job log lists which files were collected, so check there first. The other frequent cause is that the job failed before producing them: by default artifacts are only uploaded when a job succeeds, so if you want them from failures too, for example to capture logs or screenshots, set artifacts: when: always.
Yes. The GitLab API exposes them at a per-job endpoint, and there is a stable URL for the latest artifacts from a successful job on a given ref, which is what most external tooling uses. A CI job token authorises access from within another pipeline, which is how multi-project pipelines consume builds from an upstream project without a personal access token.
Speed up GitLab CI with caching: define cache paths and keys, use cache policies, see how cache differs from artifacts and cache node_modules and pip.
Use GitLab CI/CD variables safely: predefined and custom variables, masked and protected values, group-level and scoped variables, and precedence rules.
Learn what a GitLab Runner is and how it runs jobs: shared versus specific runners, executors, installing and registering on Ubuntu, and a Docker executor.