Skip to content
Beta. VectorStep is in beta.Let us know if something breaks.

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.

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.

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:

Terminal window
mkdir -p ~/.vectorstep/skills/status-update-style/reference

~/.vectorstep/skills/status-update-style/SKILL.md:

---
name: status-update-style
description: >
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 no
context beyond what you write. Getting the tone wrong erodes trust faster
than 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 and
fixed.

~/.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.

Edit ~/.vectorstep/agents/first-responder/agent.yaml, adding skills: alongside the existing tools::

name: first-responder
model: anthropic/claude-sonnet-4-6
max_tokens: 4096
tools:
- fetch
- filesystem
skills:
- status-update-style

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

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:

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

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:

Terminal window
curl -X POST http://localhost:8000/reload \
-H "Authorization: Bearer $(grep VECTORSTEP_ADMIN_TOKEN ~/.vectorstep/.env | cut -d= -f2)"

Same fixture as before — the point of this tutorial is the skill, not a new trigger:

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

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' succeeded
Tool 'fetch__fetch' succeeded
Tool 'filesystem__read_text_file' succeeded
Tool 'skills__load_skill' succeeded
Tool 'skills__read_skill_file' succeeded

Evidence-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.)

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 the agent <hash> chip — hover it for how many times it loaded, which bundled files it read, and which scripts ran (none, here).
  • Skills (/ui/skills) lists status-update-style with its description, version, and which agents are granted it (first-responder, after step 2) — click through for its rendered SKILL.md, bundled files, and version history.
  • Insights → Skills (/ui/insights/skills) shows the load counted against status-update-style’s load rate, with this run in its recent-runs list.
  • 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.md reference, 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.