What you'll learn in this article
- The catch nobody warns you about: the google ads scripts settargetroas portfolio bidding strategy path you'd expect doesn't exist in the API
- Why the BiddingStrategy object is read-only, and the campaign-level route I use instead
- The compact script I run to google ads scripts update portfolio bid strategy target roas across a whole set of campaigns in one pass
- When bulk-updating a shared strategy by script is actually worth it, and when the UI wins
- The real API limits: no setters on the strategy, PMax selectors, the percentage-vs-decimal trap
A portfolio bid strategy is meant to be the single lever: one shared Target ROAS, applied to a dozen campaigns, changed in one place. So the first time I wanted to move that number on a schedule, I assumed a google ads scripts settargetroas portfolio bidding strategy call would be a one-liner. It isn't. That gap between what you expect and what the API actually exposes is the whole reason this article exists, and it's the kind of thing you only learn by hitting the wall yourself.
This sits under the broader pillar on google ads automation, but the angle is narrow and hard-won: how I update a shared target across many campaigns in bulk, when it's worth writing code for, and where the API simply says no. If you already reach for scripts to set Target ROAS on individual campaigns, this is the sharper, portfolio-level version of the same problem.
Some context on why I bother at all. A portfolio strategy is the natural home for automated bid strategies that need to behave consistently across multiple campaigns: instead of setting a goal per campaign, you pool them so the algorithm optimises against one shared objective and, importantly, one shared pool of conversion data. When several campaigns individually run thin on conversion volume, grouping them under a portfolio is often the difference between the system having enough signal to bid well and it flailing. That pooling is exactly why so many of the value-based accounts I inherit already lean on portfolios before I ever touch a script.
The moment automation gets interesting is when you want to move the shared goal on a cadence. Google's automated bidding will react to the target you give it, but it won't decide for you that Q4 margins justify a higher return goal, or that a promo window calls for a looser one. That decision is yours, and once you've made it, pushing it onto a portfolio spanning many google ads campaigns is precisely the kind of repetitive, error-prone edit a script should own.
The catch: the portfolio object is read-only
Here's the thing that cost me an afternoon. When you retrieve a flexible (portfolio) bid strategy with AdsApp.biddingStrategies(), the object you get back is deliberately thin. Per Google's official BiddingStrategy reference documentation,
it exposes getName(), getType(), campaigns(), stats and IDs and nothing to set a target. There is no setTargetRoas() on the strategy itself.
So the naive mental model grab the portfolio, call a setter, done is a dead end. The strategy object is for reading and for walking to the entities that use it, not for writing new goals into it. Once I internalised that, the whole job reframed itself: I stopped trying to edit the shared strategy directly and started editing the campaigns that sit under it. That's not a hack, it's the supported route, and it changes how you structure the script entirely.
It's worth being precise about what the object does give you, because the read-only methods are still genuinely useful. You get the strategy's name and type, its resource name, its stats, and selectors for the campaigns, ad groups, keywords and shopping campaigns attached to it. That last set is the payoff: you can ask the strategy which campaigns or ad groups it governs and iterate exactly those. What you can't do is reach in and rewrite the shared goal whether that goal is a target return on ad spend or a target CPA. Both live on the strategy conceptually, but neither is writable through it.
This limitation isn't arbitrary, and inferring the reason helped me stop fighting it. A portfolio is a shared configuration object; letting a script mutate its core goal in place would be a blunt instrument with account-wide blast radius. Google's design nudges you toward the campaign level, where the change is explicit and, crucially, loggable per campaign. I've come to prefer it: when I write a new tROAS to each member campaign, my log tells me exactly which entities moved, rather than a single opaque "strategy edited" line.
The workaround that actually works
The setter that exists lives one level down, on the campaign. Every campaign object has a bidding() property, and on it is setTargetRoas(). When a campaign is attached to a portfolio Target ROAS strategy, writing
a new value at campaign level is how you move the number the shared strategy expresses. In practice, to google ads scripts update portfolio bid strategy target roas, I iterate the campaigns using that portfolio and
call the campaign-level setter on each one.
There's a clean way to find exactly the right campaigns: the strategy's own campaigns() selector. You retrieve the portfolio by name, then walk its member campaigns directly, so you never touch a campaign that isn't part of
that shared strategy. This is the inference that took me longest to trust: the read-only object is still useful precisely because it hands you the exact set of campaigns to write to.
Two caveats I learned the blunt way. First, the value is a percentage: a goal of five dollars back per dollar spent is 500, not 5. Pass 5 and you've quietly set a five-percent target that floods the
account with cheap traffic. Second, the call only succeeds when the applied strategy genuinely uses Target ROAS; on any other strategy it throws, which is fine as long as you catch it.
The same shape applies if your portfolio is a target CPA strategy rather than a value one. There's a sibling setTargetCpa() on the campaign bidding object, and everything I say here about walking the strategy's member campaigns
holds identically for target CPA bidding you just call the other setter and pass a money amount instead of a percentage. In accounts where I juggle both a target cpa target roas mix across different portfolios, I keep two near-identical
scripts rather than one clever branching monster, because a script that tries to guess whether a campaign wants a cpa target roas value tends to guess wrong at 4am.
One inference about scope worth stating plainly: this technique moves the target, not the structure. It won't set cpc bid limits, it won't touch a mobile bid adjustment, and it won't rewrite a maximum cpc bid on manual campaigns. Portfolio Target ROAS and Target CPA are goal-based smart bidding strategies, so the only lever this script pulls is the goal itself. If you need to manage bid ceilings and floors alongside the goal, that's a different, more involved job that I deliberately keep out of this script to preserve its single, predictable purpose.
The code I run
This is the compact version I carry between accounts. It takes the portfolio strategy by name, walks its member campaigns, and applies one shared target to all of them in a single pass. No dependencies, so it drops into any account in seconds.
function main() {
// Percentage scale: 500 means 500% (=$5 back per $1 spent)
var STRATEGY_NAME = 'Portfolio - Value tROAS';
var NEW_ROAS = 500;
var stratIt = AdsApp.biddingStrategies()
.withCondition("bidding_strategy.name = '" + STRATEGY_NAME + "'")
.get();
if (!stratIt.hasNext()) {
Logger.log('No strategy named ' + STRATEGY_NAME);
return;
}
var strategy = stratIt.next();
Logger.log('Strategy: ' + strategy.getName() + ' (' + strategy.getType() + ')');
var campIt = strategy.campaigns().get();
while (campIt.hasNext()) {
var campaign = campIt.next();
try {
campaign.bidding().setTargetRoas(NEW_ROAS);
Logger.log('OK: ' + campaign.getName() + ' -> ' + NEW_ROAS + '%');
} catch (e) {
Logger.log('SKIP: ' + campaign.getName() + ' (' + e + ')');
}
}
}
Three deliberate choices. The try/catch means one campaign on the wrong strategy logs a clean SKIP instead of killing the run. The Logger.log lines give me a record I can show a client when they ask why the target
moved on a Tuesday. And the percentage convention lives in a comment so future-me doesn't repeat the five-versus-five-hundred mistake. If a colleague owns margins, I swap the hard-coded value for a sheet read using the exact pattern
I document for wiring Google Sheets into Google Ads scripts, so the plumbing stays boring and proven.
Notice what the loop deliberately does not do. It reads the campaigns from the strategy and writes exactly one field the google ads bid goal to each. It doesn't inspect performance, it doesn't make bid adjustments of its own, and it doesn't try to be clever about which campaigns or ad groups deserve a different number. That restraint is the point. A script that only pushes a single shared goal onto a known set of campaigns is trivial to reason about, trivial to preview, and trivial to trust. The moment you bolt on conditional logic that reads stats and decides targets, you've built a bidding system, and a bidding system deserves far more testing than a fifteen-line maintenance script.
If you do want per-campaign differentiation, the honest answer is: use the spreadsheet. Put campaign name in column A and the desired target in column B, and let the strategist set different values row by row. That keeps the code dumb and the judgment human, which is exactly the division of labour I want when the bidder is spending real money against whatever number I hand it.
When bulk-updating by script is worth it
Honestly? Not always. If a portfolio strategy covers three campaigns and you change the target twice a year, open the shared library and edit the cell. The script earns its keep only when scale and repetition stack up: a portfolio spanning many campaigns, a target that shifts with seasonality or margins, and a schedule where doing it by hand is both slow and error-prone. That's the same threshold I apply to bidding decisions generally, which I unpack in the wider piece on Google Ads bidding strategies.
The quiet win isn't speed, it's the audit trail. Every run leaves a log of which campaigns got which value, so a target change is never a mystery a week later. Combined with a spreadsheet front-end, it also takes me out of the loop: the person who owns margins edits a cell, the script runs on schedule, and I'm not the bottleneck. That hands-off property is usually what tips a job from "do it in the UI" to "write the script."
There's also a subtler argument about consistency. When the same target has to land on many campaigns or ad groups at once, doing it by hand introduces drift you fat-finger one value, or you finish half the list and get pulled into a meeting. A script applies the identical number to every member of the portfolio in one atomic pass, so the shared strategy stays genuinely shared. For accounts where I'm coordinating targets across several portfolios, that uniformity is worth more than the minutes saved.
Where I draw the line is complexity of intent. If the change is "raise the whole portfolio's goal by ten points," script it without hesitation. If the change is "raise it for high-margin lines, hold it for the rest, and lower it where inventory is tight," that's a judgment call I'd rather make in a spreadsheet and merely execute with the script. The script should move numbers; it shouldn't decide them. Keeping that boundary clean is what stops a handy automation from quietly becoming a bidding engine nobody fully understands.
A final practical note on cadence, because it's shaped how I schedule these. I resist the temptation to run the update daily. A portfolio target that moves every day gives the bidder no stable ground to learn on, and you end up chasing noise. Weekly, or aligned to genuine business events a margin update, a promo start, a seasonal shift is usually the right rhythm. The script makes the edit so cheap that daily runs feel free, but "cheap to run" and "wise to run" are different questions. I set the schedule to match how often the underlying decision actually changes, not how often the code is capable of firing, and that single discipline has saved more campaigns from thrash than any code cleverness ever has.
Limits and gotchas from experience
This is the section I wish someone had handed me before I started.
You cannot create a strategy from a script
Scripts don't create portfolio bid strategies. If the shared strategy doesn't already exist, you build it once in the Google Ads UI, then access it by name. The script's job is to nudge an existing target, never to bootstrap the strategy itself. Plan the strategy in the interface first; automate the maintenance second.
Performance Max needs a different selector
The strategy's campaigns() selector returns search and display campaigns, not Performance Max. If your value-based campaigns are PMax, you iterate them with AdsApp.performanceMaxCampaigns() and call the bidding
setter there. I've watched a "working" script silently touch zero campaigns because every one it wanted was PMax. Check what campaign types you actually have before you trust the log.
The strategy must already use Target ROAS
The campaign-level setTargetRoas() only works when the applied strategy uses a target ROAS. It won't convert a campaign onto value-based bidding for you, and it won't rewrite the portfolio's type. It adjusts an existing target;
it doesn't create one.
Preview, then schedule, then wait
I never trust the first run. I preview, read the log, confirm the OK and SKIP lines match what I expected, and only then schedule. And after the change, I give the bidder a conversion cycle or two to settle rather than yanking the number again the next day. Fast tooling, patient hand.
The number is a goal, not a guarantee
Last thing, and it's more mindset than mechanics. Writing a new target roas bid onto a portfolio tells the algorithm what you're aiming for; it doesn't force the outcome. If a portfolio is starved of signal, a shinier target won't rescue it the fix is more conversion history or a wider grouping, not a bolder goal. I've watched people ratchet a target up and down weekly, treating the script like a throttle, when the real problem was thin data. The script is a clean, auditable way to express intent across many campaigns at once; the results still come from the bidder having enough to learn from. Respect that separation and the automation stays a help rather than a habit of nervous over-adjustment.
FAQ on portfolio bid strategy scripts
Can a script set Target ROAS directly on a portfolio strategy?
setTargetRoas() at campaign level.How do I change the shared target across all its campaigns?
AdsApp.biddingStrategies(), then loop its campaigns() and apply setTargetRoas() to each. That's how you google ads scripts update portfolio bid strategy target roas in one pass without touching unrelated campaigns.Why does my script skip some campaigns?
AdsApp.performanceMaxCampaigns() for those.