β‘ Add this skill
One command
Agent onboarding
Fetch this skill when a customer already uses Plain and wants its AI support agent to consume Autoplay live user activity.View the skill β
- Live Chat App ID (
liveChatApp_...) β from the Live Chat app you created. - Machine User API key (Settings β Machine users) β only needed if you havenβt created one yet; skip if itβs already running.
mcp_urlandmcp_keyβ printed by your ownonboard_productcall (see Quickstart), not something Plain issues.
π¬ Watch the walkthrough
Prefer to watch first? This Loom walks through the full setup β workspace, Machine User, and a working chat bubble.How it works
A server-side API route bridges the Autoplay SDK and Plainβs GraphQL API. Hereβs the full sequence:- User opens a thread in Plain β Plain fires the
onNewThreadcallback in your widget. - Widget POSTs to your webhook β sends
{ customerId, threadId }. - Server fetches live activity β calls the Autoplay SDK for the userβs last 10 in-app actions, scoped to your Product ID.
- Server resolves the Plain customer β queries Plainβs GraphQL API to get the Plain-internal customer ID from the thread.
- A note appears on the thread β your team sees the last 10 actions before theyβve typed a word.
Prerequisites
- A Plain workspace with a Live Chat app created and the
liveChatApp_...App ID copied - A Plain Machine User with an API key (see below)
- A registered Autoplay product β run
onboard_productfrom the Quickstart if you havenβt yet
π€ Create a Machine User
A Machine User is a service account that authenticates server-side API calls to Plain β this integration reads thread data and writes notes on behalf of this user. Create one in Settings β Machine users, assign at least the Member role (required to read threads and create notes), then generate an API key. See Plainβs Machine User docs for the setup flow.π Set your environment variables
Runningonboard_product from the Quickstart registers your product and prints your Autoplay credentials to the terminal, including mcp_url and mcp_key. Map those into the variables below and add all four to your environment config before running.
This integration doesnβt speak MCP.
mcp_url/mcp_key are just the credential names onboard_product returns β theyβre named after the connectorβs primary MCP interface, but Plain has no MCP client, so this integration calls the plain REST live-activity endpoint directly instead.π Create the API route
Write the handler once as plain JavaScript, with no framework imports β then mount it under whatever server you run. It receives{ customerId, threadId } from the widget, fetches the userβs recent Autoplay actions, and attaches them as a note on the Plain thread.
plain-chat-webhook.js β expand to copy
plain-chat-webhook.js β expand to copy
handlePlainChatWebhook to a POST endpoint on your server β pass it the parsed JSON body and return its result as the JSON response. The widget below expects it at /plain-chat-webhook, but the path is arbitrary as long as it matches what the widget script fetches.
π¬ Add the onNewThread callback
Add this plain <script> snippet to your page β no framework required. Replace YOUR_PLAIN_APP_ID with the liveChatApp_... App ID you copied in the prerequisites. The callbacks.onNewThread handler fires the moment a user opens a new thread β it posts the logged-in userβs id and the thread id to the route you just created. It reads the user id off USER_ID, which your server sets before this script runs (next section).
plain-chat-widget.js β expand to copy
plain-chat-widget.js β expand to copy
/public/plain-chat-widget.js) and load it with a plain <script src="/plain-chat-widget.js" defer></script> tag, right after the snippet that sets USER_ID below.
π Wire up identity β set USER_ID from your server
Whatever server-renders the page must set USER_ID before plain-chat-widget.js loads, using the logged-in userβs Autoplay id. Always JSON-encode the value when inlining it into a <script> tag β it safely escapes special characters and inlines null when no user is logged in, so the widgetβs if (!uid) return guard skips the fetch.
How the pieces fit: your app identifies the user in Autoplay β Autoplay stores activity under that id β your server sets
USER_ID to that same id before the widget script loads β the widget sends it to /plain-chat-webhook β the handler fetches activity for that exact id β the note appears on the thread.β Test the full loop
- Set all four environment variables and restart your server
- Log in to your app as a test user
- Interact with your app for a few minutes β visit pages, click buttons β so Autoplay has recorded actions for this user
- Open the Plain chat widget and send a first message (this creates a new thread)
- Open the thread in your Plain inbox β you should see a note βRecent user activity (last 10 actions)β automatically attached
- Note never appears? Check your server logs β the route logs the action count, resolved Plain customer ID, and note text. If
actions.lengthis0, the user has no Autoplay activity yet β interact with the app first, then open a fresh thread. onNewThreadnot firing? Open the browser console and confirmPlain.init()ran without errors.401 Unauthorized? Re-copyPLAIN_API_KEYfrom Plain β Settings β Machine users.
βNo noteβ = identity mismatch. Confirm the same value in all three:
- the id your activity source identifies the user with,
- the
userIdyour server resolves and sets onUSER_ID, - the
customerIdarriving at/plain-chat-webhook.
Why upsertCustomTimelineEntry no longer works
Older Plain SDK versions (β€ 2.x) and some Plain support documentation reference a mutation called upsertCustomTimelineEntry. This mutation has been permanently removed from Plainβs GraphQL API server-side β it does not appear in the schema returned by any key type (Machine User or workspace admin).
Confirmed via live schema introspection (June 2026):
@team-plain/typescript-sdk to v2.x makes the method reappear in your IDE but the call fails at runtime with the same error β the SDK is just a wrapper around the same GraphQL endpoint.
Plain split upsertCustomTimelineEntry into two replacements in SDK v3.0.0:
Implementation (Events API plan required)
When the Events API is unlocked on your plan, withcreateCustomerEvent for Ari context. Run both in parallel so human agents see the note too:
Timing matters. The event must be written to the thread before Ari is assigned. In the Plain workflow, the order must be: HTTP request step (your route) β Assign to AI agent. If Ari is assigned first, it wonβt see the event.
Machine User permissions required
ForcreateCustomerEvent and createThreadEvent, grant these permissions to the Machine User in Plain β Settings β Machine users:
customerEvent:createthreadEvent:createthread:read(to look up the customer from the thread ID)
thread:read youβll get FORBIDDEN: missing thread:read. Without customerEvent:create / threadEvent:create youβll get FORBIDDEN: missing [permission]. Once permissions are correct but plan is insufficient, you get FORBIDDEN: Events APIs are not available on your current billing plan.
Once notes are appearing on Plain threads automatically, jump into our Discord β weβll confirm the enrichment is pulling activity cleanly and help you tune what gets surfaced to your support team. Once Plain is enriching threads automatically, youβre done with Step 1. Next: Step 2 β Define proactive triggers.