QuantumAdsLab Logo Quantum Ads Lab
google ads scripts set target roas campaign: a script updating tROAS across many campaigns from a spreadsheet
Updating tROAS at scale: google ads scripts set target roas campaign

Google Ads Scripts to Set Target ROAS on a Campaign

Summary

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?
You call 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?
Open the sheet with 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?
Two usual causes. Either the campaign is on a strategy that doesn't use target ROAS, so the call throws and your catch logs a SKIP, or it's a Performance Max campaign, which AdsApp.campaigns() can't find. Use AdsApp.performanceMaxCampaigns() for those.
Is 5 the same as 500% when I set the target?
No, and this trips people up. The value is a percentage: 500% is passed as 500. Passing 5 sets a 5% target, which floods the campaign with cheap traffic. Comment your convention in the script and never rely on memory for the scale.