Google Tag Manager

Install StepsKit with Google Tag Manager

For teams who'd rather not ship a code change to add a script. Everything below happens in the GTM UI.

One caveat worth knowing up front: identifying users through GTM requires your app to push the user into the data layer, which is a code change — a one-line one. Without it you get tours, but no per-user targeting and no reliable show-once capping. See step 2.

Prerequisites

  • Your project API key, from the Integrations page in the dashboard.
  • Edit access to the GTM container that's already installed on your site.

1. Add the loader

In your container: Tags → New → Tag Configuration → Custom HTML, and paste:

<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>

Then:

  • Trigger: All Pages (or Initialization - All Pages).
  • Support document.write: leave unchecked. The snippet uses insertBefore, not document.write.
  • Name it something like StepsKit — loader and save.

Do not add a History Change trigger

This is the one thing GTM users get wrong here. The reflex on a single-page app is to add a History Change trigger so the tag fires on client-side navigation. Don't — the tag would run again on every route change and load a second SDK instance each time.

StepsKit already handles SPA navigation itself: it patches history.pushState and replaceState and listens for popstate, so it re-evaluates which tours apply without any help from GTM. All Pages, once, is correct.

2. Identify the user

This is half the install, not a nice-to-have. Without an id, show-once capping degrades to a per-tab check and any tour with a "filtered" audience is hidden outright.

Push the user into the data layer

In your app, once you know who's signed in:

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  user_id: user.id,
  user_email: user.email,
  user_plan: user.plan,
});

If you already push user data for GA4, reuse it — you don't need new keys.

Create the variables

Variables → New → Data Layer Variable, one per field, named to match: user_id, user_email, user_plan.

Add the identify tag

Tags → New → Custom HTML, trigger All Pages, firing after the loader tag (set the loader as a setup tag under Advanced Settings → Tag Sequencing if you want to be explicit — see the note below on why you usually don't have to):

<script>
  (function () {
    var id = {{user_id}};
    if (!id) return;

    window.stepskit = window.stepskit || {};
    window.stepskit._q = window.stepskit._q || [];
    window.stepskit._q.push([
      "identify",
      {
        id: String(id),
        email: "{{user_email}}",
        plan: "{{user_plan}}",
      },
    ]);
  })();
</script>

Mind the quotes. GTM substitutes {{variable}} as raw text, not as a JavaScript value. A bare {{user_email}} becomes user@example.com — an undeclared identifier and a syntax error. String values need wrapping quotes; the numeric-or-missing user_id is read unquoted and coerced with String().

You don't need tag sequencing

Because the loader snippet installs a queue stub synchronously, pushing onto window.stepskit._q works whether or not the SDK has finished downloading — StepsKit drains the queue on startup. That's why this tag can fire in any order relative to the loader, as long as the loader tag exists on the page.

3. Verify it worked

Use GTM Preview mode to confirm both tags fire on a page load, then load the real site and check:

  1. Dashboard — the project's Integrations page flips to Connected.
  2. Console, on a signed-in page — run window.stepskit.validateEnvironment(). You want a non-null visitorId and a populated userAttributes. A null visitorId means the identify tag didn't fire, or the data layer push happened after it.
  3. Networkcdn.stepskit.com/stepskit.latest.js returns 200.

validateEnvironment() also returns a toursFiltered table that says exactly why any given tour isn't showing.

Remember to Submit the container version — Preview mode only affects you.

GTM notes

Consent Mode. If your container runs Consent Mode v2, the StepsKit tag may be held back until the visitor consents, and tours won't render before then. Decide deliberately which consent category product tours belong to — for most B2B products this is functional rather than marketing.

Content Security Policy. GTM Custom HTML injects an inline script, so your script-src needs 'unsafe-inline' unless you've configured GTM's nonce propagation. StepsKit itself needs:

  • script-src: https://cdn.stepskit.com
  • connect-src: https://stepskit.com
  • style-src: 'unsafe-inline'required. The embed injects its entire stylesheet as a <style> element when the script evaluates. Without this the tour engine renders completely unstyled rather than merely off-brand.
  • img-src: https://*.supabase.co — only if your steps use images.

When to move it into the codebase. GTM is a fine permanent home. But if you find yourself fighting consent gating, CSP, or a slow container load before your tours appear, installing StepsKit directly is a ten-line change — pick your framework from the install overview.

Troubleshooting

Tag fires in Preview but the dashboard says "Not connected". You haven't submitted the container version yet.

SDK loads more than once per page. You added a History Change trigger, or the tag is on both All Pages and a second trigger. See above.

Console syntax error from the identify tag. A {{variable}} is missing its wrapping quotes. See above.

Connected, but visitorId is null. Either the data layer push runs after the tag fires, or the variable names in GTM don't match the keys you pushed. Check both in Preview mode's Data Layer tab.

Tour renders as unstyled text. Your CSP is missing style-src 'unsafe-inline'.

Next steps