# Install StepsKit with an AI agent (no MCP required)

> A copy-pasteable prompt that tells any AI coding agent how to install StepsKit properly: load the SDK, wire identify() so targeting and show-once work, pass user attributes, and verify the install. Works in Claude Code, Cursor, Copilot, or any chat.

*Source: https://stepskit.com/docs/ai-agents/install-prompt*

The [MCP server](/docs/ai-agents.md) is the best way to work with StepsKit — your
agent gets real tools, reads live project state, and authors tours directly. But
setting it up takes a few minutes, and not every agent supports it.

This page is the fallback: **one prompt you paste into any coding agent** to get
StepsKit installed properly. No server, no token, no configuration.

## Why "properly" matters

Most SDK installs stop at the script tag. That gets you a working embed and a
broken product: with no visitor identity, "show this tour once" degrades to a
per-tab check that dies when the tab closes, and no audience-targeting rule
that tests a value can match — while every health check still reports green.

The prompt below is written to prevent exactly that. It tells the agent to find
where your app resolves the authenticated user, wire `identify()` there, pass the
attributes your user object already carries, and then actually verify that it
worked.

> **Note**
>
> This is the same prompt the **Copy AI prompt** button emits on your project's
> install page — that version already has your real API key baked in. Use it
> instead of hand-substituting `YOUR_API_KEY` below.

## The prompt

````markdown
# Install StepsKit embed into this codebase

You are an AI coding assistant. Your task is to install the **StepsKit** embed into the current project — in the place and in the language that this codebase's framework actually calls for. Do not ask the user for the API key; it is included below.

**Read Step 0 before you open a single file to edit.** The most common way to get this wrong is to skip straight to pasting the HTML snippet.

## What StepsKit is

StepsKit is a user-onboarding platform. The tour engine itself is a tiny (~10 KB) script served from a CDN and always current — you never version it. What you install is the small typed loader that fetches it.

**Which install form you use depends on one thing: does this project have a `package.json` and a bundler?**

- **Yes → install the `stepskit` npm package (Form C below).** It's typed, ships `stepskit/react`, and needs no hand-pasted global declaration.
- **No → paste the CDN loader** (Form B for HTML, Form A for a JS/TS entrypoint in a project that won't take a new dependency), the same way Google Analytics, Intercom, or PostHog are installed.

Either way the engine streams from the CDN, so a StepsKit engine fix reaches the site without a release.

## Inputs (already known)

- **API key:** `YOUR_API_KEY`
- **CDN URL:** `https://cdn.stepskit.com/stepskit.latest.js`
- **API origin (for CSP):** `https://stepskit.com`

## Docs (verify the latest before you act)

These instructions may go stale. **Before you start, fetch the official docs** and prefer them over anything below if they conflict:

- Install overview — every framework, plus the `stepskit` package itself (options, React component, CJS): https://stepskit.com/docs/install
- Vanilla JS / static HTML: https://stepskit.com/docs/install/js
- React (Vite, CRA, etc.): https://stepskit.com/docs/install/react
- Next.js (App + Pages Router): https://stepskit.com/docs/install/nextjs
- Vue 3: https://stepskit.com/docs/install/vue
- Nuxt: https://stepskit.com/docs/install/nuxt
- Angular: https://stepskit.com/docs/install/angular
- SvelteKit: https://stepskit.com/docs/install/sveltekit
- Astro: https://stepskit.com/docs/install/astro
- Remix / React Router 7: https://stepskit.com/docs/install/remix
- Laravel / plain PHP: https://stepskit.com/docs/install/laravel
- Ruby on Rails: https://stepskit.com/docs/install/rails
- WordPress: https://stepskit.com/docs/install/wordpress
- Google Tag Manager: https://stepskit.com/docs/install/gtm
- Identifying users / user attributes: https://stepskit.com/docs/concepts/user-attributes
- Frequency capping: https://stepskit.com/docs/concepts/frequency-capping

If you have web-fetch capabilities, fetch the install page that matches the detected stack first.

---

## Step 0 — Analyse the codebase before you edit anything

Do not open an editor until you have done this. Read, in order:

1. **`package.json`** — dependencies and scripts. This is the primary framework signal: `next`, `vite`, `react-scripts`, `nuxt`, `@remix-run/*`, `astro`, `@sveltejs/kit`, `vue`, `@angular/core`.
2. **Config files** — `vite.config.*`, `next.config.*`, `nuxt.config.*`, `astro.config.*`, `svelte.config.*`, `angular.json`, or for non-JS stacks `Gemfile`, `manage.py`, `composer.json`, `artisan`.
3. **The entry point** — `src/main.tsx|jsx|ts`, `src/index.tsx`, `app/layout.tsx`, `pages/_app.tsx`, `app/root.tsx`, `src/app.html`, `index.html`, or the server-rendered base template.
4. **Where auth resolves** — grep for `useAuth`, `AuthProvider`, `getSession`, `currentUser`, `useUser`, `middleware`. This is where Step 2's `identify()` call goes, and you need it before you start.
5. **Existing global third-party scripts** — analytics, Sentry, PostHog, Intercom. **Whatever pattern this codebase already uses for those outranks the table below.** Install StepsKit the same way, in the same file.
6. **Any Content-Security-Policy** — HTTP headers, `next.config`, middleware, or a `<meta http-equiv>` tag.

**Then state, before your first edit:**

- the framework you detected and the evidence for it,
- **which install form you're using and why** — Form C if there's a `package.json` and a bundler, Form B if there's no build step,
- the exact file you will install into,
- the exact file and function where you will call `identify()`.

If no signal matches a known framework, **say so** and use the canonical HTML snippet in the root HTML document. Do not infer a framework from one ambiguous signal.

---

## Step 1 — Install the loader

The rule, once: **install at the earliest place that runs exactly once per page load, written in the language this codebase is written in.** Treat it like a Google Analytics tag — one global location, before user interaction — but express it the way the framework expresses things.

### Where it goes, by stack

| Detected stack | Install at | Mechanism | Full guide |
| --- | --- | --- | --- |
| React + Vite / CRA | `src/main.tsx` (or your root component) | Form C — `<StepsKit />` | /docs/install/react |
| Next.js App Router | `app/layout.tsx` | Form C — `<StepsKit />` | /docs/install/nextjs |
| Next.js Pages Router | `pages/_app.tsx` | Form C — `<StepsKit />` | /docs/install/nextjs |
| Remix / React Router 7 | `app/root.tsx` | Form C — `<StepsKit />` | /docs/install/remix |
| Nuxt | `plugins/stepskit.client.ts` | Form C in a client-only plugin (`.client` suffix is required) | /docs/install/nuxt |
| SvelteKit | `src/routes/+layout.svelte` | Form C, guarded by `browser` | /docs/install/sveltekit |
| Astro | layout `.astro` | Form C in a `<script>` (bundled, NOT `is:inline`) | /docs/install/astro |
| Vue + Vite | `src/main.ts` | Form C | /docs/install/vue |
| Angular | `src/main.ts` | Form C (NOT `angular.json`) | /docs/install/angular |
| Laravel / plain PHP | `resources/views/layouts/app.blade.php` | Form B — HTML snippet | /docs/install/laravel |
| Rails (jsbundling/esbuild) | `app/javascript/application.js` | Form C | /docs/install/rails |
| Rails (importmap-only) | `app/views/layouts/application.html.erb` | Form B via `javascript_include_tag` | /docs/install/rails |
| WordPress | a site-specific plugin | Form B via `wp_enqueue_script` + `script_loader_tag` | /docs/install/wordpress |
| Google Tag Manager | a Custom HTML tag on All Pages | Form B | /docs/install/gtm |
| Django / other MPA | base layout template | Form B — HTML snippet | /docs/install/js |
| Static site / unknown | root HTML document | Form B — HTML snippet | /docs/install/js |

Three forms follow. Pick exactly one — **never hand-convert between them.**

- **Form C (`npm install stepskit`)** — the default whenever there's a `package.json` and a bundler. Typed, ships its own React component, and no global declaration to paste.
- **Form A (module loader)** — the same install hand-rolled, for a bundler project that must not take a new dependency. Use only if the user says so.
- **Form B (HTML `<script>`)** — no build step: HTML, server-rendered templates, CMSes, tag managers.

All three queue `identify()` / `playTour()` calls made before the engine finishes loading, so none of them can fire too early.

### Form C — the npm package (preferred when there's a bundler)

Install it with the project's own package manager — check for `pnpm-lock.yaml`, `yarn.lock`, or `bun.lock` before defaulting to npm:

```bash
npm install stepskit
```

**React, Next.js, Remix — use the component.** It mounts once, is StrictMode-safe, and re-identifies when the user's values change:

```tsx
import { StepsKit } from "stepskit/react";

// Render once, at the root of your app.
<StepsKit apiKey="YOUR_API_KEY" />
```

**Vue, Angular, Svelte, or any other bundler — call `init()` once at the entry point.** It's SSR-safe (a no-op on the server) and idempotent:

```ts
import stepskit from "stepskit";

// Call once, at your app's entry point. SSR-safe and idempotent.
stepskit.init("YOUR_API_KEY");
```

Nothing else to configure: the package resolves the CDN URL itself, ships its own TypeScript types, and installs the pre-load call queue. **Skip the `stepskit.d.ts` section below entirely** — pasting it alongside the package creates a second, conflicting declaration.

If the page already carries the HTML snippet, **remove it**. Two loaders mean two embed instances.

### Form A — JS / TS module contexts, without the dependency

Only when Form C is ruled out. Its own file, `src/stepskit.ts`, imported once from the app entrypoint. No IIFE: an ES module is already a private scope, so nothing leaks onto `window`.

```ts
// Queue API calls made before the SDK finishes loading.
const QUEUED = [
  "identify",
  "setUserAttributes",
  "track",
  "refresh",
  "playTour",
  "stopTour",
  "dismissAnnouncement",
  "on",
  "off",
  "destroy",
  "validateEnvironment",
];

const w = window as unknown as { stepskit?: Record<string, unknown> };
const stepskit = (w.stepskit ??= {});
stepskit._q ??= [];

for (const method of QUEUED) {
  stepskit[method] ??= (...args: unknown[]) =>
    (stepskit._q as unknown[]).push([method, ...args]);
}

// Load the StepsKit SDK asynchronously.
const script = document.createElement("script");
script.async = true;
script.src = "https://cdn.stepskit.com/stepskit.latest.js";
script.dataset.apiKey = "YOUR_API_KEY";
document.head.appendChild(script);

// Marks this file a module, so the consts above stay local to it.
export {};
```

If the project is plain JavaScript rather than TypeScript, name it `src/stepskit.js` and delete the three type annotations — `as unknown as { stepskit?: Record<string, unknown> }`, `as unknown[]`, and `: unknown[]`. The rest is valid JS as-is.

### Form B — HTML contexts

For `index.html`, `src/app.html`, and server-rendered base templates. Goes just before `</body>`:

```html
<script>
  (function () {
    var stepskit = window.stepskit = window.stepskit || {};
    stepskit._q = stepskit._q || [];

    // Queue API calls made before the SDK finishes loading.
    var methods = ['identify', 'setUserAttributes', 'track', 'refresh', 'playTour', 'stopTour', 'dismissAnnouncement', 'on', 'off', 'destroy', 'validateEnvironment'];
    methods.forEach(function (method) {
      stepskit[method] = stepskit[method] || function () {
        stepskit._q.push([method].concat([].slice.call(arguments)));
      };
    });

    // Load the StepsKit SDK asynchronously.
    var script = document.createElement('script');
    script.async = true;
    script.src = 'https://cdn.stepskit.com/stepskit.latest.js';
    script.setAttribute('data-api-key', 'YOUR_API_KEY');

    var first = document.getElementsByTagName('script')[0];
    first.parentNode.insertBefore(script, first);
  })();
</script>
```

### Worked example: React + Vite (and Create React App)

Install the package, then render `<StepsKit />` once at the root of the tree in `src/main.tsx`:

```tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { StepsKit } from "stepskit/react";
import App from "./App";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <StepsKit apiKey="YOUR_API_KEY" />
    <App />
  </StrictMode>,
);
```

The component renders nothing and loads StepsKit once — it is StrictMode-safe, so the double-invoked mount in development does not inject a second script. Put it above `<App />` so the call queue exists before any component can call `identify()`.

If this project is on a router with a persistent root layout, that layout works equally well. What matters is that it mounts once for the lifetime of the page, not per route.

### Worked example: Next.js App Router

Render the same component in `app/layout.tsx`. It is already client-safe (`"use client"` ships inside the package), so the layout stays a server component:

```tsx
import { StepsKit } from "stepskit/react";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <StepsKit apiKey="YOUR_API_KEY" />
      </body>
    </html>
  );
}
```

Do **not** reach for `next/script` and a raw CDN URL here — the package handles loading, typing, and the pre-load queue, and `next/script` would need the global declaration back.

For any stack not in the table, apply the same rule: if it bundles, install the package and call `init()` at its single global entry point; if it doesn't, Form B is plain, non-module, async-safe HTML and works almost everywhere.

### If you used Form A or B and this codebase is TypeScript — add the declaration below verbatim

**Skip this entirely on Form C.** The npm package ships its own types, and a hand-pasted global declaration alongside it is a duplicate that will fight the real one.

On the snippet paths there is no import to pull types from, so `window.stepskit` has no types and every reference to it is a compile error until you declare the global. **Write this file exactly as printed. Do NOT compose your own declaration from the method list in the loader** — that list is the queue stub, not the API. It omits the three synchronous getters (`isPlaying`, `getTours`, `getUserAttributes`), which cannot be queued and so never appear in it, and it carries no return types, which is how hand-written versions end up typing `identify` as `void` (it returns a promise) and `validateEnvironment` as `unknown` (which blocks the verification in Step 4).

```ts
// src/stepskit.d.ts
// Only needed for the <script> snippet install. If you installed the "stepskit"
// npm package, delete this file — the package ships its own types.
export {};

type StepsKitAttributes = Record<string, string | number | boolean>;

interface StepsKitToursFilteredEntry {
  tourId: string;
  name: string;
  reason:
    | "url_pattern_mismatch"
    | "screen_width_too_narrow"
    | "targeting_failed"
    | "frequency_capped"
    | "inactive";
  detail?: string;
}

interface StepsKitEnvironmentReport {
  apiKey: string; // masked (e.g. "sk_live_a3f...***")
  baseUrl: string;
  visitorId: string | undefined;
  userAttributes: StepsKitAttributes | undefined;
  initialized: boolean;
  isPlaying: boolean;
  currentTourId: string | null;
  toursLoaded: number;
  tooltipsLoaded: number;
  toursFiltered: StepsKitToursFilteredEntry[];
  warnings: string[];
}

declare global {
  interface Window {
    stepskit?: {
      identify(visitor: StepsKitAttributes): Promise<void>;
      setUserAttributes(
        attrs: StepsKitAttributes,
        options?: { autoRefresh?: boolean },
      ): Promise<void>;
      getUserAttributes(): StepsKitAttributes | undefined;
      playTour(tourId: string): Promise<void>;
      stopTour(): void;
      isPlaying(): boolean;
      getTours(): Array<{ id: string; name: string }>;
      refresh(): Promise<void>;
      on(event: string, handler: (...args: unknown[]) => void): void;
      off(event: string, handler: (...args: unknown[]) => void): void;
      dismissAnnouncement(announcementId: string): void;
      destroy(): void;
      validateEnvironment(): StepsKitEnvironmentReport;
      /** No-op today: warns once and discards. Do not wire product events to it. */
      track(event: string, properties?: Record<string, unknown>): void;
    };
  }
}
```

Vite and CRA pick up any `.d.ts` under `src/` automatically. For other setups, put it wherever the `tsconfig.json` `include` already covers.

---

## Step 2 — Identify the logged-in user (NOT optional)

Installing the loader is only half the install. Everything below is the other half. Wire it at the auth site you identified in Step 0.

### Identify the visitor — this decides whether targeting works at all

A snippet-only install shows the same thing to every visitor. An install that
identifies the visitor lets every tour, tooltip, survey, and banner be aimed at
who the user is, what plan they're on, and what they've already seen. The
difference is one function call — don't skip it.

Find where this codebase resolves the authenticated user — an auth context, a
session hook, middleware, a current-user helper — and call:

```ts
// Installed the npm package:
import stepskit from "stepskit";
stepskit.identify({ id: user.id, email: user.email, plan: user.plan });

// Installed the <script> snippet — same call, off the global:
window.stepskit?.identify({ id: user.id, email: user.email, plan: user.plan });
```

Use whichever matches how StepsKit was installed here. The package form is
typed and does not need the optional-chaining guard; both queue the call if the
runtime hasn't finished loading, so neither can fire too early.

Call it once **per page load**, as soon as auth state resolves — not on every
client-side route change. The SDK keeps user context across SPA navigation, but
**not across a full page load**: nothing persists attributes, so in a
server-rendered app (Rails, Django, Laravel, any MPA) the identify call belongs
in the same global layout as the snippet, or render the values as
`data-user-*` attributes on the script tag. Re-call it when the user switches
organisation or workspace, is impersonated, or changes role, so the attributes
you target on don't go stale.

**What breaks without it.** With no attributes at all, an audience set to
"filtered" is hidden outright — the matcher bails before evaluating a single
rule, so nothing matches, not even a negative rule. (Pass *some* attributes but
omit `id` and a bare `notExists` rule on `id` will match.) An "everyone"
audience is unaffected and still shows. Show-once capping also degrades to a
per-tab check that dies with the tab, so a returning visitor sees the tour
again. The install will still verify as live. It will still be broken.

Identify anonymous visitors freely if you want pre-login experiences — StepsKit
bills a flat rate and does not meter tracked users.

### Pass the attributes this codebase already has

`id` is the only required key. Beyond it, pass whatever the user object
already carries: email, name, plan, role, company, signup date — plus anything
product-specific worth targeting on (team size, trial status, feature flags,
seat count).

Use the codebase's real field names; do not rename them to fit a schema.
StepsKit has no fixed schema — every key you pass becomes targetable, and values
may be strings, numbers, or booleans.

### Do not wire `track()`

`track()` appears in the SDK's method list but is a no-op today: it warns once
and discards the call. Any event-tracking calls you add would be dead code.
Custom events are on the roadmap; targeting runs off the attributes above.

### Content Security Policy — only if this app sets one

- `script-src`: `https://cdn.stepskit.com` (plus `'unsafe-inline'` if you
  used the inline snippet — the npm package, `next/script`, and a plain
  external `<script src>` all need no inline block. On the package, pass
  `init(key, { nonce })` to stamp a nonce onto the script it injects.)
- `connect-src`: `https://stepskit.com`
- `style-src`: `'unsafe-inline'` — **required**, and not only for theming:
  the embed injects its entire stylesheet as a `<style>` element when the
  script evaluates. Without it the tour engine renders completely unstyled.
- `img-src`: `https://*.supabase.co` — only if your steps use images.

---

## Step 3 — Tell the user to allow-list their domain

After installing, **stop and tell the user** they need to add their production and staging domains to the StepsKit dashboard at:

> Project → Integrations → Domains

Otherwise the embed will be rejected on those domains and no tours will render. Do not attempt to do this for them — it's a dashboard action.

---

## Hard constraints — do NOT do any of these

- ❌ Do NOT skip Step 0. Never paste a snippet into a file before you have named the framework and the target file.
- ❌ Do NOT default to `index.html` because it's the easiest place to paste. In a React, Vue, or Angular app StepsKit belongs in the `src/` entrypoint (Form C) unless the codebase's own third-party scripts say otherwise.
- ❌ Do NOT hand-convert between forms. Use the one printed above for the context you're in; retyping the method list drops calls from the queue stub.
- ❌ Do NOT add the `stepskit` dependency to a project with no build step — a WordPress site, a CMS theme, a Google Tag Manager container, or a plain HTML page. There is nothing to bundle it with. Those get Form B.
- ❌ Do NOT install any StepsKit package other than `stepskit`. `@stepskit/embed` and `@stepskit/core` are internal workspace packages, not published install paths; `@stepskit/mcp` is the MCP server, not a runtime dependency.
- ❌ Do NOT paste the `stepskit.d.ts` global declaration when you installed the package. It ships types; a second declaration conflicts with them.
- ❌ Do NOT leave the HTML snippet in place after installing the package. Two loaders mean two embed instances.
- ❌ Do NOT try to inline or vendor the SDK body. Only the bootstrap loader (Form A or Form B above) goes in source — the real SDK is fetched from the CDN at runtime.
- ❌ Do NOT block first paint. The loader is already `async`; in Next.js use `strategy="afterInteractive"`. Never use `strategy="beforeInteractive"`.
- ❌ Do NOT put the loader inside a React/Vue component, a `useEffect`, or anything that mounts and unmounts. Module scope in the entrypoint, or the root layout — **exactly once**, same lifetime as the page.
- ❌ Do NOT add `crossorigin` or `integrity` attributes. The file is versioned and updated continuously; SRI would break.
- ❌ Do NOT modify the project's CSP silently. If you detect a strict `Content-Security-Policy` (in HTTP headers, `next.config`, middleware, or a `<meta http-equiv>` tag), tell the user which directives to add — they are listed in Step 2 above.
- ❌ Do NOT wire `window.stepskit.track()` to product events. It is a no-op today (see Step 2).

---

## Step 4 — Verify

DONE MEANS all of these, not just the first:

  1. The embed is live and has pinged StepsKit (it only proves a page loaded —
     it says nothing about identity).
  2. On a LOGGED-IN page, window.stepskit.validateEnvironment() run in the
     browser console shows a non-null `visitorId` and a populated
     `userAttributes`. Its toursFiltered table also tells you exactly why any
     experience isn't showing.
  3. Anything the user asked to be targeted actually has the attribute it
     targets on.

If you can't drive a browser, print checks 1-3 for the user to run and leave a
`// TODO: verify StepsKit identify()` comment at the integration point. Never
report the install complete on the snippet alone.

Also confirm a network request to `https://cdn.stepskit.com/stepskit.latest.js` returns `200`, and that there are no `[StepsKit]` error logs in the console.

Once those pass, the embed is installed and visitors are identified. The user can now build tours in the StepsKit dashboard and they will appear on the site automatically, subject to the visibility rules they configure.
````

## After it runs

Ask the agent to show you its diff before you accept it — particularly where it
placed `identify()`. The right home is wherever auth state first resolves, once
per session, not inside a component that re-renders on navigation.

Then load a logged-in page and run this in the browser console:

```js
window.stepskit.validateEnvironment();
```

You want a non-null `visitorId` and a populated `userAttributes`. The
`toursFiltered` table it prints also explains why any given experience isn't
showing yet — the fastest debugging tool StepsKit has.

## Next steps

- [Connect your agent](/docs/ai-agents/connect.md) — set up the MCP server so your
  agent can author tours, not just install the SDK.
- [User attributes](/docs/concepts/user-attributes.md) — everything you can pass to
  `identify` and target on.
- [Visitor identification](/docs/concepts/visitor-identification.md) — how StepsKit
  resolves a visitor, and what happens when it can't.
- [Troubleshooting](/docs/ai-agents/troubleshooting.md) — when something doesn't
  show up.
