Documentation - Actions
How GoodLads writes to your Google Ads account.
Every recommendation states what it will do before you click. This page documents every action GoodLads is capable of, grouped by how it touches your account - and every one of them is deployed only when you approve it, never automatically. Every row's API column names the exact Google Ads mutate endpoint(s) it calls and our own route that triggers it, with the literal request and response behind a "Show request & response" toggle.
We never edit a live ad. The only path to changing your ad copy is a new ad, created paused next to the ones already running. Almost everything else follows the same shape: prove it works on a reversible test before it ever touches your budget. Two actions are the exception, and they're marked clearly - in the product, a live-spend button is always orange with a two-step confirm; every other button is black.
A/B experiments
Runs on a copy of the campaign, split-tested against what you're doing today. Nothing about your live campaign changes until you review the result and choose to conclude it. Every experiment lever below shares the same three-call scaffold before it does anything lever-specific: create the experiment, split it into a control and treatment arm, then schedule it once the lever-specific write (shown per row) lands on the treatment copy.
Show the shared experiment scaffold
POST customers/1234567890/experiments:mutate
{
"operations": [{
"create": {
"resourceName": "customers/1234567890/experiments/-1",
"name": "Target ROAS test - Search - Brand",
"type": "SEARCH_CUSTOM",
"status": "SETUP"
}
}]
}
POST customers/1234567890/experimentArms:mutate
{
"operations": [
{ "create": { "experiment": "customers/1234567890/experiments/998",
"control": true, "trafficSplit": 50 } },
{ "create": { "experiment": "customers/1234567890/experiments/998",
"control": false, "trafficSplit": 50 } }
]
}
POST customers/1234567890/experiments/998:scheduleExperiment| Action | What happens in your account | Where it comes from | API | Note | Status |
|---|---|---|---|---|---|
| Bidding-target test | Runs a target-CPA or target-ROAS test on a copy of the campaign, split against your current target. | Recommendations feed & Chat | experiments:mutate → experimentArms:mutate → campaigns:mutate → experiments/{id}:scheduleExperimentPreview: POST /api/insights/preview · Apply: POST /api/insights/applyShow request & responsePOST customers/1234567890/campaigns:mutate
{
"operations": [{
"update": {
"resourceName": "customers/1234567890/campaigns/9876543210",
"targetRoas": { "targetRoas": 3.2 }
},
"updateMask": "target_roas.target_roas"
}]
}{ "results": [{ "resourceName": "customers/1234567890/campaigns/9876543210" }] } | Search campaigns only. | Ready |
| Ad-group bid-target test | The same bidding-target test, scoped to a single ad group. | Recommendations feed | experiments:mutate → experimentArms:mutate → adGroups:mutate → experiments/{id}:scheduleExperimentApply: POST /api/insights/applyShow request & responsePOST customers/1234567890/adGroups:mutate
{
"operations": [{
"update": {
"resourceName": "customers/1234567890/adGroups/222111",
"targetRoas": 3.2
},
"updateMask": "target_roas"
}]
}{ "results": [{ "resourceName": "customers/1234567890/adGroups/222111" }] } | Not yet available in Chat. | Ready |
| Location bid test | Tests a bid adjustment for a specific location. | Recommendations feed | experiments:mutate → experimentArms:mutate → campaignCriteria:mutate → experiments/{id}:scheduleExperimentApply: POST /api/insights/applyShow request & responsePOST customers/1234567890/campaignCriteria:mutate
{
"operations": [{
"update": {
"resourceName": "customers/1234567890/campaignCriteria/9876543210~1007735",
"bidModifier": 1.2
},
"updateMask": "bid_modifier"
}]
}{ "results": [{ "resourceName": "customers/1234567890/campaignCriteria/9876543210~1007735" }] } | Not yet available in Chat. | Ready |
| Ad-group split test | Splits a mixed-intent ad group into focused ones and tests the split. | Recommendations feed & Chat | experiments:mutate → experimentArms:mutate → googleAds:mutate (adGroup + adGroupCriterion + adGroupAd, one batch) → experiments/{id}:scheduleExperimentApply: POST /api/insights/applyShow request & responsePOST customers/1234567890/googleAds:mutate
{
"mutateOperations": [
{ "adGroupOperation": { "create": {
"resourceName": "customers/1234567890/adGroups/-1",
"name": "Same-day delivery - Business",
"campaign": "customers/1234567890/campaigns/9876543210",
"status": "ENABLED" } } },
{ "adGroupCriterionOperation": { "create": {
"adGroup": "customers/1234567890/adGroups/-1",
"status": "ENABLED",
"keyword": { "text": "business courier account", "matchType": "PHRASE" } } } },
{ "adGroupAdOperation": { "create": {
"adGroup": "customers/1234567890/adGroups/-1",
"status": "ENABLED",
"ad": { "responsiveSearchAd": {
"headlines": [{ "text": "Business courier accounts" }],
"descriptions": [{ "text": "Dedicated support for recurring business shipments." }] } } } } }
]
}{
"mutateOperationResponses": [
{ "adGroupResult": { "resourceName": "customers/1234567890/adGroups/222222" } },
{ "adGroupCriterionResult": { "resourceName": "customers/1234567890/adGroupCriteria/222222~333" } },
{ "adGroupAdResult": { "resourceName": "customers/1234567890/adGroupAds/222222~444" } }
]
} | Multiple ad groups and ads at once, so you review the full plan before deploying. | Ready |
| Messaging-angle test | Tests new ad copy on a messaging angle you haven't tried yet. | Recommendations feed & Chat | experiments:mutate → experimentArms:mutate → adGroupAds:mutate → experiments/{id}:scheduleExperimentApply: POST /api/insights/applyShow request & responsePOST customers/1234567890/adGroupAds:mutate
{
"operations": [{
"create": {
"adGroup": "customers/1234567890/adGroups/222111",
"status": "ENABLED",
"ad": {
"responsiveSearchAd": {
"headlines": [{ "text": "Delivered in 90 minutes" }, { "text": "Track it live" }],
"descriptions": [{ "text": "Real-time tracking on every same-day booking." }]
}
}
}
}]
}{ "results": [{ "resourceName": "customers/1234567890/adGroupAds/222111~556677" }] } | Delivered as an experiment - nothing about your current ads changes until you conclude it. | Ready |
| Budget-raise test | Tests a higher daily budget against your current one, split-tested rather than a raw raise. | Not yet available | Not built yet. | We already tell you whether a capped campaign would benefit from more budget. Turning that into a one-click reversible test is next. | Not ready |
New things
Created paused. Nothing you already have changes, and nothing new starts serving until you enable it.
| Action | What happens in your account | Where it comes from | API | Note | Status |
|---|---|---|---|---|---|
| New ad | Adds a new ad alongside your current ones, created paused. | Recommendations feed & Chat | adGroupAds:mutatePreview: POST /api/insights/preview · Apply: POST /api/insights/apply · Pause/enable: POST /api/insights/adShow request & responsePOST customers/1234567890/adGroupAds:mutate
{
"operations": [{
"create": {
"adGroup": "customers/1234567890/adGroups/222111",
"status": "PAUSED",
"ad": {
"finalUrls": ["https://example.com/book"],
"responsiveSearchAd": {
"headlines": [{ "text": "Same-day delivery" }, { "text": "Book a courier now" }],
"descriptions": [{ "text": "Send a parcel across the city in under two hours." }]
}
}
}
}]
}{ "results": [{ "resourceName": "customers/1234567890/adGroupAds/222111~333444" }] } | Nothing about your existing ads changes. | Ready |
| New Search campaign | Builds a whole new Search campaign, created paused. | Recommendations feed & Chat | googleAds:mutate (campaignBudget + campaign + adGroup + adGroupCriterion + adGroupAd, one atomic batch)Apply: POST /api/insights/applyShow request & responsePOST customers/1234567890/googleAds:mutate
{
"mutateOperations": [
{ "campaignBudgetOperation": { "create": {
"resourceName": "customers/1234567890/campaignBudgets/-1",
"name": "Courier - Same day [GoodLads]",
"amountMicros": "20000000" } } },
{ "campaignOperation": { "create": {
"resourceName": "customers/1234567890/campaigns/-1",
"name": "Courier - Same day [GoodLads]",
"status": "PAUSED",
"advertisingChannelType": "SEARCH",
"campaignBudget": "customers/1234567890/campaignBudgets/-1" } } },
{ "adGroupOperation": { "create": {
"resourceName": "customers/1234567890/adGroups/-1",
"name": "Same-day delivery",
"campaign": "customers/1234567890/campaigns/-1",
"status": "ENABLED" } } },
{ "adGroupCriterionOperation": { "create": {
"adGroup": "customers/1234567890/adGroups/-1",
"status": "ENABLED",
"keyword": { "text": "same day courier", "matchType": "PHRASE" } } } },
{ "adGroupAdOperation": { "create": {
"adGroup": "customers/1234567890/adGroups/-1",
"status": "ENABLED",
"ad": { "responsiveSearchAd": {
"headlines": [{ "text": "Same-day delivery" }],
"descriptions": [{ "text": "Book a courier and track it to the door." }] } } } } }
]
}{
"mutateOperationResponses": [
{ "campaignBudgetResult": { "resourceName": "customers/1234567890/campaignBudgets/998877" } },
{ "campaignResult": { "resourceName": "customers/1234567890/campaigns/998878" } },
{ "adGroupResult": { "resourceName": "customers/1234567890/adGroups/998879" } },
{ "adGroupCriterionResult": { "resourceName": "customers/1234567890/adGroupCriteria/998878~998880" } },
{ "adGroupAdResult": { "resourceName": "customers/1234567890/adGroupAds/998879~998881" } }
]
} | You review every ad group, keyword and ad before deploying. | Ready |
| New App campaign | Builds a new App install campaign, created paused, for an app you already advertise. | Chat | googleAds:mutate (campaignBudget + campaign [App channel] + campaignCriterion [locations, optional] + adGroup + adGroupAd, one atomic batch - no keywords)Apply: POST /api/insights/applyShow request & responsePOST customers/1234567890/googleAds:mutate
{
"mutateOperations": [
{ "campaignBudgetOperation": { "create": {
"resourceName": "customers/1234567890/campaignBudgets/-1",
"name": "App install - Courier app [GoodLads]",
"amountMicros": "15000000" } } },
{ "campaignOperation": { "create": {
"resourceName": "customers/1234567890/campaigns/-1",
"name": "App install - Courier app [GoodLads]",
"status": "PAUSED",
"advertisingChannelType": "MULTI_CHANNEL",
"advertisingChannelSubType": "APP_CAMPAIGN",
"campaignBudget": "customers/1234567890/campaignBudgets/-1",
"appCampaignSetting": {
"appId": "com.example.courier",
"appStore": "GOOGLE_APP_STORE",
"biddingStrategyGoalType": "OPTIMIZE_INSTALLS_TARGET_INSTALL_COST"
},
"targetCpa": { "targetCpaMicros": "3000000" } } } },
{ "adGroupOperation": { "create": {
"resourceName": "customers/1234567890/adGroups/-1",
"campaign": "customers/1234567890/campaigns/-1",
"status": "ENABLED" } } },
{ "adGroupAdOperation": { "create": {
"adGroup": "customers/1234567890/adGroups/-1",
"status": "ENABLED",
"ad": { "appAd": {
"headlines": [{ "text": "Book a courier in seconds" }],
"descriptions": [{ "text": "Same-day delivery, tracked door to door." }] } } } } }
]
}{
"mutateOperationResponses": [
{ "campaignBudgetResult": { "resourceName": "customers/1234567890/campaignBudgets/998890" } },
{ "campaignResult": { "resourceName": "customers/1234567890/campaigns/998891" } },
{ "adGroupResult": { "resourceName": "customers/1234567890/adGroups/998892" } },
{ "adGroupAdResult": { "resourceName": "customers/1234567890/adGroupAds/998892~998893" } }
]
} | Ask for it directly - it works end to end, but it's still new: not yet part of the automatic recommendations feed, and we're still gathering live results before recommending it by default. | In progress |
Live changes
Two actions take effect the moment you deploy them, because there's no reversible test version of either one. Both are still entirely your decision, one click at a time - and both are just as reversible to undo as they were to apply.
| Action | What happens in your account | Where it comes from | API | Note | Status |
|---|---|---|---|---|---|
| Negative keywords | Blocks wasted search terms on your live campaign or ad group. | Recommendations feed & Chat | campaignCriteria:mutate (campaign scope) or adGroupCriteria:mutate (ad-group scope)Apply: POST /api/insights/apply · Undo: POST /api/insights/negativesShow request & responsePOST customers/1234567890/campaignCriteria:mutate
{
"operations": [
{ "create": { "campaign": "customers/1234567890/campaigns/998877",
"negative": true,
"keyword": { "text": "free courier", "matchType": "PHRASE" } } },
{ "create": { "campaign": "customers/1234567890/campaigns/998877",
"negative": true,
"keyword": { "text": "courier jobs", "matchType": "EXACT" } } }
]
}{
"results": [
{ "resourceName": "customers/1234567890/campaignCriteria/998877~111" },
{ "resourceName": "customers/1234567890/campaignCriteria/998877~112" }
]
} | Takes effect immediately - there's no test version of this, because a negative keyword has to block terms to save you anything. A two-step confirm always precedes it. | Ready |
| App campaign bid target | Adjusts an App campaign's live bidding target directly. | Recommendations feed | googleAds:search (reads the current target first, for the undo) → campaigns:mutateApply: POST /api/insights/apply · Undo: POST /api/insights/live-editShow request & responsePOST customers/1234567890/campaigns:mutate
{
"operations": [{
"update": {
"resourceName": "customers/1234567890/campaigns/998900",
"targetCpa": { "targetCpaMicros": "2500000" }
},
"updateMask": "target_cpa.target_cpa_micros"
}]
}{ "results": [{ "resourceName": "customers/1234567890/campaigns/998900" }] } | App campaigns can't run our experiments at all, so this is the only way to move that lever. A two-step confirm always precedes it. | Ready |
See how these actions get proposed in the first place on the Insights page, the account journey they fit into on Jobs to be done, or which skill pairs which read with which write on Skills.