Skip to content

Lifecycle and Types

A thread is never just “a row in a table.” It’s a row plus a lifecycle, plus a channel that classifies how the work arrived, plus a priority that’s independent of status. This page walks all three. Together they shape how the harness’s perception layer (Threads) presents work to operators and agents — the lifecycle is what makes a thread findable months after the fact, and the channel + priority axes are what let an operator triage a thousand-thread queue without re-reading every one. Both are concrete implementations of the Traceability operating guarantee: every status transition, every channel choice, every priority bump is a durable signal an agent or auditor can read later.

Every thread sits in one of seven statuses. The state machine is small and deliberate:

StatusMeaningTypical trigger
BACKLOGCaptured, but not scheduled for work yetNew thread from a integration, or a user drafting without sending
TODOReady to be picked upAn assignee has the thread in their queue, or an agent is about to run
IN_PROGRESSCurrently being worked onAn agent turn is running, or a human is actively typing
IN_REVIEWWaiting on human reviewAgent requested approval (see Admin: Inbox); or an operator needs to confirm
BLOCKEDStuck on a dependency or errorTool call failed with a non-retryable error, or waiting on external input
DONECompletedWork is finished and no more turns are expected
CANCELLEDAbandonedOperator cancelled before completion

Status is set by the agent, by the operator, or by the system — not by the user directly. A user sending a new message into a DONE thread implicitly moves it back to IN_PROGRESS.

A thread can move freely between statuses other than DONE and CANCELLED; the terminal statuses are still reopenable if needed, but the default UI treats them as closed.

Status answers “where is this in the workflow?” Priority answers “how important is it?” They’re orthogonal:

PriorityUse
CRITICALPage-me-now work — outages, security incidents
URGENTSame-day, pre-empts other work
HIGHNext up in the queue
MEDIUMDefault
LOWEventually

The admin app lets operators sort, filter, and group by either axis. The Kanban board groups by status; the grouped-table view most often groups by priority or assignee.

On top of the explicit status column, the admin app computes a per-row inbox status that isn’t stored:

  • running — a turn is actively executing right now (drives the subtle shimmer in the thread row)
  • unread — the thread has new turns since the operator last read it (drives the unread indicator)
  • read — otherwise

That indicator is what makes the thread list feel alive without an operator having to babysit it.

Every thread has a channel column set at creation time. Channel determines the prefix, the inbound mapping logic, and the outbound behavior when an agent replies.

ChannelPrefixHow work arrivesHow agents reply
CHATCHAT-User sends message in admin or mobile appAppSync subscription streams response back to the app
AUTOAUTO-EventBridge / AWS Scheduler fires a scheduled job or event triggerResponse recorded on the thread; optional side-effects via tool calls
SLACKSLACK-Slack Events API webhook to the Slack integration LambdaSlack integration posts the response back to the originating channel/thread
GITHUBGITHUB-GitHub webhook (issue, PR, comment)GitHub integration posts a comment via the GitHub API
EMAILEMAIL-SES inbound email rule to the email integrationSES sending domain sends a reply; inReplyTo and References headers are preserved
TASKTASK-External task system webhookTask integration normalizes state changes back to the external system

The prefix is part of the thread id — a thread id is <CHANNEL>-<ULID>, e.g. SLACK-01H8QKPZ4X8M4NXDRM9N8KBJ9P. The prefix isn’t a display-only formatter; it’s on the row, indexed, and used to pick the outbound response handler.

When a user sends a new message into a DONE or CANCELLED thread — say, someone replies to a Slack thread ThinkWork had archived — the system reopens the existing thread rather than starting a new one. The key for that reopen is the channel-specific metadata (Slack’s messageTs, GitHub’s issueNumber, Email’s messageId + inReplyTo), not the thread id directly, because the external system doesn’t know our ids.

That reopen guarantees conversation continuity: an agent that answered a Slack question in March can see its own earlier answer when someone follows up in April.

The lifecycle, channel, and priority axes all surface in Admin: Threads. Most knobs are tenant-scoped and live on a single page; a few cross-tenant defaults are deployment-scoped (Terraform).

  • Status auto-transitions. Off by default. Status writes only on explicit mutations — agent activity, operator action, integration event. To enable agent-driven IN_PROGRESS → DONE on reply, set the per-template “auto-close on assistant turn” toggle (a template-level setting, not a thread setting). Most pilots leave this off and let operators mark DONE deliberately.
  • Default priority on inbound. Defaults to MEDIUM. Per-integration overrides set this when an external system already encodes urgency — e.g., a PagerDuty webhook that should always create a CRITICAL thread. Configure on the integration, not the thread.
  • Inbox status filters. The admin Threads view ships with running / unread / read as default filter chips. Add tenant-specific filters (e.g., “all CRITICAL priority”) via the saved-views API. The computed inbox status is not a column you write to.
  • Channel routing rules. When a integration creates a thread, which agent gets it? Configured under each integration’s Routing settings — by @-mention, by source channel, by default fallback. See Routing and Metadata.
  • The channel set itself. CHAT / AUTO / SLACK / GITHUB / EMAIL / TASK is a Postgres enum. New channels require code changes in the API schema, the integration Lambda, and the outbound reply handler — they’re not a configuration knob.
  • Status set + priority set. Same — Postgres enums, application-coupled. The seven statuses and five priorities are deliberately fixed; expanding them creates UX cliffs in the admin app.
  • Concurrency invariant. “One running turn per thread” is hard-coded as a thread-level lock at the AgentCore invocation boundary; not a knob. Don’t try to relax it — the alternative is races and corrupted thread state.

Reopen a Slack thread on a follow-up (SLACK channel)

Section titled “Reopen a Slack thread on a follow-up (SLACK channel)”

A customer asks a question on Slack, your agent answers, the thread closes. A week later they reply in the same Slack thread. ThinkWork should reopen the existing thread — not start a new one — so the agent sees its earlier answer and continues the conversation.

This is automatic. The Slack integration watches threadTs (Slack’s thread timestamp) on every inbound event:

  1. Inbound event. Slack POSTs the new message to the integration’s webhook with the original threadTs.
  2. Reopen lookup. The integration queries threads for channel = 'SLACK' AND metadata->>'threadTs' = $1.
  3. Reopen. If found, the thread’s status flips from DONE (or wherever it sat) back to IN_PROGRESS, the new message is appended, and the agent assigned to the thread fires a turn — which sees the entire prior conversation.
  4. Create. If no match, a new SLACK-<ULID> thread is minted.

For email and GitHub, the same pattern uses inReplyTo chains and (repoFullName, issueNumber) pairs respectively. See Routing and Metadata for the full per-channel keying.

Promote a BLOCKED thread on dependency clear

Section titled “Promote a BLOCKED thread on dependency clear”

When a tool call fails non-retryably (a integration hits an external auth failure, a knowledge-base query times out), the agent flips the thread to BLOCKED and writes the failure context as a turn. The operator inbox surfaces BLOCKED threads in a distinct queue.

To resume: an operator clicks “Resume” in the admin thread detail. The status flips BLOCKED → IN_PROGRESS and a new turn fires against the same thread. Memory and prior turns are visible to the new turn — the agent doesn’t repeat the failed approach unless it has reason to.

For pilots, the BLOCKED → IN_PROGRESS transition is the most common operator-side action. Watch the rate; a high BLOCKED rate is a tuning signal that integration credentials, guardrails, or template capability grants need a refresh.

  • Channel set is closed. Adding a new channel requires code changes in the API schema, the integration Lambda, and the outbound reply handler. The channel column is a Postgres enum, not an open string.
  • No cross-channel merging. A GitHub issue and an Email thread about the same subject stay as two threads. The agent can reference both, but the data model does not link them.
  • Inbox status is not persisted. A server restart loses unread tracking for already-open clients until they reload.

Status, channel, and priority are Postgres enums on the threads table (migrations live in packages/database-pg/drizzle/). The thread id format — <CHANNEL>-<ULID> — is generated when the row is created.

Inbound integration handlers (in packages/api/src/handlers/) look up existing threads by channel-scoped metadata before deciding to create a new thread vs. append to an existing one. For the operator view of lifecycle in the admin UI, see Admin: Threads.