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.
The status lifecycle
Section titled “The status lifecycle”Every thread sits in one of seven statuses. The state machine is small and deliberate:
| Status | Meaning | Typical trigger |
|---|---|---|
BACKLOG | Captured, but not scheduled for work yet | New thread from a integration, or a user drafting without sending |
TODO | Ready to be picked up | An assignee has the thread in their queue, or an agent is about to run |
IN_PROGRESS | Currently being worked on | An agent turn is running, or a human is actively typing |
IN_REVIEW | Waiting on human review | Agent requested approval (see Admin: Inbox); or an operator needs to confirm |
BLOCKED | Stuck on a dependency or error | Tool call failed with a non-retryable error, or waiting on external input |
DONE | Completed | Work is finished and no more turns are expected |
CANCELLED | Abandoned | Operator 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.
Priority is separate
Section titled “Priority is separate”Status answers “where is this in the workflow?” Priority answers “how important is it?” They’re orthogonal:
| Priority | Use |
|---|---|
CRITICAL | Page-me-now work — outages, security incidents |
URGENT | Same-day, pre-empts other work |
HIGH | Next up in the queue |
MEDIUM | Default |
LOW | Eventually |
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.
Inbox status (computed)
Section titled “Inbox status (computed)”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.
The channel model
Section titled “The channel model”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.
| Channel | Prefix | How work arrives | How agents reply |
|---|---|---|---|
CHAT | CHAT- | User sends message in admin or mobile app | AppSync subscription streams response back to the app |
AUTO | AUTO- | EventBridge / AWS Scheduler fires a scheduled job or event trigger | Response recorded on the thread; optional side-effects via tool calls |
SLACK | SLACK- | Slack Events API webhook to the Slack integration Lambda | Slack integration posts the response back to the originating channel/thread |
GITHUB | GITHUB- | GitHub webhook (issue, PR, comment) | GitHub integration posts a comment via the GitHub API |
EMAIL | EMAIL- | SES inbound email rule to the email integration | SES sending domain sends a reply; inReplyTo and References headers are preserved |
TASK | TASK- | External task system webhook | Task 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.
Reopen, not recreate
Section titled “Reopen, not recreate”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.
How to configure it
Section titled “How to configure it”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).
What’s tenant-scoped (admin web)
Section titled “What’s tenant-scoped (admin web)”- Status auto-transitions. Off by default. Status writes only on explicit mutations — agent activity, operator action, integration event. To enable agent-driven
IN_PROGRESS → DONEon 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 markDONEdeliberately. - 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 aCRITICALthread. Configure on the integration, not the thread. - Inbox status filters. The admin Threads view ships with
running / unread / readas default filter chips. Add tenant-specific filters (e.g., “allCRITICALpriority”) via the saved-views API. The computedinbox statusis 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.
What’s deployment-scoped (Terraform)
Section titled “What’s deployment-scoped (Terraform)”- The channel set itself.
CHAT / AUTO / SLACK / GITHUB / EMAIL / TASKis 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.
Common patterns
Section titled “Common patterns”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:
- Inbound event. Slack POSTs the new message to the integration’s webhook with the original
threadTs. - Reopen lookup. The integration queries
threadsforchannel = 'SLACK' AND metadata->>'threadTs' = $1. - Reopen. If found, the thread’s status flips from
DONE(or wherever it sat) back toIN_PROGRESS, the new message is appended, and the agent assigned to the thread fires a turn — which sees the entire prior conversation. - 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.
Known limits
Section titled “Known limits”- 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.
Related pages
Section titled “Related pages”- Threads (overview) — why ThinkWork picks one universal container
- Routing and Metadata — how inbound events map into the thread model
- Admin: Threads — the operator view of this lifecycle
- Integrations — how each inbound channel is implemented
Under the hood
Section titled “Under the hood”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.