What you'll learn in this article
- The real use case for using google ads scripts set target roas campaign work instead of clicking through the UI
- Exactly which method does the job (setTargetRoas) and the scale it expects, learned the hard way
- The compact script I actually reuse across accounts, iterating campaigns and writing the new tROAS
- How to google ads scripts update target roas from spreadsheet so a non-technical colleague can drive it
- The gotchas: strategy mismatches, PMax selectors, the percentage-vs-decimal trap, and previewing before you run
Setting one Target ROAS by hand is nothing. Setting it across twenty campaigns, on a schedule, every time margins shift, is where the afternoon disappears. That's the exact job I built a script for, and using google ads scripts set target roas campaign logic has quietly become one of the most reused pieces of code I carry from account to account. This article is that script and the reasoning around it: what it does, where it breaks, and how I hand the wheel to a spreadsheet so I'm not the bottleneck.
It sits under the broader pillar on google ads automation, but the angle here is deliberately narrow: not bidding theory, just the mechanics of pushing a tROAS value onto many campaigns without doing it manually. If you already run other Google Ads scripts, this slots in beside them with no new infrastructure.
Why I reach for a script to set Target ROAS
The honest trigger is scale plus repetition. On a single campaign I'll never write code, I'll just edit the bid strategy in the interface. But the accounts I manage often carry a dozen or more value-based campaigns, and the tROAS on them isn't static: it moves with seasonality, with margin changes finance sends me, and with promo calendars. Editing each one by hand is slow and, worse, error-prone, because the fifteenth manual edit is where you fat-finger a number.
A script fixes both problems at once. It touches every campaign in a defined set in one pass, it applies the same logic uniformly, and it leaves a log I can read. That last part matters more than people expect: when a client asks why the target changed on a Tuesday, I have a record. This is the same instinct behind why I generally lean on scripts over the UI for anything repeated. The script isn't replacing Smart Bidding's judgment; it's just moving the lever faster than my hands can.
The method that does the work: setTargetRoas
Every campaign object exposes a bidding() property, and on it lives the method that matters here: setTargetRoas(). Per Google's official CampaignBidding reference documentation,
the value is a number representing a percentage, and it only works when the campaign's applied strategy actually uses a target ROAS. Call it on a Maximize Clicks campaign and it throws. That single sentence saved me hours once I internalised
it.
The scale caught me out early, so I'll be blunt about it. The documentation frames the value as a percentage: a goal of five dollars back for every dollar spent is 500, not 5. I've seen people, myself included
on day one, pass 5.0 expecting 500% and quietly set a target of five percent instead. The bidder obeys, the campaign floods with cheap traffic, and you spend a morning wondering what happened. Decide your convention, comment
it in the code, and never trust memory on it.
There's also a strategy-shape question. The method sets the target on the strategy the campaign already has. If a campaign is on Maximize Conversion Value with a target ROAS, this is the clean path. If you need to switch a campaign's whole strategy type, that's a different method entirely, and I keep those two jobs in separate scripts so I never conflate "nudge the target" with "change the strategy."
The code I reuse
Here's the compact version I carry between accounts. It reads a small in-script map of campaign name to desired tROAS, iterates the matching campaigns, and applies the value. I keep it dependency-free on purpose so it drops into any account in seconds.
function main() {
// Percentage scale: 500 means 500% (=$5 back per $1 spent)
var TARGETS = {
'Brand - Value': 450,
'Generic - Value': 300,
'Shopping - Core': 550
};
for (var name in TARGETS) {
var it = AdsApp.campaigns()
.withCondition("campaign.name = '" + name + "'")
.get();
while (it.hasNext()) {
var campaign = it.next();
var newRoas = TARGETS[name];
try {
campaign.bidding().setTargetRoas(newRoas);
Logger.log('OK: ' + name + ' -> ' + newRoas + '%');
} catch (e) {
Logger.log('SKIP: ' + name + ' (' + e + ')');
}
}
}
}
Three deliberate choices in there. The try/catch means one campaign on the wrong strategy logs a clean SKIP instead of killing the whole run, which is exactly what you want when a script touches many campaigns and one is misconfigured.
The Logger.log lines give me the record I mentioned. And the percentage convention lives in a comment at the top so future-me doesn't repeat the five-versus-five-hundred mistake. This is the read-light, act-light shape
I trust; anything heavier I'd rethink, since a script that grows past what one person can hold in their head is a liability, not a convenience.
Driving it from a spreadsheet
The in-script map is fine for me, but it's useless to a colleague who doesn't touch code. The upgrade that made this genuinely hands-off was to google ads scripts update target roas from spreadsheet: the campaign names and target values live in a Google Sheet, and the script just reads them. Now the strategist who owns margins edits a cell, the script runs on schedule, and I'm out of the loop entirely.
Mechanically it's a small change. Instead of a hard-coded object, I open the sheet with SpreadsheetApp.openByUrl(), read the used range into an array, and loop the rows: column A is the campaign name, column B is the tROAS.
Each row feeds the same setTargetRoas() call from above. I keep the spreadsheet-reading pattern identical to the one I document in detail for connecting Google Sheets to Google Ads scripts,
so the plumbing is boring and proven rather than reinvented each time.
function main() {
var SHEET_URL = 'https://docs.google.com/spreadsheets/d/YOUR_ID/edit';
var rows = SpreadsheetApp.openByUrl(SHEET_URL)
.getActiveSheet().getDataRange().getValues();
// Skip header row (i = 1). Col A: name, Col B: tROAS %
for (var i = 1; i < rows.length; i++) {
var name = rows[i][0];
var roas = Number(rows[i][1]);
if (!name || !roas) continue;
var it = AdsApp.campaigns()
.withCondition("campaign.name = '" + name + "'").get();
while (it.hasNext()) {
try {
it.next().bidding().setTargetRoas(roas);
Logger.log('OK: ' + name + ' -> ' + roas + '%');
} catch (e) { Logger.log('SKIP: ' + name + ' (' + e + ')'); }
}
}
}
One inference from doing this repeatedly: put a header row and a sanity check on the values. The Number() cast plus the if (!name || !roas) continue; guard have saved me from a blank cell silently becoming a zero
and a target being cleared. Cheap insurance, and it turns a fragile script into one I'm comfortable scheduling unattended.
Gotchas I've hit in practice
This is the section I wish someone had handed me before I started.
Performance Max needs a different selector
The AdsApp.campaigns() selector will not find Performance Max campaigns. If your value-based campaigns are PMax, you iterate them with AdsApp.performanceMaxCampaigns() instead, and the bidding call sits on that
object. I've watched people run a "working" script that silently touched zero campaigns because every one they wanted was PMax. Check what campaign types you actually have before you trust the log.
The strategy must already support tROAS
As covered above, setTargetRoas() only works when the applied strategy uses a target ROAS. It won't convert a campaign onto value-based bidding for you. If you're moving a campaign to tROAS for the first time, do that transition
thoughtfully, ideally with enough conversion history behind it, which is a judgment call I unpack in the wider piece on Google Ads bidding strategies. The script is for
adjusting an existing target, not bootstrapping one.
Preview, then schedule
I never trust the first run. I use the script editor's preview mode, read the log, confirm the OK and SKIP lines match what I expected, and only then schedule it. A script that writes to bid strategy is exactly the kind you want to watch once before letting it run on its own at 4am.
Give the bidder time
Changing the target is instant; the bidder reacting is not. After a tROAS change the algorithm needs a conversion cycle or two to settle, so I don't yank the number again the next day in a panic. The script makes the edit trivially fast, which paradoxically makes it easy to over-adjust. Fast tooling, patient hand.
FAQ on setting Target ROAS with scripts
Which method sets Target ROAS on a campaign in a script?
setTargetRoas() on the campaign's bidding() object. It only works when the applied strategy already uses a target ROAS, and the value is a percentage, so 500 means 500%. That single method is the
core of any google ads scripts set target roas campaign workflow.How do I update Target ROAS from a spreadsheet?
SpreadsheetApp.openByUrl(), read the rows, and feed each campaign name and tROAS value into setTargetRoas(). To google ads scripts update target roas from spreadsheet cleanly,
add a header row and guard against blank cells so a missing value never clears a target.Why does my script skip some campaigns?
AdsApp.campaigns() can't find. Use AdsApp.performanceMaxCampaigns() for those.