Automation

Script cookbook

Ready-to-paste scripts for the most common jobs — copy one, preview it, adjust the thresholds.

Every script below is ready to paste into the editor. The thresholds are sensible starting points, not gospel — a $5 t-shirt niche and a $30 hoodie niche need different numbers. Always preview first, and start new rules in Review mode.

Pause bleeders

The classic first rule: keywords with plenty of clicks and nothing to show for them.

FOR EACH keyword IN LAST 30 DAYS: IF keyword.clicks >= 25 AND keyword.orders = 0: keyword.pause() keyword.note("{clicks} clicks, {spend:money} spent, 0 orders") END

Pause a keyword that only bleeds when you add it up

$4 wasted in each of ten campaigns is invisible to the rule above — every instance looks harmless. An accumulated report sums the keyword across all of them first, then pauseEverywhere() switches off every instance at once.

CREATE ACCUMULATED KEYWORD REPORT IN LAST 60 DAYS FOR EACH accumulated_keyword: IF accumulated_keyword.spend >= $40 AND accumulated_keyword.orders = 0: accumulated_keyword.pauseEverywhere() accumulated_keyword.note("{spend:money} across {campaigns} campaigns, no orders") END

Stop an ASIN that quietly eats budget everywhere

The same idea one level up: an ASIN summed across every campaign it advertises in. Swap pauseEverywhere() for note() alone if you want a list to read before acting. The # ACTIVE ONLY line is the default written down — leave it out and the rule behaves identically, but with it there you can see at a glance that ASINs you have already switched off are left alone.

# ACTIVE ONLY CREATE ACCUMULATED ASIN REPORT IN LAST 30 DAYS FOR EACH accumulated_asin: IF accumulated_asin.spend >= $25 AND accumulated_asin.orders = 0 AND accumulated_asin.live_ads > 1: accumulated_asin.pauseEverywhere() accumulated_asin.note("{spend:money} over {ads} ads, no orders") END

Bid up the keywords of an ASIN that is proven across every campaign

Judges each ASIN on its total across all campaigns, then raises bids on the keywords that actually sell it. The loop reaches every ad group the ASIN delivered through, so the two guards are what narrow it: COUNT(ASINS(...)) keeps the raise off ad groups advertising more than one product, where the winner's keywords are shared with its neighbours, and the keyword count keeps it off ad groups big enough to make one ASIN an expensive decision. Drop either one if you want the wider reach.

CREATE ACCUMULATED ASIN REPORT IN LAST 60 DAYS FOR EACH accumulated_asin: IF accumulated_asin.orders > 10 AND accumulated_asin.cpo < $2.50: FOR EACH keyword IN accumulated_asin: IF keyword.state = "ENABLED" AND COUNT(ASINS(keyword.adgroup)) = 1 AND COUNT(KEYWORDS(keyword.adgroup)) <= 20: keyword.setBid(MIN($1.50, keyword.bid * 1.10)) keyword.note("ASIN {accumulated_asin.asin} proven: {accumulated_asin.orders} orders at {accumulated_asin.cpo:money}") END END

Bid down high-ACOS keywords (with floor and cool-down)

Cuts bids 15% where ACOS runs hot, never below $0.05, and waits a week between cuts so changes can take effect before being judged.

FOR EACH keyword IN LAST 30 DAYS: IF keyword.acos > 45% AND keyword.orders >= 1 AND DAYS_SINCE_CHANGE("bid") > 7: keyword.setBid(MAX($0.05, keyword.bid * 0.85)) keyword.note("ACOS {acos:percent} over target") END

Bid up proven winners (with ceiling)

FOR EACH keyword IN LAST 30 DAYS: IF keyword.orders >= 3 AND keyword.acos < 20% AND DAYS_SINCE_CHANGE("bid") > 7: keyword.setBid(MIN($1.20, keyword.bid * 1.10)) keyword.note("{orders} orders at {acos:percent} ACOS — scaling") END

Fill a new auto campaign's empty clause bids

Every auto ad group ships with four predefined clauses, and by default all four bid the ad group's default. This is Actions → Adjust Auto Targeting Bid on a schedule: it gives each clause its own bid the night the campaign appears. when: "empty" means it only fills clauses that have no bid of their own, so a bid you set by hand is never overwritten — and the clauses are read live from Amazon, so it reaches the three that have never delivered and are in no report.

FOR EACH campaign: IF campaign.targetingType = "auto": campaign.setAutoTargetingBid("close match", $0.45, when: "empty") campaign.setAutoTargetingBid("loose match", $0.30, when: "empty") campaign.setAutoTargetingBid("substitutes", $0.30, when: "empty") campaign.setAutoTargetingBid("complements", $0.20, when: "empty") campaign.note("Auto clauses filled — they were on the ad group default") END

Scale budgets on performance

Raises budgets on campaigns that convert efficiently, trims campaigns that spend without selling.

FOR EACH campaign IN LAST 30 DAYS: IF campaign.acos < 25% AND campaign.orders >= 5 AND DAYS_SINCE_CHANGE("budget") > 7: campaign.setBudget(MIN($50, campaign.budget * 1.25)) campaign.note("{orders} orders at {acos:percent} — raising budget") IF campaign.spend > $30 AND campaign.sales = 0 AND DAYS_SINCE_CHANGE("budget") > 7: campaign.setBudget(MAX($1, campaign.budget * 0.50)) campaign.note("{spend:money} spent, no sales — cutting budget") END

Harvest converting search terms

Promotes shopper queries that convert into exact keywords in their own ad group.

The targetingType test is not optional. createKeyword builds the keyword in the ad group its search term ran in, and Amazon allows only negative keywords inside an auto campaign — so terms harvested out of an auto campaign have to land somewhere else, which this rule cannot do. Terms from auto campaigns are skipped (the run log says how many), as is any term that is itself an ASIN — Amazon only accepts those as product targets. Moving an auto campaign’s winners into a manual campaign is the one harvesting job a rule cannot do for you: pick the destination ad group in the Amazon console, and let a rule keep it tuned from there.

FOR EACH searchTerm IN LAST 60 DAYS: IF searchTerm.orders >= 2 AND searchTerm.acos < 30% AND searchTerm.campaign.targetingType = "MANUAL": searchTerm.createKeyword(searchTerm.searchTerm, "exact", MAX($0.10, searchTerm.cpc * 1.10)) searchTerm.note("Harvested: {orders} orders at {acos:percent}") END

Negate wasteful search terms

Blocks queries that click but never convert — the highest-leverage cleanup there is, because a negative stops the spend completely rather than trimming it.

Exact is the default, and usually the right one. A negative exact blocks only that precise query; a negative phrase blocks every query containing it, which is what you want for a genuinely toxic word (a competitor brand, “free”, an irrelevant niche) and a quiet disaster otherwise. Placement follows the source: the negative goes into the ad group the term actually delivered from, so the same query stays free everywhere else. Terms already covered by an exact negative are skipped rather than re-sent, and every negative MerchDash adds is in Change history and revertible in one click.

FOR EACH searchTerm IN LAST 60 DAYS: IF searchTerm.clicks >= 12 AND searchTerm.orders = 0 AND searchTerm.negated = FALSE: searchTerm.addNegative(searchTerm.searchTerm, "exact") searchTerm.note("{clicks} clicks, no orders — negated") END

Trend brake: this week vs. your baseline

Compares a keyword's recent ACOS to its own longer history and reacts only to genuine deterioration.

FOR EACH keyword IN LAST 7 DAYS: LET recent_acos = keyword.acos LET baseline_acos = keyword.acos IN FROM 8 DAYS AGO TO 60 DAYS AGO IF keyword.clicks >= 10 AND recent_acos > baseline_acos * 1.5: keyword.setBid(MAX($0.05, keyword.bid * 0.85)) keyword.note("ACOS {recent_acos} vs baseline {baseline_acos}") END

Placement tuning

Rewards campaigns that convert well with a top-of-search boost.

FOR EACH campaign IN LAST 30 DAYS: IF campaign.acos < 20% AND campaign.orders >= 10 AND campaign.placement.top_of_search < 50: campaign.setPlacementBid("TOP OF SEARCH", 50) campaign.note("Efficient at {acos:percent} — boosting top-of-search") END

Portfolio housekeeping

Files paused, long-dead campaigns into an archive portfolio so working views stay clean.

FOR EACH campaign IN LIFETIME: IF campaign.state = "PAUSED" AND (campaign.clicks IN LAST 90 DAYS) = 0 AND campaign.portfolioName != "Archive": campaign.moveToPortfolio(PORTFOLIO("Archive")) campaign.note("Paused and idle 90+ days") END

Move winners into a portfolio, and build it the first time

The housekeeping rule above needs the Archive portfolio to exist already — PORTFOLIO() stops the run when it does not. createPortfolio() is the version that makes it: the portfolio with that name, created when there is none, so the rule works on a fresh account and every night after it. It never answers NONE, so it needs no guard, and it proposes nothing once the portfolio is there. Approving the creation looks Amazon up first, so a portfolio you made in the console keeps its campaigns and history instead of being duplicated.

FOR EACH campaign IN LAST 30 DAYS: IF campaign.orders >= 10 AND campaign.acos < 20%: LET scaling = createPortfolio("Scaling") campaign.moveToPortfolio(scaling) campaign.note("Winner: {orders} orders at {acos:percent}") END

Placement tuning, one placement at a time

Raise top of search where the campaign converts, back it off where it does not, and leave each campaign alone for a week afterwards so the adjustment has time to show. DAYS_SINCE_CHANGE("top of search") is the cool-down for that one placement — a product-pages change made yesterday does not block it. See placement cool-downs.

FOR EACH campaign IN LAST 30 DAYS: IF campaign.adType = "SP" AND campaign.orders >= 5 AND campaign.acos < 25% AND DAYS_SINCE_CHANGE("top of search") > 7: campaign.setPlacementBid("TOP OF SEARCH", MIN(100, campaign.placement.top_of_search + 10)) campaign.note("{orders} orders at {acos:percent} — more top of search") IF campaign.adType = "SP" AND campaign.clicks >= 50 AND campaign.acos > 60% AND DAYS_SINCE_CHANGE("top of search") > 7: campaign.setPlacementBid("TOP OF SEARCH", MAX(0, campaign.placement.top_of_search - 10)) campaign.note("ACOS {acos:percent} on {clicks} clicks — less top of search") END

Use campaign.daysSincePlacementChange instead when nothing about the campaign's placements should move if any of the three was touched recently.

Season ramp (pair with a season window)

Save with a season window (e.g. Nov 1 – Dec 20) and a second “ramp-down” rule after it.

# Season: 2026-11-01 → 2026-12-20, schedule: weekly FOR EACH campaign IN LAST 30 DAYS: IF campaign.acos < 35% AND campaign.orders >= 3 AND DAYS_SINCE_CHANGE("budget") > 6: campaign.setBudget(MIN($100, campaign.budget * 1.30)) campaign.note("Q4 ramp: {orders} orders at {acos:percent}") END
Habits that keep scripts safe. Bound every computed bid/budget with MAX()/MIN(); add a DAYS_SINCE_CHANGE() cool-down to anything that adjusts values; always attach a note() — future-you reads it in Change history; and demand enough data (clicks/orders minimums) before letting a rule judge anything.