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(orInitialization - All Pages). - Support document.write: leave unchecked. The snippet uses
insertBefore, notdocument.write. - Name it something like
StepsKit — loaderand 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:
- Dashboard — the project's Integrations page flips to Connected.
- Console, on a signed-in page — run
window.stepskit.validateEnvironment(). You want a non-nullvisitorIdand a populateduserAttributes. A nullvisitorIdmeans the identify tag didn't fire, or the data layer push happened after it. - Network —
cdn.stepskit.com/stepskit.latest.jsreturns200.
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.comconnect-src:https://stepskit.comstyle-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
- JavaScript API reference — every method on
window.stepskit. - User attributes — what you can target on.
- Frequency capping — how "show once" works.