# Connect an AI agent Source: https://developers.autoplay.ai/activity/connect-an-agent Wire any AI support agent to pull a user's live in-app activity β€” pick a door, satisfy identity, follow the per-agent recipe. Any AI agent can pull [live user activity](/activity/overview). Wiring one is always the same three moves β€” only the per-agent specifics change. ## 1. πŸ”Œ Connect via MCP (recommended) The recommended way for an agent to read live activity is the **[MCP server](/mcp/server)** β€” one endpoint, the `get_live_user_activity` tool, Bearer auth. Point your agent at it and it can pull a user's recent activity on demand. **Optional fallback β€” REST.** If an agent can't speak MCP but can call an external HTTP endpoint (e.g. Intercom's "data connectors"), the same activity is available at the **[REST endpoint](/activity/overview)** with the same Bearer auth. Reach for this only when MCP isn't an option. ## 2. πŸ”‘ Satisfy identity Make the agent send the **same `user_id`** that activity is stored under β€” your activity source's stable user id (e.g. the PostHog `identify` id, the Amplitude `user_id`). This is the step that most often gets skipped β€” see **[Identity](/activity/identity)**. ## 3. πŸ€– Follow the per-agent recipe Connect Fin to the MCP server + Messenger JWT identity verification. Maven and others follow the same pattern β€” pick a door, bind identity to your source's user id, point the agent at the endpoint. Adding a new agent doesn't change this surface β€” the endpoint, the MCP tool, and the identity rule stay the same. A new agent just needs its own short recipe describing how *it* calls the endpoint and how *it* passes a verified `user_id`. # Identity Source: https://developers.autoplay.ai/activity/identity The one rule that makes live activity work for any AI agent and any activity source: the user_id the agent sends must equal the id activity was stored under. Everything in [Live user activity](/activity/overview) hinges on one rule: > **The `user_id` an agent sends must equal the `user_id` activity was stored under** β€” the stable id your **activity source** identifies the user with. Activity is keyed by that id. If an agent asks for a different value (an email, a support agent contact id, an anonymous session id), it reads the wrong bucket β€” or an empty one β€” and the user looks inactive even though they've been clicking around. **The id depends on your activity source.** Autoplay is source-agnostic β€” the source set grows over time: | Source | The stable user id | | ------------- | --------------------------------------------------------------------- | | **PostHog** | the id you pass to `posthog.identify(user_id, …)` (the `distinct_id`) | | **Amplitude** | the Amplitude `user_id` set on events | | *others* | whatever stable id that source stamps on events | Whichever source you use, the rule is the same: the agent must send **that** id. ## 🧩 The three layers that must agree ``` 1. your activity source identifies the user ← PostHog identify / Amplitude user_id / … 2. the connector's activity store ← keyed by that same id 3. the id your AI agent sends ← must equal layer 1 ``` Layers 1 and 2 line up automatically β€” the connector stores activity under the stable id your source sends. The work is making **layer 3** carry that exact value. Use a **stable user id** β€” your internal user primary key, the same one your activity source identifies with. Do **not** key on **email**: emails change, and activity is stored under the stable id, so an email lookup reads the wrong bucket. ## 🀝 How each agent satisfies layer 3 Every agent has its own way of passing a trusted identity to the connector. The mechanism differs per agent; the required *value* is always the same (your activity source's stable user id). | Agent | How it passes identity | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Intercom Fin** | Messenger **JWT identity verification** β€” sign a JWT carrying the user id and boot the Messenger with it. See [the Intercom Fin recipe β†’ Verify identity](/recipes/intercom-tutorial/step-1-connect-real-time-events#verify-identity-with-a-messenger-jwt). | | *Other agents (Maven, etc.)* | Their own verified-identity mechanism β€” bind it to the same stable user id. | For **anonymous** (not-logged-in) users there's no trusted identity to pass, so an agent can't reliably pull their activity. This surface is designed for **logged-in** users your source has identified. ## πŸ›Ÿ Debugging "no recent activity" Almost always an identity mismatch. Confirm the **same** value appears in all three places: 1. the id your activity source identifies the user with (e.g. PostHog `identify`, Amplitude `user_id`), 2. whatever your agent uses as its verified identity (e.g. Fin's JWT `user_id` claim), 3. the `{user_id}` the agent actually sends to the endpoint / MCP tool. For where identity is set on the source side, see your source's setup β€” e.g. the [Quickstart](/quickstart) covers `posthog.identify`. # REST API (optional) Source: https://developers.autoplay.ai/activity/overview The optional REST fallback for pulling a user's recent in-app activity β€” for agents that can't speak MCP. Same data as the MCP tool. πŸ”Œ **Most agents should use the [MCP server](/mcp/server) instead** β€” it's the recommended way to read live activity. This page is the **optional REST fallback** for agents that can't speak MCP. It returns the **same data** via plain HTTP. The REST endpoint hands any AI agent a user's recent in-app footsteps β€” pages viewed, buttons clicked, forms submitted β€” at the exact moment it needs context to answer. Think of it as a **lookup desk**: an agent walks up with a `product_id` and a `user_id`, and the connector hands back the last few things that user did. ## 🌐 The endpoint ``` GET https://mcp.autoplay.ai/users/{product_id}/{user_id}/live-activity ``` | Part | Meaning | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `{product_id}` | Your Autoplay product id β€” the same `YOUR_PRODUCT_ID` you registered in the [Quickstart](/quickstart). | | `{user_id}` | The **stable** user identifier your activity source stamps events with (e.g. the id you pass to `posthog.identify(...)`; the Amplitude `user_id`; …). This match is the linchpin; see **[Identity](/activity/identity)**. | | `?limit=` | Optional. Max recent actions to return. Omitted or `<= 0` falls back to the configured cap (50). | **Auth** β€” `Authorization: Bearer YOUR_MCP_KEY` (the `mcp_key` from your Quickstart product registration). * **401** β€” token missing or invalid. * **403** β€” the token is valid but its `external_id` doesn't match the `{product_id}` in the URL (a key for product A can't read product B). ## πŸ“¦ The response The envelope, with `actions` ordered **oldest β†’ newest**: ```json theme={null} { "product_id": "YOUR_PRODUCT_ID", "user_id": "user_12345", "count": 3, "as_of": 1736940750.881, "actions": [ { "type": "pageview", "title": "Page Load: Dashboard", "description": "User landed on the dashboard page", "timestamp_start": 1736940685.103, "timestamp_end": 1736940691.250, "raw_url": "https://app.example.com/dashboard", "canonical_url": "https://app.example.com/dashboard", "index": 0 }, { "type": "click", "title": "Click Export Csv", "description": "User clicked the Export Csv button on the dashboard page", "timestamp_start": 1736940691.250, "timestamp_end": 1736940705.610, "raw_url": "https://app.example.com/orders/12345", "canonical_url": "https://app.example.com/orders/:id", "index": 1 }, { "type": "submit", "title": "Submit Payment Form", "description": "User submitted the Payment form on the checkout page", "timestamp_start": 1736940705.610, "timestamp_end": 1736940705.610, "raw_url": "https://app.example.com/checkout", "canonical_url": "https://app.example.com/checkout", "index": 2 } ] } ``` Each action carries `type`, `title`, `description`, `timestamp_start`, `timestamp_end`, `raw_url`, `canonical_url`, and `index`. The `user_id` lives on the envelope, not inside each action. **Test it from your terminal** before wiring any agent. Use a real product id, a `user_id` you've actually identified, and your `mcp_key`: ```bash theme={null} curl "https://mcp.autoplay.ai/users/YOUR_PRODUCT_ID/user_12345/live-activity?limit=10" \ -H "Authorization: Bearer YOUR_MCP_KEY" ``` A `200` with a populated `actions` array means the desk is open and the user has footsteps on file. ## ⏳ Before activity appears This surface only returns something once the connector has **recorded** activity for that user β€” your activity source must be wired up and the user must have generated events. **With PostHog** (the source used in the Quickstart): complete **[Quickstart](/quickstart) Steps 1–3** β€” the snippet, `posthog.identify(...)`, product registration, and the ingest webhook. Other sources (e.g. Amplitude) follow the same idea through their own ingestion path. **Identifying a user alone stores nothing.** With PostHog, activity is built only from **`$pageview`** and **`$autocapture`** events (page loads, clicks, form submits) β€” a user who has identified but not yet browsed returns an **empty** `actions` array. Other sources capture their own equivalent events. Make sure capture is enabled and the user has actually navigated/clicked before expecting results. **Retention** β€” this is short-lived "live" memory, not an archive: * **4-hour TTL** (`ACTIVITY_TTL_S` = `14400`) β€” older activity expires. * **50 actions max** per user (`ACTIVITY_MAX_EVENTS` = `50`) β€” older ones are trimmed. # Conversational Design Source: https://developers.autoplay.ai/best-practices/conversational-design Guidance for teams building proactive, agentic onboarding experiences with Autoplay. Follow these patterns to build agents that feel helpful rather than intrusive, and that consistently guide users to value. Quickstart connects activity to your AI support agent. This page covers what the agent should do with that context once it can read it. ## Treat every proactive message as an interruption No matter how good a suggestion is, being interrupted has a cost. Think of a retail store: even if someone walks up with a genuinely useful recommendation, the fact that they inserted themselves into what you were doing is what registers first. It can feel annoying regardless of how useful the suggestion turns out to be. The same is true for a proactive agent. Good timing from `get_proactivity_criteria` reduces how often this happens, but it does not eliminate the interruption itself. The agent is still choosing to speak before being asked. The fix is not to suppress proactive suggestions. It is to change how they are framed: * Never assume the interruption is welcome just because the suggestion is good. * For every interruption, say plainly that you have something useful to show and ask whether now is a good time before sharing it. * Make declining effortless. β€œNot now” should require no explanation and should never lead to another ask in the same turn. > I have something that could help with what you’re working on. Would now be a good time for me to show you? A prompt controls what the agent says after it is invoked. It does not make a reactive support platform initiate conversations. Your platform trigger or [Autoplay.js connection](/quickstart#-step-3-β€”-add-the-proactive-layer) controls delivery. ## Introduce the agent before offering advice People appreciate knowing what they are talking to and why before an agent starts making suggestions. A good introduction does three things: says who the agent is, explains what it is designed to help with, and asks permission before sharing anything. > Hi, I’m an agent designed to help you get set up with the product. I have a suggestion for you. Would now be a good time to share it? This matters even more when the suggestion is personalized. Asking first signals that the agent is offering help rather than pushing an agenda, and gives the user an easy way to say β€œnot now.” ## Give new users something useful to respond to Users are often too new to know what to ask. Do not open with an empty β€œHow can I help?” That puts the burden on the user to know what is possible. Instead, use `get_live_user_activity` and `get_onboarding_context` to offer one to three likely next questions or actions as concrete choices. > It looks like you haven’t invited teammates yet. Want me to walk you through that, or show you how the dashboard works first? If one action is clearly the most relevant, recommend only that action. Offer multiple choices only when they are genuinely useful alternatives. ## Use a consistent vocabulary Define a small vocabulary so the agent does not sound like it is improvising a new personality in every message. * Choose one way to describe progress. For example, use β€œmilestone” consistently rather than alternating between β€œstep,” β€œstage,” and β€œlevel.” * Prefer warm, plain language over internal product jargon. * Use a consistent style for follow-up questions. The agent should still mirror terminology the user introduces. Consistency should make the conversation easier to understand, not make it sound scripted. ## Guide the user toward one valuable next action The agent’s core job is to onboard the user and guide them to their next useful action, not only to answer questions. Once the user gives permission to continue, the conversation should follow three steps: **Acknowledge progress β†’ Suggest the next step β†’ Explain its value** 1. Acknowledge the user’s meaningful progress and connect it to related activity or existing progress. Do not use a generic β€œgreat job.” 2. Suggest one concrete next action. 3. Explain why that action is valuable for this user, not merely why it is a useful feature. > I can see your first automation is live, and you connected Slack earlier, so you’re set up for real-time alerts. I’d turn one on for that automation next. You’ll know the moment it fires instead of having to check back. Want me to help you set up the alert? Treat the sequence as a structural reasoning pattern: | Part | What the agent establishes | | --------------------- | ----------------------------------------------------------------------------------------------------------------- | | Acknowledge progress | β€œI saw you `[meaningful recent activity]`. Alongside `[related progress]`, this means `[relevance to the user]`.” | | Suggest the next step | β€œThe next useful action is `[one concrete step]`” | | Explain value | β€œThis gives you `[specific benefit]` instead of `[relevant limitation]`” | Do not expose those labels in the response or repeat the same phrases mechanically. The result should read as one natural train of thought. The acknowledgement should demonstrate that the agent has been paying attention to more than the single event that triggered the message. Fold relevant context into the acknowledgement rather than treating it as a separate step. That is what earns the agent the standing to suggest a next action. Without context, the response feels scripted. Without the value, it feels like a directive rather than a reasoned suggestion. ## Never tell users what they already know Nothing breaks trust in a smart agent faster than explaining something the user clearly knows or suggesting an action they already completed. It signals that the agent is not paying attention. This is why `get_onboarding_context` and `get_live_user_activity` should be used together: * Do not explain a UI element the user just used successfully. * Do not suggest a milestone that onboarding context shows as complete. * Do not narrate an action that live activity shows the user is already completing correctly. If there is any doubt, check context first. Do not guess or default to over-explaining. ## End with an easy, personalized follow-up Default to a specific question the user can answer in one tap or a few words. > Want me to set up alerts for that next? This is more useful than: > Let me know if there’s anything else I can help with. Keep a generic β€œI’m looking for something else” path available as a fallback, but do not make it the primary close. If the user declines, requests a human, or ends the conversation, accept that without asking another question. ## Continue with the agent configuration Add the provider-neutral behavior layer to your existing agent instructions. Tell the agent when to call each Autoplay tool and how to use its result. # System Prompt Source: https://developers.autoplay.ai/best-practices/system-prompt A provider-neutral prompt layer for natural, context-aware onboarding conversations. Add the following instructions alongside your support or onboarding agent’s existing system prompt. They add Autoplay-aware behavior; they do not replace your agent’s identity, product scope, escalation rules, or approved knowledge. Replace `` with the fixed product ID issued by Autoplay. Resolve `user_id` from verified session context. Never ask the user to provide either value. ```text theme={null} You are a proactive onboarding assistant. In addition to answering questions, you use real-time product context to help users reach their next valuable outcome. Being proactive does not mean interrupting whenever you see an opportunity. Only initiate when get_proactivity_criteria allows it, and always ask permission before sharing a recommendation. These instructions add live-activity awareness and proactive onboarding. For identity, voice, product scope, escalation, safety, and content restrictions, follow the agent's existing instructions. CONTEXT AND PRIORITY product_id is always "". user_id comes from verified session context. Never ask the user for either identifier. The user's current message always takes priority. Use Autoplay context to make the response more relevant, not to ignore or redirect their question. Use get_live_user_activity to understand what the user recently did, what screen or feature they are working with, and whether they are already midway through an action. Use get_onboarding_context to understand which onboarding milestones are complete and determine the single most relevant next incomplete milestone. Use get_proactivity_criteria immediately before any agent-initiated message. If can_be_proactive_now is false, do not initiate. Reactive answers and user-requested help do not require this check. PROACTIVE CONVERSATION Treat every agent-initiated message as an interruption. For every interruption, briefly say what you are designed to help with, say that you have something useful to show, and ask whether now is a good time before sharing it. Do not launch straight into advice. If the user accepts: - Acknowledge one meaningful pattern from recent activity and connect it to related progress so the response feels informed. Do not praise trivial clicks or repeat a raw event name. - Recommend exactly one concrete action that is incomplete and not already in progress. - Explain its practical value for this user: the outcome it enables or the problem it prevents. - End with one concrete, low-effort offer to help with that action. Example structure: "You've [meaningful recent progress]. With [related context], [what that combination means]. I'd [one action] next so [specific benefit], instead of [relevant limitation or missed outcome]. Want me to [concrete assistance]?" Treat Acknowledge progress -> Suggest the next step -> Explain its value as a reasoning sequence, not fixed phrasing. Fold relevant context into the acknowledgement and express the sequence as one natural train of thought. Do not expose labels such as "Next," "Why," "Recommendation," or "Benefit." Do not repeat the same structure or repeatedly rely on phrases such as "Since you've," "That way," or "Now that." ACTIVE ASSISTANCE Once the user accepts help, stop promoting the action and begin helping them complete it. Use the agent's approved product knowledge for accurate instructions. Give the smallest useful next instruction rather than a long list, then ask one concrete check-in question. Do not suggest another milestone until the current task is completed, declined, or blocked. If the user asks a different question, answer it directly first. Use context only where it improves that answer. Do not force an unrelated onboarding suggestion into the response. End with a specific follow-up based on their question when useful. If the user reports confusion, an error, or being stuck, diagnose or resolve the immediate problem. Do not recommend a new milestone until the blocker is resolved. Ask one focused clarifying question only when the available evidence is insufficient. If the user says they completed the action, acknowledge the specific result and explain what it enables. Recommend one genuinely new next action only after current context supports completion. Never claim telemetry confirms completion unless the latest tool result does. If the user declines or says not now, accept immediately and end the turn. Do not ask for a reason or make another suggestion in the same turn. VOICE AND GROUND RULES - Default to 50 words or fewer. Exceed this only when the user explicitly asks for detail or the task cannot be completed safely without it. - Address one immediate action at a time. Do not reproduce an entire help article, list every option, or explain future steps before they become relevant. - Use warm, plain language and the user's terminology. - Prefer outcome language over feature language. - Make one recommendation at a time. - Never say the user is "in onboarding" or reveal internal stages, workflows, gates, experiments, events, recommendation logic, Autoplay, or MCP tools. - Never invent activity, progress, intent, or product capability. - Never invent the purpose of a field, page, action, or user goal. - Never explain a feature the user just used successfully unless they ask. - Avoid standalone praise such as "Perfect," "Great job," or "That's great to hear." Name the meaningful result instead. - Use "I can see..." or "I can see you already..." only when activity is explicit. Use "From what I can see..." or "It looks like..." when evidence is less conclusive. Never claim to see an intention, motivation, or action the available context does not support. Use first-person awareness for a new personalized recommendation or meaningful new progress, not during every instructional reply. - Use bullets only for genuine choices, sequential instructions, or checks. Never use bullets to expose the response structure. For instructions, give no more than three short steps before checking in. - After "yes," "yep," "done," or another brief confirmation, name the concrete result and advance. Do not recap the conversation or use standalone praise. - Do not send filler or status messages before the useful response. FOLLOW-UP QUESTIONS End every active assistance turn with exactly one personalized question. Make it the final sentence, tie it to the current action, and make it answerable with yes or a few words. Do not use generic closes such as "Anything else?", "Let me know," or "If you want, I can also..." Do not ask a follow-up question when the user declines, requests a human, or ends the conversation. FALLBACKS If onboarding context is unavailable but live activity is useful, help with the current activity without asserting a broader next milestone. If live activity is unavailable but onboarding context is useful, recommend the next incomplete milestone without pretending to know what the user just did. If both are unavailable, answer the explicit question from approved product knowledge. If there is no explicit question, say: "I couldn't retrieve your progress just now, but I can still help with what you're working on. What are you trying to get done?" ``` ## Adapt it without weakening it Change the agent name, product terminology, tone, escalation policy, and knowledge-source instructions to fit your product. Keep the behavioral boundaries: verified identity, current-message priority, context before claims, one recommendation, explicit consent, and an effortless decline. Give each Autoplay tool a focused description so the agent calls it for the right reason. # Tool Prompts Source: https://developers.autoplay.ai/best-practices/tool-prompts Descriptions that teach an AI support agent when and why to call each Autoplay MCP tool. Attach these descriptions to each Autoplay MCP tool so the agent calls it with the right intent and interprets its result correctly. The exact field name varies by agent provider. ## `get_live_user_activity` ```text theme={null} Use this tool to retrieve what the current user has just been doing in the product: their current page or screen, recent actions, and any in-progress or abandoned flows. Call this tool: - Before grounding a proactive message in what the user is actually doing. - When the user's question is ambiguous and recent activity may clarify their intent. - To detect signs of friction, such as repeated actions, idle time on a page, or an abandoned flow. Do not use this tool to make assumptions about the user's goals beyond what the activity supports. If the evidence is inconclusive, ask a focused clarifying question instead of guessing. Never expose raw events or mention the tool to the user. ``` This tool answers **what the user is doing now**. It does not determine their overall onboarding priority or decide whether the agent may initiate contact. ## `get_onboarding_context` ```text theme={null} Use this tool to retrieve where the current user sits in their onboarding journey: which milestones are complete, which are outstanding, and the next relevant workflow. Call this tool: - At the start of a proactive interaction, to identify the most relevant milestone to surface next. - When acknowledging a completed action, to identify the correct next milestone and why it matters. - To avoid re-suggesting a milestone the user has completed or explaining something they have already figured out. Combine it with get_live_user_activity. Onboarding context shows where the user should be headed and what they have already done. Live activity shows what they are doing right now, including actions they are already correctly midway through. Check both before making a personalized recommendation. Never expose internal stages, gates, experiments, workflow keys, or tool-output labels to the user. If degraded is true, do not assert specific progress; provide general help or ask a focused question instead. ``` This tool answers **what is genuinely next**. It does not authorize agent-initiated contact. ## `get_proactivity_criteria` ```text theme={null} Use this tool to determine whether the current moment is appropriate for the agent to proactively initiate contact. Call it immediately before any proactive, agent-initiated message. Do not call it before a reactive response to a user message or before help the user explicitly requested. Respect its result strictly. Treat can_be_proactive_now as the source of truth. If it is false, do not initiate a message, even when get_live_user_activity or get_onboarding_context suggests a useful opportunity. Re-check only after the blocking condition may have changed. Do not reveal the verdict, blocked reason, agent state, explore gate, or internal criteria to the user. ``` This tool answers **whether the agent may interrupt now**. It does not choose the recommendation by itself. ## Keep identity out of the conversation All three tools require: * `product_id`: the fixed Autoplay product identifier associated with the authenticated connection. * `user_id`: the exact stable application ID from verified session context, matching the ID sent by the activity source. Do not configure either value as information the agent should collect from the user. Never substitute an email, display name, conversation ID, or guessed value. If the agent provider cannot bind verified identity to the tool argument, resolve that integration gap before enabling personalized recommendations. ## Verify the behavior Test three separate paths: 1. Ask a question about the current screen and confirm the agent uses live activity without exposing raw events. 2. Ask what to do next and confirm the agent does not recommend a completed or in-progress milestone. 3. Attempt an agent-initiated message when `can_be_proactive_now` is false and confirm no message is sent. See how these tool instructions fit into the provider-neutral conversation behavior. # Changelog Source: https://developers.autoplay.ai/changelog Version history and release notes for the autoplay-sdk Python package. The canonical changelog for releases and PyPI sdists also lives in the repository as [`CHANGELOG.md`](https://github.com/Autoplay-AI/real-time-poc/blob/main/later-rho-event-connector/src/customer_sdk/CHANGELOG.md). This page mirrors that file for the documentation site, with links pointed at these docs. *** ## \[0.13.0] β€” 2026-07-30 ### Changed * **`onboard_product` now uses provider-project identity for AWS registration.** Customers pass their analytics provider project id (`provider_project_id`) and Autoplay issues an opaque `product_id` used in ingest and MCP/live-activity paths. * The AWS registration response now exposes `provider_project_id` and the one-time `owner_token`; re-registration/rotation requires the saved `owner_token` instead of the old unauthenticated overwrite flow. ### Documentation * Updated PostHog, Amplitude, Autoplay core, agentic setup, and bridge tutorials to show the issued `product_id`, issued-id `ingest_url`, and owner-token re-registration flow. * Synced the generated `docs//SKILL.{md,txt}` copies with the source skill changes. ### Breaking changes `onboard_product` callers must pass the provider's project id as the first positional argument and must use `result.product_id` / `result.ingest_url` from the response. Do not build ingest URLs from the provider project id. *** ## \[0.11.0] β€” 2026-07-02 ### Changed * **`onboard_product` / `onboard_product_sync` now register against the AWS activity connector.** `DEFAULT_CONNECTOR_URL` is `https://connector.autoplay.ai` (was the legacy Render connector). The `POST /products` request body now carries `provider`, `provider_project_id`, `contact_email`, optional `ingest_secret`, optional `name`, and optional `owner_token` for re-registration. The connector issues an opaque `product_id`, and the response is read from `credentials.{ingest_secret, mcp_key, owner_token}` + `setup.{ingest_url, ingest_auth, mcp_url, mcp_auth}`. * `OnboardProductResult` reshaped to the AWS model: `product_id`, `provider`, `provider_project_id`, `ingest_url`, `ingest_secret`, `ingest_auth`, `mcp_url`, `mcp_key`, `mcp_auth`, `owner_token`, `connector_response`. ### Added * `build_aws_register_payload` and `post_aws_register_product` in `autoplay_sdk.admin.connector_registration_http`. ### Removed * Render/SSE-shaped fields from `OnboardProductResult`: `webhook_url`, `stream_url`, `webhook_secret`, legacy stream-key fields, `integration_type`, `render_sync_performed`. * `onboard_product` keyword args `webhook_secret` and the old unauthenticated overwrite flag (replaced by `ingest_secret` and `owner_token`); `user_activity_provider` is now required. ### Breaking changes `onboard_product` targets a different connector with a different request/response contract. **Existing Render customers are unaffected** β€” they stay on their pinned SDK version and the legacy connector; only new onboarding is routed to AWS. If you call `onboard_product`, pass the provider project id and use `result.ingest_url` + `result.ingest_secret` to configure your provider's webhook, `result.mcp_url` + `result.mcp_key` for the agent's MCP read, and save the one-time `result.owner_token` for future re-registration. The legacy Render onboarding flow remains available in `autoplay_sdk.admin.product_onboarding` / `autoplay_sdk.admin.onboard` for internal Render tooling. *** ## \[0.10.0] β€” 2026-06-21 ### Added * **`autoplay_sdk.storage`** β€” a new, self-contained subsystem for cross-session persistence of `UserAdoptionState` plus frozen per-session snapshots, behind a pluggable adapter interface. * **`AutoplayStorageAdapter`** β€” a `@runtime_checkable Protocol` (mirrors `BufferBackend`) with async `save_user_state` / `get_user_state` / `save_session_snapshot` / `get_session_snapshot`. The wire boundary is plain JSON dicts, so custom adapters stay decoupled from the SDK's model classes. * **`StorageManager`** β€” fans writes out to every adapter (one failing sink never blocks the others) and reads from the first adapter that returns a value (list the readable primary first). * **`RedisStorageAdapter(redis_url=...)`** β€” primary read+write adapter; owns a lazy `redis.asyncio` pool with graceful degradation (mirrors `RedisSessionStateStore`), `SET NX` write-once for snapshots. Needs the `redis` extra. * **`InMemoryStorageAdapter`** β€” tests/dev adapter with write-once snapshot semantics. * **Product-scoped key builders** β€” `user_state_key(product_id, user_id)` and `session_key(product_id, user_id, session_id)`. * **Session lifecycle helpers** β€” `init_user_state`, `on_session_start`, `on_session_end`, `build_agent_context`. `on_session_end` resolves the onboarding summary via an injected `summarizer` callable or a pre-computed `summary=` β€” **the SDK never calls an LLM**. * **Write-only analytics sinks** β€” `AmplitudeStorageAdapter` / `PostHogStorageAdapter` (deep-import from `autoplay_sdk.storage.adapters.{amplitude,posthog}`), gated behind a new `autoplay-sdk[analytics]` extra. They `save_*` (with optional `property_prefix=`) and return `None` from `get_*`; failures are best-effort (logged, never raised). * **`SessionSnapshot`** β€” a new frozen, write-once per-session model with `to_dict` / `from_dict` (defensive) and a `SessionSnapshot.from_adoption_state(...)` helper that builds the snapshot from a live `UserAdoptionState` (reusing the model's derived helpers β€” no hand-rolled mapping). * **`SessionIndexEntry`** + a bounded sessions index on `UserAdoptionState` β€” `sessions_count` (monotonic lifetime total), `current_session_id`, and `sessions_index` (capped to the most recent 50 via `record_session`), plus `set_current_session`. * **`OnboardingState.summary`** and **`OnboardingState.completion_rate`** β€” cross-session briefing + cached completion snapshot. * **`ONBOARDING_SUMMARY_PROMPT`** (v0.1, plain-text output) in `autoplay_sdk.prompts` and a pure **`build_onboarding_summary_input(snapshot)`** builder in `autoplay_sdk.user_adoption_state`. * **Exports** β€” storage symbols re-exported from top-level `autoplay_sdk`; `SessionSnapshot`, `SessionIndexEntry`, `COMPLETION_RATE_DENOMINATOR_*`, and `build_onboarding_summary_input` from `autoplay_sdk.user_adoption_state`. Sink adapters stay deep-import only. * **`autoplay_sdk.observability`** β€” a pluggable error-reporter seam (`set_error_reporter` / `report_error`) so a host application can forward SDK-internal failures to its own sink (Slack, Bugsnag, logs, …). The SDK calls `report_error` at its swallow points β€” RAG embed/upsert, Redis state/snapshot writes, and event-buffer overflow drops β€” so those otherwise-silent failures become observable. Best-effort and a no-op until a reporter is registered; reporting never raises into SDK logic. ### Documentation * New page [State storage & session capture](/sdk/storage) covering the three-step integration flow, the adapter/manager model, Redis key schema, write-only sinks, and the injected-summarizer pattern. * [User adoption state](/sdk/user-adoption-state) persistence note updated to point at the new storage adapters. ### Breaking changes None β€” purely additive. The new `UserAdoptionState` fields (`sessions`, `onboarding.summary`, `onboarding.completion_rate`) read with safe defaults, so existing snapshots load unchanged and the `_v` snapshot version is unchanged. *** ## \[0.9.2] β€” 2026-06-21 ### Added * **Onboarding workflow steps** β€” each `OnboardingWorkflow` can now declare an ordered list of \*\*`WorkflowStep`\*\*s (`step_id`, `name`, `description`, `required`), configured per product under `integration_config.onboarding_workflows[].steps`. Steps are *context/signal* for the LLM (what "done" looks like) and for explainability β€” they are **not** a checklist gate. `required` is a hint, not a hard gate. Helper `OnboardingWorkflow.required_step_ids`. * **`StepProgress`** β€” the agent's per-step belief (`step_id`, `status` reusing `WorkflowStatus`, `confidence`, `evidence`, `completed_at`, `last_evaluated_at`), a deliberate parallel to `WorkflowProgress` one level down. * **`WorkflowProgress.steps`** β€” a `dict[str, StepProgress]` map of per-step beliefs, with a `completed_step_ids` helper and an `upsert_step_progress(step_id, *, status, confidence, evidence="", now=None)` mutation (write-once `completed_at`, mirroring `upsert_workflow_progress`). * **`is_plan_complete(plan, workflow_progress)`** β€” thin convenience wrapper over `split_workflows` (`True` when no outstanding workflows; an empty plan is never complete). Completion stays the LLM's call via `WorkflowProgress.status` β€” steps never gate it. * **`step_completion_ratio(workflow, wp)`** β€” fraction of a workflow's declared steps the LLM believes are `completed`. For display/telemetry only; never used to decide completion. * **Exports** β€” `WorkflowStep`, `StepProgress`, `is_plan_complete`, `step_completion_ratio` added to `autoplay_sdk.user_adoption_state`. ### Documentation * [User adoption state](/sdk/user-adoption-state) updated with the steps data model, the product-config `steps` schema, and the completion semantics (LLM-authoritative; steps are signal, not a gate). ### Breaking changes None β€” purely additive. `steps` is read with safe defaults, so existing snapshots (no `steps` key) load unchanged and the `_v` snapshot version is unchanged. *** ## \[0.9.1] β€” 2026-06-16 ### Added * **Amplitude activity provider support** β€” adds the SDK provider wiring and activity skill needed to configure Amplitude as an event source. ### Documentation * Expanded the setup and quickstart docs for activity providers, agent skills, MCP server setup, and Intercom tutorial assets. *** ## \[0.9.0] β€” 2026-06-12 ### Added * **`PostHogProvider.create_destination()` + `.verify()`** β€” deterministic, idempotent creation (and verification) of the PostHog β†’ Autoplay "Autoplay Event Stream" hog\_function destination, built on the provider abstraction. Powers the one-command `autoplay-setup` onboarding flow (plumbing runs as tested code, not via an agent). ### Changed * **`activity-posthog` skill is now the single source** (modular: `SKILL.md` + `references/` + `examples/`), replacing the forked `posthog-setup` skill the setup bundle used to ship. Scoped to *frontend code only* (the CLI now owns destination create/verify and the end-to-end event check). Identify uses the app's **stable user id** on login (never the anonymous id) + `reset()` on logout. *** ## \[0.8.0] β€” 2026-06-09 ### Added * **`autoplay_sdk.user_adoption_state`** β€” a new per-user adoption-state module, orthogonal to the per-session `SessionState` FSM and keyed by `user_id`. Tracks where a user is in their product lifecycle and how proficient they are, decided by an LLM judge and fed back into chat / proactive prompts. * **Enums** β€” `JourneyState` (`onboarding` / `onboarded`), `MasteryLevel` (`novice` β†’ `beginner` β†’ `intermediate` β†’ `proficient` β†’ `power_user`), and `WorkflowStatus` (`not_started` / `in_progress` / `completed`). * **`UserAdoptionState`** β€” top-level per-user record: a fully dynamic, schema-less `metadata` bag (with a `role` convenience property), `journey_state`, `mastery`, `discovered_features`, and an `onboarding` sub-object. Mandatory non-empty `user_id` (raises otherwise, like `SessionState`); `to_dict()` / `from_dict()` with `_v` snapshot versioning and defensive enum coercion; transition helpers (`set_journey_state`, `set_mastery`, `record_discovered_features`, `mark_welcomed`, `mark_judged`) that log structured events; and `to_prompt_block()` for compact prompt injection. * **`Mastery`** (0-10 rating, clamped, + `MasteryLevel` + reason), **`WorkflowProgress`** (per-flow `status` / `confidence` / `evidence` / timestamps β€” the agent's evolving belief from the live activity stream), **`StepInAssessment`** (latest "should I step in?" decision + timing rationale), and **`OnboardingState`** (welcome lifecycle, `workflow_progress` map, derived `completed` / `in_progress` / `outstanding` helpers, `upsert_workflow_progress`, `record_step_in`). * **`ExplorationGateConfig`** + **`evaluate_hard_gate()`** β€” pure, side-effect-free "let the user explore first" floor (role allowlist, min time on app, min distinct features, welcome cooldown, nudge cap). Returns `(passed, reason)`; the LLM owns timing on top of the floor. * **`OnboardingWorkflow`** / **`OnboardingPlan`** (parsed from config, role-filterable via `for_role`) + pure **`split_workflows()`** β†’ `(completed, in_progress, outstanding)`. * **Query β†’ tour matching primitive** β€” **`build_tour_match_input()`** and **`parse_tour_match_output()`** (validates ids against a role-filtered catalog, safe fallback on malformed JSON), plus **`TourCatalogEntry`** and **`TourMatchResult`**. * **Versioned prompts** in `autoplay_sdk.prompts` (each a `dict` with `name` / `description` / `version` / `content`, JSON-object output): **`ADOPTION_STATE_JUDGE_PROMPT`**, **`TOUR_MATCH_PROMPT`**, **`ONBOARDING_WELCOME_PROMPT`** (all v0.1). * **Top-level exports** from `autoplay_sdk`: `user_adoption_state`, `UserAdoptionState`, `JourneyState`, `MasteryLevel`, `WorkflowStatus`. ### Documentation * New [User adoption state](/sdk/user-adoption-state) reference page covering the two axes (journey vs mastery), the full state model, the explore-first gate, onboarding plans, and the query β†’ tour matcher. The SDK ships pure logic + prompt definitions only; persistence and `acall_llm` runners are integration-owned. ### Breaking changes None β€” the module and exports are purely additive. *** ## \[0.7.9] β€” 2026-05-25 ### Breaking changes * `SessionState.session_id` is now a **required** field β€” no default. `SessionState()` raises `TypeError`. Use `SessionState(session_id="...")` or `InMemorySessionStateStore.get_or_create(session_id)`. `from_dict()` still handles old snapshots gracefully via `.get("session_id", "")`. ### Added * **`SessionState.can_deliver_proactive() -> tuple[bool, str]`** β€” canonical FSM gate check. Returns `(True, "ok")` when the session is THINKING with no active cooldown; `(False, reason)` otherwise. Callers must call `tick()` before this method. Replaces any inline `current_state == THINKING and not thinking.cooldown_active` pattern. * **`SessionState`** now holds `session_id: str` (the only mandatory scope key) and `metadata: dict[str, Any]` (open bag for optional identity context β€” `user_id`, `email`, any future fields). Both fields are persisted via `to_dict()` / `from_dict()` and default to empty for backward compatibility. * **`RedisSessionStateStore`** β€” Redis-backed `SessionState` store for production use. Key pattern `autoplay:session_state:{session_id}`, configurable TTL (default 24 h), graceful fallback on Redis errors. Requires `pip install "autoplay-sdk[redis]"`. * **`AutoplayChatbotManager`** β€” high-level wrapper that handles all session lifecycle automatically. Developers implement only `_post_note` and call two methods: `on_actions(payload)` from the Autoplay stream and `on_chatbot_event(session_id, conversation_id)` from the chatbot webhook. * **`AutoplayChatbotManager(session_store=...)`** β€” optional parameter to inject a custom store. Defaults to `InMemorySessionStateStore()`. Pass `RedisSessionStateStore(redis_url=...)` for production. * **`AutoplayChatbotManager.save_state(state)`** β€” public method for persisting state after external FSM transitions (`transition_to_proactive`, `transition_to_reactive`, `tick`, `start_cooldown`, interaction recorders). `on_actions` and `on_chatbot_event` call this automatically; use `save_state` only for mutations that happen outside those entry points. * **`InMemorySessionStateStore`** β€” canonical store for `SessionState` objects. `get_or_create(session_id)` is the first call in every handler. `async` interface for drop-in Redis-backed replacement. * **`BaseChatbotWriter.__init_subclass__` enforcement** β€” raises `TypeError` at class-definition time (module import) when a subclass sets `SESSION_LINK_WEBHOOK_TOPICS` without overriding `_parse_session_link_webhook_payload`. Converts a silent production crash into a startup error caught by tests and local runs. Subclasses that leave `SESSION_LINK_WEBHOOK_TOPICS = ()` are unaffected. * `link_conversation` now auto-detects `ConversationEventType.NEW` vs `REPLY_EXISTING` from the store when `event` is not passed β€” removes the last manual determination from integration authors. * `link_conversation` now infers `session_id` from `state.session_id` when not passed explicitly. Existing callers that pass `session_id` are unaffected. * `link_conversation` raises `ValueError` if `session_id` is empty after resolution, with a clear message pointing to `InMemorySessionStateStore.get_or_create`. * `AutoplayChatbotManager` and `InMemorySessionStateStore` exported from top-level `autoplay_sdk`. * `AutoplayChatbotManager.on_chatbot_event` now persists the updated `SessionState` back to `RedisSessionStateStore` after linking so the conversation link survives restarts. * `redis` optional extra added to `pyproject.toml`: `pip install "autoplay-sdk[redis]"`. ### Changed * **`ConversationEvent` renamed to `ConversationEventType`** β€” the old name was misleading because it read as a data container rather than a type discriminator. A deprecated backward-compat alias `ConversationEvent = ConversationEventType` is exported from both `autoplay_sdk` and `autoplay_sdk.chat`; it will be removed in the next major version. Update any import of `ConversationEvent` to `ConversationEventType`. * **`proactive_fsm_gate_for_session` delivery invariant clarified** β€” no linked conversation β†’ first proactive β†’ always allowed (a new conversation will be created and linked). A linked conversation β†’ check FSM via `can_deliver_proactive()`. The `fallback_conversation_id` parameter has been removed. * **Polling gates use `session.can_deliver_proactive()` directly** β€” polling paths (`context_payload.py`, `routes.py`) load and tick the session for the known `conversation_id`, then call `can_deliver_proactive()` on the already-loaded session. They no longer go through `proactive_fsm_gate_for_session` (which has delivery-specific semantics). * `AutoplayChatbotManager.on_chatbot_event` removes the `isinstance(store, RedisSessionStateStore)` guard β€” `save` is always called regardless of which store is wired. `on_actions` does not save (no state mutation occurs during action delivery; first-creation is already persisted by `get_or_create`). * `InMemorySessionStateStore` gains a no-op `save(state)` method so all call sites are uniform. ### Documentation * [BaseChatbotWriter](/sdk/support-agent-writer): rewritten to lead with `AutoplayChatbotManager` as the recommended path, updated session-first linking section to show `InMemorySessionStateStore` + `get_or_create` pattern, added Production persistence section with `RedisSessionStateStore` one-line swap. * `autoplay-core` skill: added **Integration Self-Reasoning Checklist** β€” 5 categories agents must reason through before an integration is complete: session identity, race conditions, chatbot linking, proactive triggers, and production persistence. Section 4 updated to use `can_deliver_proactive()` as the canonical gate check and fixes a stale method reference (`to_proactive` β†’ `transition_to_proactive`). Added **Universal Scoping Pattern** β€” the canonical 4-step flow (scope β†’ parse webhook β†’ link β†’ deliver) that applies identically to every chatbot integration. * `SessionState` class docstring now lists `conversation_linked` and `conversation_id` in its Fields section. * [Agent session states](/sdk/agent-states) Key Methods table now includes `can_deliver_proactive()`; `ThinkingState` table now includes `active_cooldown_period_s`. *** ## \[0.7.8] β€” 2026-05-21 ### Changed * Bumped package version to `0.7.8`. *** ## \[0.7.7] β€” 2026-05-21 ### Changed * Bumped package version to `0.7.7`. * Fixed circular import between `autoplay_sdk.integrations.intercom` and `autoplay_sdk.proactive.triggers` by moving `proactive_trigger_canonical_url_ping_pong` and `proactive_trigger_canonical_url_ping_pong_projects_either_leg` into `autoplay_sdk.proactive.triggers._url_predicates`. `intercom.py` re-exports both for backward compatibility. *** ## \[0.7.6] β€” 2026-05-18 ### Fixed * **`autoplay_sdk/skills/autoplay-migrate-imports/SKILL.md`** β€” tightened the discovery `rg` filter to require a word boundary (`(\.|\s)`) after the canonical-domain alternation group. Without it, deprecated sub-modules whose names start with a canonical prefix (`context_store`, `proactive_triggers`, `agent_state_v2`, `rag_query`) were silently excluded from results, leaving 4 of 6 legacy imports unflagged. ### Changed * **`autoplay_sdk/skills/autoplay-migrate-imports/SKILL.md`** β€” inlined the full legacyβ†’canonical path mapping table directly in the skill file. The previous wording ("see the changelog mapping table") referenced a doc that does not ship inside the wheel, so agents had no static replacement guidance and had to rely on runtime `DeprecationWarning` messages to discover new paths. ### Documentation * Updated [BaseChatbotWriter](/sdk/support-agent-writer) with a new **session-first linking** section covering: * `ConversationEventType.NEW` vs `ConversationEventType.REPLY_EXISTING` * canonical dual-write flow via `link_conversation(...)` (`ConversationLinkStore` write, then `SessionState.on_conversation_linked(...)`) * hot-path routing with `resolve_linked_conversation_id(state)` from session-owned state. * Expanded [Agent session states](/sdk/agent-states) v2 docs with: * explicit `SessionState` link fields (`conversation_linked`, `conversation_id`) * `on_conversation_linked(link)` overwrite/no-op semantics and invariants * a state/flag matrix for `THINKING`, `PROACTIVE`, and `REACTIVE`. * Added cross-SDK data-structure mapping in [Typed payloads](/sdk/typed-payloads) and [Payload schema](/sdk/payload-schema), clarifying boundaries between session state, link state, webhook parse models, and stream payloads. *** ## \[0.7.5] β€” 2026-05-17 ### Changed * Bumped package version to `0.7.5`. * Expanded the `test` optional dependency extra to include async/runtime test dependencies (`pytest-asyncio`, `fakeredis`, `respx`) so `pip install -e ".[serve,test]"` is enough for SDK CI and local test runs. * Enabled `asyncio_mode = "auto"` in pytest config so async tests are discovered consistently without requiring explicit per-test markers. *** ## \[0.7.4] β€” 2026-05-16 ### Summary Domain-oriented internal reorganization with one-release compatibility shims. Old import paths still work in `0.7.4` and emit `DeprecationWarning`; they are removed in `1.0.0`. ### Added * `autoplay_sdk.compat` shim registry and helper. * `tests/compat/test_import_compat.py` import parity tests. * CI workflow for import compatibility across Python 3.10/3.11/3.12. * Migration skill: `autoplay-migrate-imports`. ### Changed * Canonical import domains now use `core`, `chat`, `context`, `proactive`, `agent_state`, `api`, `rag`. * `autoplay-install-skills` now supports `--migrate`. ### Deprecated paths (remove in 1.0.0) `autoplay_sdk.rag` remains a stable alias in `0.7.4` (no deprecation warning). | Old | New | | --------------------------------------------------------------------- | --------------------------------------------------------------------- | | `autoplay_sdk.serve` | `autoplay_sdk.api` | | `autoplay_sdk.serve.fastapi` | `autoplay_sdk.api.fastapi` | | `autoplay_sdk.agent_states` | `autoplay_sdk.agent_state.v1.states` | | `autoplay_sdk.agent_states.state_machine` | `autoplay_sdk.agent_state.v1.state_machine` | | `autoplay_sdk.agent_states.types` | `autoplay_sdk.agent_state.v1.types` | | `autoplay_sdk.agent_states.proactive_idle_expiry` | `autoplay_sdk.proactive.state.idle_expiry` | | `autoplay_sdk.agent_state_v2` | `autoplay_sdk.agent_state.v2.states` | | `autoplay_sdk.agent_state_v2.session_state` | `autoplay_sdk.agent_state.v2.session_state` | | `autoplay_sdk.agent_state_v2.types` | `autoplay_sdk.agent_state.v2.types` | | `autoplay_sdk.rag_query` | `autoplay_sdk.rag.query` | | `autoplay_sdk.rag_query.assembly` | `autoplay_sdk.rag.query.assembly` | | `autoplay_sdk.rag_query.formatters` | `autoplay_sdk.rag.query.formatters` | | `autoplay_sdk.rag_query.pipeline` | `autoplay_sdk.rag.query.pipeline` | | `autoplay_sdk.rag_query.watermark` | `autoplay_sdk.rag.query.watermark` | | `autoplay_sdk.proactive_resilience` | `autoplay_sdk.proactive.resilience.resilience` | | `autoplay_sdk.proactive_resilience.circuit` | `autoplay_sdk.proactive.resilience.circuit` | | `autoplay_sdk.proactive_resilience.config` | `autoplay_sdk.proactive.resilience.config` | | `autoplay_sdk.proactive_resilience.keys` | `autoplay_sdk.proactive.resilience.keys` | | `autoplay_sdk.proactive_resilience.outcomes` | `autoplay_sdk.proactive.resilience.outcomes` | | `autoplay_sdk.proactive_resilience.protocol` | `autoplay_sdk.proactive.resilience.protocol` | | `autoplay_sdk.chatbot` | `autoplay_sdk.chat.chatbot` | | `autoplay_sdk.chat_pipeline` | `autoplay_sdk.chat.chat_pipeline` | | `autoplay_sdk.context_store` | `autoplay_sdk.context.context_store` | | `autoplay_sdk.agent_context` | `autoplay_sdk.context.agent_context` | | `autoplay_sdk.user_index` | `autoplay_sdk.context.user_index` | | `autoplay_sdk.summarizer` | `autoplay_sdk.context.summarizer` | | `autoplay_sdk.models` | `autoplay_sdk.core.models` | | `autoplay_sdk.exceptions` | `autoplay_sdk.core.exceptions` | | `autoplay_sdk.metrics` | `autoplay_sdk.core.metrics` | | `autoplay_sdk.onboarding` | `autoplay_sdk.admin.onboarding` | | `autoplay_sdk.proactive_idle_expiry` | `autoplay_sdk.proactive.state.idle_expiry` | | `autoplay_sdk.proactive_triggers` | `autoplay_sdk.proactive.triggers` | | `autoplay_sdk.proactive_triggers.builtin_catalog` | `autoplay_sdk.proactive.triggers.builtin_catalog` | | `autoplay_sdk.proactive_triggers.context_source` | `autoplay_sdk.proactive.triggers.context_source` | | `autoplay_sdk.proactive_triggers.defaults` | `autoplay_sdk.proactive.triggers.defaults` | | `autoplay_sdk.proactive_triggers.entity` | `autoplay_sdk.proactive.triggers.entity` | | `autoplay_sdk.proactive_triggers.judge` | `autoplay_sdk.proactive.triggers.judge` | | `autoplay_sdk.proactive_triggers.pending_tour_offer` | `autoplay_sdk.proactive.triggers.pending_tour_offer` | | `autoplay_sdk.proactive_triggers.predicate_trigger` | `autoplay_sdk.proactive.triggers.predicate_trigger` | | `autoplay_sdk.proactive_triggers.proactive_intercom_config` | `autoplay_sdk.proactive.triggers.proactive_intercom_config` | | `autoplay_sdk.proactive_triggers.quick_reply_match` | `autoplay_sdk.proactive.triggers.quick_reply_match` | | `autoplay_sdk.proactive_triggers.registry` | `autoplay_sdk.proactive.triggers.registry` | | `autoplay_sdk.proactive_triggers.scope` | `autoplay_sdk.proactive.triggers.scope` | | `autoplay_sdk.proactive_triggers.section_activity` | `autoplay_sdk.proactive.triggers.section_activity` | | `autoplay_sdk.proactive_triggers.tour_registry` | `autoplay_sdk.proactive.triggers.tour_registry` | | `autoplay_sdk.proactive_triggers.trigger_config` | `autoplay_sdk.proactive.triggers.trigger_config` | | `autoplay_sdk.proactive_triggers.triggers` | `autoplay_sdk.proactive.triggers.triggers` | | `autoplay_sdk.proactive_triggers.triggers.canonical_ping_pong` | `autoplay_sdk.proactive.triggers.triggers.canonical_ping_pong` | | `autoplay_sdk.proactive_triggers.triggers.scoped_canonical_ping_pong` | `autoplay_sdk.proactive.triggers.triggers.scoped_canonical_ping_pong` | | `autoplay_sdk.proactive_triggers.triggers.section_playbook` | `autoplay_sdk.proactive.triggers.triggers.section_playbook` | | `autoplay_sdk.proactive_triggers.triggers.user_page_dwell` | `autoplay_sdk.proactive.triggers.triggers.user_page_dwell` | | `autoplay_sdk.proactive_triggers.types` | `autoplay_sdk.proactive.triggers.types` | | `autoplay_sdk.proactive_triggers.url_scope` | `autoplay_sdk.proactive.triggers.url_scope` | See [Migration 0.7.4](/sdk/migration-0.7.4) for upgrade steps. *** ## \[0.7.3] β€” 2026-05-14 ### Documentation * Added dedicated references for [`UserSessionIndex`](/sdk/user-session-index), [`compose_chat_pipeline(...)`](/sdk/compose-chat-pipeline), and [`build_copilot_app(...)`](/sdk/build-support-agent-app), including lifecycle details, endpoint status semantics, and failure troubleshooting. * Updated [Logging](/sdk/logging) with explicit observability guidance for `autoplay_sdk.chat_pipeline`, `autoplay_sdk.user_index`, and `autoplay_sdk.serve.fastapi`, including recommended app-layer log points for self-hosted bridges. * Added a clearer self-hosted quick-reference and troubleshooting section in `README.md` (repository package docs), covering primitive selection and common `404`/identity/product-scope failure modes. *** ## \[0.7.2] β€” 2026-05-14 ### Removed * **`TourDefinition.label`** and **`TourDefinition.user_tour_exists`** β€” these fields belong to `proactive_intercom.messages`, not the tour registry. Removed from the dataclass, `to_dict`, and `from_dict`. Legacy configs that still include these keys are silently ignored on parse. ### Added * **`TOUR_OFFER_QUICK_REPLY_BODY`** β€” constant (`"Would you like me to show you?"`); default body for the tour-offer Yes/No quick reply. * **`tour_offer_quick_reply_body(integration_config)`** β€” single SDK source-of-truth for the tour-offer message body; reads `integration_config.tour_offer_body` for product-level overrides, falls back to the constant. Call this after `resolve_tour_offer_for_inbound` returns a non-`None` flow id. Both symbols exported from `autoplay_sdk.proactive_triggers`. *** ## \[0.7.1] β€” 2026-05-13 ### Added * **`autoplay_sdk.install_skills`** β€” CLI command `autoplay-install-skills` that copies bundled Cursor/Claude agent skills into the current project. Supports `--chatbot` and `--user-activity` flags. * **`autoplay_sdk/skills/`** β€” 10 agent skill files shipped inside the wheel: `autoplay-core`, `chatbot-ada`, `chatbot-intercom`, `chatbot-botpress`, `chatbot-dify`, `chatbot-crisp`, `chatbot-landbot`, `chatbot-tidio`, `activity-fullstory`, `activity-posthog`. ### Documentation * Ada tutorial (`docs/recipes/ada/`), FullStory Streams tutorial (`docs/recipes/fullstory/how-to-setup.mdx`), and quickstart `autoplay-install-skills` tip block. *** ## \[0.7.0] β€” 2026-05-12 ### Added * **`autoplay_sdk.agent_state_v2`** β€” `SessionState` FSM with `THINKING` / `PROACTIVE` / `REACTIVE` states, timeout-only exit rules, and `InvalidTransitionError`. Coexists with v1 `AgentStateMachine`. * **`autoplay_sdk.proactive_triggers.trigger_config`** β€” `ProactiveTriggerConfig`, `TriggerMessage`, and recursive `ProactiveCriteria` for config-driven proactive triggers. * **`autoplay_sdk.proactive_triggers.tour_registry`** β€” `TourDefinition` and `TourRegistry`; per-tour `interaction_timeout_s` / `cooldown_period_s` overrides; `get_by_user_tour_id()`. * **`SessionState.record_tour_step()`**, **`SessionState.set_visual_guidance()`**, **`SessionState.tick(tour_registry=None)`**. * **`parse_tour_registry(integration_config, product_id)`** helper in `proactive_intercom_config`. ### Changed * **`integration_config.proactive_intercom`** β€” now a **list** of `ProactiveTriggerConfig` objects (was a single dict). * **`TriggerMessage`** β€” renamed `offers_tour` β†’ `user_tour_exists`, `flow_id` β†’ `user_tour_id`; backward-compatible properties retained. ### Documentation * [Intercom proactive triggers step 2](/recipes/intercom-tutorial/step-2-define-proactive-triggers), [Agent session states](/sdk/agent-states) β€” agent state v2 accordion. *** ## \[0.6.8] β€” 2026-05-05 * SDK branch release tag/version for current branch cut. * `AgentStateMachine.enter_reactive_from_user_message` docs + behavior notes now include rejection logging and preserved `InvalidTransitionError` semantics. *** ## \[0.6.7] β€” 2026-04-30 Agent FSM, Intercom proactive **`quick_reply`**, **`autoplay_sdk.proactive_triggers`** (replaces **`integrations.proactive`**), registry timings/entity, context builders, scope validation, product-scoped context-store buckets, and proactive idle teardown for chat: **`run_proactive_idle_expiry`**, **`ProactiveIdleExpiryHooks`**, **`ProactiveIdleExpiryResult`**, Intercom **`DELETE /conversations/{id}`** helpers (**`build_intercom_delete_conversation_request`**, etc.). For full **Added** / **Changed** / **Breaking** detail, see the same version in the repository [`CHANGELOG.md`](https://github.com/Autoplay-AI/real-time-poc/blob/main/later-rho-event-connector/src/customer_sdk/CHANGELOG.md). ### Removed * **`autoplay_sdk.integrations.proactive`** β€” use **`autoplay_sdk.proactive_triggers`** (`import` paths and submodules mirror the old layout). ### Added * **`autoplay_sdk.agent_states`** β€” **`AgentState`**, **`AgentStateMachine`**, **`TaskProgress`**, **`SessionMetrics`**, **`InvalidTransitionError`**, **`InvalidSnapshotError`**; five-state FSM; **`transition_on_disengagement()`**; **`to_snapshot()`** / **`from_snapshot()`**; **`can_show_proactive_with_reason()`**. * **`autoplay_sdk.agent_states.AgentStateMachine`** β€” **`expire_proactive_to_thinking_if_idle`**. * **`autoplay_sdk.integrations.intercom`** β€” **`quick_reply`** helpers, **`INTERCOM_PROACTIVE_QUICK_REPLY_DEFAULT_BODY`**, **`proactive_trigger_canonical_url_ping_pong`**, connector helpers for **`POST /intercom/proactive/{product_id}`**, **`IntercomProactivePolicyConfig`**. * **`autoplay_sdk.proactive_triggers`** β€” **`ProactiveTriggerContext`**, **`ProactiveTriggerResult`**, registry, **`CanonicalPingPongTrigger`**, **`default_proactive_trigger_registry`**, timings (**`ProactiveTriggerTimings`**, **`ProactiveTriggerEntity`**, **`interaction_timeout_s`**, **`cooldown_s`**). * **`autoplay_sdk.proactive_triggers.defaults`** β€” **`ProactiveTriggerIds`**, **`get_proactive_trigger_ids()`**, **`DEFAULT_PROACTIVE_QUICK_REPLY_BODY`**. * **`PredicateProactiveTrigger`**; **`from_actions_payloads`** / **`from_slim_actions`**; **`DEFAULT_PROACTIVE_CONTEXT_*`** exports; **`scope`** (**`ScopePolicy`**, validation helpers); **`validate_scope`** (**`require_conversation`**). * **`autoplay_sdk.context_store`** β€” **`actions_bucket_id`**, optional **`product_id`** on **`get`**, **`enrich`**, **`reset`**; composite **`_actions`** keys when **`ActionsPayload.product_id`** is set. ### Changed * **`autoplay_sdk.integrations.intercom`** β€” **`INTERCOM_PROACTIVE_QUICK_REPLY_DEFAULT_BODY`** aliases **`proactive_triggers.defaults`**. * **`ProactiveTriggerContext`** β€” optional **`recent_actions`**, summaries, **`context_extra`**. * **`from_slim_actions` / `from_actions_payloads`** β€” default **`scope_policy=STRICT`** (use **`LENIENT`** to relax). * **`POST /intercom/proactive/{product_id}`** documented as always-on when authenticated (**`ENABLE_INTERCOM_PROACTIVE_PROMPTS`** removed). ### Documentation * [Agent session states](/sdk/agent-states), [Intercom integration](/integrations/intercom), [Proactive triggers](/sdk/proactive-triggers), [Authoring proactive triggers](/sdk/proactive-triggers-authoring). ### Breaking changes * **`expire_proactive_to_thinking_if_idle`** β€” returns **`ProactiveIdleExpiryResult`** (use **`if result:`** / **`result.transitioned`**, not **`is True`** on the return value). See [`CHANGELOG.md`](https://github.com/Autoplay-AI/real-time-poc/blob/main/later-rho-event-connector/src/customer_sdk/CHANGELOG.md#067--2026-04-30). * Migrate imports from **`autoplay_sdk.integrations.proactive`** to **`autoplay_sdk.proactive_triggers`**. * Strict scope defaults; **`LENIENT`** for old permissive behaviour. * Match **`product_id`** on context-store reads when payloads carry **`product_id`**. *** ## \[0.6.6] β€” 2026-04-23 ### Added * **`SlimAction`** / **`ActionsPayload`** β€” optional **`conversation_id`** (Intercom thread when linked); merge semantics unchanged at batch level. * **`autoplay_sdk.rag_query`** β€” query-time RAG assembly (**`assemble_rag_chat_context`**, providers, **`ChatContextAssembly`**, formatters). * **`autoplay_sdk.prompts`** β€” versioned Adoption Copilot defaults (**`RAG_SYSTEM_PROMPT`**, **`REASONING_PROMPT`**, **`RESPONSE_PROMPT`**). * **`autoplay_sdk.rag_query.watermark`** β€” delta activity watermarks for Intercom / connector chat. * **`autoplay_sdk.prompts.intercom_readability`** β€” shared **`INTERCOM_READABILITY_RULES`** fragment for Messenger replies. ### Changed * Root **`__all__`** documents query-time RAG vs ingestion **`RagPipeline`**. * **`RAG_SYSTEM_PROMPT`** / **`RESPONSE_PROMPT`** (1.2) and connector **`INTERCOM_CHAT_PROMPT`** (5) β€” readability + guidance framing for product how-to questions. * **`assemble_rag_chat_context`** β€” structured DEBUG / WARNING logging. ### Documentation * [Support AI agent context assembly](/sdk/support-agent-context-assembly) β€” delta activity and observability cross-links. ### Breaking changes None for JSON consumers β€” new fields remain optional. *** ## \[0.6.4] β€” 2026-04-23 ### Added * **`SlimAction`** β€” optional per-action `session_id`, `user_id`, and `email` (aligned with batch-level payload identity). Present on each entry in `actions` for SSE, push webhooks, and typed parsing. * **`RedisEventBuffer`** β€” serialized JSON now includes those fields per action so buffered events round-trip cleanly. ### Documentation * [Payload schema](/sdk/payload-schema) and [Typed payloads](/sdk/typed-payloads) β€” document per-action identity fields; payload schema clarifies shared shape for SSE and push webhooks. ### Breaking changes None β€” new fields are optional (`None` when omitted). *** ## \[0.6.2] β€” 2026-04-16 ### Added * **`POST /products` terminal feedback** β€” TTY-only Rich stderr spinner and stdout success/failure lines in `post_register_product_payload`; no output when stdout is not a TTY. ### Changed * **`run_product_onboarding`** β€” single `POST /products`; connector performs Redis + Render when configured. `render_sync_performed` reflects connector `dual_write` in the JSON response. * **`unkey.py` is now a default dependency** β€” plain `pip install autoplay-sdk` includes operator onboarding (`autoplay_sdk.admin` / Unkey). ### Removed * **`autoplay-sdk[admin]` extra** β€” install `autoplay-sdk` only. *** ## \[0.6.1] β€” 2026-04-14 ### Added * **`ConversationEventType`** (`autoplay_sdk.chatbot`) β€” enum for chatbot session-link event semantics (`NEW` / `REPLY_EXISTING`). * **`BaseChatbotWriter` session-link webhook flow** β€” `SESSION_LINK_WEBHOOK_TOPICS`, `extract_conversation_event()`, `_parse_session_link_webhook_payload()` (subclasses override parse only). * **`autoplay_sdk.integrations.intercom`** β€” webhook topic constants, `intercom_chatbot_webhook_url()`, optional `format_reactive_session_link_script` (no logging from this subpackage). ### Removed * **`format_proactive_session_start_script`** and the proactive **`/sessions/start`** snippet β€” use Intercom webhooks to `POST /chatbot-webhook/{product_id}`; optional **`format_reactive_session_link_script`** remains for `POST /sessions/link`. ### Documentation * [Logging](/sdk/logging) β€” logger hierarchy, app-owned `logging` configuration, third-party subclass guidance, changelog cross-link. * README β€” logging section aligned with the above; metrics pointer (`SdkMetricsHook`). * [Intercom integration](/integrations/intercom) β€” SDK helpers and connector mapping (webhooks, optional snippet, inbox UX). ### Breaking changes * **`format_proactive_session_start_script`** removed from `autoplay_sdk.integrations.intercom`. ### Deprecations None. *** ## \[0.6.0] β€” 2026-04-13 ### Documentation * **BaseChatbotWriter β€” Note body format** β€” [BaseChatbotWriter](/sdk/support-agent-writer) now documents the full plain-text contract for `_post_note` bodies (header via `format_chatbot_note_header`, sorted 1-based action lines, binning vs post-link, empty list, summary notes). `_format_note` docstring points to that page as the single source of truth. * **Logging** β€” New [Logging](/sdk/logging) reference page (module loggers, `%` formatting, `exc_info`, structured `extra`, secrets guidance including HTTP bodies, common logging mistakes). [Quickstart](/quickstart) links to it for discoverability. ### Bug fixes / observability * `BaseChatbotWriter` β€” pre-link flush failure `warning` now includes structured `extra` (`session_id`, `product_id`, `conversation_id`). * `BaseChatbotWriter` β€” post-link debounced flush: if `_post_note` returns no part id after the debounce buffer was popped, logs a `warning` with the same `extra` shape and explains that this flush is not retried automatically. ### Breaking changes None. ### Deprecations None. *** ## \[0.5.0] β€” 2026-04-10 ### New features * **`ActionsPayload.merge(payloads)`** β€” class method that merges a non-empty list of `ActionsPayload` objects for the same session into one. Actions are concatenated and re-indexed from `0`; `user_id`/`email` resolved from the first non-`None` value; `forwarded_at` set to the latest timestamp. Raises `ValueError` on empty input. * **`AsyncAgentContextWriter(debounce_ms=N)`** β€” new optional constructor parameter for a per-session trailing-edge accumulation window. When `> 0`, multiple `add()` calls arriving within the window are merged via `ActionsPayload.merge()` before `write_actions` is called, reducing destination API calls during event bursts. Default is `0` (no debounce β€” existing behaviour unchanged). * **`BaseChatbotWriter`** (`autoplay_sdk.chatbot`) β€” new public base class providing the complete pre-link/post-link delivery policy for building chatbot destinations. Subclass it and implement `_post_note` and `_redact_part`; pre-link buffering (sliding window), at-link flush (binned note), and post-link debouncing are all included. `IntercomChatbot` in the event connector already extends this class. ### Breaking changes None. All changes are additive: * `AsyncAgentContextWriter.__init__` gains `debounce_ms: int = 0` β€” existing code passing positional or keyword arguments is unaffected. * `ActionsPayload.merge()` is a new class method; no existing method is renamed or removed. * `BaseChatbotWriter` is a new public export; no existing symbols are removed. ### Deprecations None. ### Bug fixes / error handling improvements * `BaseChatbotWriter.on_session_linked` β€” now no-ops when the same `conversation_id` is passed again (idempotent guard). The product worker includes the conv\_id on every batch for already-linked sessions; without this guard, each batch would cancel the in-flight 150ms post-link debounce task and restart the window, causing notes to be delayed indefinitely during fast user interactions. * `BaseChatbotWriter.on_session_linked` β€” pre-link buffer (`_pending`) is now only cleared after `_post_note` confirms success (returns a non-`None` part id). Previously the buffer was popped before the API call; a transient Intercom failure would permanently lose those events. On failure the buffer is now preserved and the `_conv_map` entry is rolled back so the next `on_session_linked` call retries automatically. * `BaseChatbotWriter.write_actions`: the post-link debounce `asyncio.Task` now has a `done_callback` that logs any unhandled exception at `ERROR` level with structured `extra` (previously silent β€” Python only emitted a `DEBUG`-level "Task exception was never retrieved"). * `AsyncAgentContextWriter._flush_session` done-callback: now includes `product_id` in the log message and `extra` dict for structured log filtering. ### Migration notes **`AsyncAgentContextWriter` + `BaseChatbotWriter` β€” avoid double-debouncing** `BaseChatbotWriter` already coalesces rapid `write_actions()` calls via its `post_link_debounce_s` window (default 150 ms). When wiring an `AsyncAgentContextWriter` to a `BaseChatbotWriter` subclass, keep `debounce_ms=0` (the default): ```python theme={null} # CORRECT β€” BaseChatbotWriter handles debouncing; no stacking needed writer = AsyncAgentContextWriter( summarizer=summarizer, write_actions=chatbot_subclass.write_actions_cb, overwrite_with_summary=overwrite_cb, debounce_ms=0, # ← default, explicit for clarity ) # AVOID β€” stacks two debounce windows, adds latency without benefit writer = AsyncAgentContextWriter(..., debounce_ms=200) ``` Use `debounce_ms > 0` only when `write_actions` points to a raw destination with no internal coalescing (e.g. a direct Zendesk or Salesforce API call). *** ## \[0.4.0] β€” 2026-04-09 ### Bug fixes * Fixed TOCTOU race in `RedisEventBuffer._get_redis()`: concurrent callers could each create their own connection pool; now serialised with `asyncio.Lock` and double-checked locking. * Fixed `AsyncSessionSummarizer.flush()` cancellation safety: replaced sequential `for q in queues: await q.join()` with `asyncio.gather(*[asyncio.shield(q.join()) for q in queues])` so all queues are drained even when the caller is cancelled. ### Other * Added `CHANGELOG.md` to document breaking changes and new features going forward. *** ## \[0.3.0] β€” 2026-04-09 ### Breaking changes * **`AsyncSessionSummarizer.get_context(session_id)` is now `async`** β€” callers must `await` it. * **`AsyncSessionSummarizer.reset(session_id)` is now `async`** β€” callers must `await` it. * **`AsyncSessionSummarizer.active_sessions` is now an `async` property** β€” callers must `await` it. * **`AsyncSessionSummarizer.add()` now returns immediately** β€” the LLM call is dispatched to a background worker queue rather than awaited inline. Code that relied on the LLM having completed by the time `await add()` returned must call `await summarizer.flush()` before inspecting state. ### New features * **`AsyncSessionSummarizer.flush()`** β€” waits for all queued payloads to be fully processed; cancellation-safe via `asyncio.gather` + `asyncio.shield`. * **`SdkMetricsHook` protocol** (`autoplay_sdk.metrics`) β€” a `@runtime_checkable Protocol` that customers can implement to receive Prometheus / Datadog / OTEL counters for: dropped events, summarizer latency, Redis operation latency, queue depth, and semaphore timeouts. * **`metrics=` constructor parameter** on `ConnectorClient`, `AsyncConnectorClient`, `AsyncSessionSummarizer`, and `RedisEventBuffer`. * **`initial_backoff_s`, `max_backoff_s`, `max_retries` constructor parameters** on both SSE clients β€” exposes and documents the reconnect policy with configurable jitter-backed exponential backoff. * **Per-session ordering guarantee in `AsyncSessionSummarizer`** β€” each session now has its own `asyncio.Queue` + background `asyncio.Task` worker, ensuring that concurrent `add()` calls for the same session are always processed in arrival order, even if an earlier LLM call fails. * **`py.typed` marker** β€” the package is now PEP 561-compliant; static type-checkers will find type stubs automatically. ### Bug fixes (v0.2.x β†’ v0.3.0) The following 24 items were addressed across two audit passes: **Concurrency & correctness** * Fixed `AsyncSessionSummarizer` ordering bug: concurrent adds during an LLM failure could produce out-of-order `on_summary` callbacks (replaced single lock with per-session queue). * Fixed TOCTOU race in `RedisEventBuffer._get_redis()`: concurrent callers could each create their own connection pool; now serialised with `asyncio.Lock` and double-checked locking. * Replaced `asyncio.Semaphore._value` (private API, breaks across CPython minor versions) with an explicit `_TrackedSemaphore` counter. * Replaced `asyncio.get_event_loop()` (deprecated) with `asyncio.get_running_loop()` in `app.py` and `async_client.py`. * Replaced `asyncio.ensure_future()` with `loop.create_task()` in `async_client.py`. * Added `done_callback` on fire-and-forget `asyncio.Task`s to log unhandled exceptions instead of silently swallowing them. **Error handling** * `RedisEventBuffer._payload_from_json()` now wraps `json.loads` in `try/except json.JSONDecodeError`; corrupt ZSET members no longer crash the drain loop. * `ConnectorClient` now sets `self._running = False` on `KeyboardInterrupt` so callers can inspect the state after shutdown. * All `except` clauses that were swallowing exceptions now pass `exc_info=True` to the logger so tracebacks appear in structured logs. **Data structures** * `RedisEventBuffer` ZSET members now carry a unique UUID prefix, preventing silent deduplication when two events arrive at the same millisecond timestamp. * `SessionSummarizer` now deletes `_history[session_id]` and `_counts[session_id]` after summarisation to prevent unbounded memory growth. * `AsyncConnectorClient._session_semaphores` changed from plain `dict` to `collections.OrderedDict` with LRU eviction to cap memory when many short-lived sessions are processed. **Redis connection management** * Extracted `LazyRedisClient` helper (`storage/_redis.py`) so all storage modules share one lazily-initialised, thread-safe Redis client instead of each implementing the same racy pattern. * `session_store` now uses `LazyRedisClient` and exposes a `SessionState.from_redis_link()` classmethod that owns the full reconstruction logic (preventing silent field omissions on restore). * `SessionState` gains an `error: Optional[str]` field to surface last-known error reason through the API. **Logging & observability** * Replaced custom `_JsonFormatter` in `app.py` that ignored `extra={}` fields with a correct implementation that merges them into the JSON line. * All structured log calls now use `extra={}` dicts consistently. * Metrics instrumentation added at every observability-relevant site: event drops, queue depth, semaphore timeouts, summarizer latency, Redis add/drain latency. **Package hygiene** * Added `__all__` exports to `__init__.py` so `from autoplay_sdk import *` is well-defined. * Added `__version__ = "0.3.0"` to `__init__.py`. * Added `py.typed` marker for PEP 561 compliance. * Removed dead `_dropped_count` / `_total_count` metrics fields that were incremented but never surfaced. * Standardised public API: `on_drop` callback signature is now consistent across `ConnectorClient`, `AsyncConnectorClient`, and `RedisEventBuffer`. *** ## \[0.2.0] β€” prior Initial internal release. No changelog maintained at this version. # Botpress Source: https://developers.autoplay.ai/integrations/botpress Dedicated Botpress integration helpers are coming soon. Stream events to your connector and support AI agent today. **Dedicated helpers for Botpress are coming soon.** That is **not a blocker**: you can **still stream events to your connector and support AI agent (or delivery) endpoints today** using the core SDK and event pipeline. See the [Botpress tutorial](/recipes/botpress) for a full end-to-end pattern. When first-party helpers ship, they will be **optional extras** β€” not required for implementation. *** We plan first-party helpers so Botpress workflows can consume Autoplay session context with less glue code. Until then, use the standard client and push payloads into your own integration layer. # Dify Source: https://developers.autoplay.ai/integrations/diffy Dedicated Diffy integration helpers are coming soon. Stream events to your connector and support AI agent today. **Dedicated helpers for Diffy are coming soon.** That is **not a blocker**: you can **still stream events to your connector and support AI agent (or delivery) endpoints today** using the core SDK and event pipeline. When they ship, these helpers will be **optional extras** β€” not required for implementation. *** We plan first-party helpers so Diffy workflows can consume Autoplay session context with less glue code. Until then, use the standard client and push payloads into your own integration layer. # Help Scout Source: https://developers.autoplay.ai/integrations/help-scout Dedicated Help Scout integration helpers are coming soon. Stream events to your connector and support AI agent today. **Dedicated helpers for Help Scout are coming soon.** That is **not a blocker**: you can **still give your Help Scout integration real-time context today** by pulling a user's live activity on demand from the Autoplay connector. When dedicated helpers ship, they will be **optional extras** β€” not required for implementation. *** We plan first-party helpers so Help Scout conversations can show the same structured context as other channels. Until then, pull a user's recent activity synchronously β€” from a webhook handler, or right before you post a note/reply β€” the same way the [Plain tutorial](/recipes/plain-tutorial/step-1-connect-real-time-events) does: ```bash theme={null} curl "https://mcp.autoplay.ai/users/YOUR_PRODUCT_ID/YOUR_USER_ID/live-activity?limit=10" \ -H "Authorization: Bearer YOUR_MCP_KEY" ``` `YOUR_USER_ID` must be the same stable id your activity source identifies the user with (e.g. `posthog.identify(...)` / `amplitude.setUserId(...)`). No stream client or background process is required β€” see the [Quickstart](/quickstart) for how to get `product_id` and `mcp_key`. # HubSpot Chat Source: https://developers.autoplay.ai/integrations/hubspot-chat Dedicated HubSpot Chat integration helpers are coming soon. Stream events to your connector and support AI agent today. **Dedicated helpers for HubSpot Chat are coming soon.** That is **not a blocker**: you can **still stream events to your connector and support AI agent (or delivery) endpoints today** using the core SDK and event pipeline. When they ship, these helpers will be **optional extras** β€” not required for implementation. *** We plan first-party helpers to streamline HubSpot Chat alongside Autoplay’s real-time events. Until then, stream through the SDK and connect HubSpot from your backend or automation stack. # Intercom Source: https://developers.autoplay.ai/integrations/intercom Intercom-specific SDK helpers in autoplay_sdk.integrations.intercom and how they map to the event connector. `INTERCOM_WEBHOOK_TOPICS` is the same tuple **`IntercomChatbot`** uses in the event connector for session-link webhooks, so what you subscribe to in Intercom stays aligned with parsing and linking. `intercom_chatbot_webhook_url` builds the correct `/chatbot-webhook/{product_id}` URL and rejects invalid `product_id`s; `format_reactive_session_link_script` matches optional **`POST /sessions/link`** when you still need browser-side linking. Proactive **`quick_reply`** helpers (correct **`Intercom-Version: Unstable`**, ping‑pong trigger, optional connector LLM-label URL) are documented in **[Proactive Messenger quick replies](#proactive-messenger-quick-replies)** below. *** ## Module: `autoplay_sdk.integrations.intercom` Small, dependency-light helpers in `autoplay_sdk/integrations/intercom.py` for Developer Hub URL/topics, the reactive snippet, and proactive Messenger quick replies. ### Webhook topics Subscribe in Intercom to **exactly** these topics so session linking and support AI agent delivery stay aligned with `IntercomChatbot` parsing in the connector: | Name | Constant | Value | | ------------ | ------------------------------------- | --------------------------- | | User created | `INTERCOM_WEBHOOK_TOPIC_USER_CREATED` | `conversation.user.created` | | User replied | `INTERCOM_WEBHOOK_TOPIC_USER_REPLIED` | `conversation.user.replied` | **Tuple for loops / UI:** `INTERCOM_WEBHOOK_TOPICS` β€” both strings above. Other topics are ignored by the connector. ### `intercom_chatbot_webhook_url(connector_host, product_id) -> str` Builds the absolute **HTTPS** URL for Intercom outbound webhooks: `{origin}/chatbot-webhook/{product_id}` * **`connector_host`:** hostname (`event-connector-xxxx.onrender.com`) or full origin (`https://…`). Trailing slashes stripped; bare host gets `https://` prepended. * **`product_id`:** non-empty, no `/` (single path segment). Raises `ValueError` if host or product id is invalid. ### `format_reactive_session_link_script(connector_host, product_id) -> str` Returns an HTML/JS snippet that registers `Intercom("onConversationStarted", …)` and `POST`s to **`/sessions/link`** with `product_id`, PostHog `session_id`, and `conversation_id`. Use only when you still need **browser-side** linking; **prefer webhooks** to `/chatbot-webhook/{product_id}` first. *** ## Proactive Messenger quick replies A **proactive quick reply** is an **admin** message on the conversation with **`message_type: quick_reply`**: an intro **`body`** plus up to three tappable **`reply_options`**. Send it with **`POST {INTERCOM_REST_API_BASE}/conversations/{conversation_id}/reply`**. Split responsibilities: | Concern | Module | What you use | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **When** to show an offer | [`autoplay_sdk.proactive_triggers`](/sdk/proactive-triggers) | **`ProactiveTriggerContext`**, **`ProactiveTriggerRegistry.evaluate_first`**, **`ProactiveTriggerResult`** (copy, optional labels, **`interaction_timeout_s`**, **`cooldown_s`**). | | **How** to send on the wire | [`autoplay_sdk.integrations.intercom`](#module-autoplay_sdkintegrationsintercom) | **`intercom_quick_reply_http_headers`**, **`build_intercom_quick_reply_reply_payload`** β€” never hand-roll **`Intercom-Version`**. | | **Idle teardown** (chat) | [`autoplay_sdk.agent_states`](/sdk/agent-states) + [`autoplay_sdk.integrations.intercom`](#delete-conversation-idle-teardown) | **`run_proactive_idle_expiry`** with **`delete_remote_chat_thread`** using **`build_intercom_delete_conversation_request`** + **`DELETE`**; then **`clear_local_chat_thread_state`** (session↔thread + persisted FSM β€” host-owned). | ### Prerequisites * **`conversation_id`** β€” From Intercom webhooks, [`SlimAction`](/sdk/typed-payloads) **`conversation_id`**, or after **`POST /sessions/link`** links the browser session. * **Access token** β€” Bearer token for the Intercom API (app allowed to reply as admin). * **`admin_id`** β€” Workspace/agent id Intercom expects on **`quick_reply`** payloads (from Intercom app settings). ### Recipe: end-to-end 1. **Build context** β€” Instantiate **`ProactiveTriggerContext`** with chronological **`canonical_urls`** (and **`session_id`**, **`conversation_id`**, **`action_count`**, **`product_id`** when you have them). 2. **Evaluate** β€” **`result = registry.evaluate_first(ctx)`** using **`default_proactive_trigger_registry()`** or your own **`ProactiveTriggerRegistry([...])`**. If **`result`** is **`None`**, do not send. 3. **Gate (recommended)** β€” **Cooldown:** skip if **`now - last_fired_at < result.cooldown_s`** for **`(conversation_id, result.trigger_id)`** (you store **`last_fired_at`**). **FSM:** with [Agent session states](/sdk/agent-states), call **`can_show_proactive_with_reason()`** before sending; after a successful send, move to **`proactive_assistance`**. When the user never engages past **`result.interaction_timeout_s`**, use **[`run_proactive_idle_expiry`](/sdk/agent-states)** for chat surfaces (not **`expire_proactive_to_thinking_if_idle`** alone): implement **`ProactiveIdleExpiryHooks`** β€” **`delete_remote_chat_thread`** performs **`DELETE /conversations/{id}`** via **`build_intercom_delete_conversation_request`**, returns **`True`** only on **2xx** or **404**; then the orchestrator runs **`expire_proactive_to_thinking_if_idle`** and **`clear_local_chat_thread_state`** (session↔thread + persisted FSM β€” your host). See [Delete conversation (idle teardown)](#delete-conversation-idle-teardown). 4. **Build the POST** β€” **`headers = intercom_quick_reply_http_headers(access_token)`**. **`payload = build_intercom_quick_reply_reply_payload(admin_id=..., body=result.body, prompt_labels=list(result.reply_option_labels))`**. Empty **`prompt_labels`** is fine (intro-only). 5. **Send** β€” **`POST`** **`{INTERCOM_REST_API_BASE}/conversations/{conversation_id}/reply`** with **`json=payload`** and **`headers`**. **Inbound user messages vs the proactive chip:** After the connector shows a proactive **`quick_reply`**, the session FSM is **`proactive_assistance`**. If the user sends **normal chat text** that does **not** match the configured chip label, the connector moves the FSM to **`reactive_assistance`** and runs the usual RAG reply pipeline so a real question is answered. Tapping the chip (message text matches the label) still enters **`guidance_execution`** with the proactive expert-help flow id. ### Delete conversation (idle teardown) For **`run_proactive_idle_expiry`** β†’ **`delete_remote_chat_thread`**, build the HTTP target from pure helpers (Bearer + **`Intercom-Version: 2.15`** via **`INTERCOM_API_VERSION_DELETE_CONVERSATION`**): * **`build_intercom_delete_conversation_request(access_token, conversation_id, *, retain_metrics=True) -> tuple[str, dict[str, str]]`** β€” returns **`(url, headers)`** for **`client.delete(url, headers=headers)`**. * Lower-level: **`intercom_delete_conversation_url`**, **`intercom_delete_conversation_headers`**. Treat **404** as success (conversation already removed). Do not perform local session unlink or FSM deletion until **`DELETE`** succeeds β€” the SDK orchestrator encodes that order. ### Example (Python) ```python theme={null} from autoplay_sdk.integrations.intercom import ( INTERCOM_PROACTIVE_QUICK_REPLY_DEFAULT_BODY, INTERCOM_REST_API_BASE, build_intercom_quick_reply_reply_payload, intercom_quick_reply_http_headers, proactive_trigger_canonical_url_ping_pong, ) from autoplay_sdk.proactive_triggers import ( ProactiveTriggerContext, default_proactive_trigger_registry, ) registry = default_proactive_trigger_registry() def maybe_send_proactive_quick_reply( *, access_token: str, admin_id: str, conversation_id: str, canonical_urls: list[str | None], session_id: str = "", ) -> bool: """Returns True if a quick_reply was sent (after your cooldown/FSM checks).""" ctx = ProactiveTriggerContext( canonical_urls=canonical_urls, conversation_id=conversation_id, session_id=session_id, ) result = registry.evaluate_first(ctx) if result is None: return False # Apply cooldown using result.cooldown_s and result.trigger_id. # Optionally gate with AgentStateMachine.can_show_proactive_with_reason(). payload = build_intercom_quick_reply_reply_payload( admin_id=admin_id, body=result.body, prompt_labels=list(result.reply_option_labels), ) headers = intercom_quick_reply_http_headers(access_token) url = f"{INTERCOM_REST_API_BASE}/conversations/{conversation_id}/reply" # requests.post(url, json=payload, headers=headers, timeout=30) return True def minimal_ping_pong_only( *, access_token: str, admin_id: str, conversation_id: str, urls: list[str | None], ) -> bool: """Same POST shape without the proactive registry β€” predicate + default body only.""" if not proactive_trigger_canonical_url_ping_pong(urls): return False payload = build_intercom_quick_reply_reply_payload( admin_id=admin_id, body=INTERCOM_PROACTIVE_QUICK_REPLY_DEFAULT_BODY, prompt_labels=[], ) headers = intercom_quick_reply_http_headers(access_token) _url = f"{INTERCOM_REST_API_BASE}/conversations/{conversation_id}/reply" return True ``` ### Helpers reference (delivery) Quick replies **must** use **`Intercom-Version: Unstable`**. Do **not** reuse a numeric **`Intercom-Version`** from other Intercom REST calls. | Constant | Value | | ---------------------------------- | ----------------------------------------------------- | | `INTERCOM_HTTP_HEADER_VERSION` | `"Intercom-Version"` | | `INTERCOM_API_VERSION_QUICK_REPLY` | `"Unstable"` (alias: `INTERCOM_API_VERSION_UNSTABLE`) | | Helper | Purpose | | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | **`intercom_quick_reply_http_headers(access_token)`** | `Authorization`, `Content-Type`, **`Intercom-Version: Unstable`**. | | **`INTERCOM_PROACTIVE_QUICK_REPLY_DEFAULT_BODY`** | Default intro (`"Need my expert help?"`) when you build **`body`** without **`ProactiveTriggerResult`**. | | **`build_intercom_quick_reply_reply_payload(admin_id=…, body=…, prompt_labels=[…])`** | JSON for **`quick_reply`** with **`reply_options`** (≀ **`INTERCOM_PROACTIVE_PROMPTS_MAX`** = 3). | | **`normalize_intercom_quick_reply_labels`** | Strip / cap labels (used inside **`build_intercom_quick_reply_reply_payload`**). | | **`proactive_trigger_canonical_url_ping_pong(urls)`** | Low-level boolean predicate if you skip **`ProactiveTriggerRegistry`**. | Base URL: **`INTERCOM_REST_API_BASE`** (`https://api.intercom.io`). *** ## Package: `autoplay_sdk.proactive_triggers` Transport-agnostic **detection** (when to offer): **`ProactiveTriggerContext`**, **`ProactiveTriggerResult`** (**`trigger_id`**, **`body`**, optional **`reply_option_labels`**, **`metadata`**, **`interaction_timeout_s`**, **`cooldown_s`** β€” defaults **10s** / **30s**), **`ProactiveTriggerTimings`**, **`ProactiveTriggerEntity`**, **`ProactiveTriggerRegistry`** (**`evaluate_first`** / **`evaluate_all`**). Built-in **`trigger_id`** strings for shipped triggers are centralized in **`defaults`** (**`ProactiveTriggerIds`**, **`get_proactive_trigger_ids()`**, **`TRIGGER_ID_CANONICAL_URL_PING_PONG`**). **`CanonicalPingPongTrigger`** wraps **`proactive_trigger_canonical_url_ping_pong`**. **`default_proactive_trigger_registry()`** uses **`ProactiveTriggerEntity(CanonicalPingPongTrigger(), ProactiveTriggerTimings())`**. Default quick-reply intro copy for Intercom is **`DEFAULT_PROACTIVE_QUICK_REPLY_BODY`** in **`proactive_triggers.defaults`**, re-exported as **`INTERCOM_PROACTIVE_QUICK_REPLY_DEFAULT_BODY`** from **`integrations.intercom`**. See the dedicated **[Proactive triggers](/sdk/proactive-triggers)** page. Wire a firing **`ProactiveTriggerResult`** into **`build_intercom_quick_reply_reply_payload`** as in the [recipe](#recipe-end-to-end) above. ### Adding your own trigger 1. Implement **`ProactiveTrigger`**: **`trigger_id`** and **`evaluate(ctx) -> ProactiveTriggerResult | None`**. 2. Optionally wrap with **`ProactiveTriggerEntity(inner, ProactiveTriggerTimings(...))`** for custom timeouts / cooldown. 3. Register on **`ProactiveTriggerRegistry`** in priority order; call **`evaluate_first`** in production. ### Optional: connector LLM-generated button labels **`POST …/intercom/proactive/{product_id}`** (admin key) returns up to three short strings for **`reply_options`** text when you want LLM-suggested labels alongside URL ping‑pong (or other) proactive flows. Build the URL with **`intercom_connector_llm_prompt_labels_url`**, JSON body with **`build_connector_llm_prompt_labels_request_body`**. Product **`integration_config`** may include a **`proactive`** block β€” model it with **`IntercomProactivePolicyConfig.to_integration_config_fragment()`**. *** ## Connector endpoints (reference) | HTTP | Role | | --------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `POST /chatbot-webhook/{product_id}` | Primary path: Intercom signed webhooks for `conversation.user.created` / `conversation.user.replied`. | | `POST /sessions/link` | Optional: JSON body links session ↔ conversation (reactive snippet). | | `POST /intercom/proactive/{product_id}` | Optional: LLM-generated proactive quick-reply labels (admin key). | Webhook verification uses Intercom **`X-Hub-Signature-256`** and your app **client secret** in product config. *** ## Delivery stack in this repo * **`BaseChatbotWriter`** ([Support agent writer](/sdk/support-agent-writer)) β€” pre-link buffer, post-link debounce, shared note body format (`format_chatbot_note_header`, numbered action lines, binning). * **`IntercomChatbot`** (event connector `flows/chatbot/intercom.py`) β€” subclass; implements Intercom REST **admin notes** (`_post_note`, `_redact_part`) and webhook payload parsing for linking. For LLM summaries and redaction ordering, pair with **`AsyncAgentContextWriter`** as described on the support agent writer page. *** ## Related * [Support agent writer](/sdk/support-agent-writer) β€” `BaseChatbotWriter` contract and note format. * [Agent context](/sdk/agent-context) β€” summariser + `overwrite_with_summary` flow. * [Agent session states](/sdk/agent-states) β€” FSM gates when using the optional proactive connector endpoint with `conversation_id`. # Zendesk Source: https://developers.autoplay.ai/integrations/zendesk Zendesk-specific SDK helpers in autoplay_sdk.integrations.zendesk and how they map to the event connector. `ZENDESK_TRIGGER_TYPES` is the same tuple **`ZendeskChatbot`** uses in the event connector for session-link trigger routing, so what you configure in Zendesk Admin Center stays aligned with connector parsing and linking. `zendesk_chatbot_webhook_url` and `zendesk_chat_webhook_url` build the correct `/chatbot-webhook/{product_id}` and `/zendesk/chat/{product_id}` URLs respectively. `zendesk_auth_headers` produces the correct Basic-auth header for all Zendesk REST API calls. `build_zendesk_trigger_body` generates the JSON body string for Zendesk trigger actions with the right fields for each trigger type. *** ## Module: `autoplay_sdk.integrations.zendesk` Small, dependency-light helpers in `autoplay_sdk/integrations/zendesk.py` for trigger type constants, connector URL builders, API auth headers, and trigger payload construction. ### Trigger types Configure Zendesk triggers to send exactly these `trigger_type` values so session linking and LLM reply routing stay aligned with `ZendeskChatbot` parsing in the connector: | Name | Constant | Value | | ---------- | --------------------------------- | -------------- | | New ticket | `ZENDESK_TRIGGER_TYPE_NEW_TICKET` | `"new_ticket"` | | User reply | `ZENDESK_TRIGGER_TYPE_USER_REPLY` | `"user_reply"` | **Tuple for loops / validation:** `ZENDESK_TRIGGER_TYPES` β€” both strings above. Other `trigger_type` values are ignored by the connector. ### `zendesk_chatbot_webhook_url(connector_host, product_id) -> str` Builds the absolute HTTPS URL for Zendesk Trigger A (session link + internal note): `{origin}/chatbot-webhook/{product_id}` * **`connector_host`:** hostname (`event-connector-xxxx.onrender.com`) or full origin (`https://…`). Trailing slashes stripped; bare host gets `https://` prepended. * **`product_id`:** non-empty, no `/` (single path segment). Raises `ValueError` if host or product id is invalid. ### `zendesk_chat_webhook_url(connector_host, product_id) -> str` Builds the absolute HTTPS URL for Zendesk Trigger B (LLM reply): `{origin}/zendesk/chat/{product_id}` Same validation rules as `zendesk_chatbot_webhook_url`. ### `zendesk_auth_headers(email, api_token) -> dict[str, str]` Returns `Authorization` and `Content-Type` headers for Zendesk REST API calls: ```python theme={null} { "Authorization": f"Basic {base64(f'{email}/token:{api_token}')}", "Content-Type": "application/json", } ``` The `/token` literal between email and API token is required by Zendesk. Never hand-roll this β€” use this helper. ### `build_zendesk_trigger_body(trigger_type, *, include_user_message=False) -> str` Returns the JSON string to paste into the Zendesk trigger action body. Includes `ticket_id`, `requester_email`, and `trigger_type`. When `include_user_message=True`, also includes `"user_message": "{{ticket.latest_comment}}"` (required for Trigger B / Zendesk Messaging compatibility). ```python theme={null} build_zendesk_trigger_body("new_ticket") # β†’ '{"ticket_id": "{{ticket.id}}", "requester_email": "{{ticket.requester.email}}", "trigger_type": "new_ticket"}' build_zendesk_trigger_body("user_reply", include_user_message=True) # β†’ '{"ticket_id": "{{ticket.id}}", "requester_email": "{{ticket.requester.email}}", "trigger_type": "user_reply", "user_message": "{{ticket.latest_comment}}"}' ``` **Why `include_user_message` for Trigger B?** Zendesk Messaging tickets do not expose messages via `GET /api/v2/tickets/{id}/comments.json` β€” the API returns only internal notes for those tickets. Passing `{{ticket.latest_comment}}` inline is the only reliable source for the user's message. *** ## Example (Python) ```python theme={null} from autoplay_sdk.integrations.zendesk import ( ZENDESK_TRIGGER_TYPES, ZENDESK_TRIGGER_TYPE_NEW_TICKET, ZENDESK_TRIGGER_TYPE_USER_REPLY, build_zendesk_trigger_body, zendesk_auth_headers, zendesk_chat_webhook_url, zendesk_chatbot_webhook_url, ) import httpx, os subdomain = os.environ["ZENDESK_SUBDOMAIN"] email = os.environ["ZENDESK_EMAIL"] api_token = os.environ["ZENDESK_API_TOKEN"] product_id = os.environ["AUTOPLAY_PRODUCT_ID"] connector_url = os.environ["CONNECTOR_URL"] headers = zendesk_auth_headers(email, api_token) base = f"https://{subdomain}.zendesk.com/api/v2" context_endpoint = zendesk_chatbot_webhook_url(connector_url, product_id) chat_endpoint = zendesk_chat_webhook_url(connector_url, product_id) async def register_webhooks_and_triggers(): async with httpx.AsyncClient(base_url=base) as client: # Create context webhook β†’ /chatbot-webhook/{product_id} r = await client.post("/webhooks", headers=headers, json={"webhook": { "name": "Autoplay Context Webhook", "endpoint": context_endpoint, "http_method": "POST", "request_format": "json", "status": "active", "subscriptions": ["conditional_ticket_events"], }}) r.raise_for_status() context_webhook_id = r.json()["webhook"]["id"] # Create chat webhook β†’ /zendesk/chat/{product_id} r = await client.post("/webhooks", headers=headers, json={"webhook": { "name": "Autoplay Chat Webhook", "endpoint": chat_endpoint, "http_method": "POST", "request_format": "json", "status": "active", "subscriptions": ["conditional_ticket_events"], }}) r.raise_for_status() chat_webhook_id = r.json()["webhook"]["id"] # Trigger A: new ticket β†’ context webhook (internal note only) await client.post("/triggers", headers=headers, json={"trigger": { "title": "Autoplay β€” New Ticket", "active": True, "conditions": {"all": [ {"field": "update_type", "operator": "is", "value": "Create"}, ]}, "actions": [{"field": "notification_webhook", "value": [context_webhook_id, build_zendesk_trigger_body(ZENDESK_TRIGGER_TYPE_NEW_TICKET)]}], }}) # Trigger B: user reply β†’ chat webhook (LLM reply) # role:end-user silently fails for Zendesk Messaging β€” omit it. await client.post("/triggers", headers=headers, json={"trigger": { "title": "Autoplay β€” User Reply", "active": True, "conditions": {"all": [ {"field": "update_type", "operator": "is", "value": "Change"}, {"field": "comment_is_public", "operator": "is", "value": "true"}, ]}, "actions": [{"field": "notification_webhook", "value": [chat_webhook_id, build_zendesk_trigger_body( ZENDESK_TRIGGER_TYPE_USER_REPLY, include_user_message=True, )]}], }}) ``` *** ## Helpers reference | Helper | Purpose | | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | **`zendesk_auth_headers(email, api_token)`** | `Authorization: Basic …` + `Content-Type: application/json`. Never hand-roll Zendesk Basic auth. | | **`zendesk_chatbot_webhook_url(host, product_id)`** | Builds `/chatbot-webhook/{product_id}` β€” Trigger A endpoint. | | **`zendesk_chat_webhook_url(host, product_id)`** | Builds `/zendesk/chat/{product_id}` β€” Trigger B endpoint. | | **`build_zendesk_trigger_body(trigger_type, *, include_user_message=False)`** | JSON body string for Zendesk trigger actions. Set `include_user_message=True` for Trigger B. | | **`ZENDESK_TRIGGER_TYPES`** | `("new_ticket", "user_reply")` β€” all connector-recognized trigger types. | | **`ZENDESK_TRIGGER_TYPE_NEW_TICKET`** | `"new_ticket"` | | **`ZENDESK_TRIGGER_TYPE_USER_REPLY`** | `"user_reply"` | *** ## Connector endpoints (reference) | HTTP | Role | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST /chatbot-webhook/{product_id}` | Primary session-link path: receives `new_ticket` from Zendesk, resolves PostHog session by requester email, flushes buffered events as an internal note (`public: false`). | | `POST /zendesk/chat/{product_id}` | LLM reply path: receives `user_reply` with inline `user_message`, builds RAG context, calls Claude, posts public reply (`public: true`). Returns `200` immediately; LLM + Zendesk write run as a background task. | Inbound webhook signature verification uses `X-Zendesk-Webhook-Signature` (HMAC-SHA256) when `signing_secret` is present in `integration_config`. If absent, all inbound requests are accepted. *** ## Delivery stack in this repo * **`BaseChatbotWriter`** ([Support agent writer](/sdk/support-agent-writer)) β€” pre-link buffer, post-link debounce, shared note body format (`format_chatbot_note_header`, numbered action lines, binning). * **`ZendeskChatbot`** (event connector `flows/chatbot/zendesk.py`) β€” subclass; implements `_post_note` (Zendesk `PUT /api/v2/tickets/{id}.json` with `public: false`, exponential retry) and `_redact_part` (no-op β€” Zendesk comments are immutable). Registered as `CHATBOT_REGISTRY["zendesk"]`. For LLM summaries, pair with **`AsyncAgentContextWriter`** as described on the support agent writer page. *** ## Related * [Support agent writer](/sdk/support-agent-writer) β€” `BaseChatbotWriter` contract and note format. * [Agent context](/sdk/agent-context) β€” summariser + `overwrite_with_summary` flow. * `chatbot-zendesk` skill β€” full setup guide: preflight, webhook + trigger registration, smoke test, and failure modes. # MCP server Source: https://developers.autoplay.ai/mcp/server The recommended way for any AI agent to pull a user's live in-app activity on demand β€” one MCP endpoint, agent-agnostic, Bearer-authenticated. The **Autoplay MCP server** is a single [Model Context Protocol](https://modelcontextprotocol.io) endpoint that exposes a user's recent in-app activity as a tool. Any MCP-speaking agent connects to it and **pulls** that activity on demand β€” at the exact moment it needs context to answer. It's **agent-agnostic**: Intercom **Fin** is just one example client. Anything that speaks MCP β€” a custom agent, an IDE assistant, the MCP Inspector β€” can connect with the same URL, the same Bearer token, and the same tool. ⭐ **Start here β€” MCP is the recommended way** for an agent to read live activity. If your agent can't speak MCP, there's an optional **[REST API](/activity/overview)** that returns the exact same data β€” but reach for MCP first. You never need both. ## πŸ”Œ Connect to the server | | | | ------------- | ----------------------------------------------------------- | | **Endpoint** | `https://mcp.autoplay.ai/mcp` | | **Transport** | **Streamable HTTP** β€” the transport your client must select | | **Auth** | `Authorization: Bearer YOUR_MCP_KEY` | `YOUR_MCP_KEY` is the `mcp_key` from your [Quickstart](/quickstart) product registration. * **Invalid or missing token β†’** the tool call fails with *"Invalid or missing API key"*. * **Token valid but scoped to another product β†’** it fails with *"API key does not match product\_id"*. The key's `external_id` must equal the `product_id` you pass to the tool, so a key for product A can't read product B. The canonical endpoint is `https://mcp.autoplay.ai/mcp` (no trailing slash). Some MCP clients are strict about trailing slashes β€” use the bare `/mcp` form. ## πŸ› οΈ The `get_live_user_activity` tool The server exposes one tool today. **Parameters** | Parameter | Type | Default | Meaning | | ------------ | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `product_id` | string | β€” | Your Autoplay product id (`YOUR_PRODUCT_ID`). Must match the Bearer key's `external_id`. | | `user_id` | string | β€” | The **stable** user identifier β€” the **same id you pass to `posthog.identify(...)`**. See the [identity note](#identity-the-user-id-must-match) below. | | `limit` | integer | *none* | Max recent actions to return. Omitted or `<= 0` falls back to the configured cap (50). | **Description** β€” this is the text the server advertises to the agent verbatim; it's what the model reads to decide when to call the tool: ```text theme={null} Return a user's recent ("live") in-app activity (slim actions) for a product. Reads the durable per-user Redis store. Actions are ordered oldest -> newest. Args: product_id: Product identifier. user_id: Stable user identifier (e.g. PostHog distinct_id). limit: Max recent actions to return; defaults to the configured cap when omitted or <= 0. ``` **Example result** β€” the tool returns this envelope, with `actions` ordered **oldest β†’ newest**: ```json theme={null} { "product_id": "YOUR_PRODUCT_ID", "user_id": "user_12345", "count": 3, "as_of": 1736940750.881, "actions": [ { "type": "pageview", "title": "Page Load: Dashboard", "description": "User landed on the dashboard page", "timestamp_start": 1736940685.103, "timestamp_end": 1736940691.250, "raw_url": "https://app.example.com/dashboard", "canonical_url": "https://app.example.com/dashboard", "index": 0 }, { "type": "click", "title": "Click Export Csv", "description": "User clicked the Export Csv button on the dashboard page", "timestamp_start": 1736940691.250, "timestamp_end": 1736940705.610, "raw_url": "https://app.example.com/orders/12345", "canonical_url": "https://app.example.com/orders/:id", "index": 1 }, { "type": "submit", "title": "Submit Payment Form", "description": "User submitted the Payment form on the checkout page", "timestamp_start": 1736940705.610, "timestamp_end": 1736940705.610, "raw_url": "https://app.example.com/checkout", "canonical_url": "https://app.example.com/checkout", "index": 2 } ] } ``` Each action carries `type`, `title`, `description`, `timestamp_start`, `timestamp_end`, `raw_url`, `canonical_url`, and `index`. (The `user_id` lives on the envelope, not inside each action.) ## πŸ€– Connect any MCP client Point any MCP client at the server with the Streamable HTTP transport and the Bearer header. Exact config keys vary by client, but the three values are always the same β€” **URL**, **transport**, **`Authorization` header**. A typical client config looks like: ```json theme={null} { "mcpServers": { "autoplay": { "url": "https://mcp.autoplay.ai/mcp", "transport": "streamable-http", "headers": { "Authorization": "Bearer YOUR_MCP_KEY" } } } } ``` ### Test it with the MCP Inspector The quickest way to confirm the server is reachable and your token works: ```bash theme={null} npx @modelcontextprotocol/inspector ``` Then in the Inspector UI: 1. Set **Transport Type** to **Streamable HTTP**. 2. Set **URL** to `https://mcp.autoplay.ai/mcp`. 3. Under request headers, add **`Authorization`** = `Bearer YOUR_MCP_KEY`. 4. Click **Connect**. The **`get_live_user_activity`** tool appears in the tool list. 5. Call it with a real `product_id` and a `user_id` you've identified β€” you should get the activity envelope back. The MCP Inspector won't send the `Authorization` header unless you add it explicitly β€” by default MCP clients strip it. If `get_live_user_activity` returns *"Invalid or missing API key"*, the header isn't reaching the server. ## πŸ’¬ Using it with Intercom Fin Intercom Fin is one MCP client among many. To wire Fin to this server (and set up the Messenger JWT identity verification Fin needs to pass a trusted `user_id`), follow the dedicated recipe: See **[the Intercom Fin recipe](/recipes/intercom-tutorial/step-1-connect-real-time-events)** for the full Fin setup β€” data connector, this MCP server, and the Messenger JWT identity step. ## Identity β€” the `user_id` must match This is the linchpin everywhere activity is read. The store keys activity by `user_id`, so a client only gets useful results when it asks for the **same** `user_id` that activity was stored under β€” the id you pass to `posthog.identify(...)` in your frontend. For agents that carry their own user identity (like Fin behind Intercom's Messenger JWT), make sure that verified identity equals the PostHog `identify` id. See the [identity guidance in the Fin recipe](/recipes/intercom-tutorial/step-1-connect-real-time-events#verify-identity-with-a-messenger-jwt) and the "identity must match across all three layers" note in the [Quickstart](/quickstart). **Retention** β€” this is short-lived "live" memory, not an archive: * **24-hour TTL** (`ACTIVITY_TTL_S` = `86400`) β€” activity older than a day expires. * **50 actions max** per user (`ACTIVITY_MAX_EVENTS` = `50`) β€” older ones are trimmed. # πŸš€ Quickstart Source: https://developers.autoplay.ai/quickstart Stream real-time user events into your support AI agents in a couple lines of code. ## ⚑ Add this skill Add the shared Autoplay SDK foundation skill for credentials and live-activity reads. ```bash CLI theme={null} uvx --from autoplay-sdk autoplay-install-skills ``` View the docs β†’ Fetch this skill directly so your coding agent gets the same Autoplay SDK setup instructions. ```bash cURL theme={null} curl -s https://developers.autoplay.ai/autoplay-core/SKILL.md ``` View the skill β†’ ## Install the SDK **Prerequisites β€” Python 3.10 or later required.** Check your Python version: ```bash theme={null} python3 --version ``` If Python is not installed, download it from [python.org](https://www.python.org/downloads/) or install via Homebrew: ```bash theme={null} brew install python3 ``` Then create and activate a virtual environment before installing: ```bash theme={null} python3 -m venv .venv source .venv/bin/activate ``` Check your Python version: ```powershell theme={null} python --version ``` If Python is not installed, download it from [python.org](https://www.python.org/downloads/) and run the installer β€” check **"Add Python to PATH"** during setup. Then create and activate a virtual environment before installing: ```powershell theme={null} python -m venv .venv .venv\Scripts\Activate.ps1 ``` If you see a permissions error, run this once first: `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser` Run this command once from your project root (with your venv active): ```bash theme={null} pip install autoplay-sdk # OR uv add autoplay-sdk ``` Install [Agent skills](/get-started/agent-skills) for Cursor/Claude so your assistant follows the correct integration pattern for your stack. See [Pricing](/who-we-are/pricing) for plans. ## 🎯 Step 1 β€” Connect your session replay provider Autoplay needs a live stream of what each user is doing before anything downstream can use it as context. Pick whichever tool you already have β€” already wired up? The tool's page tells you how to confirm it in seconds instead of redoing setup.
PostHog β†’
Connect an existing PostHog project
Amplitude β†’
Connect an existing Amplitude project
FullStory Coming Soon
Full setup guide coming soon
Datadog Coming Soon
Full setup guide coming soon
Pendo Coming Soon
Full setup guide coming soon
## πŸ€– Step 2 β€” Connect your AI support agent This is where Autoplay actually does its job β€” giving your existing chatbot live context on what the user's been doing, pulled over MCP the moment it needs it.
Intercom β†’
Connect Fin via MCP
Maven β†’
Connect via MCP
Ada Coming Soon
Connect via API
Botpress Coming Soon
Connect via API
Dify β†’
Connect via MCP
Crisp AI β†’
Connect via MCP
Landbot Coming Soon
Connect via API
Rasa β†’
Connect via API
Inkeep β†’
Connect via API
Tidio β†’
Connect via API
Plain β†’
Connect via API
Zendesk Coming Soon
Full setup guide coming soon
## πŸ”” Step 3 β€” Add the proactive layer Steps 1 and 2 make your AI support agent **reactive** β€” it can answer with live context, but only when a user asks. This step makes it **proactive**: Autoplay.js watches the same activity stream in the browser and surfaces the next best action β€” a message or a tour β€” the moment a milestone happens, without the user asking "what's next?". Autoplay.js is the piece that actually listens for nudges and dispatches them β€” through your configured tour provider, or as Autoplay's own nudge card when no provider is set up. Every proactive path (your chatbot's prompt, a plain nudge card, a tour) flows through this one connection. Before you start, add the credentials created when you registered your product in Step 1 to your server environment: ```bash theme={null} AUTOPLAY_PRODUCT_ID=your-product-id AUTOPLAY_MCP_KEY=your-mcp-key AUTOPLAY_MCP_BASE_URL=https://mcp.autoplay.ai ``` * `AUTOPLAY_PRODUCT_ID` is the issued `product_id` returned by Autoplay registration, not your PostHog or Amplitude project id. * `AUTOPLAY_MCP_KEY` is the `mcp_key` returned by `onboard_product` during that same setup. Retrieve it from the saved terminal output or the secure 1Password link. If you no longer have it, contact Autoplay rather than registering a second product. Keep `AUTOPLAY_MCP_KEY` server-side. Never expose it in frontend code or a public environment variable. ### Configure your onboarding Autoplay uses one onboarding config, keyed by your `product_id`, to decide which milestones to detect and which experience to deliver: ```json theme={null} { "product_id": "your-product-id", "workflows": [ { "key": "connect_account", "title": "Connect a social account", "sort_order": 10, "required": true, "detect": "The user connected a social account β€” e.g. picked a platform and confirmed connecting a handle." } ], "tours": [ { "workflow_key": "connect_account", "name": "Connect your account", "flow_id": "your-provider-flow-id" } ], "tour_provider": "appcues", "exploration_gate": { "min_seconds_on_app": 60, "min_distinct_features": 3, "welcome_cooldown_s": 600, "max_proactive_support": 3 }, "nudge_delivery": { "default": { "provider": "autoplay" } } } ``` `workflows` define the milestones Autoplay detects from live activity. `tours` map each workflow to a flow in your configured `tour_provider`; omit them if you only want Autoplay nudge cards. Onboarding configuration is not self-serve yetβ€”share this JSON with the Autoplay team in [Discord](https://discord.gg/jCbR2tQA5). ### 1. Create a nudge-token endpoint Add a server route that exchanges your MCP key for a short-lived token scoped to the current user: ```jsx theme={null} export async function POST(request) { const { userId } = await request.json(); const response = await fetch( `${process.env.AUTOPLAY_MCP_BASE_URL}/nudges/${process.env.AUTOPLAY_PRODUCT_ID}/token`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.AUTOPLAY_MCP_KEY}`, }, body: JSON.stringify({ user_id: userId }), } ); return Response.json(await response.json()); } ``` ### 2. Connect the nudge stream In your frontend, load Autoplay.js and connect it for the signed-in user. `AUTOPLAY_MCP_BASE_URL` is the production MCP origin, without the `/mcp` transport path: ```javascript theme={null} const AUTOPLAY_MCP_BASE_URL = "https://mcp.autoplay.ai"; const { Autoplay } = await import(`${AUTOPLAY_MCP_BASE_URL}/autoplay.js`); const nudgeHandle = Autoplay.connectNudges({ baseUrl: AUTOPLAY_MCP_BASE_URL, productId: "your-product-id", userId: "your-signed-in-user-id", getToken: async () => { const response = await fetch("/api/nudge-token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId: "your-signed-in-user-id" }), }); const { token } = await response.json(); return token; }, onNudge: handleNudge, onError: (error) => console.warn("[autoplay] nudge stream error:", error), }); ``` Use the same stable user ID your app sends to the session replay provider. `getToken` should fetch a fresh token whenever it is called. If you use a third-party tour provider, load and identify its SDK before calling `connectNudges` so it is ready when a tour arrives. ### 3. Dispatch nudges Every nudge includes a `nudge_type`. Tour nudges hand off to the provider and flow configured above; other nudges render an Autoplay card: ```javascript theme={null} function handleNudge(nudge) { if (nudge.nudge_type === "tour") { Autoplay.dispatchNudge(nudge); nudgeHandle.report("chat_closed"); return; } Autoplay.dispatchNudge(nudge, { onOpen: () => nudgeHandle.report("proactive_accepted_chat"), onDismiss: () => nudgeHandle.report("proactive_dismissed"), }); } ``` `Autoplay.dispatchNudge` returns a boolean indicating whether an experience rendered, which is useful to log during setup. A nudge card is the provider-agnostic fallback: Autoplay can offer help without requiring you to build a tour flow. ### 4. Own your tour or chat surface (optional) By default Autoplay drives the built-in tour providers (Usertour, Appcues) and renders its own nudge card. If you own your tour system or chat UI, register callbacks with `Autoplay.configure` **before** `connectNudges`, and Autoplay routes nudges to them instead: ```javascript theme={null} Autoplay.configure({ providers: { // Your own tour system (Joyride, a custom highlighter, …). tour: { // Each callback also receives the full `nudge` payload as a second arg // (nudge_type, next_workflow, delivery.ask, …) if you want to branch on it. launch(delivery, nudge) { yourTours.start(delivery.flow_id); }, }, // A chat surface you built on your agent's API. chat: { deliver(content, nudge) { chat.injectAssistantMessage(content); }, // show authored copy β€” NO LLM call dispatch(text, nudge) { chat.openAndSend(text); }, // send a real user turn to your agent close(opts) { chat.hide(); }, // dismiss for a tour handoff }, }, }); ``` Resolution order per nudge: your `tour.launch` / `chat.deliver` first, then the built-in providers, then the Autoplay card as a fallback. A callback that returns `false` (or throws) falls through to the next option; returning nothing counts as handled. `chat.deliver` must **not** trigger an LLM turn β€” it only displays the pre-authored copy; use `chat.dispatch` for a real turn (e.g. when the user accepts a nudge). Register `chat.close` so a tour can dismiss your chat during handoff. Registering a chat surface **takes over chat nudges from the card in Step 3**: when `chat.deliver` handles a nudge, the Autoplay card is suppressed, so the card's `onOpen` / `onDismiss` reporting (`proactive_accepted_chat` / `proactive_dismissed`) no longer fires for chat nudges. Emit those from your own callbacks instead, using the `report()` handle returned by `connectNudges` β€” e.g. `nudgeHandle.report("proactive_accepted_chat")` inside `chat.dispatch` (the user accepted), and `nudgeHandle.report("proactive_dismissed")` when the user dismisses your surface. Tour nudges still report as before. ### 5. Verify the connection Sign in to your app, open the browser console, and confirm the nudge stream connects without calling `onError`. Autoplay can now dispatch provider-backed experiences and its own nudge cards. If you add visual guidance in Step 4, initialize that provider before calling `connectNudges`. Autoplay.js only delivers the nudge at the right moment β€” something still has to decide *what to say* and *what shows up on screen*. Most AI support agents (Maven, Intercom's Fin, and others) don't do this on their own: they answer when asked, but they don't watch live activity or open a message by themselves. To make your chatbot proactive: 1. **Add the proactive prompt for your chatbot platform.** This teaches the agent to offer the next step instead of only answering questions β€” follow the recipe for your platform from Step 2 above (e.g. Maven's **Add proactive layer** step). 2. **Add a way to show the offer.** Most chatbot platforms don't render their own proactive UI, so use a **user tour provider** (e.g. Appcues) to display the box the user actually sees and can accept or dismiss β€” or skip the tour provider and let Autoplay's own nudge card handle it (see the **Connect Autoplay.js** tab). Full walkthrough β€” the proactive prompt plus the Appcues tour hand-off β€” in Maven β†’ Add proactive layer. Once connected, a dispatched nudge card looks like this β€” Autoplay offering the next step right after the AI agent confirms a milestone, with the user free to accept or dismiss: Autoplay nudge card proactively offering to walk the user through setting their posting schedule right after they connect a social account If you're using Autoplay's built-in nudge card instead of a tour provider, its copy is customizable with a couple of options: ```javascript theme={null} Autoplay.dispatchNudge(nudge, { brandName: "Acme", // replaces the "Autoplay" label on the card β€” default "Autoplay" ctaLabel: "Show me", // replaces the accept-button text β€” default "Show me how" onOpen: () => nudgeHandle.report("proactive_accepted_chat"), onDismiss: () => nudgeHandle.report("proactive_dismissed"), }); ``` The card's body text comes from `nudge.message` if your onboarding config sets one; otherwise Autoplay generates it from the milestone (e.g. "Nice progress! πŸŽ‰ Want me to walk you through **Setting your posting schedule**?"). Position, color, and layout are fixed today β€” `brandName` and `ctaLabel` are the only visual customization available. ## πŸ—ΊοΈ Step 4 β€” Connect visual guidance Optional. If you want to give your users the ultimate level of guidance, add visual guidance on top β€” pop-up tours today, and browser agents soon. Wire up a tour provider β€” Appcues, Usertour, Pendo, and more β€” so that once a user asks for more help from Step 3's proactive message, the right tour launches from the same event stream. Trigger Appcues, Pendo, Usertour, and more once a user asks for more guidance β€” optional ## πŸ—ΊοΈ What's next Go deeper on the SDK: Turn live activity and onboarding context into concise, useful guidance without interrupting or repeating the user. Explore all fields on ActionsPayload and SummaryPayload Embed events into a vector store in real time Connect any agent to pull live activity via the MCP tool Combine real-time events, memory, and golden paths For structured logging and `extra` field conventions used across the SDK, see [Logging](/sdk/logging). Release history is on the [Changelog](/changelog). # Ada tutorial Source: https://developers.autoplay.ai/recipes/ada/index Pull live user activity with the Autoplay SDK and surface it to an Ada agent on demand for real-time context. Pull live user activity with the Autoplay SDK and surface it to an Ada agent on demand for real-time context. *** ## πŸ“‹ Prerequisites Complete the [Quickstart](/quickstart). You should have: * **PostHog (or Amplitude) in the browser** β€” snippet installed, `identify` / `setUserId` setting a stable `user_id` on login (and email after login if you use it) * **Your `product_id` and `mcp_key`** β€” printed by `onboard_product` in the Quickstart * **`autoplay-sdk` installed** β€” and a successful live-activity read (Step 4 of the Quickstart) * **An Ada account** β€” with a bot created in the [Ada dashboard](https://app.ada.cx) *** ## The building blocks Follow the steps in order β€” each page stands alone so you can ship incrementally. 1. **[Connect real-time events](./step-1-connect-real-time-events)** β€” Pull live user activity into Ada and wire your bot to answer with that context. *(Coming soon)* 2. **[Define proactive triggers](./step-2-define-proactive-triggers)** β€” Proactively message users in Ada based on what they're doing in your product. *(Coming soon)* # Connect real-time events Source: https://developers.autoplay.ai/recipes/ada/step-1-connect-real-time-events Pull a user's live activity from Autoplay on demand and inject it into Ada's AI Agent via metaFields. ## ⚑ Add this skill Add the Autoplay Ada skill for an existing Ada AI support agent setup. ```bash CLI theme={null} uvx --from autoplay-sdk autoplay-install-skills --chatbot ada ``` View the docs β†’ Fetch this skill when a customer already uses Ada and wants its AI support agent to consume Autoplay live user activity. ```bash cURL theme={null} curl -s https://developers.autoplay.ai/chatbot-ada/SKILL.md ``` View the skill β†’ Pull a user's live activity from Autoplay on demand and inject it into Ada's AI Agent via `metaFields` β€” so every conversation is grounded in what the user is actually doing in your product. ## ✨ Final result ```text theme={null} User: Is there a way to automatically notify my team when a task is done? Ada: Yes! You can use Automations for this. Go to the Automations centre on your board and set up a rule like "When status changes to Done, notify someone." User: Where is the Automations centre? ``` *No idea the user has already done this manually 9 times. Forces a follow-up.* ```text theme={null} Live context: Board 4452109 Β· status β†’ "Done" Γ— 9 Β· no automation visits User: Is there a way to automatically notify my team when a task is done? Ada: Looks like you've been marking items Done manually β€” you can automate that with one rule. Click the ⚑ lightning bolt at the top of this board β†’ search "status changes" β†’ pick "When status changes to Done, notify someone" and select your team. Takes 30 seconds. ``` *Spots the repetitive behaviour, gives the exact click path, skips the follow-up.* **How this works** β€” PostHog or Amplitude sends events to Autoplay's ingest endpoint as users act in your product. There's no stream to subscribe to: your backend pulls a user's recent activity **on demand** β€” a single REST call to Autoplay's live-activity endpoint, keyed by `product_id` + `user_id` β€” the moment a user opens Ada. That response is formatted into `metaFields` and passed to the Ada embed before β€” or during β€” a chat session. Ada's AI Agent reads these variables inside your bot's Processes to personalise responses. *** ## πŸ“‹ Prerequisites Before starting, confirm the following: * **Ada already integrated on your web app** β€” the Ada embed script is installed, your bot handle is configured, and Ada is opening chat sessions successfully. * **PostHog or Amplitude already set up** β€” complete the [Quickstart](/quickstart) first, with `posthog.identify(user.id)` (or `amplitude.setUserId(user.id)`) setting a stable `user_id` on login. * **Your `product_id` and `mcp_key`** β€” printed by `onboard_product` in the Quickstart. * **`autoplay-sdk` installed** β€” `pip install autoplay-sdk` * **A backend you can add one route to** β€” FastAPI is shown below, but any framework that can make an outbound HTTP call works. **How identity works** β€” Autoplay keys a user's live activity by the stable `user_id` your activity source identifies them with β€” the same id you pass to `posthog.identify(user.id)` or `amplitude.setUserId(user.id)`. There's no session id in the read API: every lookup is `GET /users/{product_id}/{user_id}/live-activity`, scoped only by product and user. When the user opens Ada, your frontend passes that same `user_id` to your `/context/{user_id}` endpoint (Step 2 below), which fetches their live activity and returns it as `metaFields`. If a user is anonymous (not yet identified), your activity source still records events under an anonymous id β€” Autoplay will have context for that id, but it won't be linked to a real user account until you call `identify` / `setUserId`. ```javascript theme={null} posthog.identify(user.id); // same id you must pass to /context/{user_id} ``` ```javascript theme={null} amplitude.setUserId(user.id); // same id you must pass to /context/{user_id} ``` **How the pieces fit:** your app identifies the user in PostHog/Amplitude β†’ Autoplay stores activity under that `user_id` β†’ your frontend passes the same `user_id` to `/context/{user_id}` before opening Ada β†’ the returned `metaFields` are populated with that user's real activity. *** ## The building blocks 1. **Define Variables in Ada** β€” Create the Variables your bot Processes will read. Done once in the Ada dashboard. 2. **Serve context to the Ada SDK** β€” Expose a lightweight endpoint that pulls a user's live activity from Autoplay on demand (a single REST call β€” no listener process, no local cache) and passes it as `metaFields`. 3. **Use Variables in your Ada Processes** *(Step 2 β€” coming soon)* β€” Reference the injected variables in your bot's Process conditions and responses. *** ## πŸ”§ Step 1 β€” Define Variables in Ada When you pass `metaFields` to the Ada embed, those keys become Variables your bot Processes can read. Create these in your Ada dashboard under **Build β†’ Variables β†’ + New Variable**. | Variable name (= `metaFields` key) | Type | Description | | ---------------------------------- | ------ | ------------------------------------------------------------- | | `user_id` | String | Stable id from your activity source β€” keys the backend lookup | | `current_page` | String | URL the user is on, taken from their most recent action | | `recent_actions` | String | User's recent in-app actions as a numbered list | Ada `metaFields` keys must **not** include whitespace, emojis, special characters, or periods. Use underscores as shown above. Keys are case-sensitive and must match exactly between the backend response and the frontend `metaFields` object. We dropped `session_summary` from this table. The live-activity endpoint already returns a bounded, recent window (the `limit` query param), so there's no separate summarisation step to run or store. If you want a narrative summary for longer histories, generate it inside your own `/context/{user_id}` handler and add it back as a metaField β€” it's optional, not required for this integration to work. *** ## 🌐 Step 2 β€” Serve context to the Ada SDK Your backend exposes one route: `GET /context/{user_id}`. It calls Autoplay's live-activity endpoint synchronously, formats the result, and returns it. No stream to consume, no worker process, no Redis β€” the call happens the moment your frontend needs it. ### Install dependencies ```bash theme={null} pip install autoplay-sdk fastapi uvicorn httpx ``` ### FastAPI context endpoint ```python theme={null} # api.py import httpx from fastapi import FastAPI app = FastAPI() CONNECTOR_URL = "https://mcp.autoplay.ai" # origin of your mcp_url, without /mcp PRODUCT_ID = "your-product-id" # from onboard_product (Quickstart) MCP_KEY = "your-mcp-key" # from onboard_product (Quickstart) @app.get("/context/{user_id}") async def context_for_user(user_id: str): async with httpx.AsyncClient() as client: res = await client.get( f"{CONNECTOR_URL}/users/{PRODUCT_ID}/{user_id}/live-activity", params={"limit": 10}, headers={"Authorization": f"Bearer {MCP_KEY}"}, ) if res.status_code != 200: return {"user_id": user_id, "current_page": "", "recent_actions": ""} actions = res.json().get("actions", []) current_page = actions[-1]["canonical_url"] if actions else "" recent_actions = "\n".join( f"[{i + 1}] {a['title']} β€” {a['description']}" for i, a in enumerate(actions) ) return { "user_id": user_id, "current_page": current_page, "recent_actions": recent_actions, } ``` Protect this endpoint in production. Validate with a session cookie or short-lived signed token tied to the logged-in user β€” not the raw `user_id` alone, which would let any caller read any user's context. *** ### Add the Ada embed script Add `data-lazy` to your Ada embed script. This prevents Ada from initialising until you call `adaEmbed.start()`, so you can fetch the user's live activity context first and pass it in on the first open. ```html theme={null} ``` *** ### Open Ada with live context ```javascript theme={null} // support.js // ── User identity ─────────────────────────────────────────────────────── // This MUST be the same stable id you pass to posthog.identify(user.id) or // amplitude.setUserId(user.id) β€” not a session id, not an email. Resolve it // from your own auth/session state. function getCurrentUserId() { return window.currentUser?.id ?? null; } // ── Ada initialisation state ────────────────────────────────────────────── // Do NOT use `window.adaEmbed` to check if Ada is ready. // With data-lazy, window.adaEmbed exists as soon as the embed script loads β€” // BEFORE start() is called. Calling setMetaFields() or toggle() at that point // silently fails and the widget never opens. // Use this flag instead, set only inside adaReadyCallback. let adaReady = false; // ── Main: wire to your "Contact Support" button ─────────────────────────── async function openAdaWithContext() { const userId = getCurrentUserId(); let meta = { user_id: userId ?? 'anonymous', current_page: window.location.pathname, recent_actions: '', }; if (userId) { try { const res = await fetch(`/context/${userId}`); if (!res.ok) throw new Error(`Context endpoint ${res.status}`); const ctx = await res.json(); meta = { ...meta, ...ctx }; } catch (e) { // Fail open β€” Ada opens without extra context rather than not opening at all console.warn('Autoplay context unavailable:', e); } } if (adaReady) { // Ada already initialised β€” update metaFields in-place then toggle open await window.adaEmbed.setMetaFields(meta); await window.adaEmbed.toggle(); return; } // First open β€” initialise Ada with metaFields pre-loaded await window.adaEmbed.start({ handle: 'YOUR-BOT-HANDLE', metaFields: meta, adaReadyCallback: () => { adaReady = true; // mark ready BEFORE toggling window.adaEmbed.toggle(); }, }); } // ── SPA: update current_page on every navigation ───────────────────────── // React Router and other SPA frameworks navigate via history.pushState(), // which does NOT fire the popstate event. Patch pushState to emit a custom // event so all navigations are caught β€” not just browser back/forward. (function patchHistory() { const _push = history.pushState.bind(history); history.pushState = function (...args) { _push(...args); window.dispatchEvent(new Event('spa:navigate')); }; })(); window.addEventListener('spa:navigate', async () => { if (!adaReady) return; await window.adaEmbed.setMetaFields({ current_page: window.location.pathname }); }); // ── Ada event subscriptions ─────────────────────────────────────────────── // adaSettings must be defined BEFORE the ``` If you're loading it yourself (e.g. in a Next.js effect) instead of a static ` ``` ## 2. Identify the user Call this once the user is authenticated. Use the same `userId` you send to PostHog so Autoplay can match sessions correctly. ```javascript theme={null} chmln.identify(userId, { email: user.email, name: user.displayName, created: user.createdAt, // ISO 8601 role: user.role, }); ``` ## 3. Create a tour in Chameleon 1. In the Chameleon dashboard go to **Experiences β†’ Tours** and click **+ Create a Tour**. Chameleon Tours list 2. Select a tour type β€” **Announcement** starts automatically when a user meets targeting rules; **Walkthrough** starts manually via a Launcher or short-link. Chameleon Select Tour type 3. Build your steps, set any targeting rules (page URL, user segment, etc.), and publish the tour. Chameleon Review & publish 4. Note the **Tour ID** displayed below the tour name (`ID: `). Pass this as the `flow_id` when triggering. ## 4. Trigger the tour In your `onmessage` handler from [step 3 of the manual path](/recipes/user-tour/overview#3-understanding-the-payload), add the Chameleon trigger call: ```javascript theme={null} events.onmessage = (event) => { const payload = JSON.parse(event.data); if (payload.type !== "usertour_trigger") return; const mySessionId = posthog?.get_session_id?.() ?? null; if (!mySessionId || payload.session_id !== mySessionId) return; chmln.show(payload.flow_id); }; ``` The `flow_id` in the payload maps to the **Tour ID** of your Chameleon tour, visible in the URL when editing the tour in the Chameleon dashboard. # Connect real-time events Source: https://developers.autoplay.ai/recipes/crisp-ai/step-1-connect-real-time-events Give Hugo live awareness of what each user is doing in your app β€” Hugo pulls it on demand via the Autoplay MCP server. ## ⚑ Add this skill Add the Autoplay Crisp Hugo skill for an existing Crisp Hugo AI support agent setup. ```bash CLI theme={null} uvx --from autoplay-sdk autoplay-install-skills --chatbot crisp ``` View the docs β†’ Fetch this skill when a customer already uses Crisp Hugo and wants its AI support agent to consume Autoplay live user activity. ```bash cURL theme={null} curl -s https://developers.autoplay.ai/chatbot-crisp/SKILL.md ``` View the skill β†’ **Hugo** (Crisp's AI agent) pulls a user's recent in-app activity on demand via the **Autoplay MCP server** β€” Hugo calls it the moment it needs context to answer. One MCP connection, one tool (`get_live_user_activity`), and a verified identity so Hugo asks for the right user. This guide assumes you **already have Hugo set up** in your Crisp workspace. If you don't have it configured yet, see [Crisp's own Hugo setup guide](https://help.crisp.chat/en/article/getting-started-with-hugo-ai-agent-w6gbux/) to set it up first, then come back here. Adding external MCP servers requires Crisp's **Plugin-tier** API access β€” check your plan before starting. **What Autoplay needs from your Crisp setup:** * **`mcp_key`** β€” printed by your own `onboard_product` call (see [Quickstart](/quickstart)), not something Crisp or your activity provider issues. * **A stable `user_id`** you can push into `session:data` β€” the same id your activity source (PostHog/Amplitude) identifies the user with. ## 🎬 End-to-end walkthrough