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
/**
* UTMStamp — Google Ads final URL suffix tool
* dan.marketing / library/ppc-paid-search
* Original (Dan) — MIT
*
* Writes one consistent final URL suffix across campaigns and ad groups so
* every landing page carries the same UTM/value-tracker parameters without
* touching the Google Ads UI entity by entity.
*
* The suffix template decides the scope:
* - contains {AdGroupName} -> stamps each ad group
* - {CampaignName} only -> stamps each campaign
* - neither -> aborts before touching anything
*/
const config = {
// The suffix written to each entity. Must keep at least one of
// {CampaignName} / {AdGroupName} — the script refuses to run without it.
SUFFIX_TEMPLATE:
"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}",
// Name filters: set to a substring to touch only entities whose name contains it.
CAMPAIGN_NAME_CONTAINS: "",
ADGROUP_NAME_CONTAINS: "",
STATUS: "ENABLED", // ENABLED | PAUSED
// Preview by default; flip to false to actually write suffixes.
DRY_RUN: true,
};
function main() {
const template = config.SUFFIX_TEMPLATE;
if (!template.includes("{CampaignName}") && !template.includes("{AdGroupName}")) {
Logger.log("UTMStamp: abort — template lacks {CampaignName}/{AdGroupName}.");
return;
}
// {AdGroupName} drives one pass over ad groups; otherwise one pass over campaigns.
const adGroupScope = template.includes("{AdGroupName}");
const targets = adGroupScope ? fetchAdGroups() : fetchCampaigns();
let counted = 0;
while (targets.hasNext()) {
const target = targets.next();
const suffix = adGroupScope
? buildSuffix(template, target.getName(), target.getCampaign().getName())
: buildSuffix(template, target.getName(), target.getName());
if (config.DRY_RUN) {
Logger.log(`UTMStamp [dry-run] ${target.getName()} => ${suffix}`);
} else {
target.urls().setFinalUrlSuffix(suffix);
Logger.log(`UTMStamp ${target.getName()} => ${suffix}`);
}
counted++;
}
Logger.log(
`UTMStamp: finished — ${counted} entit${counted === 1 ? "y" : "ies"}` +
(config.DRY_RUN ? " (preview only, nothing written)." : " updated.")
);
}
function fetchCampaigns() {
let query = AdsApp.campaigns();
if (config.CAMPAIGN_NAME_CONTAINS) {
query = query.withCondition(`Name contains '${config.CAMPAIGN_NAME_CONTAINS}'`);
}
return query.withCondition(`Status = ${config.STATUS}`).get();
}
function fetchAdGroups() {
let query = AdsApp.adGroups();
if (config.CAMPAIGN_NAME_CONTAINS) {
query = query.withCondition(`CampaignName contains '${config.CAMPAIGN_NAME_CONTAINS}'`);
}
if (config.ADGROUP_NAME_CONTAINS) {
query = query.withCondition(`Name contains '${config.ADGROUP_NAME_CONTAINS}'`);
}
return query.withCondition(`Status = ${config.STATUS}`).get();
}
// URL-encode names so spaces become %20; decode on the analytics side.
function buildSuffix(template, adGroupName, campaignName) {
return template
.replace(/{CampaignName}/g, encodeURIComponent(campaignName))
.replace(/{AdGroupName}/g, encodeURIComponent(adGroupName));
}
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_NAME_CONTAINS and ADGROUP_NAME_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
DRY_RUN is ON by default, so the first run only writes preview lines to the log ([dry-run]) and changes nothing. When that looks right, set DRY_RUN to false and run again. Then 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.