The gap

ValueTrack is the mechanism Google Ads uses to stamp click context into your URLs: {campaignid}, {adgroupid}, {keyword}, {device}, and a few dozen more. What it has never offered is {campaignname}. The click arrives knowing exactly which campaign sent it, as a number nobody downstream can read.

That gap matters the moment names carry meaning your other systems need. A utm_campaign that says what the campaign is, rather than an ID you have to look up. Call tracking, where platforms attribute a phone call to the campaign name they find in the landing page URL (the architecture in the call-based conversion piece depends on exactly this). A hidden field on a lead form, so the CRM shows sales a campaign name instead of a number. A warehouse join that arrives pre-joined because the name landed with the click.

The usual answers are both bad. Hand-editing URLs per campaign rots the day someone launches without the ritual. Maintaining an ID-to-name lookup table somewhere rots the first time someone renames a campaign, which in a well-run account is routine hygiene, not an edge case.

The injection mechanism

Google Ads custom parameters are user-defined ValueTrack slots. Set campaign = MY_CAMPAIGN_NAME on a campaign and {_campaign} resolves to that value in any tracking template, final URL suffix, or final URL. They cascade: a parameter defined at a lower level overrides the same key defined above it. So the whole pattern is one tracking template, set once at account level, that references two parameters the entities themselves carry:

// tracking template · account level, set once
# self-maintaining UTMs
{lpurl}?utm_source=google&utm_medium=cpc&utm_campaign={_campaign}&utm_content={_adgroup}

# call-tracking payload (Infinity-style)
{lpurl}?infinity=ict2~net~gaw~cmp~{_campaign}~ag~{_adgroup}~ar~{creative}

The template never changes again. What has to stay true is the parameters themselves: every campaign carrying its own name in {_campaign}, every ad group in {_adgroup}, through launches and renames alike. That is a synchronisation job, and synchronisation jobs belong to software, not to checklists.

Campaign name injection flow Five stages: a campaign is renamed in Google Ads; a scheduled script re-syncs the campaign custom parameter; the account-level tracking template references the parameter; the click lands carrying the readable name; downstream systems receive it with no lookup table. Campaign renamed in Google Ads BRAND_UK_PMAX_V2 (was V1) {_campaign} custom parameter scheduled script re-syncs the value Tracking template references it {lpurl}?utm_campaign={_campaign} Click lands carrying the name utm_campaign=BRAND_UK_PMAX_V2 Tracker, CRM, warehouse no lookup table, nothing to rot
The injection path. The only moving part is the parameter sync; everything downstream reads names for free.

A script keeps the names true

The tool is a single Google Ads Script, open source at google-ads-campaign-name-sync. On every run it walks each enabled campaign (Search, Display, Performance Max, Shopping, Video, Demand Gen where the runtime exposes it) and each ad group inside enabled campaigns, standard and Shopping alike. For each entity it compares the stored parameter against the entity's current URL-encoded name and rewrites it on any mismatch. That comparison is the point: it catches missing parameters, blanked values, and renames with the same check, so an account that renames campaigns weekly stays as clean as one that never does.

It merges rather than replaces. setCustomParameters() overwrites an entity's whole parameter set, so the script reads the existing set, updates its own two keys, and writes the merged result back; parameters owned by other tools survive. Google's limits are guarded rather than tripped over: entities hold at most 8 custom parameters and values cap at 200 characters, and entities that would breach either limit are skipped with a logged warning instead of failing the run.

// google ads scripts · run log (condensed)
Account: Example Account (123-456-7890)
Mode: LIVE
[Search/Display campaigns]  scanned=3  updated=0 unchanged=3  skipped=0 errors=0
[Performance Max campaigns] scanned=4  updated=1 unchanged=3  skipped=0 errors=0
[Shopping campaigns]        scanned=0  updated=0 unchanged=0  skipped=0 errors=0
[Ad groups]                 scanned=36 updated=0 unchanged=36 skipped=0 errors=0
----------------------------------------
TOTAL scanned=43 updated=1 unchanged=42 skipped=0 errors=0

The logging is half the tool. One line per change, a summary per entity type, a grand total, and a thrown error if any entity failed, which flips a scheduled run to Failed and triggers Google's script-failure email. A sync job that cannot tell you what it did is a sync job you will eventually distrust for good reason.

The Performance Max trap

This script exists because the commonly shared version of the same idea, including the one at least one tracking vendor hands to its customers, has a silent failure mode. It updates campaigns through AdsApp.bulkUploads(), a CSV bulk upload, and bulk uploads do not apply campaign edits to Performance Max campaigns. The upload reports nothing back to the script, so the logs look healthy while every PMax campaign in the account carries no name at all.

I found this on a live account where every Search campaign dutifully carried its name and all four PMax campaigns carried nothing. Nothing in the account was misconfigured; the delivery mechanism simply skips the campaign type quietly. The tracker showed nameless PMax traffic for as long as the script had been "working".

// DON'T

Don't sync custom parameters through bulk uploads. The upload silently skips Performance Max campaigns and gives the script no per-row feedback, so the failure is invisible in the logs. Set parameters directly on each entity instead.

The fix is structural. Google Ads Scripts now exposes every campaign type behind its own selector, and AdsApp.performanceMaxCampaigns() supports urls().setCustomParameters() exactly like the others. Set the parameter on the entity, skip the upload entirely, and PMax behaves like everything else. The same structure is why the script covers Shopping, Video, and Demand Gen: each is one more selector in a list, not a special case.

Asset groups

PMax has asset groups instead of ad groups, so {_adgroup} is empty on PMax traffic by default. Until recently that was final: asset groups had no URL fields at all. Google has now quietly added asset group URL options (campaign, then Asset groups, then Edit assets, then Asset group URL options): a tracking template, a final URL suffix, and custom parameters, per asset group.

Because custom parameters resolve lowest-level-first, that enables a neat trick: add adgroup = ASSET_GROUP_NAME as a custom parameter on each asset group, and the existing account-level template's {_adgroup} resolves per asset group. No template changes, and the tracker receives asset group names in the same field ad group names arrive in.

The honest caveat: this is a manual step per asset group for now. Scripts cannot set it (the AssetGroupUrls object only exposes final URL and path methods), and at the time of writing the field is not even queryable in the Google Ads API. Put it in the asset group creation checklist; the script will absorb it as one more entity source the day Google exposes it. If numeric IDs are enough for your reporting, {assetgroupid} is now also a valid ValueTrack parameter in PMax tracking templates.

Set it up

Three minutes, four steps. In Google Ads, go to Tools, then Bulk actions, then Scripts, create a new script and paste in sync-custom-parameters.gs. Authorise it when prompted. Click Preview first: preview saves nothing, and the log shows exactly what a live run would change, which is also the fastest way to audit how many of your entities are currently nameless. Then run it live and schedule it hourly or daily; new and renamed entities are picked up on the next run.

Reference {_campaign} and {_adgroup} in your tracking template, and the names look after themselves from there. MIT licensed; the README carries the Infinity-specific notes and the asset group walkthrough.