How Threads & Agents Work
You don’t need to know most of this to use the SDK. But knowing the shape of the world makes the hooks feel less arbitrary.
The pieces
Section titled “The pieces”A tenant is an organization using ThinkWork. Everything lives inside one.
An agent is an AI persona the tenant has configured — “Marco the support lead”, “a research assistant”, whatever. Agents can respond to messages and take actions.
A thread is a conversation. One user talking to one agent about one topic. Threads have statuses (BACKLOG, TODO, IN_PROGRESS, DONE…) so they double as work items when you want them to.
A message belongs to a thread. It’s either from the user (USER) or from an agent (ASSISTANT, sometimes TOOL for agent‑internal reasoning).
Threads need agents to be useful
Section titled “Threads need agents to be useful”A thread without an agent attached is a thread nobody will respond to. The SDK lets you create agentless threads because there are edge cases where that’s what you want, but for chat flows you should always set an agentId at creation time:
await createThread({ tenantId, agentId: selectedAgent.id, // this is the one that matters title: "...", channel: "CHAT",});If you forget agentId, your user will type, nothing will happen, and they’ll wonder if the app is broken.
Start a thread + send its first message in one shot
Section titled “Start a thread + send its first message in one shot”The old way (pre‑0.2) was: create a thread, then send a message to it. Two round trips, and the timing was awkward because the second call needs the first call’s result.
The new way:
await createThread({ tenantId, agentId, title: "...", channel: "CHAT", firstMessage: "What are the last five opportunities?",});One call. The backend creates the thread and the opening message together. The agent starts responding immediately.
Read state lives on the server, but feels instant
Section titled “Read state lives on the server, but feels instant”When a user opens a thread, you mark it read:
await updateThread(threadId, { lastReadAt: new Date().toISOString() });The server remembers. The unread count decrements automatically on the next render because useUnreadThreadCount and useThreads share a cache that knows they both care about the same Thread. You don’t wire any of that; it just works.
If you want the dot to disappear before the round trip finishes (instant feedback), keep a local “just read” set alongside. There’s a recipe for that on the next page.
Message roles are uppercase
Section titled “Message roles are uppercase”One small gotcha: roles come back as "USER" / "ASSISTANT", not "user" / "assistant". If your chat UI uses lowercase, normalize at the boundary:
const role = m.role === "USER" ? "user" : "assistant";Tenancy is everywhere
Section titled “Tenancy is everywhere”Every hook that touches data takes a tenantId. Don’t hard‑code it — read it from useThinkworkAuth().user.tenantId. That value is populated as soon as the user is signed in, even for Google accounts where the tenant info lives in a different place than you’d expect. The SDK takes care of it.