A Plain custom agent built on the Vercel AI SDK, with no framework in between.
The protocol is documented here.
This example was example-aisdk-agent in team-plain/example-internal-agents until it was split into this repository. The package name changed with it, to match the repository.
-
Create a machine user under Settings → Machine users and give it an API key. Turn the "Custom agent" toggle on.
Permissions:
threadDiscussion:read,threadDiscussion:edit,threadDiscussionMessage:create,threadDiscussionMessage:edit,thread:readandthread:reply. -
Copy
.env.exampleto.envat the repository root and fill inPLAIN_API_KEY,PLAIN_WEBHOOK_SECRETfrom Settings → Request Signing, andAI_GATEWAY_API_KEY. -
Get a public https URL that reaches this process. By default it runs on port
8082. Locally,ngrok http 8082. -
Create the webhook under Settings → Webhooks → Add webhook target, pointed at
$PUBLIC_URL/plain/webhookon version2026-09-06, subscribed todiscussion.message_createdanddiscussion.tool_call_approval_resolved.bun run checkprints the exact event list, so create the target after running it rather than guessing.
bun install
bun run help # the commands and what .env is still missing
bun run check # identity, events, model, and whether anything is indexed to search
bun run serve
Then open a thread in Plain, click Ask Sidekick, pick your agent, and ask it to help with customer requests.
list_thread_queue and search_threads find a thread the discussion was not opened on.
Plain does not always attach one, and without these a threadless Sidekick session has nothing to
work with. Every id they return becomes reachable for the rest of the turn.
read_customer_thread paginates timelineEntries and concatenates llmText, which is Plain's
own rendering of a timeline entry for a language model.
search_knowledge calls searchKnowledgeSources, so Plain does the retrieval and you don't have to deal with vectorising your knowledge sources.
reply_to_customer is the call a customer sees, and the only one gated behind human approval. It reports
the call, asks for approval, waits, and on approval calls replyToThread on the parent thread.