| name | kv-storage |
| description | Use the swarm KV store (Redis-like, namespaced) for cross-task / cross-session / per-page state. Auto-scoped to your context (Slack thread / PR / Linear issue / agent / page). Use for counters, cursors, page state. Do NOT use for secrets (`swarm_config`), embedded knowledge (`memory`), or files (`agent-fs`). |
KV Storage
Namespaced key/value store inside the swarm SQLite DB. Auto-scoped to your
calling context — same string used by agent_tasks.contextKey.
Capability gate: the kv-* MCP tools are only available when your
CAPABILITIES includes kv (default-on; check my-agent-info). The REST
endpoints under /api/kv/* are always present on the API server.
When to use KV
| You need… | Use this | Not this |
|---|
| Count something in this Slack thread / PR / Linear issue | KV (auto-scoped) | memory / agent-fs |
| Save a cursor / last-seen state for a recurring schedule | KV | swarm_config |
| Page-internal counter / vote / state across reloads | KV via swarmSdk.kv | memory |
| Cross-task state in the same conversation | KV (auto-scoped to task:slack:...) | parentTaskId only |
| Secrets, API tokens, OAuth creds | swarm_config (encrypted + masked) | NOT KV |
| Cross-session knowledge for this agent ("how do I…") | memory_search / memory-get | NOT KV |
| Files, binaries, long documents | agent-fs | NOT KV |
| Workflow run state | workflow vars (own KV) | NOT KV |
Rule of thumb:
- If a future invocation should find this without knowing the key → memory.
- If a future invocation will know exactly which key to read → KV.
- If it has secrets in it →
swarm_config.
- If it's bytes (image, pdf, large doc) → agent-fs.
Namespacing
Namespace is just a string. It mirrors the contextKey schema
(src/tasks/context-key.ts). When you don't pass one, the server resolves it
from request headers in this order:
X-Page-Id (only the page-proxy sets this) → task:page:<id>
X-Source-Task-Id → that task's contextKey (e.g. task:slack:C123:1776...)
X-Agent-ID → task:agent:<id> (per-agent scratchpad)
So inside a session triggered by a Slack thread, KV is automatically scoped
to that thread — your sibling tasks (re-runs, retries, follow-ups in the same
thread) read the same store with no setup. Same for PRs (task:trackers:github:owner:repo:pr:N),
Linear issues (task:trackers:linear:DES-42), schedules, workflows.
You can override the namespace explicitly when you need to — see "Explicit
override" below.
Quick recipes
MCP — inside any agent session
kv-set key="vote-count" value=0 valueType="integer" # → namespace = task:slack:...
kv-incr key="vote-count" # → 1
kv-incr key="vote-count" by=5 # → 6
kv-get key="vote-count" # → entry with value=6
kv-list prefix="vote-" # → all matching entries
kv-delete key="vote-count" # → done
kv-set defaults to valueType: 'json' and JSON-encodes whatever you pass.
Use 'string' to skip encoding (good for short tokens, URLs) and
'integer' for counters (required by kv-incr).
REST — humans, scripts, external clients
curl -H "Authorization: Bearer $API_KEY" \
-H "X-Agent-ID: $AGENT_ID" \
"$MCP_BASE_URL/api/kv/last-cursor"
curl -H "Authorization: Bearer $API_KEY" \
"$MCP_BASE_URL/api/kv/_/task:trackers:linear:DES-42/last-comment-id"
curl -X PUT -H "Authorization: Bearer $API_KEY" -H "X-Agent-ID: $AGENT_ID" \
-H "Content-Type: application/json" \
-d '{"value":{"n":42},"valueType":"json","expiresInSec":600}' \
"$MCP_BASE_URL/api/kv/snapshot"
curl -H "Authorization: Bearer $API_KEY" -H "X-Agent-ID: $AGENT_ID" \
"$MCP_BASE_URL/api/kv?prefix=daily-&limit=50"
Pages browser SDK — inside an authed page
Page proxy forces the namespace to task:page:<id> — no namespace argument is
exposed. Use it for page-local counters, vote tallies, multi-step form state,
"remember this number from last refresh" UX:
const count = await swarmSdk.kv.incr('clicks');
await swarmSdk.kv.set('lastSeen', Date.now());
const entry = await swarmSdk.kv.get('clicks');
await swarmSdk.kv.del('clicks');
const all = await swarmSdk.kv.list({ prefix: 'click', limit: 50 });
Public pages (authMode: 'public') cannot reach /@swarm/api/* and so cannot
use KV. Promote to authed or password mode if the page needs state.
Explicit override
Pass namespace to read/write somewhere other than your auto-context:
kv-get key="seed" namespace="swarm:experiments" # ad-hoc namespace
kv-set key="note" value="hi" namespace="task:agent:OTHER-AGENT-ID"
# → 403 unless caller is lead
Rules:
- Reads: any authenticated caller can read any namespace.
- Writes to
task:agent:<X> where X ≠ caller agentId: 403 unless lead.
- Writes to
task:page:<X> from anywhere except a page-proxy request: 403.
- Everything else: writable by any authenticated caller.
TTL & expiry
Default = no expiry. Opt in by passing expiresInSec:
kv-set key="lock-token" value="xyz" valueType="string" expiresInSec=60
Expiry is lazy: reads on an expired key return null and delete the row;
kv-list filters expired rows out of the SELECT but doesn't delete them
(keeps cursor pagination stable). No background sweeper — expired rows that
never get touched stay on disk harmlessly.
Body cap
2 MiB per value. Over the cap returns 413. If you want to store something
larger, write it to agent-fs and stash the path in KV.
Gotchas
- Namespaces ARE contextKey strings. The same string that lets the swarm
find sibling tasks for a PR also indexes KV for that PR.
- Reads return
null for missing AND expired keys — you can't tell the
difference from one call. (If you need to know, list the key.)
- INCR collides if the existing row has
valueType 'json' or 'string'
(409 / KvTypeCollisionError). Delete and re-create as 'integer' first,
or use a different key.
- JSON values round-trip through
JSON.parse on read. If you wrote
{a:1}, you'll get back the object — not the raw string. Use
valueType: 'string' if you want byte-exact storage.
- No CAS / SETNX yet. Use
kv-incr for atomic counters; for
"claim a token" patterns, set with a short TTL and re-check.
- Page SDK has no
namespace argument. Pages are always scoped to
task:page:<id>. Don't try to encode another namespace in the key path —
the URL gets rewritten anyway.
See also
src/be/migrations/061_kv_store.sql — schema (kv_entries)
src/http/kv.ts — REST handler + namespace resolution
src/tools/kv/* — MCP tool registrars
src/artifact-sdk/browser-sdk.ts — swarmSdk.kv for pages
plugin/skills/pages/SKILL.md — companion skill for authed pages