Skip to main content
Version: v5.6.1

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:

  • put always 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:create event — 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:create event) 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_release on 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_release twice 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, and artifacts get <code> --node … [--label …] fetches the newest artifact matching that identity.
  • A run: step's artifacts: 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 Gone on download — rather than silently deleting them.

Next: this closes the module Run work on a server — back to your track (opschain learn).