What it does
Sets a standardized final URL suffix (tracking template) on campaigns and/or ad groups. It replaces the {CampaignName} and {AdGroupName} value parameters with the actual, URL-encoded account names, so analytics tools receive clean, consistent campaign and ad group dimensions instead of raw parameter text. Optional name filters let you target only campaigns or ad groups that contain a given substring.
The code
/**
* Google Ads Script: Standardize UTM Tracking Templates
* Applies a consistent final URL suffix (tracking template) with proper
* {CampaignName} and {AdGroupName} parameters across campaigns and ad groups.
*
* Filters:
* _CAMPAIGN_CONTAINS - only campaigns whose name contains this substring
* _ADGROUP_CONTAINS - only ad groups whose name contains this substring
* STATUS - ENABLED, PAUSED, etc.
*/
function main() {
var SuffixTemplate = "utm_source=google&utm_medium=cpc&utm_campaign={CampaignName}&gclid={gclid}&utm_adgroup={AdGroupName}&utm_term={keyword}&utm_content={adcontent}&utm_device={device}&utm_matchtype={matchtype}&utm_network={network}&utm_location={loc_physical_ms}";
var _CAMPAIGN_CONTAINS = ""; // Filter by Campaign name
var _ADGROUP_CONTAINS = ""; // Filter by Adgroup name
var STATUS = "ENABLED"; // ENABLED, PAUSED
if (SuffixTemplate.search(/{AdGroupName}|{CampaignName}/g) == -1) {
Logger.log("Enter at least one of the {CampaignName} or {AdGroupName} parameter in the tracking template");
return;
}
// Case: only {CampaignName} is used -> apply at the campaign level.
if (SuffixTemplate.search("{CampaignName}") > 0 && SuffixTemplate.search("{AdGroupName}") == -1) {
var campaignIterator = _CAMPAIGN_CONTAINS == "" ?
AdsApp.campaigns().withCondition("Status = " + STATUS).get() :
AdsApp.campaigns().withCondition("Name contains '" + _CAMPAIGN_CONTAINS + "'").withCondition("Status = " + STATUS).get();
if (!campaignIterator.hasNext()) {
Logger.log("No Campaigns matched with this condition");
return;
}
while (campaignIterator.hasNext()) {
var campaign = campaignIterator.next();
var campaigntemplate = SuffixTemplate.replace(/{CampaignName}/g, campaign.getName().replace(/\s/g, '%20'));
campaign.urls().setFinalUrlSuffix(campaigntemplate);
Logger.log(campaign.getName() + " => " + campaigntemplate);
}
}
// Case: {AdGroupName} is used -> apply at the ad group level.
if (SuffixTemplate.search("{AdGroupName}") > 0) {
var adgroupIterator = { hasNext: function () { return false; } };
if (_ADGROUP_CONTAINS == "" && _CAMPAIGN_CONTAINS == "") {
adgroupIterator = AdsApp.adGroups().withCondition("Status = " + STATUS).get();
} else if (_ADGROUP_CONTAINS == "" && _CAMPAIGN_CONTAINS !== "") {
adgroupIterator = AdsApp.adGroups().withCondition("CampaignName contains '" + _CAMPAIGN_CONTAINS + "'").withCondition("Status = " + STATUS).get();
} else if (_ADGROUP_CONTAINS !== "" && _CAMPAIGN_CONTAINS !== "") {
adgroupIterator = AdsApp.adGroups().withCondition("CampaignName contains '" + _CAMPAIGN_CONTAINS + "'").withCondition("Name contains '" + _ADGROUP_CONTAINS + "'").withCondition("Status = " + STATUS).get();
} else if (_ADGROUP_CONTAINS !== "" && _CAMPAIGN_CONTAINS == "") {
adgroupIterator = AdsApp.adGroups().withCondition("Name contains '" + _ADGROUP_CONTAINS + "'").withCondition("Status = " + STATUS).get();
}
if (!adgroupIterator.hasNext()) {
Logger.log("No Campaigns/Adgroups matched with this condition");
return;
}
while (adgroupIterator.hasNext()) {
var adgroup = adgroupIterator.next();
var adgrouptemplate = SuffixTemplate.replace(/{AdGroupName}/g, adgroup.getName().replace(/\s/g, '%20'));
if (SuffixTemplate.search("{CampaignName}") > 0) {
adgrouptemplate = adgrouptemplate.replace(/{CampaignName}/g, adgroup.getCampaign().getName().replace(/\s/g, '%20'));
}
adgroup.urls().setFinalUrlSuffix(adgrouptemplate);
Logger.log(adgroup.getCampaign().getName() + " => " + adgroup.getName() + " => " + adgrouptemplate);
}
}
}
When to use this
Use this when tracking is inconsistent across an account and you need one canonical UTM template everywhere. It's the fastest way to standardize utm_campaign / utm_adgroup naming at scale, apply the template to a subset of campaigns (via the name filters) before a full rollout, or retrofit a template across hundreds of campaigns or ad groups without editing each one in the UI.
Prerequisites
- Script access at the account or MCC level (Tools & Settings > Bulk Actions > Scripts).
- A final URL suffix format that matches what your analytics/CRM expects.
How to implement
- 1
Create a new script
Tools & Settings > Bulk Actions > Scripts > + New Script. Paste the full file.
- 2
Review the SuffixTemplate
The default template includes utm_source, utm_medium, utm_campaign, utm_adgroup and standard Google parameters (gclid, keyword, device, etc). Adjust it to match your naming convention.
- 3
Set the filters
Leave _CAMPAIGN_CONTAINS and _ADGROUP_CONTAINS empty to target everything, or set a substring to limit to specific campaigns/ad groups. Set STATUS to ENABLED or PAUSED.
- 4
Run and check the log
Run the script and review View > Logs. Each line shows the campaign/ad group and the exact suffix applied, so you can confirm names resolved correctly before relying on the data.
Caveats
Uses the value trackers {CampaignName} and {AdGroupName} — at least one must be present or the script exits. Campaign and ad group names are URL-encoded (spaces become %20), so decode on the analytics side if you expect human-readable names. Applying at the ad group level overrides the campaign-level suffix for those ad groups. Always run on a filtered subset first to verify the template before a full rollout.