---
title: How Threads & Agents Work
description: The mental model behind ThinkWork threads — what lives where, and a few rules the SDK assumes you'll follow.
---

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

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

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:

```tsx
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

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:

```tsx
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

When a user opens a thread, you mark it read:

```tsx
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

One small gotcha: roles come back as `"USER"` / `"ASSISTANT"`, not `"user"` / `"assistant"`. If your chat UI uses lowercase, normalize at the boundary:

```tsx
const role = m.role === "USER" ? "user" : "assistant";
```

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