Threa Developers

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.

search memos
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.

memo with sources
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:

list channels
curl "/api/v1/workspaces//streams?type=channel&limit=20" \
  -H "Authorization: Bearer "

Then post, reusing the same ID across retries:

post once
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"
  }'

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.

search messages
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.

heartbeat
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:

agent-loop.ts
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,
    })
  }
}
Faster than polling There's also a Socket.IO /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.