> ## Documentation Index
> Fetch the complete documentation index at: https://developers.autoplay.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Amplitude skill

> Connects an existing Amplitude project to Autoplay live activity using the @amplitude/unified SDK and Amplitude HTTP Event Streaming. Covers verifying autocapture and stable user IDs, running onboard_product for ingest and MCP credentials, and pointing Amplitude's event stream at Autoplay. Use when the customer already uses Amplitude, @amplitude/unified, amplitude.init, amplitude.setUserId, or asks how to send Amplitude activity to Autoplay.

# Session Replay Provider — Amplitude

> Read `autoplay-core` first for Autoplay install, credentials, and the pull-based read pattern. This skill assumes the customer already uses Amplitude; do not guide them through adopting Amplitude from scratch.
>
> **Who does this: Agent, for every step except one.** You wire the frontend
> code (Steps 1–3), run the registration script (Step 4), and run the
> verification curl (Step 6) yourself. The one step you cannot do is Step 5 —
> Amplitude has no API for creating an Event Streaming destination, so a human
> has to do that one in the dashboard.
>
> **Before Step 2, get the Amplitude API Key; before Step 4, get the Project
> ID.** You cannot fetch either yourself — ask the customer for the exact
> value and **wait for their real answer before running the step that needs
> it.** When you ask, **tell them exactly where to find each one** — don't
> just ask "what's your API key?" with no guidance:
>
> * Amplitude **API Key** — Settings → Projects → \[Your Project] → General.
>   [Walkthrough](https://app.arcade.software/share/cIZCislbf6hrApfWLSnd)
> * Amplitude **Project ID** — Settings → Projects → select your project →
>   Project ID.
>   [Walkthrough](https://app.arcade.software/share/l9TTqQzVXm9atGX80te3)
>
> Never invent a placeholder API key or project id and continue as if it
> were real — if `onboard_product` or the init call would need a value you
> don't have yet, stop and ask.
>
> **Stay inside this project's directory tree.** The `amplitude.initAll()`
> call, the registration script, and every file this skill touches live inside
> the current project root. Never search, list, or read outside it — no
> scanning the home directory, sibling folders, or other projects looking for
> "an existing onboarding script." There isn't one to find; you write it,
> right here, in this project, in Step 4.

## Step 1 — Verify the existing Amplitude SDK

**Who does this:** Agent — reads the app's dependencies/code, no dashboard involved.

Confirm the app is using `@amplitude/unified` with Analytics and Session Replay enabled. If the customer is still on the older `@amplitude/analytics-browser` package, treat that as a customer migration decision and call it out before changing dependencies.

***

## Step 2 — Verify initialization on app load

**Who does this:** Mixed — agent edits the code, but a user has to look up `YOUR_AMPLITUDE_API_KEY` in Amplitude's dashboard (Settings → Projects → \[Project] → General) first; there's no API for the agent to fetch it itself.

```javascript theme={null}
import * as amplitude from '@amplitude/unified';

amplitude.initAll('YOUR_AMPLITUDE_API_KEY', {
  analytics: { autocapture: true },   // required — captures clicks, page views, forms
  sessionReplay: { sampleRate: 1 },
});
```

This should already run once at app startup. `autocapture: true` is required — without it no click/pageview events reach Autoplay.

If the customer is on a **brand-new** Amplitude project, tell them about Amplitude's own **"Let's get set up!"** screen: it shows a setup script next to their API key, and stays on **"Waiting for your events..."** until it receives one — blocking **Data → Destinations** until then. Have them copy that script into their app (or confirm the `initAll()` call above covers it), then open the app and click around a few pages so events arrive and **Finish Setup** unlocks. Not applicable if Amplitude is already live in their app.

***

## Step 3 — Identify on login (required for user scoping)

**Who does this:** Agent — edits frontend auth/login code directly.

```javascript theme={null}
amplitude.setUserId(user.id);   // the stable id the live-activity read is keyed by

const identifyObj = new amplitude.Identify();
identifyObj.set('email', user.email);
identifyObj.set('name', user.name);
identifyObj.set('plan', user.plan);   // e.g. "trial" — rides through to user_properties as-is

// Optional: only if the customer is segmenting who should receive Autoplay.
// Replace this condition with their own rule: plan, signup date, feature flag,
// experiment assignment, workspace type, region, or any other property.
const shouldReceiveAutoplay = isInAutoplayExperiment(user);
identifyObj.set('autoplay_experiment_group', shouldReceiveAutoplay ? 'autoplay' : 'comparison');
identifyObj.set('autoplay_experiment_id', 'your-experiment-id');
amplitude.identify(identifyObj);
```

Without `setUserId`, events carry only a `device_id` — Autoplay still records activity, but no agent can look the user up by their real id. `amplitude.identify()` (email/name/plan) is optional enrichment on top.

If the customer is running an Autoplay experiment, do not use `plan` alone as the assignment. `plan = "trial"` only identifies the lifecycle cohort. Add separate stable custom user properties such as `autoplay_experiment_group = "autoplay"` and `autoplay_experiment_id = "your-experiment-id"` so Amplitude can filter the Event Streaming destination to the selected group while leaving comparison users out of the Autoplay stream. Tell the customer those custom properties appear in Amplitude as user properties after Amplitude receives the identify call and a later event for that user.

***

## Step 4 — Register with Autoplay

**Who does this:** Mixed — agent runs this script directly, but a user has to look up `YOUR_AMPLITUDE_PROJECT_ID` in Amplitude's dashboard (Settings → Projects → select project → Project ID) first.

```python theme={null}
import asyncio
from autoplay_sdk.admin import onboard_product
from autoplay_sdk.providers import AmplitudeProvider

async def main() -> None:
    result = await onboard_product(
        "YOUR_AMPLITUDE_PROJECT_ID",
        contact_email="you@yourcompany.com",
        user_activity_provider=AmplitudeProvider(),
        print_operator_summary=True,
    )

asyncio.run(main())
```

Prints the registration values — save them:

* `product_id` — the issued Autoplay id, e.g. `prod_wQ7r8kF9...`
* `provider`, `provider_project_id` (`YOUR_AMPLITUDE_PROJECT_ID`)
* `ingest_url` — the issued-id URL, e.g. `https://connector.autoplay.ai/ingest/prod_wQ7r8kF9...`
* `ingest_secret` — Amplitude sends this as `Authorization: Bearer <ingest_secret>`
* `mcp_url` — always `https://mcp.autoplay.ai/mcp`
* `mcp_key` — Bearer token for the agent's live-activity read (REST or MCP)
* `owner_token` — shown only on first registration; save it securely because it is required to re-register or rotate credentials

There is no more `stream_url` / legacy stream key / `amplitude_ingest_url` — those were legacy Render fields.

***

## Step 5 — Create the Amplitude Event Streaming destination

**Who does this:** User — Amplitude dashboard, no API for destination creation. Hand the user the field values and the Freemarker template below to paste in, along with this walkthrough: [Create the Amplitude Event Streaming destination](https://app.arcade.software/share/lUZOwnrkov0gNnkbceuS)

Once they've saved it, the destination can take a few minutes to show up under **New Destinations** in **Data → Destinations** — that's normal. This walkthrough confirms it appears and syncs it: [Confirm the destination and sync it](https://app.arcade.software/share/4ngBUmAqZsd0acSiEuMy)

**Data → Destinations → + Add Destination → search "HTTP" → Event Streaming.**

| Field                              | Value                                                                                             |
| ---------------------------------- | ------------------------------------------------------------------------------------------------- |
| URL Endpoint (method `POST`)       | `ingest_url` from Step 4                                                                          |
| REST API Headers → `Authorization` | `Bearer ` + `ingest_secret` from Step 4 (paste the value directly, e.g. `Bearer <ingest_secret>`) |

**Event Body Editor** — paste this Freemarker template exactly (do not use Amplitude's default template — it drops clicks and form events, only page views survive):

```
<#setting number_format="0.####">
<#assign et = input.event_type!''>
<#assign ep = input.event_properties!{}>
<#assign up = input.user_properties!{}>
{
  "events": [
    {
      "event_type": "<#if et?starts_with('Viewed') || et == '[Amplitude] Page Viewed' || et == 'Page Viewed'>$pageview<#elseif et == '[Amplitude] Element Clicked' || et == '[Amplitude] Element Changed' || et?starts_with('Form')>$autocapture<#else>${et?json_string}</#if>",
      "user_id": "${(input.user_id!'')?json_string}",
      "device_id": "${(input.device_id!'')?json_string}",
      "session_id": ${input.session_id!0},
      "time": ${input.time!0},
      "event_time": "${(input.event_time!'')?json_string}",
      "user_properties": {
        <#list up?keys as k>"${k?json_string}": <#if up[k]?is_string>"${up[k]?json_string}"<#elseif up[k]?is_number>${up[k]?c}<#elseif up[k]?is_boolean>${up[k]?c}<#else>null</#if><#sep>, </#sep></#list>
      },
      "event_properties": {
        "[Amplitude] Page URL": "${(ep['Page URL']!ep['Page Location']!ep['[Amplitude] Page URL']!ep['[Amplitude] Page Location']!'')?json_string}",
        "[Amplitude] Page Title": "${(ep['Page Title']!ep['[Amplitude] Page Title']!'')?json_string}",
        "$event_type": "<#if et == '[Amplitude] Element Changed'>change<#elseif et?starts_with('Form Submitted')>submit<#elseif et?starts_with('Form Started')>focus<#else>click</#if>",
        "$current_url": "${(ep['Page URL']!ep['Page Location']!ep['[Amplitude] Page URL']!ep['[Amplitude] Page Location']!'')?json_string}",
        "$button_text": "${(ep['[Amplitude] Element Text']!ep['Element Text']!ep['Page Title']!'')?json_string}",
        "$elements_chain": "${(ep['[Amplitude] Element Path']!ep['Element Path']!'')?json_string}",
        "$element_id": "${(ep['[Amplitude] Element ID']!ep['Element ID']!'')?json_string}",
        "$element_tag": "${(ep['[Amplitude] Element Tag']!ep['Element Tag']!'')?json_string}"<#list ep?keys as k><#if k?starts_with('$') || k?starts_with('[Amplitude]')><#else>, "${k?json_string}": <#if ep[k]?is_string>"${ep[k]?json_string}"<#elseif ep[k]?is_number>${ep[k]?c}<#elseif ep[k]?is_boolean>${ep[k]?c}<#else>null</#if></#if></#list>
      }
    }
  ]
}
```

If the customer wants only an experiment cohort to stream into Autoplay, tell them to add filters in **Select & filter events** before testing/releasing the sync. Use the custom properties from Step 3, for example `autoplay_experiment_group = autoplay`, optionally combined with eligibility properties like `plan = trial`. This keeps comparison-group events from being forwarded to the Autoplay connector for this destination.

For evaluation, do not rely on Autoplay as the source of truth for who was excluded. Filtered-out comparison users never reach the Autoplay connector. The customer's app, Amplitude, or warehouse should keep the stable assignment list, and conversion analysis should compare users by `autoplay_experiment_group` or their equivalent custom property. Do not randomize assignment on each page load.

On the **Testing** tab:

1. Toggle **Send Events** on. Without this, Amplitude builds the request but never sends it.
2. Under **Select & filter events**, leave **All Events** selected — unless you're limiting to an experiment cohort, as described above.
3. Click **Test Connection**. You should see a `200 OK` response.
4. Click **Release** to publish the destination.

Then, back in **Data → Destinations**, open the published destination and click **Add New Sync** to activate it. This is a standard Amplitude step, not specific to Autoplay.

***

## Step 6 — Verify

**Who does this:** Agent — runs this curl check directly.

```bash theme={null}
curl "https://mcp.autoplay.ai/users/YOUR_AUTOPLAY_PRODUCT_ID/YOUR_USER_ID/live-activity?limit=10" \
  -H "Authorization: Bearer YOUR_MCP_KEY"
```

`YOUR_USER_ID` is the id passed to `amplitude.setUserId(...)`. A `200` with a populated `actions` array confirms events are flowing — this is the same pull-based read every agent uses, over REST or MCP.

If `401`: confirm the destination's `Authorization` header is `Bearer <ingest_secret>` and the URL is `ingest_url` exactly. If `404`/empty: confirm `YOUR_AUTOPLAY_PRODUCT_ID` is the issued `product_id` from registration, and that you're querying the same id set via `setUserId`.

***

## Common mistakes

**Not calling `amplitude.setUserId()` on login.** This is the call that stamps the top-level `user_id` the connector keys activity by. Without it, events carry only a `device_id`.

**Using the default Amplitude template instead of the Freemarker one above.** Only page views arrive; clicks and form events are silently dropped.

**Removing the `?json_string` wrapping when customizing a field.** Amplitude's autocapture CSS-escapes element class names in `[Amplitude] Element Path` (e.g. `gap-1.5` becomes `gap-1\.5`, `bg-black/60` becomes `bg-black\/60`). `?json_string` re-escapes those backslashes so the outgoing JSON stays valid. Interpolating a field raw (`"${ep[...]}"` instead of `"${(ep[...])?json_string}"`) reintroduces invalid JSON escapes and causes `400 invalid json` on clicks whose element has a decimal or slash Tailwind class.

**Wrapping non-string property values in `?json_string`.** The `user_properties` and trailing custom-property loops type-switch on each value: strings go through `?json_string`, but numbers and booleans (`duration`, `step`, `is_pro`, ...) are emitted as native JSON literals via `?c`. `?json_string` only accepts string operands — feeding it a number or boolean throws Amplitude's `Error executing transformation` and the event never sends. Keep the `?is_string`/`?is_number`/`?is_boolean` switch intact. Any other value — an array or nested object (some autocapture events carry these), or an explicit `null` — takes the `<#else>` branch, which emits JSON `null`. Do **not** use `?string` there: it throws `Error executing transformation` on a sequence or hash. The connector only reads flat scalar properties, so mapping non-scalars to `null` is lossless for detection.

**`$elements_chain` must not fall back to `[Amplitude] Element Hierarchy`.** That field is an array; feeding an array to `?json_string` throws `Error executing transformation` on any event missing `Element Path`. The fallback chain is `[Amplitude] Element Path` → `Element Path` → `''` — all strings.

**Session ID is `-1` in early events.** Normal — Amplitude initializes it before the first session is established. The connector falls back to `device_id` for those events; resolves within seconds.

***

## Reference

* Quickstart: [https://developers.autoplay.ai/quickstart](https://developers.autoplay.ai/quickstart)
* Amplitude Unified SDK docs: [https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2](https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2)
* Amplitude HTTP destination docs: [https://amplitude.com/docs/data/destination-catalog/http](https://amplitude.com/docs/data/destination-catalog/http)
