Skip to content

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.

Fan out over multiple services completed — ~/.vectorstep/pipelines/alert-triage.yaml is identify-upstreams → check-upstreams (fan-out) → consolidate.

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: 7

This 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:

Terminal window
cd ~/.vectorstep
docker compose up -d --force-recreate vectorstep

2. 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.

- 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:

Terminal window
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.json

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:

Terminal window
docker compose exec vectorstep \
cat /data/artifacts/<run_id>/consolidate/investigation_notes

That’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:

Terminal window
docker compose exec vectorstep python3 -c "
import sqlite3
conn = 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.

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.artifacts column 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.