code-runner
Guides

Artifacts & Pullable Output

Persist stdout/stderr and capture files a job wrote to /workspace.

Real-time output is streamed over soketi. But sometimes you also want a pullable copy of a run — for clients that weren't subscribed, for batch jobs, or to grab files the program produced (a rendered chart, a generated CSV). That's what collectOutput and the artifact store are for.

Pullable stdout/stderr

Set collectOutput: true on the execute request:

const job = await client.execute({
  language: 'python',
  collectOutput: true,
  files: [{ name: 'main.py', content: "print('done')\n" }],
});

The worker then accumulates the same bytes it streams to soketi and persists a RunResult to Redis with a TTL (RUN_RESULT_TTL, default 600s). Pull it after the job ends:

const out = await client.getOutput(job.jobId);
// {
//   exitCode: 0, signal: null, timedOut: false, idleTimedOut: false,
//   truncated: false, durationMs: 312,
//   stdout: "done\n", stderr: "",
//   artifacts: [...], artifactsTruncated: false
// }

getOutput returns 404 (a NotFoundError) if the job had no collected output, the id is unknown, or the TTL elapsed. The single 404 is deliberate — a caller can't probe which job ids exist.

File artifacts

A job can also leave files in /workspace. After the process exits — but before the container is torn down — the worker captures top-level files from /workspace, uploads them to S3-compatible storage, and includes presigned URLs in the RunResult:

"artifacts": [
  {
    "name": "plot.png",
    "mimeType": "image/png",
    "bytes": 20481,
    "url": "https://<bucket>/artifacts/<jobId>/plot.png?X-Amz-..."
  }
],
"artifactsTruncated": false

The url is a presigned GET URL — it needs no bearer token, so you can hand it straight to a browser. Input files, the compile-output binary, and internal markers are excluded from capture automatically.

Browser-reachable URLs (split-horizon)

Presigned URLs are SigV4-signed, and the signature binds the host — so the URL must be signed with the same host the client (browser or backend) will fetch from. In most deployments the worker connects to object storage at the same address clients use, and there's nothing to do. But in a split-horizon setup the worker reaches storage at an internal address (e.g. http://minio:9000) while clients must fetch from a different public one. Set ARTIFACT_S3_PUBLIC_ENDPOINT to the client-facing endpoint: the worker still connects and uploads via AWS_ENDPOINT_URL_S3 (override ARTIFACT_S3_ENDPOINT), but signs presigned URLs against the public endpoint. Signing is pure local crypto, so there's no extra network round-trip. Left empty (the default), presigned URLs use the connect endpoint.

# Worker uploads internally, clients fetch from the public host:
AWS_ENDPOINT_URL_S3=http://minio:9000
ARTIFACT_S3_PUBLIC_ENDPOINT=http://127.0.0.1:9000

For local dev, prefer 127.0.0.1 over localhost. Cookies are scoped by host and ignore the port, so artifacts served from localhost inherit the browser's localhost cookies — a large Cookie header makes MinIO reject the request with MetadataTooLarge. 127.0.0.1 is a separate cookie jar. In production, set this to your public S3/CDN domain when it differs from the worker's internal endpoint.

Artifact capture is opt-in infrastructure: it's active only when the worker is configured with object storage (BUCKET_NAME + S3 credentials). With no storage configured, file artifacts are simply disabled — pullable stdout/stderr still work.

Artifact limits

Two per-request limits (with manifest/defaults) bound capture:

FieldDefaultMeaning
maxArtifacts20Max number of files captured.
maxArtifactBytes4 MiBMax size per captured file.

When more files exist than maxArtifacts, artifactsTruncated is true.

Storage & retention

The worker uses an S3-compatible store (MinIO in dev, Tigris/S3/R2 in prod). On boot it ensures the bucket exists and installs a lifecycle expiry rule on the artifacts/ prefix, so objects are cleaned up automatically. Three independent TTLs govern retention:

Env varDefaultControls
RUN_RESULT_TTL600sHow long the RunResult lives in Redis.
PRESIGNED_URL_TTL24hHow long a presigned artifact URL stays valid.
ARTIFACT_S3_OBJECT_TTL3 daysWhen the object is deleted from the bucket.

The object TTL must be the presigned-URL TTL (the worker validates this at startup) — otherwise a URL could outlive the file it points to. Treat artifacts as ephemeral: if you need them durably, copy them to your own storage from the presigned URL before they expire.

See Configuration for the full set of storage variables, and try it end to end with make artifacts-e2e.

On this page