A GitLab artifact is a file or a directory that one job in a pipeline produces and a later job in the same pipeline consumes. The artifact is the file system the pipeline shares between jobs, and the artifact system is the part of GitLab CI that turns a pipeline from a sequence of independent scripts into a single workflow with a real output. Most teams use artifacts for the build-then-deploy pattern: a build job compiles the binary, an artifact carries the binary to the next stage, a deploy job ships the binary. That single pattern is most of the value of GitLab artifacts.
The reason “gitlab artifacts” is its own question and not just “files in CI” is that the artifact system has rules, retention windows, dependencies between jobs, and a separate vocabulary (artifacts, artifact reports, dependencies, needs, expire_in). The vocabulary is the part the tutorials skip. The vocabulary is the part the developer has to know to make the pipeline work the way the team expects.
Table of contents
- The short version
- The four kinds of artifacts a GitLab pipeline can produce
- The three patterns that cover 80% of real pipelines
- The expiration and retention rules that quietly burn storage
- The dependencies and needs syntax that decide who can see what
- The five mistakes that make artifacts silently disappear
- The report types that turn artifacts into merge-request widgets
- FAQ
The short version
A job declares its output with artifacts: in .gitlab-ci.yml. The output is uploaded to GitLab’s object storage when the job finishes, and is available to downstream jobs that depend on this one. The output can be a single file, a directory, or a glob. The output can be a paths artifact (the build output) or a reports artifact (the test report, the coverage report, the security report). The two kinds serve different purposes and the developer should know which one they want before they write the pipeline.
The four kinds of artifacts a GitLab pipeline can produce
The artifact keyword is one block, but the kinds are four. The kind changes what GitLab does with the output, and what downstream consumers can do with it.
paths. A paths artifact is a list of files or directories that the job produces. The typical use is a build output (dist/, build/, bin/, coverage/), a packaged archive, a generated config file, or a static asset bundle. Downstream jobs download the artifact and use the files. The artifact is the pipeline’s shared file system.
reports. A reports artifact is a file in a known format (JUnit, Cobertura, cobertura, sast, container-scanning, license-management) that GitLab parses and turns into a merge-request widget, a pipeline view, a security dashboard, or a coverage chart. The file is uploaded, but the value is in the widget, not the file. The developer never reads the file directly — GitLab reads it.
expose_as. An expose_as artifact is a file that GitLab exposes in the merge-request UI as a downloadable link. The typical use is a built binary that the reviewer can download to test the change. The file is uploaded, and a “Download” link appears on the merge request.
trace. A trace artifact is a special kind of artifact that is the job’s log output. Trace artifacts are not declared — they are always produced. The trace is the log line that GitLab shows in the pipeline view.
The four kinds are not exclusive. A job can declare a paths artifact and a reports artifact at the same time. The paths artifact carries the build output, the reports artifact carries the test report, and the two serve different consumers. The typical pipeline has both.
The three patterns that cover 80% of real pipelines
A short, opinionated list of patterns that show up in real GitLab pipelines. The patterns are not the only ones, but they are the ones the developer should learn first.
The build-then-deploy pattern. A build job compiles the code and declares artifacts.paths: [dist/]. A deploy job in the next stage declares the build job as a dependency, downloads the artifact, and ships it. The pattern is the most common one in real pipelines, and the one most teams should learn first. The artifact is the bridge between the build stage and the deploy stage.
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths: [dist/]
deploy:
stage: deploy
script:
- rsync -av dist/ user@server:/var/www/
dependencies: [build]
The test-and-report pattern. A test job runs the tests and declares artifacts.reports.junit: [test-results.xml]. GitLab reads the JUnit file, surfaces the results in the pipeline view, and shows a “Tests” widget on the merge request. The pattern is the one that turns a test run from a log line into a merge-request signal.
test:
stage: test
script:
- pytest --junitxml=test-results.xml
artifacts:
reports:
junit: test-results.xml
when: always
expire_in: 30 days
The matrix-build pattern. A matrix job runs the same script across a list of inputs (Node versions, Python versions, OS images) and produces an artifact for each. The artifacts are stored as a list, and the downstream job can either download all of them (the matrix result) or a subset (a specific version). The pattern is the one that turns a single build into a fan-out.
build:
stage: build
script: npm ci && npm run build
artifacts:
paths: [dist/]
parallel: 5
The three patterns are not exhaustive. The developer can combine them, layer them, or build more complex ones. The three are the floor of what the team should know.
The expiration and retention rules that quietly burn storage
Artifacts cost storage. GitLab stores them in object storage, and the storage bill is a function of the artifact count, the artifact size, and the retention window. A pipeline that produces 100 MB of artifacts and runs 100 times a day for 30 days is a pipeline that costs 300 GB of artifact storage. The retention window is the lever the developer has.
expire_in is the explicit lever. The expire_in field on an artifact sets the retention window. The default in GitLab.com is 30 days, the default on self-managed GitLab is “forever” (which is a bug in real bills). The developer should set expire_in on every artifact, with a value that matches the artifact’s value. A build output is useful for the deploy job, and for the rollback job, and for the audit trail — 30 days is fine. A test report is useful for the merge request, and for the next run’s diff — 30 days is fine. A package that gets uploaded to the registry is useful for a year or more.
dependencies is the implicit lever. A downstream job that does not declare the upstream as a dependency will not download the artifact, and the artifact will be cleaned up when the pipeline finishes. The default in GitLab is to download all artifacts from all upstream jobs in the same stage — which is wasteful, slow, and confusing. The developer should always declare dependencies explicitly on jobs that need the artifact, and let the rest of the jobs ignore it.
artifacts:untracked: true is the dangerous lever. A job that declares untracked: true will upload every untracked file in the working directory. A job that builds with node_modules and forgets to add it to .gitignore will upload node_modules as an artifact, which is usually a disaster. The developer should always use paths instead of untracked, and should always keep the build output in a known directory.
The dependencies and needs syntax that decide who can see what
A pipeline is a directed acyclic graph. The edges are dependencies between jobs. The edges decide which jobs can see which artifacts. The developer should know the two ways to declare an edge, and the difference between them.
dependencies is the legacy syntax. A job that declares dependencies: [build] will download the artifacts from the build job, and from any other job in the same stage. The syntax works, but it has a subtle behavior: a job in the same stage as the dependency will also download the artifact, even if the job does not need it. The behavior is the source of the “the test job downloaded the build output and ran out of disk” bug.
needs is the modern syntax. A job that declares needs: [build] will download the artifact from the build job and only from the build job. The syntax is the right answer for pipelines that have a clear dependency graph, and the one the developer should reach for. The syntax is also the one that enables needs: [build, test] for jobs that depend on multiple upstream jobs.
needs:artifacts: false is the control flow. A job that declares needs: [deploy, approve] with artifacts: false will wait for the upstream jobs to finish, but will not download their artifacts. The pattern is the one that turns a pipeline into a control flow — the approve job gates the rest of the pipeline, but does not need the artifacts.
The two syntaxes are not interchangeable. A pipeline that uses dependencies will be slower, will have weirder disk usage, and will be harder to reason about. The developer should prefer needs and should not mix the two in the same pipeline.
The five mistakes that make artifacts silently disappear
A short, opinionated list of mistakes that have actually broken real GitLab pipelines. None of them are dramatic. They are the boring ones.
The path is relative to the job’s working directory. A job that declares artifacts.paths: [build/] will upload the build/ directory that exists at the end of the job, in the directory the job ran in. A job that runs cd src && make and declares artifacts.paths: [build/] will look for src/build/, not build/. The fix is to use absolute paths or to be explicit about the working directory.
The artifact is overwritten by a later stage. A job that declares the same path as an upstream job will overwrite the upstream’s artifact. The downstream job will download the new file, not the upstream’s output. The fix is to give each artifact a unique name or to scope it to a job-specific subdirectory.
The artifact is not declared as a report when it should be. A test result that is declared as paths: [test-results.xml] is uploaded as a regular file, and GitLab does not parse it. The merge-request widget never appears. The fix is to declare it as reports.junit: [test-results.xml].
The artifact is declared as a report when it should be a path. A build output that is declared as a report is parsed by GitLab as a report format, and the parsing fails. The fix is to declare it as paths, not reports.
The when: on_success is wrong for tests. A test job that declares artifacts.when: on_success will not upload the test report when the tests fail. The merge-request widget is empty. The fix is artifacts.when: always, so the report is uploaded on success and on failure. The pattern is the one that turns a failing test into a merge-request signal.
The report types that turn artifacts into merge-request widgets
The reports are the part of GitLab artifacts that turn a pipeline into a developer-experience tool. The reports are not just files — they are signals that GitLab parses and surfaces in the merge request, the pipeline view, the security dashboard, and the value stream analytics.
junit is a JUnit-format XML test report. The widget is the “Tests” panel on the merge request, with a list of failed tests, a count of passed/failed/skipped tests, and a link to the full report. The pattern is the one that turns a test run into a merge-request signal.
cobertura is a Cobertura-format XML coverage report. The widget is the “Coverage” panel on the merge request, with a coverage percentage, a diff against the base branch, and a link to the line-by-line coverage report. The pattern is the one that turns a coverage run into a merge-request signal.
sast is a Static Application Security Testing report. The widget is the “Security” panel on the merge request, with a list of detected vulnerabilities, a severity rating, and a link to the finding. The pattern is the one that turns a security scan into a merge-request signal.
container_scanning is a container image vulnerability report. The widget is the same “Security” panel, with a list of CVEs in the image, a severity rating, and a link to the CVE details. The pattern is the one that turns a container scan into a merge-request signal.
license_management is a dependency license report. The widget is the “License” panel on the merge request, with a list of licenses in the dependency tree, a flag for unknown licenses, and a flag for denied licenses. The pattern is the one that turns a license check into a merge-request signal.
The five report types are not the only ones — GitLab has more — but they are the ones the developer should learn first. The pattern is the same: produce the report in the right format, declare it under artifacts.reports, and GitLab surfaces the widget.
How this fits the rest of the stack
A pipeline is rarely the whole project. The pipeline usually builds a service, deploys a database, ships a static site, and writes to a log feed. The platform that handles the pipeline should make the rest of the stack feel like part of the same conversation.
The services layer is the part of the platform that runs the long-lived API the pipeline deploys. The database layer is the part that holds the data the API reads and writes. The static layer is the part that hosts the static site the pipeline ships. The environment variables are the part that holds the secrets the pipeline reads to authenticate against the deploy target.
A pipeline that runs on a platform where the API, the database, the storage, the secrets, and the deploy target are all in the same place is a pipeline the team is going to be able to debug. A pipeline that runs on a platform where each piece is in a different console is a pipeline the team is going to spend the first hour just opening the right tab.
For a team that wants to see the full cost of the project before it commits, the RunxBuild hosting calculator shows the line items together. The API, the database, the storage, the worker, the build minutes, the bandwidth — each one is a separate number, and the team’s mental model for the platform is the sum of those numbers.
FAQ
What is a GitLab artifact?
A GitLab artifact is a file or a directory that a job in a pipeline produces and a later job in the same pipeline consumes. The artifact is uploaded to GitLab’s object storage when the job finishes, and is available to downstream jobs that declare the upstream as a dependency. The artifact can be a build output, a test report, a coverage report, a security report, or any other file the job produces.
How do I pass artifacts between jobs in GitLab?
Declare the artifact on the upstream job with artifacts.paths: and the file or directory the job produces. Declare the dependency on the downstream job with needs: [upstream_job] (preferred) or dependencies: [upstream_job] (legacy). The downstream job will download the artifact before its script runs, and the file will be in the working directory.
How long are GitLab artifacts kept?
The retention is set with the expire_in field on the artifact. The default on GitLab.com is 30 days. The default on self-managed GitLab is “forever” (which is a bug in real bills). The developer should always set expire_in explicitly, with a value that matches the artifact’s value. A build output is fine with 30 days. A test report is fine with 30 days. A package that gets uploaded to the registry is fine with a year.
What is the difference between artifacts.paths and artifacts.reports?
artifacts.paths is a regular file or directory that GitLab uploads as-is. The downstream job downloads the file and uses it. artifacts.reports is a file in a known format (JUnit, Cobertura, sast, etc.) that GitLab parses and turns into a merge-request widget, a pipeline view, or a security dashboard. The file is uploaded, but the value is in the widget, not the file.
Why are my GitLab artifacts not showing up in the merge request?
The artifact is probably declared as paths instead of reports. A test result that is declared as paths: [test-results.xml] is uploaded as a regular file, and GitLab does not parse it. The fix is to declare it as reports.junit: [test-results.xml]. The other common cause is artifacts.when: on_success on a failing test — the fix is artifacts.when: always.
Can a job have multiple artifacts?
Yes. A job can declare both a paths artifact and a reports artifact, and the two serve different consumers. The paths artifact carries the build output for the deploy job. The reports artifact carries the test report for the merge-request widget. The pattern is the typical one in real pipelines.
What is the difference between dependencies and needs in GitLab CI?
dependencies is the legacy syntax. A job that declares dependencies: [build] will download the artifacts from the build job and from any other job in the same stage. needs is the modern syntax. A job that declares needs: [build] will download the artifact from the build job and only from the build job. The developer should prefer needs, and should not mix the two in the same pipeline.