Skip to main content

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.

A given webhook event tells you something happened, but most agents only want to act on a subset of threads. This page covers the two patterns for deciding which threads your agent acts on, and how to check whether a given thread belongs to your agent.

Pattern 1: Listen broadly, filter in code

The simplest setup is to subscribe to one of the event types you care about (e.g. thread.thread_created, thread.email_received) and decide in your handler whether the agent should act.
if (event.payload.eventType === "thread.thread_created") {
  const thread = event.payload.thread;

  // Example filters, adapt to whatever your agent cares about
  if (thread.priority !== 0) return; // only urgent threads
  if (thread.labels.some(l => l.labelType.name === "no-bot")) return;

  await runAgent(thread);
}
This is the right shape when the decision is simple and code-driven (e.g. “only urgent threads”, “only this customer tier”), or when the agent runs on every new thread regardless. This pattern works well for agents that observe and supplement what your team is doing, like classifiers, summarisers, agents that post internal notes, or autoresponders that send a single acknowledgement. It’s not well-suited if you want your agent to actually handle support autonomously, because the thread is never assigned to the agent and your reporting won’t reflect the agent’s involvement. A cleaner pattern, especially as your routing logic grows, is to assign threads to your machine user and have the agent run only on threads it’s been assigned. The “should the agent handle this?” decision lives in Plain, not in your code. This is the right pattern when you want your agent to handle support autonomously. Because the thread is actually assigned to the machine user, all of your existing reporting (volumes, resolution times, response times, and so on) attributes work done by the agent to the machine user in exactly the same way it would for a human teammate. Your webhook listens to events whose payload includes a thread, and checks whether that thread is assigned to your machine user. An example implementation could look like this:
function isAssignedToMe(thread: { assignee?: { id: string } | null }): boolean {
  // You can find your machine user's ID on the 
  // machine user settings page in Plain.
  return thread.assignee?.id === process.env.AGENT_MACHINE_USER_ID;
}

// Webhhok triggered when a thread is assigned
if (event.payload.eventType === "thread.thread_assignment_transitioned") {
  if (!isAssignedToMe(event.payload.thread)) return;
  await runAgent(event.payload.thread);
}

// Webhook triggered when an email is received on a thread
if (event.payload.eventType === "thread.email_received") {
  if (!isAssignedToMe(event.payload.thread)) return;
  await runAgent(event.payload.thread);
}
The thread.thread_assignment_transitioned payload also includes previousThread (the state before the change), so you can inspect who the thread came from if you need to. This pattern has some nice properties:
  • No need to maintain filter logic in code
  • Teammates can hand a thread to the agent by reassigning it, and the agent picks it up automatically.
  • The agent can hand back by reassigning to a teammate (see change the assignee).
  • You can change routing rules without redeploying. Just update the workflow in Plain.

Routing threads with a workflow

Plain’s workflows let you set up rules that act on threads automatically. A typical agent setup is a workflow that:
  1. Triggers when a thread is created.
  2. Optionally checks conditions like channel, labels, customer tier, or support hours.
  3. Has an action that assigns the thread to your machine user.
You configure workflows in SettingsWorkflows in Plain. The assignment action lets you pick a machine user directly, so the thread shows up as assigned to your agent the moment the workflow fires.
Workflows are configured in the Plain UI, not via the API. Once a workflow assigns a thread to a machine user, your agent’s webhook receives a thread.thread_assignment_transitioned event just like any other assignment change.

Assigning programmatically

You can also assign threads to your machine user directly from code, for example from a classifier agent that picks which thread should go to which downstream agent. The mutation is assignThread, and AssignThreadInput accepts a machineUserId instead of a userId:
await plain.mutation.assignThread({
  input: {
    threadId: thread.id,
    machineUserId: process.env.AGENT_MACHINE_USER_ID,
  },
});

Avoiding accidental loops

Whichever pattern you use, if your agent both reacts to and writes to threads, watch out for loops:
  • Make sure your filters reject events your agent itself caused. The assignee filter above handles assignment changes; for message events, check the message’s author isn’t your own machine user before reacting.
  • If the agent reassigns a thread to a human, the resulting thread.thread_assignment_transitioned event will have a different assignee on thread, so the assignee filter naturally drops it.