Skip to main content
This tutorial gets you from zero to a working Inkeep AI chat with grounded context in about 45 minutes. What Inkeep gives you in this integration: Inkeep’s AI chat framework was built for in-product support — grounded in your own product knowledge, not generic LLM replies. The self-hosted agents framework (@inkeep/agents-ui) gives you:
  • introMessage injection — 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?”
  • context passthrough — 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 appId dispatches 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
The paid Inkeep CDN widget (@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:
  1. Frontend autocapture with posthog.identify() + posthog.register({email}).
  2. Product onboarding with onboard_product for ingest + MCP credentials.
  3. PostHog destination forwarding events to your Autoplay connector.
  4. A small FastAPI bridge that pulls a user’s live activity with a plain httpx call and exposes it over a /context/{user_id} endpoint.
  5. The Inkeep agents framework running locally (Docker + pnpm dev server).
  6. A project, agent, and sub-agent configured via the Inkeep management API.
  7. An InkeepEmbeddedChat widget that pre-loads the pulled Autoplay context as its introMessage.
  8. An end-to-end check where the chat opens knowing exactly what the user was doing.
Runtime loop: click in app → event lands in the Autoplay connector via the PostHog webhook → user opens chat → frontend fetches /context/{user_id} → bridge pulls that user’s live activity over REST, on demand → InkeepEmbeddedChat opens with grounded introMessage.
The connector is pull-based, not push-based. There’s no persistent stream for the bridge to hold open and no local event store to keep in sync — the bridge asks “what has this user been doing?” once, synchronously, exactly when the frontend needs an 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

Install posthog-js and initialize it once on app load.
Mount this provider once at the top of your app (app/layout.js in Next.js). Autocapture then sends clicks, page views, and form submits automatically.
Use your Project API Key (starts with phc_). The other keys PostHog surfaces (phx_…) are personal/admin keys and posthog.init() will reject them with a misleading personal_api_key error.
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.
Create bridge/register_product.py:
Run it once:
It prints six values — save them. We’ll use all six:
  • product_id — your product’s id (echoes back the value you registered with).
  • providerposthog.
  • ingest_url — PostHog will POST events here (e.g. https://connector.autoplay.ai/ingest/YOUR_PRODUCT_ID).
  • ingest_secretX-PostHog-Secret header value for the destination.
  • mcp_url — always https://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.
  1. PostHog UI → Data pipeline → Destinations → + New destination → HTTP Webhook.
  2. Enable destination = ON.
  3. Webhook URL: paste ingest_url from Step 2.
  4. Method: POST. JSON Body: clear it. Headers: remove the default Content-Type row (the Hog code below sets headers itself).
  5. Click Edit source and paste this script (replace <INGEST_SECRET> and <PRODUCT_ID>).
  1. Click Test function — expect status 200 in under 200 ms.
  2. Create & enable.
PostHog requires the Webhook URL field on the form even though the Hog source above overrides it. Paste the same ingest_url from Step 2 into both places.
Verify: click around your app, then check destination Logs for successful POSTs.

🧰 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:
Why no /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.
Create 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_keyMCP_KEY
product_idPRODUCT_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

Create bridge/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

5b. The FastAPI app

No lifespan 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 the actions 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.
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.

Start the bridge

You should see plain uvicorn startup logs — there’s no stream to connect, so no “listening” or “connected” line to wait for:
Smoke-test:
Click around in your app for ~30 seconds, then check the context endpoint:
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.
Start the backing services (PostgreSQL on :5433, Doltgres on :5435, SpiceDB on :50051):
Copy the sample env:
Open .env and set at minimum:
INKEEP_POW_HMAC_SECRET controls browser proof-of-work (ALTCHA). Comment it out for local development. If set, the browser widget must solve a cryptographic challenge before it can open a conversation — this causes a 400 error during testing.
Start the agents API on port 3002:
Health check:

⚙️ 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 before InkeepEmbeddedChat 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 the agent:
Create the sub-agent:
Set the default sub-agent:
Enable anonymous sessions so the browser widget can authenticate without a user login:
Verify the widget can get a session token:
A JWT in the response confirms the widget can authenticate.
Why both an agent and a sub-agent? The top-level agent is the named entry point registered in your app config. The sub-agent is the conversational worker that actually calls the LLM. This separation lets you route different intents to different sub-agents later — for example, a billing sub-agent and a product sub-agent under the same top-level agent — without changing the widget’s appId.

💬 Step 8 — Wire InkeepEmbeddedChat into your app

8a. Configure Next.js

@inkeep/agents-ui ships ESM-only. Tell Next.js to transpile it:
Install the package:

8b. Environment variables

Create frontend/.env.local:

8c. The InkeepWidget component

Create frontend/components/InkeepWidget.tsx. The key pattern here is:
  1. On mount (and whenever contextKey changes), fetch /context/{user_id} from the bridge.
  2. If has_activity is true, build a warm introMessage that leads with what the user was doing.
  3. Pass introMessage to InkeepEmbeddedChat.
  4. Use key={contextKey} to force a full component remount with the new introMessage when the proactive trigger fires (Step 2). Without this prop, React reuses the old component instance and the new intro message is silently ignored.

InkeepEmbeddedChat props reference

PropWhere it livesWhat it does
baseUrlaiChatSettingsYour self-hosted agents API (:3002). For Inkeep cloud, use your cloud endpoint.
appIdaiChatSettingsWhich app config to load from the manage DB. app_playground is the default seeded entry.
introMessageaiChatSettingsThe AI’s first message in every new conversation — the injection point for Autoplay context.
contextaiChatSettingsKey-value pairs forwarded to the agent on every turn. Reference them in the system prompt as {{context.key}}.
placeholderaiChatSettingsChat input hint text shown before the user types.
onInputMessageChangeaiChatSettingsCallback 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 rootForces 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.
primaryBrandColorbaseSettingsTints the widget chrome to match your product colour.
Why 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

Add InkeepWidget 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.
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

  1. 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.
  2. Click Need help? to open the chat panel.
  3. The widget fetches /context/{user_id}has_activity is true — and mounts with:
    “I can see you’ve been working on the onboarding. What can I help with?”
  4. 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.
The agent should not recite click logs. Activity is a private signal used to warm the 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.
In Step 2 we’ll go further: the bridge will notice when the user opens the Connect Data Source step three times without completing it, and surface a proactive offer — without the user typing anything.

🛠 Troubleshooting

SymptomLikely cause
introMessage is always the default greetinghas_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 >5sThe 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_KEYMCP_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 userEvents 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 connectsCORS — allowedDomains in app config must include localhost. Verify with SELECT config FROM apps WHERE id = 'app_playground';.
tsconfig error on @inkeep/agents-ui importAdd transpilePackages: ["@inkeep/agents-ui"] to next.config.ts.
introMessage doesn’t update after proactive trigger firesThe 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 requiredPaste 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_keyUse phc_… (Project) key in posthog.init(), not phx_… (Personal).

🔄 Day-2 operations

After editing 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:
To reset the Inkeep database (wipes all projects, agents, and conversation history):

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 httpx call per context request and returns what comes back.
  • Self-hosted: your events, conversation history, and LLM keys never leave your infrastructure.
If anything in this tutorial wasn’t clear, or you hit a snag the troubleshooting matrix didn’t cover — please reply on the thread or open an issue in the Autoplay SDK repo. Feedback shapes the next version of these docs.