1
Give the agent an identity
Your agent acts as a machine user, created under Settings → Machine Users. It gets a public name and an avatar that customers see, and an audit trail separate from any person’s.Create an API key on the machine user’s page with the permissions your agent needs:
thread:read: read threads and their timelinesthread:reply: send replies withreplyToThreadgeneratedReply:create: add suggested replies for a user to reviewthread:assignandthread:unassign: hand threads to and from peoplecustomer:read: look up customer context before replying
2
Receive and verify events
Your agent finds out about new threads and customer messages through webhooks. Stand up a public HTTPS endpoint that accepts These are the events agents subscribe to most often:For development, or when verification happens upstream in an API gateway,
POST, then add a target under Settings → Webhooks and copy its signing secret.The @team-plain/webhooks package handles signature verification, replay protection, and schema validation, and gives you typed payloads:Subscribing to both
thread.thread_created and thread.email_received gives you two events for the first email in a thread. Check isStartOfThread on the email payload if you only want to react once.parsePlainWebhook skips the signature check and validates only the payload shape. See the webhooks overview for delivery semantics, retries, request signing, and mTLS.3
Decide which threads it acts on
An event tells you something happened, and most agents act on a subset of threads. There are two patterns.Filter in code. Subscribe to an event type and decide in the handler:This suits a small code-driven decision, or an agent that runs on every new thread. It fits agents that observe and supplement what your team does: classifiers, summarizers, agents that post internal notes, or an autoresponder that sends one acknowledgement. It does not suit an agent handling support autonomously, because the thread is never assigned to the agent and your reporting will not reflect its involvement.Assign threads to the machine user, which we recommend. The “should the agent handle this?” decision lives in Plain rather than your code, and your existing reporting attributes the agent’s work to it the way it would for a person, covering volumes, resolution times, and response times.That pattern keeps filter logic out of your code. A user hands a thread over by reassigning it, the agent hands back the same way, and you change routing rules without redeploying. The You can also assign from code, for example from a classifier that picks which downstream agent gets a thread. Whichever pattern you use, reject events your own agent caused. The assignee filter above handles assignment changes; for message events, check that the message author is not your own machine user before reacting. If the agent reassigns a thread to a person, the resulting event carries a different assignee, so the assignee filter drops it.
thread.thread_assignment_transitioned payload also includes previousThread, the state before the change, so you can see who the thread came from.A workflow is the usual way to do the assigning: trigger on thread creation, optionally check channel, labels, customer tier, or support hours, then assign the thread to your machine user. You configure workflows under Settings → Workflows, and the assignment action lets you pick a machine user directly.Workflows are configured in the Plain UI, not through the API. Once a workflow assigns a thread to a machine user, your agent receives a
thread.thread_assignment_transitioned event, the same as any other assignment change.AssignThreadInput accepts a machineUserId in place of a userId:4
Read the thread
Most agents read the thread before deciding what to do. The Most thread fields are scalars on the model. Related objects such as If you do not need the whole thread, read what you need directly:
thread query returns a thread by ID with its metadata and needs thread:read:customer, assignee, and labels are lazy-loaded, so reading one triggers a separate API call. See the GraphQL SDK for how that works.Every timeline entry exposes an llmText field, a plain-text rendering shaped for a language model. Paginate timelineEntries and concatenate it to get the whole thread as prompt-ready text:llmText is null for entry types with nothing meaningful to render. Skip those entries.- The customer:
thread.customer, orplain.query.customer({ customerId }), for subscription tier, external IDs, and anything else on the customer. - Custom thread fields: structured data attached to the thread. See thread fields.
- The triggering message: on events like
thread.email_receivedthe message is already on the payload, so no extra call is needed.
5
Act on the thread
Everything happens through the GraphQL SDK:Reply directly with Always provide both fields. The Add labels to classify threads, flag them for review, or drive workflows, reporting, and routing rules. A classifier that only labels each new thread is among the smallest agents you can build. Labels reference label types you create under Settings → Labels.Remove them with See assignment for the full reference.
replyToThread, which works on threads whose channel is API, CHAT, EMAIL, SLACK, or MS_TEAMS. Plain delivers the message through the right channel, and it appears as a reply from the agent’s machine user. Needs thread:reply.textContent is shown in clients that do not render markdown, and markdownContent is rendered in the Plain UI, the chat widget, and modern email clients. See reply to thread for the full reference.Suggest a reply with addGeneratedReply instead of sending. The suggestion appears in Plain attached to a specific customer message, and a user reviews, edits, and sends or discards it. The customer sees nothing until a person sends. This is a good default while you tune an agent: you get the drafting without committing to autonomous send.timelineEntryId must point at a customer message, which you get from the webhook payload (for example payload.email.timelineEntryId) or by paginating timeline entries. markdown is capped at 5,000 characters, and the call needs generatedReply:create. See suggested replies.Post a note that lives on the timeline and is never delivered to the customer. Notes are for leaving context for the next person on the thread, or recording why the agent did or did not act.removeLabels, passing the IDs of the labels rather than the label types. See labels.Change the assignee to hand off to a person, or unassign and let your workflows route from there. A common handoff is a note explaining the context followed by a reassignment.6
Report agent status
Agent status is what puts threads in the right views in Plain, so set it as part of your integration. It has three states:Only threads with an agent status of
HANDED_OFF appear in your First Response, Next Response, and Investigating queues. That is deliberate: work your agent is handling stays out of view, and only threads needing a person are visible. To see everything your agent touched, go to Plain → AI → Agent Activity.Caveats
A human reply moves the thread toHANDED_OFF automatically. When a person replies to a thread your agent marked HANDLED or IN_PROGRESS, Plain moves it to HANDED_OFF itself, which signals the handoff. This matters because a thread your agent answered, which the customer then replied to, which a person then picked up, appears in your Todo queues from that point on.
Watch for loops in error paths. A typical failure path is createNote with what happened, then updateThreadAgentStatus(HANDED_OFF), then unassignThread, then markThreadAsTodo so the thread returns to the Todo queues for a person to pick up.
Other useful operations
These mutations cover most agents, and the samePlainClient exposes the rest of Plain’s API:
The GraphQL API explorer is the fastest way to see what is available and try it.
Resources
- Machine users: the agent’s identity and API keys
- Searching knowledge: ground replies in your Help Center and indexed documents
- Internal agents: the same idea for a Sidekick discussion with your team
- GraphQL SDK: the typed client your agent calls
- Webhooks: delivery semantics, retries, and security options
- API explorer: browse and test queries interactively

