Recipes
Worked examples.
Each one is a real task. Set your credentials in the panel (top right) and the runnable samples work as you read. The last wires a local agent into your workspace, the way the Pi extension does.
Find out what was decided, and why
Memos are Threa's record of decisions and context pulled from conversations. Search them by topic (semantic by default) and you get back titles, abstracts, tags, and links to the source messages.
curl -X POST /api/v1/workspaces//memos/search \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"query": "why did we pause the auth refactor",
"limit": 5
}'
Take a memo's id from the results and pull its full provenance:
the source stream and the exact messages, with authors and timestamps.
curl /api/v1/workspaces//memos/MEMO_ID \
-H "Authorization: Bearer " Notify a stream from CI
A build or deploy script can post into a channel. Give the message a stable
clientMessageId so a retried pipeline step never double-posts.
First find a stream to post into:
curl "/api/v1/workspaces//streams?type=channel&limit=20" \
-H "Authorization: Bearer " Then post, reusing the same ID across retries:
curl -X POST /api/v1/workspaces//streams/STREAM_ID/messages \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"content": "**Deploy:** v2.4.1 shipped to production. :rocket:",
"clientMessageId": "ci-deploy-2.4.1"
}' Mirror a search into your own tool
Message search takes semantic and exact toggles and
filters by stream, type, author, and date window. Drop it behind a slash
command, an editor extension, or a dashboard.
curl -X POST /api/v1/workspaces//messages/search \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"query": "rotation flow",
"semantic": true,
"type": ["channel", "thread"],
"limit": 10
}' Connect your local agent
Threa runs a bot runtime protocol: your agent registers a
presence, then claims work whenever someone @mentions its bot in a
stream, does the work locally, and posts the reply back. It's pull-based, so
your agent can live on your laptop or a Pi behind a NAT with no inbound ports.
The Pi remote extension implements this in full.
1 · Create a bot and a bot key
In the app, create a bot (give it the mentionable trait so people
can summon it), then mint a threa_bk_ key on it with
bot-runtime:write, bot-invocations:write, and
messages:write. Use that key as the credential below.
2 · Heartbeat a presence
Tell Threa your runtime is alive and accepting work. Repeat every 15–30s. The
instanceId is any stable per-process string; runtimeKind
is one of pi-local, hermes, openclaw,
claude-code-channel, or custom.
curl -X POST /api/v1/workspaces//bot-runtime/presence \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"runtimeKind": "custom",
"instanceId": "my-laptop-1",
"status": "available",
"acceptingInvocations": true,
"capabilities": { "supportsActiveScratchpad": false }
}' 3 · Claim, work, reply
Poll for a pending invocation. When you get one, it carries the prompt and a
claimToken; do your work, then complete it with a reply (or renew
the claim first if you need longer than the TTL). A full loop in TypeScript:
const BASE = ""
const WS = ""
const KEY = "" // a threa_bk_ bot key
const INSTANCE = "my-laptop-1"
const auth = { Authorization: "Bearer " + KEY, "Content-Type": "application/json" }
const api = (path, body) =>
fetch(BASE + "/api/v1/workspaces/" + WS + path, {
method: "POST", headers: auth, body: JSON.stringify(body),
}).then((r) => r.json())
async function loop() {
// Keep presence fresh in the background.
setInterval(() => api("/bot-runtime/presence", {
runtimeKind: "custom", instanceId: INSTANCE,
status: "available", acceptingInvocations: true, capabilities: {},
}), 20000)
while (true) {
const { data: inv } = await api("/bot-invocations/claim", {
runtimeKind: "custom",
instanceId: INSTANCE,
supportedCapabilities: ["mentionable"],
claimTtlSeconds: 120,
})
if (!inv) { await new Promise((r) => setTimeout(r, 3000)); continue }
// inv.promptMarkdown is what the user said. Do real work here.
const reply = await runYourAgent(inv.promptMarkdown)
await api("/bot-invocations/" + inv.id + "/complete", {
instanceId: INSTANCE,
claimToken: inv.claimToken,
finalMessageMarkdown: reply,
})
}
} /bot namespace that pushes
bot_invocation:available events so you can react instantly and
keep a poll only as a backstop. The Pi extension uses the socket with a 30s
poll fallback. Read extensions/pi-remote/ in the repo for a
complete implementation, including session-control commands like
/model, /thinking, and /compact driven
from a scratchpad.
A bot key with streams:read and memos:read can also
read history and search the same memory Ariadne uses while it works, so your
local agent answers with the full workspace context behind the message that
summoned it.