> ## Documentation Index
> Fetch the complete documentation index at: https://www.plain.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Working on threads

> Listen for thread events, check whether your agent is assigned, and read, reply to, and hand off threads through the API.

<img src="https://mintcdn.com/plain/ROgZ-q7f4CoulDd_/public/images/build-agent-support.png?fit=max&auto=format&n=ROgZ-q7f4CoulDd_&q=85&s=abc1dc6dea69708c31ce692d5aa5c922" alt="Triage agent" width="2400" height="1250" data-path="public/images/build-agent-support.png" />

An agent working on threads subscribes to the events it cares about, decides whether to act, and uses the thread APIs to reply, update the thread, or hand it to a user.

The customer sees the agent's public name and avatar.

## Permissions

Depending on what your agent does, you will need different permissions. Basic permissions most agents need are:

* `thread:read` and `customer:read`
* `thread:reply` to reply to threads
* `thread:edit` to update the thread's status
* `thread:assign` and `thread:unassign` for handoffs
* `note:create` if it leaves [notes](/docs/agents/notes)

## Choose your events

Subscribe your [webhook target](/docs/webhooks) to the events that should wake the agent:

| Event                                                                               | When                                                             |
| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| [`thread.thread_assignment_transitioned`](/docs/webhooks/thread-assignment-transitioned) | The thread was assigned or unassigned                            |
| [`thread.email_received`](/docs/webhooks/thread-email-received)                          | An email was sent by the customer                                |
| [`thread.chat_received`](/docs/webhooks/thread-chat-received)                            | A chat message was sent by the customer                          |
| [`thread.slack_message_received`](/docs/webhooks/thread-slack-message-received)          | A Slack message was sent by the customer                         |
| [`thread.thread_created`](/docs/webhooks/thread-created)                                 | A new thread, before anyone is assigned                          |
| [`thread.thread_status_transitioned`](/docs/webhooks/thread-status-transitioned)         | The thread moved between todo, snoozed, and done                 |
| [`thread.thread_labels_changed`](/docs/webhooks/thread-labels-changed)                   | Labels were changed                                              |
| [`thread.thread_priority_changed`](/docs/webhooks/thread-priority-changed)               | Priority was changed                                             |
| [`thread.note_mention_created`](/docs/webhooks/thread-note-mention-created)              | A user mentioned the agent in a note. See [notes](/docs/agents/notes) |

Two things to handle in every listener:

* Your own actions come back as events. Skip messages whose author is your machine user.
* The first email on a thread fires both `thread.thread_created` and `thread.email_received`. Check `isStartOfThread` on the email if you want to handle it once.

## Assignment

Most agents act only on threads assigned to them. That keeps the decision of which threads the agent handles in Plain, where the team can see and change it. Assign in the UI, or with a [workflow](/docs/product/workflows) based on the thread's channel, labels, tier, or support hours.

You can also assign threads programmatically:

```ts theme={null}
await plain.mutation.assignThread({
  input: {
    threadId: thread.id,
    machineUserId: process.env.AGENT_MACHINE_USER_ID,
  },
});
```

Assignment arrives as `thread.thread_assignment_transitioned`, with `previousThread` alongside `thread`. Every thread event also carries `thread.assignee`, so check it on message events too:

```ts theme={null}
function isAssignedToMe(thread: { assignee?: { id: string } | null }): boolean {
  return thread.assignee?.id === process.env.AGENT_MACHINE_USER_ID; // Find your machine user id on its settings page
}

switch (event.payload.eventType) {
  case "thread.thread_assignment_transitioned":
  case "thread.email_received":
    if (!isAssignedToMe(event.payload.thread)) return;
    await runAgent(event.payload.thread);
    break;
}
```

To hand off a thread, unassign the machine user or assign a different user.

For an agent that never replies, such as a classifier or a note-writer, you can skip assignment and filter in the handler instead. For example, act on `thread.thread_created` only when `thread.tier?.name` is `Premium`.

## Reading the thread

```ts theme={null}
const thread = await plain.query.thread({
  threadId: "th_01H8H46YPB2S4MAJM382FG9423",
});
```

As with all of our GraphQL queries, you can selectively expand your query to include details you need such as `customer`, `assignee`, and `labels`. See the [GraphQL SDK](/docs/graphql/sdk).

Every timeline entry has `llmText`, Plain's rendering of that entry for a language model. Concatenate them for a prompt-ready thread:

```ts theme={null}
async function getThreadAsLlmText(threadId: string): Promise<string> {
  const thread = await plain.query.thread({ threadId });
  const parts: string[] = [];

  let page = await thread.timelineEntries({ first: 50 });
  while (true) {
    for (const entry of page.nodes) {
      if (entry.llmText) parts.push(entry.llmText);
    }

    const next = await page.fetchNext();
    if (!next) break;
    page = next;
  }

  return parts.join("\n\n");
}
```

The `llmText` field is `null` for entries with no user or customer messages and can be skipped. You can also read `thread.customer`, [thread fields](/docs/graphql/threads/thread-fields), and the message directly from the webhook payload.

## Replying

The `replyToThread` mutation automatically replies on the right channel (Chat, Email, Slack, MS Teams, etc.) based on the messages in the thread.

```ts theme={null}
const result = await plain.mutation.replyToThread({
  input: {
    threadId: thread.id,
    textContent: "Thanks for reaching out, let me look into this.",
    markdownContent: "Thanks for reaching out, let me look into this.",
  },
});

if (result.error) {
  console.error(result.error.message);
}
```

`markdownContent` is rendered in Plain, chat, and modern email. See [reply to thread](/docs/graphql/messaging/reply-to-thread). `textContent` is the fallback for channels that don't support markdown and multi-part emails.

If you'd rather suggest a message for a user, you can use the [suggest a reply mutation](/docs/agents/suggested-replies) instead.

## Updating the agent status

A thread's agent status tells the team what the agent is doing with a thread, and tells Plain which threads to count in response time metrics.

| Status        | When                                                                                                                                                                    |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IN_PROGRESS` | The agent is working the thread.                                                                                                                                        |
| `HANDLED`     | The agent resolved it. Also [mark it as done](/docs/graphql/threads/status-changes#mark-thread-as-done).                                                                     |
| `HANDED_OFF`  | A user is needed. Also [unassign](/docs/graphql/threads/assignment#unassigning-threads), and optionally [mark as todo](/docs/graphql/threads/status-changes#mark-thread-as-todo). |

```ts theme={null}
await plain.mutation.updateThreadAgentStatus({
  input: {
    threadId: thread.id,
    agentStatus: "IN_PROGRESS",
  },
});
```

If a user replies on a thread marked `HANDLED` or `IN_PROGRESS`, Plain sets `HANDED_OFF` itself. First Response, Next Response, and Investigating metrics only count `HANDED_OFF` threads.

## Hand off

When the agent can't help, hand the thread to a person in this order:

1. Leave a [note](/docs/agents/notes) saying what it tried and why it stopped.
2. Set agent status to `HANDED_OFF`.
3. Unassign the thread, or assign it to a user with `assignThread` and a `userId`.
4. Mark the thread as todo so it shows up in the queue.

```ts theme={null}
await plain.mutation.unassignThread({
  input: { threadId: thread.id },
});

await plain.mutation.markThreadAsTodo({
  input: { threadId: thread.id },
});
```

## Other mutations

| Action                    | Mutation                                                                                                     | Permission                                 |
| ------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------ |
| Done or todo              | [`markThreadAsDone`](/docs/graphql/threads/status-changes), [`markThreadAsTodo`](/docs/graphql/threads/status-changes) | `thread:edit`                              |
| Labels                    | [`addLabels`](/docs/graphql/labels/add), `removeLabels` (takes label IDs, not label type IDs)                     | `label:create`, `label:delete`             |
| Thread field              | [`upsertThreadField`](/docs/graphql/threads/thread-fields)                                                        | `threadField:create`, `threadField:update` |
| New outbound email        | [`sendNewEmail`](/docs/graphql/messaging/send-email)                                                              | `email:create`                             |
| Reply to a specific email | [`replyToEmail`](/docs/graphql/messaging/reply-email)                                                             | `email:create`                             |
| Customer event            | [`createCustomerEvent`](/docs/graphql/events/create-customer-event)                                               | `customerEvent:create`                     |
| Thread event              | [`createThreadEvent`](/docs/graphql/events/create-thread-event)                                                   | `threadEvent:create`                       |

Try any of them in the [API explorer](https://app.plain.com/developer/api-explorer/).
