Tutorial: store a full investigation as an artifact
Every tutorial so far has kept next_step_context short — a sentence or
two. Real investigation output is often much longer (a full writeup, a
compiled log excerpt), and stuffing that into next_step_context or
summary is exactly what artifacts
exist to avoid: content stored on disk by reference, pulled into a later
prompt by {{artifacts.step_name.key}} only when a step actually needs it.
Prerequisites
Section titled “Prerequisites”Fan out over multiple services
completed — ~/.vectorstep/pipelines/alert-triage.yaml is
identify-upstreams → check-upstreams (fan-out) → consolidate.
1. Artifact storage is already on
Section titled “1. Artifact storage is already on”The installer’s default ~/.vectorstep/config/vectorstep.yaml ships with an
artifacts: block already enabled:
artifacts: dir: /data/artifacts/data/artifacts is a path inside the container’s named Docker volume, not
a host directory — you’ll read it back with docker compose exec in step 4,
not a normal editor. If you want retention (directories older than
retention_days removed daily at 02:00 — a failed run keeps its artifacts
for the same period, useful for debugging), add it to that same block:
artifacts: dir: /data/artifacts retention_days: 7This needs a real restart, not POST /reload. The artifact store is
wired up once when the service process starts; /reload re-reads pipeline
YAML and a handful of other config keys, but not artifacts:. Plain docker compose up -d isn’t reliable for this either — artifacts: lives in a
bind-mounted YAML file, not .env or the compose file itself, so Compose’s
own change detection has nothing to notice and can silently leave the old
process running with the old value (confirmed: Container ... Running, no
error, stale config). Force it:
cd ~/.vectorstepdocker compose up -d --force-recreate vectorstep2. Have consolidate write a full investigation writeup
Section titled “2. Have consolidate write a full investigation writeup”In ~/.vectorstep/pipelines/alert-triage.yaml, extend consolidate’s
prompt_template to also return an artifacts key — everything else in the
file is unchanged from the fan-out tutorial:
- name: consolidate executor: gateway executor_config: agent: generic-pipeline-step session_key: "agent:generic-pipeline-step:{{pipeline_run_id}}:consolidate" confidence_threshold: 0.70 on_low_confidence: escalate prompt_template: | A {{severity}} alert fired for {{labels.service}} in {{labels.environment}}. Two upstream dependencies were checked in parallel:
- {{steps['check-upstreams/0'].summary}} - {{steps['check-upstreams/1'].summary}}
Summarise the overall picture across both upstreams.
Return ONLY this JSON, no other text: { "confidence": 0.0, "summary": "One sentence: overall picture across both upstreams", "next_step_context": "", "reasoning": { "supports": "Evidence that makes this assessment credible", "contradicts": "Anything that complicates the picture", "assumptions": "What you're assuming in the absence of data" }, "artifacts": { "investigation_notes": "A full paragraph or two: what each upstream check found, why it does or doesn't implicate the alert, and what an on-call engineer would need to know beyond the one-sentence summary" } }artifacts is a free-form dict — each key is a name the agent chooses, each
value is the full text content. The runner intercepts it after the step
runs, writes investigation_notes to disk, and replaces the content with an
opaque local://... reference before anything is persisted to the database.
3. Add a step that consumes the artifact
Section titled “3. Add a step that consumes the artifact” - name: write-up executor: gateway executor_config: agent: generic-pipeline-step session_key: "agent:generic-pipeline-step:{{pipeline_run_id}}:write-up" confidence_threshold: 0.70 on_low_confidence: escalate prompt_template: | Turn the following full investigation into a short incident-channel message — plain text, no markdown, suitable for pasting into chat.
{{artifacts.consolidate.investigation_notes}}
Return ONLY this JSON, no other text: { "confidence": 0.0, "summary": "One sentence: what the incident-channel message says", "next_step_context": "", "incident_message": "The actual chat-ready message" }{{artifacts.consolidate.investigation_notes}} is resolved at render time —
the runner loads the real content from disk just for this step, right before
the prompt is sent. consolidate has no hyphen so this reads directly; a
step named e.g. check-upstreams would need
{{artifacts.check_upstreams.key}} — same hyphen-to-underscore rule as
{{steps.x.y}} from Writing good prompts.
The full pipeline is now four steps: identify-upstreams →
check-upstreams (fan-out) → consolidate → write-up. Reload and
re-trigger:
curl -X POST http://localhost:8000/reload \ -H "Authorization: Bearer $(grep VECTORSTEP_ADMIN_TOKEN ~/.vectorstep/.env | cut -d= -f2)"curl -X POST "http://localhost:8000/webhook?source=alertmanager&allow_testing=true" \ -H "Authorization: Bearer $(grep VECTORSTEP_WEBHOOK_TOKEN ~/.vectorstep/.env | cut -d= -f2)" \ -H "Content-Type: application/json" \ -d @webhooks/alertmanager_critical.jsonWhat you should see
Section titled “What you should see”Open the run in http://localhost:8000/ui/. consolidate should show a
new Other fields row: artifacts: {"investigation_notes": "local://<run_id>/consolidate/investigation_notes"} — a reference, not the
full text. write-up should show completed with its own summary, and an
Other fields entry incident_message containing the composed
chat-ready message, built from the full writeup rather than just
consolidate’s one-sentence summary.
Note the <run_id> in that reference — it’s the same run_id from the
webhook’s {"status": "accepted", "run_id": "..."} response, or the one
shown at the top of the run detail page. The artifact and the database both
live inside the container’s data volume, so reach them with docker compose exec rather than a host path:
docker compose exec vectorstep \ cat /data/artifacts/<run_id>/consolidate/investigation_notesThat’s the real, full writeup — multiple sentences, likely a full paragraph.
Compare it against the database, which only ever holds the reference. The
image doesn’t ship the sqlite3 CLI, but Python’s built-in module works
just as well for a one-off query:
docker compose exec vectorstep python3 -c "import sqlite3conn = sqlite3.connect('/data/db/runs.db')print(conn.execute( \"SELECT artifacts FROM pipeline_steps WHERE run_id = ? AND step_name = 'consolidate'\", ('<run_id>',),).fetchone())"That’s the point of artifact storage: the long-form content lives on disk by
reference, not in the database, and is only ever pulled into a later
prompt’s context (here, write-up’s) when a step actually references it.
Where next
Section titled “Where next”Go to Route escalations to a real channel next — the next tutorial in the series.
Once you’re comfortable with the mechanics:
- Artifact storage — the full
reference, including the
pipeline_steps.artifactscolumn shape and cleanup/retention behaviour. ~/.vectorstep/pipelines/research-brief.yaml, seeded by the installer — a complete three-step worked example (gather → synthesise → proofread) that chains three artifacts end to end, rather than this tutorial’s one.