Tutorial: give your agent a skill
Build your first agent gave
first-responder two MCP tools and a narrow job. This tutorial gives it a
third kind of capability — one that costs nothing in every other call: a
skill it loads only when the conversation actually
needs it.
What you’ll build
Section titled “What you’ll build”Right now, first-responder gathers evidence and hands off a brief, but its
soul.md says nothing about how to phrase a public-facing update — so it
improvises, inconsistently, every time. You’ll write a
status-update-style skill capturing this org’s house style (lead with
impact, never speculate on root cause, never promise a resolution time,
never leak internal service names) and grant it to first-responder. No new
MCP server, no Gateway restart — skills reload the same lightweight way
agent edits already do.
Prerequisites
Section titled “Prerequisites”- Build your first agent
completed —
first-responderrunning,alert-triage.yamlpointed at it.
1. Write the skill
Section titled “1. Write the skill”Skills live under ~/.vectorstep/skills/ on the host (/data/skills inside
the container) — the same shape as agents/. Create the directory and its
two files:
mkdir -p ~/.vectorstep/skills/status-update-style/reference~/.vectorstep/skills/status-update-style/SKILL.md:
---name: status-update-styledescription: > Use when drafting a public-facing status update for an ongoing incident — covers this org's house style for what to say, what to never say, and how to phrase uncertainty. Not for internal engineering notes or Slack updates to the incident channel, which have no such restrictions.---
# Status Update Style
Public status updates are read by customers and leadership who have nocontext beyond what you write. Getting the tone wrong erodes trust fasterthan the incident itself does.
## Structure, every time
1. **Lead with impact**, in plain language — what a user actually experiences, not which internal service is down.2. **State what you're doing about it**, in one sentence.3. **Give a next-update commitment** — a time, not a promise of resolution.
## Never do this
- **Never speculate about root cause publicly.** "We believe this may be related to a recent deployment" is a sentence you cannot take back if you're wrong. Say "we are investigating" until you actually know.- **Never promise a resolution time you don't have.** Commit to your *next update* time instead, not resolution.- **Never use internal service names** a customer has no way to recognise — describe the user-facing capability instead.
See `reference/good-examples.md` for worked examples of this structure,including a before/after showing the "never do this" rules being broken andfixed.~/.vectorstep/skills/status-update-style/reference/good-examples.md:
# Worked examples
## Example 1 — payments outage
**Bad (breaks two rules):**> The payments-api service is experiencing elevated error rates, likely due> to the deploy at 14:02 UTC. We expect resolution within 30 minutes.
**Good:**> Some customers are currently unable to complete payments. We are actively> investigating and will post another update by 15:00 UTC.
## Example 2 — partial degradation
**Bad (internal name, speculates):**> The ingest-worker pool is degraded, possibly due to upstream provider> issues, causing delayed order processing for some regions.
**Good:**> Some customers may see delays receiving new orders. We are investigating> and will share an update within the hour.This is the shape Writing good skills argues for throughout: the body carries the routine (structure + the three “never” rules), the bundled file carries reference material (worked examples) the body points at rather than repeats — worth a read once this tutorial is done, for the reasoning behind that split.
2. Grant it to first-responder
Section titled “2. Grant it to first-responder”Edit ~/.vectorstep/agents/first-responder/agent.yaml, adding skills:
alongside the existing tools::
name: first-respondermodel: anthropic/claude-sonnet-4-6max_tokens: 4096tools: - fetch - filesystemskills: - status-update-styleAlso tell soul.md to actually use it — an agent with a skill granted but
no instruction to reach for it will often ignore it entirely. Add a line
under “What you do” in ~/.vectorstep/agents/first-responder/soul.md:
3. Draft a public-facing status update, using your available skills for this org's house style, before summarising and handing off.3. Reload — no restart needed
Section titled “3. Reload — no restart needed”This is the payoff of skills reloading the same lightweight way agent edits
already do: unlike step 2 of the previous tutorial (which needed a full
docker compose restart because it added new mcp_servers entries), a new
skill and an edited agent.yaml both pick up on an ordinary reload.
POST /reload needs the admin scope — a step up from the invoke-scoped
$VECTORSTEP_GATEWAY_TOKEN already in the gateway container’s environment
(that one’s fine for checking loaded tools,
not for a write-adjacent route like this). The admin token was printed once
when you first ran the installer, for whoever authors agents and skills —
if you saved it, use that. If not, recover it the same way install.sh
itself does, straight from the identity file:
ADMIN_TOKEN=$(docker compose exec gateway python3 -c \ "import json; print(json.load(open('/data/identity/device-auth.json'))['tokens']['admin']['token'])")docker compose exec gateway sh -c "curl -s -X POST localhost:18780/reload -H 'Authorization: Bearer $ADMIN_TOKEN'"Check the response names your skill:
{"agents": [...], "skills": [{"name": "status-update-style"}]}If it’s missing, double check SKILL.md’s name: field matches the
directory name exactly (status-update-style on both sides) — same rule,
same failure mode, as an agent’s name: not matching its directory.
4. Ask for it in the pipeline prompt
Section titled “4. Ask for it in the pipeline prompt”first-responder’s soul.md change from step 2 is enough on its own, but
being explicit in the prompt too makes the trace easier to read for this
first run. In ~/.vectorstep/pipelines/alert-triage.yaml, extend the
prompt_template from the previous tutorial with one more instruction and
one more field in the requested JSON:
prompt_template: | A {{severity}} alert fired for {{labels.service}} in {{labels.environment}}. Summary: {{summary}}
1. Use the fetch tool to check https://www.githubstatus.com/api/v2/summary.json for any active GitHub incident. 2. Use the filesystem tool to read known-issues.md and check whether an entry already matches this alert. 3. Draft a public-facing status update using your available skills.
Return ONLY this JSON, no other text: { "confidence": 0.0, "summary": "One sentence: what's happening and what you found", "next_step_context": "", "upstream_incident": true, "known_issue": true, "status_update": "", "reasoning": { "supports": "Evidence that makes this alert credible", "contradicts": "Evidence that suggests noise or a known cause", "assumptions": "What you're assuming in the absence of data" } }status_update is a free-form extra field, same as upstream_incident/
known_issue already were — nothing schema-enforced, just a place for the
drafted text to land.
Reload the service side too, same command as the previous tutorial:
curl -X POST http://localhost:8000/reload \ -H "Authorization: Bearer $(grep VECTORSTEP_ADMIN_TOKEN ~/.vectorstep/.env | cut -d= -f2)"5. Trigger it
Section titled “5. Trigger it”Same fixture as before — the point of this tutorial is the skill, not a new trigger:
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.json6. Watch it run
Section titled “6. Watch it run”Open the run in http://localhost:8000/ui/ and look at the triage
step’s trace. Alongside the fetch/filesystem tool calls from the
previous tutorial, you should now see:
TOOL_CALL: skills__load_skill({"name": "status-update-style"})TOOL_RESULT: {"name": "status-update-style", "body": "# Status Update Style\n...That’s Level 1 → Level 2 in action: the agent’s system prompt only ever
carried status-update-style’s one-line description (until this call, it
cost nothing else), and load_skill is what pulled in the full body. It may
or may not follow up with a read_skill_file call for
reference/good-examples.md — see the model note at the top of this page.
What you should see
Section titled “What you should see”This tutorial was run for real end to end — full installer, real containers,
real webhook trigger, real fetch/filesystem tool calls against the real
GitHub status API — not just the skill in isolation. The gateway’s own log
for that run, in order:
Tool 'fetch__fetch' succeededTool 'fetch__fetch' succeededTool 'filesystem__read_text_file' succeededTool 'skills__load_skill' succeededTool 'skills__read_skill_file' succeededEvidence-gathering first (exactly as soul.md steps 1-2 say), then the
skill — load_skill for the full body, read_skill_file for the bundled
worked examples. The completed run’s status_update field, honouring the
house style without being told the specific rules again in the prompt — no
internal service name, no root-cause speculation, a next-update commitment
instead of a resolution promise:
“We are investigating reports of issues with payment processing and are currently investigating the scope of the impact. We will provide another update in 30 minutes.”
That’s the actual test: the style rules live in the skill, not the prompt or
soul.md — grant the skill to a second agent and it gets the same house
style for free, no copy-paste. (Confidence landed at 1.0 on this run, both
tool calls having returned a clear match — the GitHub fetch really did hit
an active incident, and known-issues.md really did match, so the model had
a complete picture. See the model note at the top of this page for the run
where read_skill_file didn’t fire — both are normal.)
See it in VectorStep
Section titled “See it in VectorStep”Three more places show the same load, in VectorStep’s own UI:
- The run’s step detail shows a
skill status-update-style@<hash>badge next to theagent <hash>chip — hover it for how many times it loaded, which bundled files it read, and which scripts ran (none, here). - Skills (
/ui/skills) listsstatus-update-stylewith its description, version, and which agents are granted it (first-responder, after step 2) — click through for its renderedSKILL.md, bundled files, and version history. - Insights → Skills
(
/ui/insights/skills) shows the load counted againststatus-update-style’s load rate, with this run in its recent-runs list.
Where next
Section titled “Where next”- Writing good skills — the principles behind the skill you just wrote: scoping, the description as a router, and what belongs in the body versus a bundled file.
- Skills — the full
SKILL.mdreference, including script execution (run_skill_script) for a skill that needs to run deterministic logic rather than reason its way there. - Turn on the trust knobs — if you haven’t already, the natural next step from the original tutorial this one branched off of.