GTM-as-Code: Version Control with GTM CLI
The GTM-as-Code paradigm treats your Google Tag Manager container configuration as source code. By exporting your container to JSON, storing it in Git, and deploying via CI/CD pipelines, you gain:
- Audit trail: Full Git history of every tag, trigger, and variable change
- Code review: Pull requests for tag deployments with approval workflows
- Rollback capability: Revert to previous container versions via Git
- Environment parity: Manage dev, staging, and production GTM containers from a single codebase
The Owntag GTM CLI is the primary open-source tool for implementing this pattern.
Owntag GTM CLI overview
Section titled “Owntag GTM CLI overview”The Owntag GTM CLI is a command-line tool that:
- Exports GTM container configurations to human-readable JSON format
- Imports JSON back into GTM containers
- Authenticates via Google service accounts (no manual login)
- Integrates with CI/CD platforms (GitHub Actions, GitLab CI, Jenkins, etc.)
Installation
Section titled “Installation”Install via npm:
npm install -g @owntag/gtm-cli# ornpm install --save-dev @owntag/gtm-cliVerify installation:
gtm --versionAuthentication via service account
Section titled “Authentication via service account”GTM CLI authenticates to Google APIs using a service account key (a JSON file with credentials).
Step 1: Create a service account in Google Cloud Console
Section titled “Step 1: Create a service account in Google Cloud Console”- Go to Google Cloud Console
- Create a new project or select an existing one
- Navigate to IAM & Admin > Service Accounts
- Click Create Service Account
- Name it (e.g.,
gtm-cli-sa) - Grant the role Editor (or more restrictively, a custom role with GTM permissions)
- Create a JSON key and download it
Step 2: Grant the service account access to your GTM container
Section titled “Step 2: Grant the service account access to your GTM container”- Go to Google Tag Manager
- Open your container
- Navigate to Admin > User Management
- Click Add User
- Enter the service account email (found in the JSON key:
client_email) - Grant Admin role
- Click Invite
Step 3: Export the key to your repository
Section titled “Step 3: Export the key to your repository”Store the service account JSON key securely:
# DO NOT commit the actual JSON file to Git!# Store it as a GitHub Secret or CI/CD variableexport GTM_SERVICE_ACCOUNT_KEY="$(cat /path/to/service-account-key.json)"Or store the key file in a .gitignored directory:
echo "secrets/" >> .gitignorecp /path/to/service-account-key.json secrets/Exporting containers to JSON
Section titled “Exporting containers to JSON”Export a container
Section titled “Export a container”gtm export \ --service-account-key ./secrets/service-account-key.json \ --container-id YOUR_CONTAINER_ID \ --output ./gtm-container.jsonThe resulting gtm-container.json is a complete representation of your container:
{ "containerVersion": { "container": { "accountId": "123456789", "containerId": "987654321", "name": "Web Container", "containerTypeId": "web" }, "tag": [ { "accountId": "123456789", "containerId": "987654321", "tagId": "1", "name": "GA4 Page View Tag", "type": "gat", "parameter": [ {"type": "template", "key": "trackingId", "value": "G-XXXXXXXXXX"} ], "firingTriggerId": ["2"] } ], "trigger": [ { "accountId": "123456789", "containerId": "987654321", "triggerId": "2", "name": "All Pages", "type": "pageview" } ], "variable": [ { "accountId": "123456789", "containerId": "987654321", "variableId": "1", "name": "GA4 Measurement ID", "type": "c", "parameter": [{"type": "template", "key": "value", "value": "{{GA4 Tracking ID}}"}] } ] }}Commit this to Git:
git add gtm-container.jsongit commit -m "Export GTM container configuration"git push origin mainImporting and deploying via CI/CD
Section titled “Importing and deploying via CI/CD”Import a container from JSON
Section titled “Import a container from JSON”gtm import \ --service-account-key ./secrets/service-account-key.json \ --container-id YOUR_CONTAINER_ID \ --input ./gtm-container.jsonThis creates a new container version in GTM with all tags, triggers, and variables from the JSON file.
GitHub Actions CI/CD workflow
Section titled “GitHub Actions CI/CD workflow”Create .github/workflows/deploy-gtm.yml:
name: Deploy GTM Container
on: push: branches: [main] paths: - 'gtm-container.json'
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3
- uses: actions/setup-node@v3 with: node-version: '18'
- name: Install GTM CLI run: npm install -g @owntag/gtm-cli
- name: Import GTM container env: GTM_SERVICE_ACCOUNT_KEY: ${{ secrets.GTM_SERVICE_ACCOUNT_KEY }} GTM_CONTAINER_ID: ${{ secrets.GTM_CONTAINER_ID }} run: | echo "$GTM_SERVICE_ACCOUNT_KEY" > /tmp/sa-key.json gtm import \ --service-account-key /tmp/sa-key.json \ --container-id "$GTM_CONTAINER_ID" \ --input gtm-container.json rm /tmp/sa-key.json
- name: Publish container version env: GTM_SERVICE_ACCOUNT_KEY: ${{ secrets.GTM_SERVICE_ACCOUNT_KEY }} GTM_CONTAINER_ID: ${{ secrets.GTM_CONTAINER_ID }} run: | echo "$GTM_SERVICE_ACCOUNT_KEY" > /tmp/sa-key.json gtm publish \ --service-account-key /tmp/sa-key.json \ --container-id "$GTM_CONTAINER_ID" rm /tmp/sa-key.jsonGitLab CI workflow
Section titled “GitLab CI workflow”Create .gitlab-ci.yml:
deploy-gtm: stage: deploy only: - main script: - npm install -g @owntag/gtm-cli - echo "$GTM_SERVICE_ACCOUNT_KEY" > /tmp/sa-key.json - gtm import --service-account-key /tmp/sa-key.json --container-id "$GTM_CONTAINER_ID" --input gtm-container.json - gtm publish --service-account-key /tmp/sa-key.json --container-id "$GTM_CONTAINER_ID" - rm /tmp/sa-key.jsonManaging multiple containers across environments
Section titled “Managing multiple containers across environments”For a typical setup with dev, staging, and production GTM containers:
Repository structure
Section titled “Repository structure”gtm-as-code/├── containers/│ ├── dev/│ │ └── gtm-container.json│ ├── staging/│ │ └── gtm-container.json│ └── production/│ └── gtm-container.json├── .github/│ └── workflows/│ ├── deploy-dev.yml│ ├── deploy-staging.yml│ └── deploy-production.yml└── README.mdSeparate workflows per environment
Section titled “Separate workflows per environment”.github/workflows/deploy-production.yml (requires approval):
name: Deploy GTM to Production
on: workflow_dispatch: # Manual trigger only
jobs: deploy: runs-on: ubuntu-latest environment: production # Requires approval in GitHub steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - name: Install GTM CLI run: npm install -g @owntag/gtm-cli - name: Deploy to production env: GTM_SERVICE_ACCOUNT_KEY: ${{ secrets.GTM_PROD_SA_KEY }} GTM_CONTAINER_ID: ${{ secrets.GTM_PROD_CONTAINER_ID }} run: | echo "$GTM_SERVICE_ACCOUNT_KEY" > /tmp/sa-key.json gtm import --service-account-key /tmp/sa-key.json --container-id "$GTM_CONTAINER_ID" --input containers/production/gtm-container.json gtm publish --service-account-key /tmp/sa-key.json --container-id "$GTM_CONTAINER_ID" rm /tmp/sa-key.jsonComparison with manual GTM API scripting
Section titled “Comparison with manual GTM API scripting”| Approach | Pros | Cons |
|---|---|---|
| GTM-as-Code (GTM CLI) | Pre-built, intuitive CLI; handles JSON serialization; active community | Requires Node.js; external dependency |
| Manual GTM API calls | Full control; no external tools needed | Complex JSON formatting; error-prone; no built-in version management |
| GTM UI | No learning curve | No audit trail; manual error-prone; no rollback; not reproducible |
For most teams, GTM CLI is recommended because it eliminates boilerplate API calls and provides a standardized format.
Benefits of GTM-as-Code
Section titled “Benefits of GTM-as-Code”Audit trail
Section titled “Audit trail”Every commit shows what changed, who changed it, and when:
git log --onelineOutput:
a3f2c1e Add retargeting pixel to analytics tagb4e5d2f Update GA4 measurement IDc5f6e3a Remove deprecated Universal Analytics tagInspect diffs between versions:
git show a3f2c1eCode review for tag changes
Section titled “Code review for tag changes”Require pull request approvals before deploying to GTM:
- Make a change to
gtm-container.jsonon a branch - Open a pull request
- Colleagues review the diff
- After approval, merge to
mainand CI/CD automatically deploys
Rollback via Git
Section titled “Rollback via Git”If a tag deployment breaks tracking, revert in seconds:
git revert a3f2c1e# orgit reset --hard <previous-commit>git push origin mainCI/CD automatically re-deploys the previous container version.
Local testing and schema validation
Section titled “Local testing and schema validation”Validate JSON syntax locally before pushing:
gtm validate --input gtm-container.json(Check Owntag documentation for exact validation syntax; capabilities may vary by CLI version.)
Workflow example: Adding a new tag via pull request
Section titled “Workflow example: Adding a new tag via pull request”-
Create a branch:
Terminal window git checkout -b add-facebook-pixel -
Edit
gtm-container.jsonto add your tag, trigger, and variable -
Push and open a PR:
Terminal window git push origin add-facebook-pixel -
Request review: Team members review the JSON diff
-
Merge and auto-deploy: Once approved, merge to
main. GitHub Actions runs the deployment workflow and updates your GTM container -
Verify in GTM UI: Visit your GTM container and confirm the new tag appears
-
Monitor: Check GA4, Analytics dashboard, etc. to confirm the tag fires correctly
Related resources
Section titled “Related resources”Common questions
Section titled “Common questions”How do I handle sensitive data like API keys in my JSON?
Section titled “How do I handle sensitive data like API keys in my JSON?”Use GTM variables with template syntax (e.g., {{Facebook Pixel ID}}). The actual values are stored in GTM Admin, not in JSON. The JSON contains the variable reference, not the secret.
Can I use GTM-as-Code with Google Ads Manager Account (MCC)?
Section titled “Can I use GTM-as-Code with Google Ads Manager Account (MCC)?”GTM CLI works with individual GTM containers. For MCC-level automation, you’d need to run separate imports for each sub-account container, or script multiple calls.
What happens if the GTM container in the UI diverges from Git?
Section titled “What happens if the GTM container in the UI diverges from Git?”The next gtm import will overwrite the container with the JSON from Git. To avoid conflicts, establish a team rule: all changes go through Git and CI/CD, never manual edits in the UI. Periodic exports can detect drift if needed.