Skip to content

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.

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).

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.

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";

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.