Artifacts
Store durable task outputs (reports, screenshots, exports, logs) in agent-fs or the shared workspace, and serve interactive web content to a public auth-protected URL via localtunnel. Use when asked to save evidence for a PR/ticket, share a file, "create an artifact for X", "host this for me", "spin up a web server", or "give me a live link". Covers agent-fs writes and verification, binary uploads, naming and attachment conventions, the `agent-swarm artifact` CLI, Hono apps, PM2 daemonization, HTTP Basic auth, and the injected browser SDK.
Template Content
Artifacts
An artifact is any output that should outlive the session — a report, screenshot, recording, log, data export, dashboard. There are two things you can do with one, and this skill covers both:
- Store it — write it to agent-fs or the shared workspace so humans and other agents can find it later. This is the common case.
- Serve it — run a real web server behind a public, auth-protected URL so a human can interact with it (custom routes, form posts, live data).
| You need… | Use |
|---|---|
| A file, report, screenshot, or export someone will read later | Store (agent-fs) |
| A file another agent needs during this session | Store (/workspace/shared/) |
| A static HTML/JSON report shared by URL | The pages skill — cheaper, no process to babysit |
| Custom routes, websockets, file uploads, per-request computation | Serve (below) |
Rule of thumb: if the content is a snapshot, store it or make a page. If it's a running program, serve it.
Part 1 — Storing artifacts
Two stores: agent-fs (structured, searchable, shareable, survives restarts)
and the shared workspace filesystem (/workspace/shared/, fast, same-session).
When to create one
- Your task produces a deliverable humans should review.
- Another agent or future session needs to pick up where you left off.
- You want to attach evidence to a PR, Linear ticket, or Slack message.
- The output is too large for
store-progress.output.
agent-fs (preferred for anything human-shareable)
# Write to your personal drive
agent-fs write thoughts/research/2026-05-28-topic.md --content "..." -m "description"
# Write to the shared drive (humans + other agents can see it)
agent-fs --org <org-id> write \
thoughts/<agent-id>/research/2026-05-28-topic.md \
--content "..." -m "research findings"
Your org and drive context is already configured — you only need --org when
writing somewhere other than your default. Read the id from
agent-fs stat <path> --json rather than hardcoding one you saw in an example.
Always verify the write landed — agent-fs writes can fail silently with an empty payload:
agent-fs stat <path> --json | jq '.size'
# If size < 200 bytes on a non-trivial artifact, the write FAILED — re-do it.
Sharing agent-fs files with humans
The deployment configures the live viewer host through AGENT_FS_LIVE_URL; the
documented fallback is https://live.agent-fs.dev. Resolve it in the shell and
combine it with the orgId and driveId returned by agent-fs stat:
FILE_PATH='thoughts/<agent-id>/research/<file>.md'
AGENT_FS_HOST=${AGENT_FS_LIVE_URL:-https://live.agent-fs.dev}
FILE_STAT=$(agent-fs stat "$FILE_PATH" --json)
ORG_ID=$(printf '%s' "$FILE_STAT" | jq -r '.orgId')
DRIVE_ID=$(printf '%s' "$FILE_STAT" | jq -r '.driveId')
SHARE_URL="${AGENT_FS_HOST%/}/file/~/$ORG_ID/$DRIVE_ID/$FILE_PATH"
printf '%s\n' "$SHARE_URL"
Paste the printed concrete URL into Markdown or a message. Markdown does not expand environment variables.
Shared filesystem
For non-text artifacts or files other agents need during the same session:
/workspace/shared/downloads/<agent-id>/— downloaded files/workspace/shared/misc/<agent-id>/— other shared files
It does not survive container restarts. Anything that must outlive the session goes to agent-fs.
Binary artifacts (PNG, MP4)
agent-fs write --content is text-only and mangles binaries. Upload binaries
with --file (or piped stdin), which uses the binary-safe raw route
(agent-fs CLI >= 0.7.1):
agent-fs write thoughts/<agent-id>/qa/<topic>-screenshots/login.png \
--file /tmp/login.png -m "login page after fix"
agent-fs stat thoughts/<agent-id>/qa/<topic>-screenshots/login.png --json | jq '.size'
Capture with agent-browser screenshot <path> when it is installed, otherwise
Playwright, ffmpeg, or a system screenshot tool. For an image that must render
inside a PR body or a Slack message without a login, use a presigned URL
instead of the viewer link:
agent-fs signed-url thoughts/<agent-id>/qa/<topic>-screenshots/login.png --json # 24h default, --expires-in up to 7d
Naming conventions
Name paths predictably by task, date, and artifact type:
thoughts/<agent-id>/research/YYYY-MM-DD-<topic>.md
thoughts/<agent-id>/plans/YYYY-MM-DD-<topic>.md
thoughts/<agent-id>/qa/<topic>-screenshots/<filename>.png
misc/<agent-id>/<task-id>-<description>.ext
Attaching artifacts
- PR body — embed
after resolving and printing the concrete URL as shown above. - Slack — link the agent-fs URL (team members only, the live viewer needs a login). Use a signed URL (
agent-fs signed-url, up to 7 days) for anyone else. store-progress— use theattachmentsfield withkind: "agent-fs"and the path.- Linear comments — paste the live URL in the comment body.
What NOT to store
- Secrets, API keys, OAuth tokens
- Raw customer data without approval
- Oversized files without approval (check size before uploading)
- Ephemeral progress notes — those go in
store-progress.progress
Trade-offs
agent-fs vs shared filesystem — agent-fs is persistent, versioned, and searchable across sessions. The shared filesystem is faster for same-session handoffs but doesn't survive container restarts. Use agent-fs for anything that needs to outlive the current session or be reviewed by a human.
Part 2 — Serving interactive web content
Serve a directory or a Hono app to a public, auth-protected URL via localtunnel. Use this when a static page won't do: custom routes, form posts, uploads, per-request computation.
The CLI is a subcommand of agent-swarm. Always invoke it as
agent-swarm artifact <subcommand> — there is no top-level artifact
binary.
Quick start
Static content
# Create your content in a persisted directory
mkdir -p /workspace/personal/artifacts/my-report
echo '<h1>My Report</h1>' > /workspace/personal/artifacts/my-report/index.html
# Serve it (auto-assigns a free port, creates tunnel, registers in service registry)
agent-swarm artifact serve /workspace/personal/artifacts/my-report --name my-report
# -> Copy the deployment URL printed by the command; it is authoritative.
Programmatic (custom Hono server)
import { createArtifactServer } from '../artifact-sdk';
import { Hono } from 'hono';
const app = new Hono();
app.get('/', (c) => c.html('<h1>Dashboard</h1>'));
const server = createArtifactServer({ name: 'dashboard', app });
await server.start();
console.log(`Live at: ${server.url}`);
You can also run agent-swarm artifact serve ./server.ts --name dashboard if
server.ts exports a Hono instance as its default export.
See the bundled examples/ directory for complete working examples
(static-report.sh, hono-dashboard.ts, approval-flow.ts,
multi-artifact.ts).
CLI commands
| Command | Description |
|---|---|
agent-swarm artifact serve <path> --name <name> [--port <port>] [--no-auth] [--subdomain <sub>] | Start serving content. <path> is a directory (static) or a .ts/.js file exporting a default Hono app. |
agent-swarm artifact list | List active artifacts (name, agent, port, URL, status) from the service registry. |
agent-swarm artifact stop <name> | Stop an artifact: deletes the matching PM2 process and unregisters it from the service registry. See "Known limitation" below. |
Flags accepted by serve:
--name <name>— defaults to the basename of<path>. Used for the subdomain and PM2 process name.--port <port>— pin to a specific port. Default: auto-assigned ephemeral port.--no-auth— disable HTTP Basic auth on the tunnel (DANGEROUS — anyone with the URL can access).--subdomain <sub>— override the default${agentId}-${name}subdomain.
Auth & URL pattern
Tunnels are protected by HTTP Basic auth by default:
- Username:
hi(hardcoded MVP default insrc/artifact-sdk/tunnel.ts) - Password: the agent's
API_KEY
Never put the API key in the URL. The Basic-auth password is the swarm
API_KEY— it authenticates against the whole swarm API, not just this artifact. Ahttps://user:key@hostURL leaks that credential into shell history, browser history, proxy and tunnel access logs, referrer headers, and anything you paste it into. Treat a credential-bearing URL as a leaked key.
The installed @desplega.ai/localtunnel package currently defaults to
lt.desplega.ai, but deployments may configure another tunnel host. The URL
printed by agent-swarm artifact serve (or exposed as server.url) is
authoritative. Save that plain URL as ARTIFACT_URL; a browser will prompt for
the credentials.
For scripts and curl, pass the credential out-of-band instead of inlining it,
and read it from the environment so it never appears as a literal:
# -u keeps the credential out of the URL; --netrc-file keeps it out of argv too.
curl -u "hi:$API_KEY" "$ARTIFACT_URL"
# Better for anything long-lived or logged — argv is visible to other processes:
ARTIFACT_HOST=${ARTIFACT_URL#*://}
ARTIFACT_HOST=${ARTIFACT_HOST%%/*}
printf 'machine %s login hi password %s\n' \
"$ARTIFACT_HOST" "$API_KEY" > /tmp/artifact-netrc
chmod 600 /tmp/artifact-netrc
curl --netrc-file /tmp/artifact-netrc "$ARTIFACT_URL"
Do not echo the assembled command, and do not paste the credential into a task report, Slack message, PR body, or page — those are all persisted.
Use --no-auth only for genuinely public content. Anyone who learns the
subdomain can read it.
Running it as a daemon
agent-swarm artifact serve blocks on a never-resolving promise to stay alive —
you cannot inline it in a script that needs to do other work. Pick one:
Option A — nohup (quick, throwaway)
mkdir -p /workspace/personal/logs
nohup agent-swarm artifact serve /workspace/personal/artifacts/my-report \
--name my-report \
> /workspace/personal/logs/my-report.out 2>&1 &
echo $! > /workspace/personal/logs/my-report.pid
# Later, kill it manually:
kill "$(cat /workspace/personal/logs/my-report.pid)"
Option B — PM2 (recommended for anything you'll come back to)
PM2 gives you auto-restart on crash, a process name, log management, and —
crucially — lets agent-swarm artifact stop <name> actually kill it.
pm2 start agent-swarm \
--name artifact-my-report \
-- artifact serve /workspace/personal/artifacts/my-report --name my-report
# Stop it cleanly later:
agent-swarm artifact stop my-report
The PM2 process name must be artifact-<name> (matching --name) — that's
exactly what artifact stop looks for.
Known limitation — artifact stop only kills PM2-started processes
agent-swarm artifact stop <name> runs pm2 delete artifact-<name> and then
unregisters the entry from the service registry. If you started the artifact
with nohup (or &, or any non-PM2 launcher), pm2 delete silently fails and
the server keeps running and serving — even though the command prints
Artifact '<name>' stopped. Until that's fixed:
- Use PM2 if you want
artifact stopto do its job. - For
nohup/foreground processes, kill the PID yourself (kill <pid>orpkill -f 'artifact serve.*<name>') and then runagent-swarm artifact stop <name>to clear the registry row.
Multiple artifacts
Each artifact gets its own port (auto-assigned) and subdomain
(<agentId>-<name>). You can run several simultaneously — see
examples/multi-artifact.ts.
Browser SDK
Served HTML gets the same injected browser SDK that pages get. Load it and use the ready-made singleton:
<script src="/@swarm/sdk.js"></script>
<script>
// `window.swarmSdk` is constructed for you — no `new SwarmSDK()` needed.
const tasks = await window.swarmSdk.tasks.list({ status: 'in_progress' });
const agents = await window.swarmSdk.agents.list();
</script>
The SDK is domain-grouped (swarmSdk.tasks, .agents, .events,
.memory, .repos, .schedules, .approvalRequests, .assets), each domain
mapping 1:1 onto the public REST API. The pages skill documents the full
method surface — it's the same object, so don't duplicate it here.
Calls route through the /@swarm/api/* proxy, which injects auth server-side.
Browser code never sees the API key.
Storage
Always store served content in persisted directories — the working dir is wiped between sessions:
/workspace/personal/artifacts/— per-agent, persists across sessions (default)/workspace/shared/artifacts/— shared across the swarm
See also
- The
pagesskill — DB-backed static HTML/JSON, no process to run. Prefer it whenever the content is a snapshot. - The
kv-storageskill — for the small state a served page needs.