kv-storage
kv-storage provides a namespaced, auto-scoped Redis-like key-value store within the swarm SQLite database for maintaining state across tasks and sessions. Use it to track counters, cursors, and page state within Slack threads, PRs, Linear issues, or agent contexts, but avoid it for secrets (use swarm_config), learned knowledge (use memory), or files (use agent-fs).
git clone --depth 1 https://github.com/desplega-ai/agent-swarm /tmp/kv-storage && cp -r /tmp/kv-storage/plugin/skills/kv-storage ~/.claude/skills/kv-storageSKILL.md
# 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:
1. `X-Page-Id` (only the page-proxy sets this) → `task:page:<id>`
2. `X-Source-Task-Id` → that task's `contextKey` (e.g. `task:slack:C123:1776...`)
3. `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
```bash
# Header-resolved namespace (recommended for in-session calls)
curl -H "Authorization: Bearer $API_KEY" \
-H "X-Agent-ID: $AGENT_ID" \
"$MCP_BASE_URL/api/kv/last-cursor"
# Explicit namespace
curl -H "Authorization: Bearer $API_KEY" \
"$MCP_BASE_URL/api/kv/_/task:trackers:linear:DES-42/last-comment-id"
# PUT a JSON value with a 10-minute TTL
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"
# List with a prefix
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:
```js
// Inside a page's <script> tag
const count = await swarmSdk.kv.incr('clicks'); // → number-valued entry
await swarmSdk.kv.set('lastSeen', Date.now()); // → 'json' by default
const entry = await swarmSdk.kv.get('clicks'); // → { value, valueType, ... } or null
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 existinCode search agent for exploring any codebase. Use for finding code by intent, locating implementations, understanding how something works, or discovering related code. Prefer over Grep/Glob/Read for any semantic or exploratory question.
Guide for running local E2E tests with API server, Docker lead/worker containers, task creation, log verification, UI dashboard, and cleanup
Close a GitHub or GitLab issue with a summary comment
Create a pull request (GitHub) or merge request (GitLab) from the current branch
Implement a GitHub issue or GitLab issue and create a PR/MR
Investigate and triage a Sentry error issue
Respond to a GitHub issue/PR or GitLab issue/MR
Review a task that has been offered to you and decide whether to accept or reject it