Skip to main content

Auto-Generating GTM Tags from Plain English: How It Works

From prompt to deployable container config: intent parsing, template matching, validation, and the guardrails in between.

"Track when someone submits the demo request form and send it to GA4 and Google Ads." That's seventeen words. Doing it by hand is maybe twenty minutes of clicking: a GA4 event tag, a Google Ads conversion tag, a trigger, two or three data layer variables, then Preview mode, then a version and a publish. None of it is hard. All of it is fiddly, and the fiddliness is where mistakes come from.

Generating that config automatically is not a matter of handing a prompt to a language model and pasting the JSON it returns. A model asked to produce raw GTM container JSON will produce something that looks correct and imports with subtly wrong trigger types, invented parameter names, and a tag type string that hasn't existed since 2019. The interesting engineering is in the layers around the model that stop that from happening. Here's what those layers do.

What a short prompt has to carry

Every GTM implementation, regardless of how it's described, resolves to the same handful of decisions. What user action fires this? Which destination or destinations receive it? What is the event named? What parameters travel with it? Under what conditions should it not fire?

A usable prompt has to imply answers to at least the first three. "Track form submissions" implies the action, gestures at nothing else, and is not enough to generate from. "Send a generate_lead event to GA4 when the demo request form is submitted, with the plan tier from the data layer" implies all five, and is the kind of sentence that produces something deployable.

The system's first job, then, is not generation. It's determining whether the prompt has enough in it, and being willing to say no.

The intent parse step

The parse takes free text and produces a structured object with typed fields. Not GTM JSON, which is verbose and full of implementation detail, but an intermediate representation small enough to validate and show back to a human.

{
  "action": {
    "type": "form_submit",
    "selector_hint": "demo request form",
    "confidence": 0.91
  },
  "destinations": [
    { "platform": "ga4", "event_name": "generate_lead" },
    { "platform": "google_ads", "conversion_type": "lead" }
  ],
  "parameters": [
    { "name": "plan_tier", "source": "datalayer", "key": "planTier",
      "exists_in_container": false, "required": false }
  ],
  "conditions": [],
  "consent": { "required": ["ad_storage", "analytics_storage"] },
  "unresolved": ["google_ads_conversion_label"]
}

Two fields do most of the work. confidence carries a per-field score rather than one number for the whole request, because a prompt can be perfectly clear about the destination and completely vague about the trigger. And unresolved is the list of things the system knows it needs and does not have, which drives the next step.

Notice what the parse also does: it maps loose language onto GTM's actual vocabulary. "When someone hits the thank you page" becomes a Page View trigger with a path condition, not a Custom Event. "After they've been on the page ten seconds" becomes a Timer trigger. That mapping is a fixed lookup, not something the model invents each time.

Asking instead of guessing

The strongest design decision in a system like this is that ambiguity produces a question, not a default. Defaults feel helpful and cost you far more later, because a wrong default gets deployed and discovered three weeks after it started polluting your data.

Take "track purchases." That could be a GA4 purchase event on the order confirmation page, a server-side conversion sent after payment settles, or an ecommerce event with the full items array. Those are three different implementations with three different failure modes. Guessing gets it right maybe half the time.

So the system asks, and asks specifically:

Should this fire on the order confirmation page view, or on a data layer event pushed after payment confirmation? Page view is simpler. The data layer event avoids counting refreshes as duplicate purchases.

One question, the tradeoff stated, a recommendation implied. Not a form with nine fields, which is just the GTM interface with extra steps.

The rule that governs this: ask when a wrong answer produces silently bad data. Don't ask when a wrong answer is obvious and cheap to fix. A misnamed event is visible immediately and takes a minute to correct. A trigger that double-counts on page refresh is invisible until someone reconciles against the order table.

Instantiating from tested templates

Here's the part that matters most for output quality. The model does not write the tag configuration. It selects a template and fills its slots.

The template library holds a tested definition for each supported pattern: GA4 event tag, GA4 configuration, Google Ads conversion, Google Ads remarketing, Meta standard event, Meta CAPI via server container, LinkedIn Insight conversion, custom HTML with a consent gate, and so on. Each one has been imported into a real container and verified to fire correctly.

// Template: ga4_event_tag
// Slots marked {{ }} are filled from the parsed intent.
{
  "name": "GA4 Event - {{ event_name }}",
  "type": "gaawe",
  "parameter": [
    { "type": "TEMPLATE", "key": "eventName",
      "value": "{{ event_name }}" },
    { "type": "TAG_REFERENCE", "key": "measurementId",
      "value": "{{ ga4_config_tag_name }}" },
    { "type": "LIST", "key": "eventSettingsTable",
      "list": [ /* one row per parameter */ ] }
  ],
  "firingTriggerId": [ "{{ trigger_id }}" ],
  "consentSettings": {
    "consentStatus": "NEEDED",
    "consentType": { "type": "LIST",
      "list": [ { "type": "TEMPLATE", "value": "analytics_storage" } ] }
  }
}

The tag type string gaawe is the sort of detail this approach protects you from. It's the internal identifier for a GA4 event tag, it isn't documented anywhere obvious, and a model generating freeform JSON will happily produce ga4_event or googtag instead. Fixed in a template, it's right every time.

The tradeoff is honest: templates mean the system can only produce patterns someone has already built and tested. Ask for something genuinely novel and it will tell you it doesn't have a template rather than improvising. For the roughly forty patterns that make up the overwhelming majority of real implementation work, that's the right trade.

Variables that don't exist yet

A generated tag referencing {{DLV - planTier}} is worthless if that variable isn't in the container. This is the most common failure in hand-built implementations too: the tag is right, the trigger is right, and the parameter arrives undefined because nobody created the variable.

So the generator reconciles against the container's actual contents before producing anything. For each data layer key the intent needs, it checks whether a Data Layer Variable already points at that key, under any name. If one exists, reference it. If not, create it, following the container's existing naming convention rather than a hardcoded one.

That last part is worth spelling out. If your container's variables are named DLV - planTier, the new one matches. If they're named dl.plan_tier, it matches that instead. The convention is inferred from what's already there, because a container where half the variables follow one pattern and half follow another is worse than either pattern consistently applied.

New Data Layer Variables get Version 2 and a default value where a missing key would break something downstream. Where the prompt describes a value that isn't in the data layer at all, like "the pricing tier from the URL," the system generates a URL variable with the right component and query key rather than a data layer variable that would always be undefined.

Deterministic validation and dry run

Everything up to this point involves a model. Everything after this point does not. The validation layer is ordinary code with ordinary rules, and it runs on every generated config regardless of how confident the earlier steps were.

Check What it catches On failure
Schema validation Invalid tag types, malformed parameter blocks, missing required keys Block
Reference integrity Tags pointing at trigger or variable IDs that don't exist Block
Duplicate trigger detection A trigger with identical type and conditions to an existing one Reuse the existing trigger, warn
Duplicate tag detection Same destination, same event name, same trigger already live Block, show the existing tag
Naming convention Names that break the container's dominant pattern Rewrite, warn
Consent settings Advertising tags with no consent condition in a consent-configured container Block
GA4 event name rules Reserved names, over 40 characters, invalid characters, spaces Block
Parameter limits More than 25 event parameters, names over 40 characters Block

Duplicate trigger detection deserves a note because it's the check that keeps containers from rotting. Over two years, a container accumulates six triggers all listening for gtm.formSubmit with slightly different names, created by different people who didn't check first. Each one is a small thing. Collectively they make the container unreadable. Detecting the duplicate and reusing the existing trigger costs nothing and prevents the accumulation.

The dry run is the final gate. The config goes into a workspace, not the live container, and Preview mode runs against a real page. You get the actual Tags Fired list, the actual parameter values that resolved, and the actual event payload. Then you decide whether to version it.

Nothing generated should reach a live container without passing through a workspace that a person looked at. That's not a hedge, it's the same rule you'd apply to a colleague's pull request.

Where a human still decides

Generation handles the mechanical translation well. It does not handle judgment, and the categories where it needs you are fairly predictable.

The honest split: generation removes the twenty minutes of clicking and the class of errors that come from clicking. It doesn't remove the ten minutes of thinking, and you shouldn't want it to.

Writing prompts that produce good output

The difference between a prompt that generates something deployable and one that generates a clarifying question is usually four extra words. Name the trigger condition, name the destination, name the event, and name the parameters you care about.

Weak prompt Strong prompt
Track form submissions Fire a GA4 generate_lead event when the data layer pushes demo_form_submit, with plan_tier and company_size as parameters
Add Facebook tracking Add a Meta Purchase event on the existing purchase data layer push, with value and currency, gated on ad_storage consent
Track video engagement Send GA4 video_progress events at 25, 50, and 75 percent on the YouTube embeds, using the built-in video triggers

Three habits that improve output more than anything else. Say the event name you want rather than describing it, because your naming taxonomy is yours and no system should be guessing at it. Say which data layer key holds the value instead of describing the value in prose. And describe negative conditions explicitly, because "except on the internal test pages" is a filter that has to be written and won't be inferred.

Batch related requests into one prompt when they share a trigger. "Send the same purchase event to GA4, Google Ads, and Meta" produces one trigger and three tags correctly wired to it. Three separate prompts produce three triggers doing the same job, and now the duplicate detector is cleaning up after you.

The realistic gain

Automated generation isn't magic and the pitch that it is does nobody any favors. What it reliably removes is the repetitive middle of the job: the clicking, the copy-paste errors in measurement IDs, the variable someone forgot to create, the sixth duplicate form-submit trigger.

What stays with you is the part that was always the actual work. Deciding what to measure, agreeing on what counts, and checking the result against something real before you publish it.