---
title: Lifecycle and Types
description: How a thread moves through its lifecycle, what the channel prefixes mean, and where priority fits in — the full state model of a thread in ThinkWork.
---

import { Aside } from '@astrojs/starlight/components';

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](/concepts/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

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](/applications/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

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)

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

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

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

The lifecycle, channel, and priority axes all surface in [Admin: Threads](/applications/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)

- **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](/concepts/threads/routing-and-metadata/).

### What's deployment-scoped (Terraform)

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

## Common patterns

### 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](/concepts/threads/routing-and-metadata/) for the full per-channel keying.

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

- **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

- [Threads (overview)](/concepts/threads/) — why ThinkWork picks one universal container
- [Routing and Metadata](/concepts/threads/routing-and-metadata/) — how inbound events map into the thread model
- [Admin: Threads](/applications/admin/threads/) — the operator view of this lifecycle
- [Integrations](/concepts/integrations) — how each inbound channel is implemented

## 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](/applications/admin/threads/).
