Skip to content

Artifacts

Artifacts let a task produce files — reports, screenshots, build outputs — that humans can view or download from the UI, CLI, or MCP after the job completes.

Write files to /artifacts/ (or the $ARTIFACTS_DIR env var for portability):

name: nightly-report
steps:
- name: build
action:
type: script
runner: local
language: shell
script: |
./tools/report --out "$ARTIFACTS_DIR/report.html"
cp build/dist.zip "$ARTIFACTS_DIR/"

When the step succeeds, the worker uploads every file in /artifacts/ (recursively). Failed or cancelled steps discard their /artifacts/ directory.

  • Regular files (recursive — reports/q1.html keeps the slash in its name).
  • Dotfiles (e.g. .config). Don’t put .env here.
  • Empty files.
  • Symbolic links are skipped + warned.
  • Filenames with .., null bytes, or control characters are rejected + warned.
Default Configurable
Per-file 100 MiB artifact_storage.max_file_bytes
Per-job 1 GiB artifact_storage.max_job_bytes

Exceeding either fails the step loudly.

Runner Supported
Shell (local) Yes
Docker (script:docker) Yes
Docker (type:docker) Yes
Kubernetes (any) No (deferred — same limitation as state /state-out)
Agent / Approval / Task actions n/a

On unsupported runners, $ARTIFACTS_DIR is unset; scripts that try > "$ARTIFACTS_DIR/foo" will fail loudly with “ambiguous redirect”.

UI: a new Artifacts section appears near the top of the Job Detail page. Safe MIME types (PNG, JPEG, GIF, WebP, plain text) open inline in a new tab. Everything else — including HTML and SVG (for XSS hardening) — downloads.

CLI:

Terminal window
stroem-api artifacts list <job-id>
stroem-api artifacts download <job-id> report.html -o ./report.html

Hooks: hook.artifacts is a list of {name, content_type, size_bytes, url, step_name, created_at}. Example Slack notification:

on_success:
- action: slack-notify
input:
text: |
Job done. Outputs:
{% for a in hook.artifacts %}- <{{ a.url }}|{{ a.name }}> ({{ a.size_bytes }} B)\n{% endfor %}

MCP: list_artifacts(job_id) and get_artifact(job_id, name). get_artifact returns text/JSON/YAML/XML as inline text, image/* as image blocks the agent can view, and other binary (PDF, archives, …) as a base64 blob resource — all capped at 1 MiB. Larger artifacts must be fetched via the HTTP artifact URL from list_artifacts rather than embedded in the agent’s context.

Artifacts live as long as the job row does. Once a job is deleted (via the existing retention.job_days sweep), its blobs and rows go with it. There is no separate artifact TTL today.

Artifacts produced by a workflow are served with the stored Content-Type, but anything that could execute in the browser (HTML, SVG, XML) is force-downloaded rather than rendered. X-Content-Type-Options: nosniff blocks browsers from second-guessing.

View permission on the task is sufficient to list and download artifacts — the same permission required to view logs. Don’t put secrets in /artifacts/.