3.6 Artifacts
Included from Basic tier
After this you can: store an artifact against a node, let a step declare one, list what a node holds, fetch by identity or by exact digest, and read retention.
Before you start — tick each one:
- Lesson 3.1 — Your first change (
opschain learn 3.1)
In the GUI: the Artifacts tab on a node page, and the estate-wide Artifacts entry in the sidebar.
Runs produce more than log lines: a build tarball, a terraform plan, a generated report. An artifact is a file stored against an estate node — versioned, digest-recorded, and fetchable by later runs. This lesson stores one, lists it, fetches it both ways, and ends with the two hand-off patterns the module's lab (lesson 5.7) builds on.
1. The identity model
An artifact's identity is (node, code, labels):
- the node it is stored against (any estate path);
- a code naming what it is (
web_release,tf_plan— lowercase letters, numbers and underscores); - optional labels qualifying it (
nightly,v2).
Storing the same identity again creates a new version — nothing is overwritten, and every stored artifact keeps its own id and content digest. Fetching by identity resolves the newest match; fetching by id returns exactly one artifact, always.
2. Store one
opschain artifacts put --node acme/environments/dev --code web_release \
-f dist/web-2.4.1.tar.gz --label nightly
stored artifact web_release (01a13014-…-example, 4302918 bytes) against /projects/acme/environments/dev
The upload records who stored what, where, when, and the content's digest. Two notes:
putalways targets a server — on the local lane, artifacts are written by runs themselves.- A run's own steps can store artifacts through the same API, attributed
to the storing step. Either way, every store emits an
api:artifacts:createevent — the hook lesson 5.7 builds on.
3. Or let the step declare it
A run's products do not have to be uploaded by hand: a run: step in an
action file may declare its artifacts, and the engine collects them
from the run workspace at the step's completion — before any workspace
cleanup, cancelled and failed runs included — and stores them past the
workspace's deletion:
steps:
- name: build
run: make report
artifacts:
- name: integration_report # the artifact code it is stored under
path: integration-report/** # glob, relative to the run workspace
required: true # default false
The matched files are packed as one deterministic tar and stored through
the same artifact store as put — the same api:artifacts:create
event, the same retention — recording the run, the step, the file count,
size and digest. required: true fails the step as an ordinary error
when collection fails (no matching files, over budget, a storage
failure), so on_error: handling applies; the default required: false
leaves a warn:artifacts:collection event and the run continues. Two
budgets bound collection (artifacts.max_artifact_mb, default 512, and
artifacts.max_run_mb, default 1024), refusing an over-budget set with
the budget named. Declared artifacts complement step outputs: outputs
are control-plane metadata under a hard 64 KiB budget; artifacts are the
transport for anything larger.
Read a run's artifacts back with the runs-scoped verbs — on the local lane and against a server alike:
opschain runs artifacts <run-id>
NAME FILES SIZE TYPE CREATED BY
integration_report 12 81920 application/x-tar 2026-10-12T10:14:03Z alice
TYPE is the stored media type: a collected artifact is a tar archive of
the declared files (application/x-tar), while an artifact an action
stored through the artifact DSL carries whatever type it was stored with.
opschain runs artifacts <run-id> --download integration_report --out ./report
wrote artifact integration_report (01a13015-…-example, 81920 bytes) to ./report
A collected artifact downloads unpacked with its workspace-relative
paths (local runs additionally keep unpacked copies under
<opschain home>/artifacts/<run-id>/<name>/). One warning before you
declare: collected files bypass the log masker — they are copied
byte-for-byte from the workspace — so a declared path must never
select files carrying rendered secret values (a materialised properties
file, a vault export). opschain docs artifacts is the full reference.
4. List what a node holds
Against a server, --node is required — the listing is that node's own
stored artifacts, newest first:
opschain artifacts list --node acme/environments/dev
ID CODE NODE LABELS SIZE CREATED BY
01a13014-…-example web_release /projects/acme/environments/dev nightly 4302918 2026-10-12T10:14:03Z alice
01a12fe2-…-example web_release /projects/acme/environments/dev nightly 4297102 2026-10-11T22:08:47Z alice
Both rows share a code — two versions of the same identity. An artifact
whose bytes have been retired shows (purged) beside its code
(section 7). The GUI's estate-wide Artifacts view lists across every
node at once over the v2 API (ArtifactService.ListArtifacts,
opschain docs api-v2); the CLI listing stays per node.
5. Fetch one
Two addressing modes, matching the identity model.
By id — exact:
opschain artifacts get --id 01a13014-…-example --out release.tar.gz
wrote artifact 01a13014-…-example (4302918 bytes) to release.tar.gz
This fetches exactly that artifact, verified against its recorded
digest — deterministic no matter what has been published since. Ids come
from the artifacts list ID column, the summary line put prints, or
the api:artifacts:create event. Without --out the content streams to
stdout (the summary line goes to stderr, so piping stays clean). No match
is a named not-found error, exit 1.
By identity — latest match:
opschain artifacts get web_release --node acme/environments/dev --label nightly --out release.tar.gz
wrote artifact web_release (01a13014-…-example, 4302918 bytes) to release.tar.gz
This resolves the newest artifact matching (node, code, labels) — "the current nightly build", versions stacking under one code. The server performs the resolution, the same rule the local store applies, so local and remote fetches agree. No match is a named not-found error, exit 1; when every matching version has been tombstoned by retention (section 7), the error names the purge rather than silently reporting nothing ever existed.
6. Passing artifacts between runs
Two hand-off patterns, one per resolution mode:
- Exact hand-off — by id. The producer's stored artifact has an id;
hand that id to the consuming run (a property override carrying it, or
a trigger splicing it from the
api:artifacts:createevent) and the consumer fetches with--id. The consumer provably processes the bytes the producer made:
opschain runs create change acme/environments/test/assets/web --action deploy \
--override release_artifact_id=01a13014-…-example
with the consuming action resolving
${{ properties.release_artifact_id }} and running
opschain artifacts get --id … --out release.tar.gz. Lesson 5.7 wires
this pattern end to end, with a trigger doing the hand-off for you.
- Loose coupling — latest-match. The producer publishes
web_releaseon a shared node; the consumer fetches the latest match (artifacts get web_release --node …). Simple, and right when "the current one" is what you mean — at the price that a re-publish between produce and consume changes what the consumer gets. Prefer the exact id hand-off when the consumer must provably process the producer's bytes.
7. Retention
Artifacts age out under the server's retention policy (the retention:
config block's artifacts_days — opschain docs retention covers the
whole policy). Retired artifacts are tombstoned, not erased from
history: the row survives, listings mark it (purged), and a download
answers 410 Gone — your audit trail never develops silent holes. On
servers with archival configured, the bytes are exported to the archive
directory before deletion.
Check yourself
- Store
web_releasetwice and fetch it both ways: which fetch returns the first upload's bytes after the second upload, and what would a listing show once retention retires the older one?
What you learned
- An artifact is (node, code, labels) + content digest; re-storing an identity makes a new version, and every version keeps its own id.
artifacts put --node … --code … -f …stores (server target; runs store through the same API),artifacts list --node …reads a node's inventory,artifacts get --id …fetches one exact artifact, digest-verified, andartifacts get <code> --node … [--label …]fetches the newest artifact matching that identity.- A
run:step'sartifacts:declarations are collected from the run workspace at step completion and stored past its deletion —required:fails the step on a missed collection, budgets refuse over-size sets by name,opschain runs artifacts <run-id> [--download <name>]reads them back, and collected files bypass the log masker, so never declare a path that renders secrets. - Pass data between runs by handing the id over — exact ids are immune to re-publish races; the loose latest-match hand-off trades that immunity for "the current one".
- Retention tombstones old artifacts —
(purged)in listings,410 Goneon download — rather than silently deleting them.
Next: this closes the module Run work on a server — back to your track (opschain learn).