Skip to main content

How Agencies Structure GTM Workspaces for 50+ Client Containers

Naming conventions, approval workflows, and version discipline that keep multi-client tag management from descending into chaos.

Running one GTM container well is a tagging problem. Running fifty is an operations problem that happens to involve tagging. The skills barely overlap. A brilliant analytics engineer who can build a Consent Mode v2 setup in her sleep will still lose an afternoon figuring out which of three open workspaces has the Meta CAPI change the client asked about last Tuesday.

What follows is the operating model I've seen hold up across agencies with large client rosters. None of it is clever. All of it is about making the next person's job obvious, including when the next person is you in four months with no memory of what you did.

Scale Turns Tagging Into Operations

The failure mode at fifty containers isn't a broken tag. It's ambiguity. Two people editing the same container without knowing it. A workspace called "test" that has been sitting there since March containing either nothing or a half-finished server-side migration. A published version whose only note is "updates."

Three constraints shape everything else. Standard GTM gives you three concurrent workspaces per container, and 360 gives you unlimited, so most agencies are working inside a hard limit of three and have to treat workspaces as a scarce resource rather than a scratchpad. Second, GTM has no branching or merge conflict resolution worth the name: if two workspaces touch the same tag, the second one to publish gets a conflict screen and a human has to choose. Third, there's no audit trail beyond version history, so whatever context you don't write into a version note is gone.

Design around those. Short-lived workspaces, one owner each, tight naming, and version notes written for a stranger. Every practice below follows from those three facts.

Naming Conventions Enforced at Review

A convention that lives in a Notion page is a suggestion. A convention checked during approval is a rule. Put the check in the approval step, because that's the only moment someone is already looking.

Start at the account layer. One GTM account per client, named exactly as the client appears in your billing system, not as someone's shorthand. Containers inside it named by property and surface, so a client with a marketing site, an app, and a server container looks like this:

The domain in the container name matters more than it looks. When someone pastes a GTM ID into Slack and asks "is this the right container," the name should answer without opening anything.

Inside the container, the tag naming scheme is a separate discipline and deserves its own document. What belongs in the agency operating model is the review rule: a change request cannot be approved if any new or edited tag, trigger, or variable fails the naming pattern. Reviewers get a checklist, not a philosophy. It's four questions. Does every new element follow the pattern? Is every new element in the correct folder? Does the workspace name match a ticket? Is the version note written for someone who wasn't in the conversation?

You'll get pushback that this slows things down. It adds about ninety seconds to a review and saves the twenty minutes someone spends next quarter trying to work out what "GA4 - New" was for.

One Workspace Per Change Request

The rule is one workspace, one ticket, one owner, and the workspace name is the ticket ID plus a short description. NW-4412 Add Meta CAPI purchase. Nothing else.

This solves several problems at once. The three-workspace limit becomes a natural work-in-progress cap that stops people opening five parallel changes they'll never finish. Anyone looking at the workspace list can see who's working on what without asking. And when a change gets rejected or deprioritized, you delete the workspace, which is clean, instead of leaving a half-finished edit in a shared space.

Two habits make this stick. First, the person who opens the workspace publishes it, or explicitly hands it off in writing. Shared ownership means nobody chases it. Second, workspaces have a shelf life. If a workspace is older than ten working days, it gets published, deleted, or escalated in the weekly ops review. There is no fourth option, and "we're waiting on the client" means delete it and reopen later. The change will still be there in the ticket.

The one exception worth carving out is a long-running migration, like moving a client from client-side to server-side GA4. Those get a dedicated workspace that lives for weeks, but the ticket for it stays open too, and it's named so nobody mistakes it for a routine change: NW-4200 SST MIGRATION do not delete.

The Approval Path Through Preview

Preview mode is your review environment, and the workspace-scoped preview link is the artifact that moves through approval. Open the workspace, click Preview, and the debug session runs that workspace's version of the container without publishing anything.

The path has four stops, and each produces something written down:

  1. Builder self-check. The person who made the change runs the full journey in Preview, screenshots the relevant tag firing with its variables resolved, and attaches it to the ticket.
  2. Peer review. A second person on the team opens the same workspace, runs their own preview session, and checks naming, trigger conditions, and whether any existing tag was touched by accident. This catches roughly everything that matters.
  3. Client or account approval. For anything that changes what's collected or where it goes, the account lead confirms with the client. Not for a typo fix in a tag name.
  4. Publish. Same person who built it, during business hours, never on a Friday afternoon for a container attached to a live campaign.

The peer review step is the one agencies skip when they're busy, and it's the one that pays. A reviewer who didn't build the thing notices that the trigger fires on all pages instead of the checkout path, because they read it fresh. The builder reads what they meant to write.

Use GTM's container-level permissions to make this real rather than aspirational. Most of the team gets Edit. A smaller group gets Approve and Publish. If everyone can publish, the review is theatre.

Version Names That Read Like a Changelog

Every publish writes a version. Version name and description are the only durable record of why anything in the container looks the way it does, and most teams waste them.

Use a consistent shape: ticket ID, verb, object. Then use the description for the part that matters, which is the reason and the blast radius.

FieldWeakUseful
Version nameUpdatesNW-4412 Add Meta CAPI purchase
Version nameFixNW-4455 Fix begin_checkout double fire
Version nameGA4 stuffNW-4501 Migrate GA4 purchase to server container
Description(blank)Adds Meta Conversions API purchase via sst.northwind.com. Browser pixel stays live, dedup on event_id. Client approved 12 Mar. Rollback: revert to v218.

Four things belong in the description: what changed in plain language, why, who approved it, and how to roll back. That last one costs eight words and is worth all of them at 6pm when revenue reporting looks wrong and nobody remembers which version was good.

Scan a container's version list after six months of this and you have a genuine changelog. You can answer "when did tracking change on the checkout page" in twenty seconds instead of bisecting through versions in Preview.

Onboarding a New Client Container

Standardize this or every container drifts into its own dialect. The onboarding checklist below takes under an hour and sets the shape for everything after.

Do the audit of whatever was there before as a separate ticket. Mixing "set up our way of working" with "clean up the last agency's mess" makes both take longer.

Access Management and Clean Offboarding

GTM permissions work at two levels, and mixing them up is how contractors keep access for a year. Account-level permission is either User or Admin, and Admin can create containers and manage users. Container-level permission is one of No access, Read, Edit, Approve, or Publish.

RoleAccount levelContainer level
Agency leadAdminPublish
Senior analytics engineerUserPublish
Analytics engineerUserApprove
Junior or contractorUserEdit
Account managerUserRead
Client marketing teamUserRead or Edit

Grant access per container, never blanket account Admin to the whole team. It's more clicks and it means a departing contractor doesn't take fifty containers' worth of publish rights with them.

Offboarding needs a written runbook because it always happens on a bad day. When a person leaves: remove them from every container in Admin, User Management, then check whether any GTM API service account was created under their Google Cloud project, because that's the access people forget. When a client leaves: transfer account ownership to their admin, remove all agency users, export the container JSON to your archive, and record the final published version number. Then actually remove yourself. Lingering access to a former client's tag manager is a liability, not a convenience.

The Monthly Automated Sweep

Containers accumulate junk that nobody notices because nothing breaks. Paused tags from a campaign that ended in 2023. Triggers referenced by no tag. Variables referenced by nothing. Custom HTML tags loading a script from a vendor the client stopped paying.

Run a monthly sweep with the GTM API rather than clicking through fifty containers. Export the live version of each container and analyze the JSON. Every relationship you need is in there.

const { google } = require("googleapis");

async function sweep(auth, containerPath) {
  const tm = google.tagmanager({ version: "v2", auth });
  const { data: v } = await tm.accounts.containers.versions.live({
    parent: containerPath
  });

  const tags = v.tag || [], triggers = v.trigger || [], vars = v.variable || [];
  const usedTriggers = new Set(tags.flatMap(t => [
    ...(t.firingTriggerId || []), ...(t.blockingTriggerId || [])
  ]));

  const blob = JSON.stringify({ tags, triggers });
  const findings = {
    paused: tags.filter(t => t.paused).map(t => t.name),
    untriggered: tags.filter(t => !(t.firingTriggerId || []).length).map(t => t.name),
    orphanTriggers: triggers.filter(t => !usedTriggers.has(t.triggerId)).map(t => t.name),
    orphanVars: vars.filter(x => !blob.includes("{{" + x.name + "}}")).map(x => x.name)
  };
  return { container: v.name, version: v.containerVersionId, findings };
}

Pipe the output into a sheet, one row per finding, with a column for the account lead. Zero-fire tags need a second source, since the container export tells you what exists, not what fired. Cross-reference GA4 event names or your BigQuery export against the tags that should be producing them, and anything with no hits in ninety days goes on the same list.

The review itself is short. For each finding: delete, fix, or write a one-line justification for why it stays. Do the deletions in a single monthly housekeeping workspace per container, named OPS-YYYY-MM housekeeping, published with a version note listing exactly what was removed. That note is your undo.

Making It Survive Turnover

Every practice here exists to answer one question quickly: what is in this container, why, and who decided. Naming answers what. Ticket-named workspaces answer who and when. Version descriptions answer why. The monthly sweep stops the answers from rotting.

If you're starting from a messy roster, don't try to fix fifty containers at once. Apply the model to every new change from today, run the sweep across everything once to get a punch list, and clean the top ten containers by revenue over a quarter. Within two months the difference is obvious in how fast people answer client questions, which is the metric that actually matters here.