Gtm Setup logo

Gtm Setup

Organization
cognyai
gtm-setup

GTM Event Tracking & Setup Reference — dataLayer fundamentals, trigger types, variable types, GA4 tag configuration, debug mode, and best practices

Overview

Publishercognyai
Repositoryclaude-code-marketing-skills
Skill namegtm-setup
Stars
111
Forks
15
Bundled files
Instructions only
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by cognyai on GitHub. Read the source before you install it.

Installation

Install the Gtm Setup AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/cognyai/claude-code-marketing-skills/tree/main/skills/gtm-setup
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/cognyai/claude-code-marketing-skills.git /tmp/claude-code-marketing-skills
mkdir -p .claude/skills
cp -r /tmp/claude-code-marketing-skills/skills/gtm-setup .claude/skills/gtm-setup
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Gtm Setup in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Gtm Setup on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Gtm Setup is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

GTM Event Tracking & Setup Reference

Complete reference guide to Google Tag Manager event tracking, including dataLayer fundamentals, trigger and variable types, GA4 tag configuration, debug mode, and implementation best practices.

Full docs: https://cogny.com/docs/gtm-event-tracking

Usage

/gtm-setup                              # Full GTM reference overview
/gtm-setup dataLayer                    # dataLayer fundamentals and syntax
/gtm-setup purchase                     # Show purchase event dataLayer.push()
/gtm-setup triggers                     # All trigger types explained
/gtm-setup variables                    # All variable types explained
/gtm-setup tags                         # GA4 tag configuration
/gtm-setup debug                        # Debug mode and troubleshooting
/gtm-setup best practices               # Naming conventions, organization
/gtm-setup common mistakes              # Race conditions, missing data, duplicates

Instructions

You are a Google Tag Manager expert. Use this reference to help users implement event tracking, configure tags/triggers/variables, debug GTM issues, and follow best practices.

When the user asks a question, find the relevant section below and provide precise, actionable answers with ready-to-use code snippets.

If the user provides a specific topic as an argument, focus on that area. Otherwise, provide an overview of GTM concepts and ask what they need help with.

If Cogny GTM MCP tools are available, use them to inspect the user's actual GTM container, list their tags/triggers/variables, and provide recommendations based on their real configuration.

Works with

Works with zero accounts: everything below is reference and copy-paste code.

JobWithout accountsEnhanced when connected
Read the real container—GTM tool_list_tags, tool_list_triggers, tool_list_variables, tool_audit_container_privacy
Build tags / triggers / variablesuser clicks it in GTMGTM write tools (asks first; see "Building in the connected container")
Verify the event arrivesGTM Preview + DebugViewGA4 tool_run_realtime_report
Check a Custom HTML tag didn't break pagesConsoleMicrosoft Clarity get_live_insights (Script Error Count)

dataLayer Fundamentals

What Is the dataLayer?

The dataLayer is a JavaScript array that acts as a message bus between your website and GTM. When you push objects onto the dataLayer, GTM reads them to fire triggers and populate variables.

Initialization

Declare the dataLayer before the GTM snippet to capture early pushes:

html
<script>
  window.dataLayer = window.dataLayer || [];
</script>
<!-- GTM snippet follows -->

dataLayer.push() Syntax

javascript
// Push data AND fire a trigger (event key is required for trigger evaluation)
dataLayer.push({
  'event': 'custom_event_name',
  'key1': 'value1',
  'key2': 'value2'
});

// Push data without firing a trigger (no 'event' key)
dataLayer.push({
  'user_type': 'premium',
  'user_id': '12345'
});

Object Persistence and Merging

Later pushes override earlier values for the same key. GTM's data model merges nested objects (and arrays, by index) recursively: only the keys you push are updated, everything else is kept.

javascript
dataLayer.push({ 'user': { 'name': 'John', 'plan': 'free' } });
dataLayer.push({ 'user': { 'plan': 'premium' } });
// Result: user = { name: 'John', plan: 'premium' } — user.name survives

That recursive merge is why stale e-commerce data leaks from one event into the next, and why you push { ecommerce: null } first.

Clearing Stale Data (Critical for E-Commerce)

Always push null before e-commerce events to prevent stale data contamination:

javascript
dataLayer.push({ ecommerce: null });
dataLayer.push({
  'event': 'view_item',
  'ecommerce': {
    'items': [{ 'item_id': 'SKU-001', 'item_name': 'Blue T-Shirt', 'price': 29.99 }]
  }
});

Common dataLayer Events

page_view

javascript
dataLayer.push({
  'event': 'page_view',
  'page_location': window.location.href,
  'page_title': document.title,
  'page_referrer': document.referrer,
  'content_group': 'blog'
});

For SPAs (React, Next.js, Vue), fire on route change:

javascript
useEffect(() => {
  dataLayer.push({
    'event': 'page_view',
    'page_location': window.location.href,
    'page_title': document.title
  });
}, [location.pathname]);

purchase

javascript
dataLayer.push({ ecommerce: null });
dataLayer.push({
  'event': 'purchase',
  'ecommerce': {
    'transaction_id': 'T-20250211-001',
    'value': 149.97,
    'tax': 12.50,
    'shipping': 5.99,
    'currency': 'USD',
    'coupon': 'SUMMER20',
    'items': [
      {
        'item_id': 'SKU-001',
        'item_name': 'Blue T-Shirt',
        'item_brand': 'BrandName',
        'item_category': 'Apparel',
        'item_category2': 'T-Shirts',
        'item_variant': 'Blue',
        'price': 29.99,
        'quantity': 2
      },
      {
        'item_id': 'SKU-045',
        'item_name': 'Running Shoes',
        'item_brand': 'BrandName',
        'item_category': 'Footwear',
        'item_variant': 'Black/Size-10',
        'price': 89.99,
        'quantity': 1
      }
    ]
  }
});

add_to_cart

javascript
dataLayer.push({ ecommerce: null });
dataLayer.push({
  'event': 'add_to_cart',
  'ecommerce': {
    'currency': 'USD',
    'value': 29.99,
    'items': [{
      'item_id': 'SKU-001',
      'item_name': 'Blue T-Shirt',
      'item_brand': 'BrandName',
      'item_category': 'Apparel',
      'price': 29.99,
      'quantity': 1
    }]
  }
});

remove_from_cart

javascript
dataLayer.push({ ecommerce: null });
dataLayer.push({
  'event': 'remove_from_cart',
  'ecommerce': {
    'currency': 'USD',
    'value': 29.99,
    'items': [{
      'item_id': 'SKU-001',
      'item_name': 'Blue T-Shirt',
      'price': 29.99,
      'quantity': 1
    }]
  }
});

view_item

javascript
dataLayer.push({ ecommerce: null });
dataLayer.push({
  'event': 'view_item',
  'ecommerce': {
    'currency': 'USD',
    'value': 29.99,
    'items': [{
      'item_id': 'SKU-001',
      'item_name': 'Blue T-Shirt',
      'item_brand': 'BrandName',
      'item_category': 'Apparel',
      'price': 29.99
    }]
  }
});

view_item_list

javascript
dataLayer.push({ ecommerce: null });
dataLayer.push({
  'event': 'view_item_list',
  'ecommerce': {
    'item_list_id': 'category_apparel',
    'item_list_name': 'Apparel',
    'items': [
      { 'item_id': 'SKU-001', 'item_name': 'Blue T-Shirt', 'index': 0, 'price': 29.99 },
      { 'item_id': 'SKU-002', 'item_name': 'Red T-Shirt', 'index': 1, 'price': 29.99 }
    ]
  }
});

begin_checkout

javascript
dataLayer.push({ ecommerce: null });
dataLayer.push({
  'event': 'begin_checkout',
  'ecommerce': {
    'currency': 'USD',
    'value': 149.97,
    'coupon': 'SUMMER20',
    'items': [
      { 'item_id': 'SKU-001', 'item_name': 'Blue T-Shirt', 'price': 29.99, 'quantity': 2 },
      { 'item_id': 'SKU-045', 'item_name': 'Running Shoes', 'price': 89.99, 'quantity': 1 }
    ]
  }
});

form_submit

javascript
dataLayer.push({
  'event': 'form_submit',
  'form_id': 'contact_form',
  'form_name': 'Contact Us',
  'form_destination': '/thank-you'
});

generate_lead

javascript
dataLayer.push({
  'event': 'generate_lead',
  'currency': 'USD',
  'value': 50.00,
  'lead_source': 'contact_form'
});

sign_up / login

javascript
dataLayer.push({ 'event': 'sign_up', 'method': 'google' });
dataLayer.push({ 'event': 'login', 'method': 'email' });

search

javascript
dataLayer.push({
  'event': 'search',
  'search_term': 'blue t-shirt',
  'search_results_count': 42
});

scroll (custom)

javascript
dataLayer.push({
  'event': 'scroll',
  'percent_scrolled': 90
});

Video events

javascript
// video_start
dataLayer.push({
  'event': 'video_start',
  'video_title': 'Product Demo',
  'video_url': 'https://youtube.com/watch?v=abc123',
  'video_provider': 'youtube',
  'video_duration': 180
});

// video_progress
dataLayer.push({
  'event': 'video_progress',
  'video_title': 'Product Demo',
  'video_percent': 50,
  'video_current_time': 90,
  'video_duration': 180
});

// video_complete
dataLayer.push({
  'event': 'video_complete',
  'video_title': 'Product Demo',
  'video_duration': 180
});

Trigger Types

Page View Triggers

Listed in firing order:

Sub-TypeFires WhenUse Case
Consent InitializationBefore every other triggerCMP tag, consent mode defaults
InitializationBefore everything except Consent InitializationGoogle tag, tags that set up shared config
Page Viewgtm.js (immediately)Page-load tags that don't need the DOM
DOM Readygtm.dom (DOM parsed)Tags that read DOM elements
Window Loadedgtm.load (all resources loaded)Tags depending on images/scripts

Filter by Page Path, Page Hostname, or Page URL.

Click Triggers

Sub-TypeFires WhenUse Case
All ElementsAny element clickedButtons, divs, images
Just Links<a> element clickedOutbound links, downloads

Variables available: Click Element, Click Classes, Click ID, Click Target, Click URL, Click Text.

Custom Event Trigger

Fires when a specific event value is pushed to the dataLayer. Set Event Name to match the event key value. Supports regex (e.g., form_submit.*).

javascript
dataLayer.push({ 'event': 'form_submit' });
// Fires trigger where Event Name = "form_submit"

Element Visibility Trigger

Fires when an element enters the viewport.

SettingDescription
Selection MethodCSS Selector or Element ID
When to fireOnce per page / once per element / every time
Minimum % visibleDefault 50%
Minimum on-screen durationMilliseconds

Timer Trigger

Fires at a set interval (milliseconds). Configure a limit to cap the number of firings.

Scroll Depth Trigger

Fires at specified scroll thresholds (percentages or pixels). Supports vertical and horizontal.

Variables: Scroll Depth Threshold, Scroll Depth Units, Scroll Direction.

YouTube Video Trigger

Built-in support for embedded YouTube videos. Captures: Start, Complete, Pause, Seeking, Buffering, Progress (at configurable thresholds).

Variables: Video Provider, Video Status, Video URL, Video Title, Video Duration, Video Current Time, Video Percent, Video Visible.

Form Submission Trigger

Fires on form submit. Options: Wait for Tags (delay submission up to 2000ms), Check Validation (only fire if browser validation passes).

Variables: Form Element, Form Classes, Form ID, Form Target, Form URL, Form Text.

History Change Trigger

Fires on URL fragment changes or pushState/replaceState. Essential for SPAs.

Variables: New History Fragment, Old History Fragment, New History State, Old History State, History Source.

Trigger Groups

Combine multiple triggers. Tag fires only when ALL triggers in the group have fired at least once during the page lifecycle.

Variable Types

Data Layer Variable

Reads a value from the dataLayer by key name. Use dot notation for nested values (e.g., ecommerce.transaction_id). Always use Version 2.

JavaScript Variable

Reads a global JS variable (e.g., document.title, navigator.language).

DOM Element Variable

Reads text content or an attribute from a DOM element by CSS selector or ID.

URL Variable

Extracts URL components: Full URL, Protocol, Host Name, Port, Path, Query (with optional Query Key), Fragment.

Constant Variable

Stores a fixed value. Use for Measurement IDs, Pixel IDs, and configuration strings shared across tags.

Custom JavaScript Variable

Runs an anonymous function and returns its value:

javascript
// Get viewport width
function() {
  return window.innerWidth;
}
javascript
// Classify page type from URL
function() {
  var path = window.location.pathname;
  if (path === '/') return 'homepage';
  if (path.indexOf('/product/') === 0) return 'product';
  if (path.indexOf('/category/') === 0) return 'category';
  if (path.indexOf('/blog/') === 0) return 'blog';
  if (path.indexOf('/checkout') === 0) return 'checkout';
  return 'other';
}
javascript
// Read a cookie
function() {
  var match = document.cookie.match(new RegExp('(^| )user_segment=([^;]+)'));
  return match ? match[2] : undefined;
}

Lookup Table Variable

Maps input values (from another variable) to output values. Example: Page Path / maps to Homepage, /pricing to Pricing.

RegEx Table Variable

Like Lookup Table but uses regex patterns. Example: ^/product/.* maps to Product Page. Supports capture groups and case sensitivity settings.

Tag Types

Google Tag

Sets up the GA4 measurement stream (it replaced the old "GA4 Configuration" tag; existing ones were migrated automatically). Fire on Initialization - All Pages.

SettingDescription
Tag IDG-XXXXXXXXXX (use a Constant variable)
Configuration settingsKey/value pairs such as send_page_view, debug_mode, user_id (or a "Google Tag: Configuration Settings" variable)
Shared event settingsParameters sent with every event (or a "Google Tag: Event Settings" variable)

GA4 Event Tag

Sends a custom event to GA4.

SettingDescription
Measurement IDG-XXXXXXXXXX (same as the Google tag)
Event NameGA4 event name (e.g., generate_lead)
Event ParametersKey-value pairs (e.g., transaction_id, value)

Custom HTML Tag

Runs arbitrary HTML/JavaScript. Always wrap in try/catch:

html
<script>
  (function() {
    try {
      thirdPartySDK.track('event', { value: {{DLV - Value}} });
    } catch (e) {
      console.warn('GTM Custom HTML error:', e);
    }
  })();
</script>

Custom Image Tag

Fires a pixel request (1x1 image). Configure Image URL with tracking parameters. Enable cache busting.

Built-In Variables

Click Variables

Click Element, Click Classes, Click ID, Click Target, Click URL, Click Text

Form Variables

Form Element, Form Classes, Form ID, Form Target, Form URL, Form Text

Page Variables

Page URL, Page Hostname, Page Path, Referrer

Scroll Variables

Scroll Depth Threshold, Scroll Depth Units, Scroll Direction

Video Variables

Video Provider, Video Status, Video URL, Video Title, Video Duration, Video Current Time, Video Percent, Video Visible

Utility Variables

Event, Environment Name, Container ID, Container Version, HTML ID, Random Number, Debug Mode

Debug Mode

GTM Preview/Debug

Click "Preview" in GTM workspace. Tag Assistant opens and shows:

  • Tags Fired / Tags Not Fired per event
  • dataLayer state at each event
  • Variables and resolved values

Console Inspection

javascript
// View entire dataLayer
console.table(dataLayer);

// Filter for specific events
dataLayer.filter(function(item) { return item.event === 'purchase'; });

// Watch pushes in real time
(function() {
  var orig = dataLayer.push;
  dataLayer.push = function() {
    console.log('dataLayer.push:', arguments[0]);
    return orig.apply(dataLayer, arguments);
  };
})();

GA4 DebugView

Add debug_mode: true to your Google tag's configuration settings, then view events at GA4 Admin > DebugView.

Network Tab

Filter by collect in DevTools Network tab to see GA4 hits. Verify payload parameters.

Verify from the data (enhanced when connected)

If Cogny isn't connected, skip this and note what connecting adds.

  • GA4: right after publishing, tool_run_realtime_report (property_id, metrics: [eventCount], dimensions: [eventName]). The new event should appear within minutes of a test conversion. Missing: back to Preview.
  • Clarity: a day after publishing a Custom HTML tag, get_live_insights (num_of_days: 1, metrics: [Script Error Count], dimensions: [URL]). A jump on the pages the tag fires on means the tag throws.

Building in the connected container (writes, asks first)

If Cogny isn't connected, skip this and note what connecting adds.

Every call below changes the container. Show the exact name, type and parameters, and wait for a yes before each batch. Never publish without a separate yes.

  1. tool_create_workspace (account_id, container_id, name): work in a feature workspace, not Default.
  2. tool_create_variable (…, workspace_id, name, variable_type, parameters).
  3. tool_create_trigger (…, workspace_id, name, trigger_type, parameters).
  4. tool_create_tag (…, workspace_id, name, tag_type, parameters, firing_trigger_ids).
  5. Ask the user to test in GTM Preview, then tool_create_version (…, workspace_id, name, notes).
  6. tool_publish_version (account_id, container_id, version_id) only on an explicit "publish".

Best Practices

Naming Conventions

ElementConventionExample
Tags[Platform] - [Action] - [Detail]GA4 - Event - Purchase
Triggers[Type] - [Condition]CE - form_submit
Variables[Type prefix] - [Name]DLV - Transaction ID
Custom Eventssnake_casegenerate_lead
FoldersBy platform or functionGA4, Meta, Consent

Folder Organization

GA4/
  Google Tag - G-XXXX - All Pages
  GA4 - Event - Purchase
  GA4 - Event - Add to Cart
Meta/
  Meta - Pixel - Base Code
  Meta - Event - Purchase
Consent/
  HTML - Consent Banner Init
  HTML - Consent Mode Update

Workspace Management

  • Use feature workspaces for each project (e.g., Q1-2025-GA4-ecommerce)
  • Always Preview before creating a version
  • Delete or merge unused workspaces

Version Control

  • Create a version before and after significant changes
  • Write descriptive version names: "v42 - Added GA4 e-commerce checkout funnel"
  • Roll back if issues arise post-publish

Tag Sequencing

Use Tag Sequencing to ensure setup tags fire before dependent tags. Use tag firing priority (higher number = fires first) when order matters on the same trigger.

Consent Mode v2

Consent is set with the gtag('consent', ...) command, not a dataLayer event. A dataLayer.push({event: 'consent_default', consent: {...}}) does nothing for consent mode. Put the default above the GTM snippet (or use a CMP template / consent-mode template on the Consent Initialization trigger, which calls setDefaultConsentState / updateConsentState).

javascript
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}

// Default (before user choice)
gtag('consent', 'default', {
  'ad_storage': 'denied',
  'analytics_storage': 'denied',
  'ad_user_data': 'denied',
  'ad_personalization': 'denied'
});

// After user grants consent (from the banner's callback)
gtag('consent', 'update', {
  'ad_storage': 'granted',
  'analytics_storage': 'granted',
  'ad_user_data': 'granted',
  'ad_personalization': 'granted'
});

Common Mistakes

1. Race Conditions

Tags fire before dataLayer contains required data. Fix: use Custom Event triggers tied to the specific event push, not Page View triggers.

2. Incorrect Trigger Firing

Tags fire on all pages instead of specific ones. Fix: always add trigger filters (e.g., Page Path equals /thank-you).

3. Missing ecommerce Null Reset

Stale data from previous pushes contaminates events. Fix: always dataLayer.push({ ecommerce: null }) before ecommerce events.

4. Duplicate Tags

Same event sent multiple times. Causes: multiple triggers, conditions too broad, wrong firing option. Fix: use "Once per Page" or review trigger conditions.

5. Custom HTML Breaking Pages

JS errors in Custom HTML block other tags. Fix: wrap all Custom HTML in try/catch.

6. Data Layer Variable Returns Undefined

Causes: key name mismatch (case-sensitive), data pushed after trigger, wrong DLV version, incorrect dot-notation path.

7. SPA Page Views Not Tracked

Route changes not captured. Fix: use History Change trigger or push custom page_view events on route change:

javascript
// Next.js App Router
'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';

export function GTMPageView() {
  const pathname = usePathname();
  useEffect(() => {
    dataLayer.push({
      'event': 'page_view',
      'page_location': window.location.href,
      'page_title': document.title
    });
  }, [pathname]);
  return null;
}

8. Cross-Domain Tracking Issues

Users counted as new sessions across domains. Fix: configure cross-domain linker in the Google tag settings or GA4 Admin > Data Streams > Configure your domains.

9. Preview Traffic Inflating Analytics

Fix: add blocking trigger where Debug Mode equals true, or filter internal traffic via traffic_type parameter.

Resources

Frequently asked questions

What does the Gtm Setup AI skill do?

GTM Event Tracking & Setup Reference — dataLayer fundamentals, trigger types, variable types, GA4 tag configuration, debug mode, and best practices

Why use Gtm Setup on TypingMind?

Because you install it once and use it with any model. Gtm Setup is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Gtm Setup in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/cognyai/claude-code-marketing-skills/tree/main/skills/gtm-setup. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Gtm Setup?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Gtm Setup?

As many as you like. As long as a model supports skills, you can use Gtm Setup with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Gtm Setup AI skill free?

It is published on GitHub by cognyai. Check the repository for licensing terms. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇