@inkeep/agents-ui) gives you:
introMessageinjection — the widget’s opening message is set before the conversation starts. This is the prop this tutorial uses to deliver Autoplay context — so the first reply says “I can see you’ve been on the Connect Data Source step” instead of “How can I help?”contextpassthrough — structured session data (session ID, user ID, email) flows from the browser into the agent’s system prompt on every turn, scoping replies to the right user- Agent / sub-agent routing — a single
appIddispatches to different LLM workers by intent. Add a billing sub-agent later without touching the widget code - Your LLM key, your data — conversation history, system prompts, and user messages live in a Postgres DB you own; nothing is sent to Inkeep’s cloud
@inkeep/cxkit-js) requires an Inkeep cloud API key and calls Inkeep’s hosted endpoints. This tutorial uses the open-source agents framework (@inkeep/agents-ui) which you self-host with your own LLM key. If you already have an Inkeep account, skip Steps 6–7 and point NEXT_PUBLIC_INKEEP_BASE_URL at your cloud endpoint instead.What you’ll build:
- Frontend autocapture with
posthog.identify()+posthog.register({email}). - Product onboarding with
onboard_productfor ingest + MCP credentials. - PostHog destination forwarding events to your Autoplay connector.
- A small FastAPI bridge that pulls a user’s live activity with a plain
httpxcall and exposes it over a/context/{user_id}endpoint. - The Inkeep agents framework running locally (Docker + pnpm dev server).
- A project, agent, and sub-agent configured via the Inkeep management API.
- An
InkeepEmbeddedChatwidget that pre-loads the pulled Autoplay context as itsintroMessage. - An end-to-end check where the chat opens knowing exactly what the user was doing.
/context/{user_id} → bridge pulls that user’s live activity over REST, on demand → InkeepEmbeddedChat opens with grounded introMessage.
introMessage. This is the same pattern used in the Plain tutorial and Intercom Fin tutorial — no listener process required.🪝 Step 1 — Capture clicks in your web app with PostHog
Installposthog-js and initialize it once on app load.
app/posthog-provider.js — PostHog provider setup (expand to copy)
app/posthog-provider.js — PostHog provider setup (expand to copy)
app/layout.js in Next.js). Autocapture then sends clicks, page views, and form submits automatically.
Verify: open the app, click around, then check PostHog → Activity for $autocapture events on your user.
📝 Step 2 — Register your product with Autoplay
Run a one-time script to create your ingest + MCP credentials.bridge/register_product.py:
product_id— your product’s id (echoes back the value you registered with).provider—posthog.ingest_url— PostHog will POST events here (e.g.https://connector.autoplay.ai/ingest/YOUR_PRODUCT_ID).ingest_secret—X-PostHog-Secretheader value for the destination.mcp_url— alwayshttps://mcp.autoplay.ai/mcp.mcp_key— Bearer token used both for the connector’s REST read and for an MCP tool call, if you ever add one.
contact_email is required. It is stored on the connector product row so Autoplay can reach you. Re-registering the same product_id returns 409 unless you pass force=True, in which case ingest_secret rotates — update the PostHog destination in Step 3 to match.🔗 Step 3 — Wire PostHog → Autoplay via a HogQL destination
Configure a PostHog destination to forward each autocapture event to your Autoplay webhook.- PostHog UI → Data pipeline → Destinations → + New destination → HTTP Webhook.
- Enable destination = ON.
- Webhook URL: paste
ingest_urlfrom Step 2. - Method: POST. JSON Body: clear it. Headers: remove the default
Content-Typerow (the Hog code below sets headers itself). - Click Edit source and paste this script (replace
<INGEST_SECRET>and<PRODUCT_ID>).
PostHog destination — HogQL forwarding script (expand to copy)
PostHog destination — HogQL forwarding script (expand to copy)
- Click Test function — expect status 200 in under 200 ms.
- Create & enable.
🧰 Step 4 — Scaffold the bridge project
The bridge is the only service that touches the Autoplay SDK. It assembles user context and serves it over a simple HTTP endpoint — Inkeep handles the LLM call. You already created~/nexus-cloud/bridge/ in Step 2. Add the remaining dependencies:
/reply endpoint, and no LLM key in this bridge? With Inkeep the LLM call happens inside the Inkeep agents framework — your bridge only needs to pull and return the context string, it never talks to an LLM itself. That’s also why the old session-summarizer’s OPENAI_API_KEY is gone: there’s no rolling summary to generate anymore, just a bounded window of recent actions the connector already returns. Your LLM credentials live in exactly one place — the agents framework .env from Step 6.bridge/.env with three of the six credentials returned by onboard_product. Map them as follows:
onboard_product field | .env variable |
|---|---|
mcp_url (origin only, drop /mcp) | CONNECTOR_URL |
mcp_key | MCP_KEY |
product_id | PRODUCT_ID |
ingest_url | (used in Step 3 — PostHog destination URL) |
ingest_secret | (used in Step 3 — PostHog destination header) |
🔌 Step 5 — Wire the context endpoint
Createbridge/copilot_server.py. There’s no SDK pipeline to compose anymore — the bridge makes one httpx call to the Autoplay connector’s REST live-activity endpoint, synchronously, the moment the frontend asks for context. No listener process, no local event store, no reconnect logic.
5a. Imports and config
copilot_server.py imports and config (expand to copy)
copilot_server.py imports and config (expand to copy)
5b. The FastAPI app
Nolifespan hook is needed — there’s no client to start or stop when the server boots. The app object is just a plain FastAPI().
5c. A helper to pull and format live activity
This is the only piece of “pipeline” left: one function that calls the connector and turns theactions array into a readable block of text. There’s no summarizer step — the connector already returns a bounded, recent window, so raw actions are passed straight through.
copilot_server.py — pull_live_activity helper (expand to copy)
copilot_server.py — pull_live_activity helper (expand to copy)
actions comes back oldest → newest, so the lines above read as a timeline of what the user just did, in order — the same ordering guarantee the old streamed pipeline gave you, just resolved fresh on every call instead of maintained incrementally.5d. The /context/{user_id} endpoint and health check
This bridge does not call the LLM for chat replies — Inkeep handles that. It exposes a single read endpoint keyed by the stable user_id (the same id posthog.identify() set) — the frontend fetches it before opening the chat widget to build the introMessage. There’s no session_id anywhere in this API.
copilot_server.py endpoints (expand to copy)
copilot_server.py endpoints (expand to copy)
Start the bridge
YOUR_USER_ID is the same id you passed to posthog.identify(...) in Step 1. That confirms events are flowing through PostHog into the connector, and that the bridge’s REST pull is working.
🤖 Step 6 — Run the Inkeep agents framework
Inkeep is an open-source AI agent framework (ELv2 license) that you self-host with your own LLM key. The agents framework ships as a pnpm monorepo..env and set at minimum:
⚙️ Step 7 — Create a project, agent, and sub-agent
The Inkeep agents framework uses a two-layer model: an agent is a named entry point with a routing prompt, a sub-agent is the LLM worker that actually calls the model. Both must exist beforeInkeepEmbeddedChat can start a conversation.
All calls below use the bypass auth header (Authorization: Bearer test-bypass-secret-for-ci), which sets userId='system' and skips permission checks — suitable for local setup only.
Create the project:
Create agent curl (expand to copy)
Create agent curl (expand to copy)
Create sub-agent curl (expand to copy)
Create sub-agent curl (expand to copy)
appId.💬 Step 8 — Wire InkeepEmbeddedChat into your app
8a. Configure Next.js
@inkeep/agents-ui ships ESM-only. Tell Next.js to transpile it:
8b. Environment variables
Createfrontend/.env.local:
8c. The InkeepWidget component
Create frontend/components/InkeepWidget.tsx.
The key pattern here is:
- On mount (and whenever
contextKeychanges), fetch/context/{user_id}from the bridge. - If
has_activityis true, build a warmintroMessagethat leads with what the user was doing. - Pass
introMessagetoInkeepEmbeddedChat. - Use
key={contextKey}to force a full component remount with the newintroMessagewhen the proactive trigger fires (Step 2). Without this prop, React reuses the old component instance and the new intro message is silently ignored.
frontend/components/InkeepWidget.tsx — embedded chat widget (expand to copy)
frontend/components/InkeepWidget.tsx — embedded chat widget (expand to copy)
InkeepEmbeddedChat props reference
| Prop | Where it lives | What it does |
|---|---|---|
baseUrl | aiChatSettings | Your self-hosted agents API (:3002). For Inkeep cloud, use your cloud endpoint. |
appId | aiChatSettings | Which app config to load from the manage DB. app_playground is the default seeded entry. |
introMessage | aiChatSettings | The AI’s first message in every new conversation — the injection point for Autoplay context. |
context | aiChatSettings | Key-value pairs forwarded to the agent on every turn. Reference them in the system prompt as {{context.key}}. |
placeholder | aiChatSettings | Chat input hint text shown before the user types. |
onInputMessageChange | aiChatSettings | Callback fired on every keystroke. Used in Step 2 to detect “yes” client-side and trigger the guided tour without waiting for message submission. |
key (React prop) | component root | Forces a full remount. InkeepEmbeddedChat is stateful — changing introMessage after mount has no effect. Pass a new key (e.g. a timestamp) to reset the conversation with fresh state and a new opening message. |
primaryBrandColor | baseSettings | Tints the widget chrome to match your product colour. |
key and not just updating introMessage? InkeepEmbeddedChat manages its own conversation state internally. Once mounted, it ignores introMessage prop changes — the conversation has already started. Changing key tells React to unmount and remount the component, which creates a fresh anonymous session with the new introMessage as the AI’s opening line. Step 2 relies on this pattern every time a proactive offer fires.8d. Mount the widget in your page
AddInkeepWidget to your onboarding page. Pass the stable identify id (available from posthog.get_distinct_id()) as userId so the bridge can pull the right user’s activity.
app/onboarding/page.tsx — page with chat panel (expand to copy)
app/onboarding/page.tsx — page with chat panel (expand to copy)
posthog.get_distinct_id() returns the identity posthog.identify(...) set on login — the same stable user_id that flows through PostHog → Autoplay connector. This is the correct join key. There’s no session_id in this API anymore; if you use a custom identity call, make sure the id you pass to InkeepWidget matches the id you pass to posthog.identify().✅ Step 9 — Try it
- Open your app, click through the onboarding wizard for ~30 seconds — e.g. navigate to Step 1 (Connect Data Source), paste a URL into the API endpoint field, click Test Connection.
- Click Need help? to open the chat panel.
- The widget fetches
/context/{user_id}—has_activityistrue— and mounts with:“I can see you’ve been working on the onboarding. What can I help with?”
- Ask “my test connection keeps failing” → the agent replies with specific guidance about the Connect Data Source step, grounded in the fact that you just tried it.
introMessage and keep the agent’s reply focused on where you are in the flow.
If context is missing, check bridge logs and the troubleshooting matrix.
🛠 Troubleshooting
| Symptom | Likely cause |
|---|---|
introMessage is always the default greeting | has_activity is false. Check (1) bridge is running, (2) PostHog destination is enabled and POSTing, (3) the userId passed to InkeepWidget matches the id you passed to posthog.identify(...) |
/context/{user_id} times out or takes >5s | The connector call in pull_live_activity hit its 5s httpx timeout. Check CONNECTOR_URL is reachable from the bridge host and that the connector isn’t degraded. |
Bridge logs live-activity 401 — check MCP_KEY | MCP_KEY in bridge/.env doesn’t match the mcp_key printed by onboard_product, or the product was re-registered with force=True (which rotates ingest_secret but not mcp_key — double-check you copied the current value). |
/context/{user_id} returns {"context":"","has_activity":false} for a real, active user | Events not reaching the connector, or wrong user_id. Check PostHog destination logs for failed POSTs; confirm PRODUCT_ID in bridge/.env matches the product_id you registered with; confirm you’re querying the exact id posthog.identify() was called with — a device id or anonymous id will read an empty bucket. |
| Widget shows “Failed to fetch anonymous session: 401” | allowAnonymous not set in the apps table. Re-run the UPDATE apps SET config = ... from Step 7. |
| 400 — “Proof-of-work challenge required” | INKEEP_POW_HMAC_SECRET is set in ~/inkeep-agents/.env. Comment it out and restart agents-api. |
| ”Agent does not have a default sub-agent configured” | defaultSubAgentId is null. Re-run the PATCH to set defaultSubAgentId to onboarding-worker. |
| Chat widget renders but never connects | CORS — allowedDomains in app config must include localhost. Verify with SELECT config FROM apps WHERE id = 'app_playground';. |
tsconfig error on @inkeep/agents-ui import | Add transpilePackages: ["@inkeep/agents-ui"] to next.config.ts. |
introMessage doesn’t update after proactive trigger fires | The key prop on InkeepEmbeddedChat is not changing. Pass a new contextKey (e.g. incrementing counter) to force remount. This is wired in Step 2. |
PostHog destination test returns url: This field is required | Paste the same ingest_url into the form-level URL field too — PostHog requires it even though the Hog source overrides it. |
API key is not valid: personal_api_key | Use phc_… (Project) key in posthog.init(), not phx_… (Personal). |
🔄 Day-2 operations
bridge/copilot_server.py: re-run uvicorn (or start it with --reload during development).
After editing the agent system prompt via the API (curl -s -X PATCH …), the change takes effect immediately — no restart needed; the agents framework loads prompt from the database on each conversation turn.
To inspect the agent config at any time:
What you’ve built
You now have an Inkeep AI chat widget whose opening message is grounded in what the user was actually doing — no generic “How can I help?” when you can see they just failed to connect a data source.- Reusable bridge: the REST pull pattern is identical to every other support AI agent recipe — swap Inkeep for a different widget later without rewriting context logic.
- Bring-your-own model: the Inkeep agents framework calls your LLM key directly; no per-seat or per-call fee to Inkeep for the chat itself.
- Nothing to keep alive: there’s no stream to reconnect, no local event store to keep consistent, no summarizer to tune — the bridge makes one bounded
httpxcall per context request and returns what comes back. - Self-hosted: your events, conversation history, and LLM keys never leave your infrastructure.