Skip to content

update_campaign: campaign-level broad match - #117

Open
xgp wants to merge 1 commit into
kLOsk:mainfrom
xgp:feat/campaign-broad-match
Open

xgp wants to merge 1 commit into
kLOsk:mainfrom
xgp:feat/campaign-broad-match

Conversation

@xgp

@xgp xgp commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Closes #106.

Adds an optional keyword_match_type to update_campaign only, with the safety behaviour agreed on the issue. draft_campaign is unchanged, so new campaigns keep Google's default.

Values

Value Effect Confirmation
BROAD Campaign-level broad match on: every keyword in the campaign matches as broad double, and refused without Smart Bidding
UNSPECIFIED Off: each keyword keeps its own match type (the API's own name for the unset value) single
empty unchanged n/a

Turning it on

  • Smart Bidding check: the draft reads campaign.keyword_match_type and campaign.bidding_strategy_type. BROAD is refused unless the strategy is in _SMART_BIDDING_STRATEGIES, the same set _validate_campaign and the ad-group BROAD-keyword check use. No second list.
  • Bidding change in the same call: a bidding_strategy in the same plan counts, because both go out in one mutate. Switching to MAXIMIZE_CONVERSIONS and turning broad on together is allowed; switching to MANUAL_CPC together with broad on is refused.
  • Preview: shows current_keyword_match_type → keyword_match_type. It warns that the change affects how every keyword in the campaign matches, and sets requires_double_confirm.
  • Already on: if the campaign is already BROAD, there's no warning and no double confirmation, because nothing widens.

Apply and dry run

  • Apply: keyword_match_type goes on the existing campaign operation with its own update_mask path. UNSPECIFIED under the mask clears it.
  • Dry run: the existing validate_only path.
  • Live account: I ran it against a paused Maximize clicks Search campaign. BROAD was refused at draft, and UNSPECIFIED came back DRY_RUN_SUCCESS with validated_calls: 1. Nothing was applied.

Rules and tests

  • Rules: .cursor/rules/adloop.mdc now has the tool table entry and one bullet: what it does, the Smart Bidding rule, and never to turn it on for more traffic on an exact/phrase campaign. Synced.
  • Tests: tests/test_campaign_broad_match.py (15 tests) covers:
    • each Smart Bidding strategy allowed with a double confirmation;
    • MANUAL_CPC and TARGET_SPEND refused;
    • a bidding switch in the same plan, in both directions;
    • already broad, turning it off, other values refused before any read, an unknown campaign;
    • the update mask for both values, and the parameter documented on update_campaign only.
  • Suite: uv run pytest gives 1678 passed, 2 skipped.

One question, not in this PR. If a campaign already has broad on and someone moves it off Smart Bidding with bidding_strategy, nothing stops that today. Should update_campaign refuse, or require keyword_match_type="UNSPECIFIED" in the same call? Happy to add it either way.

It touches the same functions as #115 and #116; I'll rebase whichever lands later.

@kLOsk kLOsk left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the core is right. Two things are needed before merging:

  1. Re-check at apply. Smart Bidding is only checked at draft time. Two drafts (broad match, then Manual CPC) applied in reverse order, or a strategy change in the UI, end up broad with Manual CPC. Please re-read bidding_strategy_type in _apply_update_campaign and refuse unless it's Smart Bidding. That covers the dry run too.
  2. An accurate preview. Google's help (answer 13389795) says turning it on "will convert all existing phrase and exact match keywords into broad match". The originals show as Removed, and turning it off doesn't restore them. Since September 2026 such campaigns are also upgraded to AI Max. Please say all three in the preview and the docs. UNSPECIFIED stops new conversions, but it doesn't bring back match types.

On your follow-up question: refuse. When bidding_strategy moves a BROAD campaign off Smart Bidding, refuse unless the same call passes keyword_match_type="UNSPECIFIED", so both change in one mutate. Warn that the converted keywords stay broad. Please also rebase once #115 is in.

@xgp
xgp force-pushed the feat/campaign-broad-match branch from be03550 to dc88182 Compare October 27, 2026 22:01
@xgp

xgp commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on main (after #115 and a6d492d) and updated:

  1. Re-check at apply. _apply_update_campaign now calls _check_broad_match_bidding before building anything. It re-reads bidding_strategy_type and keyword_match_type and refuses, with nothing sent, when the plan would turn broad on without Smart Bidding or move a broad campaign off Smart Bidding. Draft and apply share one helper, _broad_match_bidding_error, so the two can't disagree. It also runs in the dry run, since the validate-only client passes reads through. Turning broad off needs no read.
  2. Accurate preview. Turning broad on now warns that every existing phrase and exact keyword is converted to broad: the originals show as Removed, turning the setting off doesn't restore them, and since September 2026 such campaigns are upgraded to AI Max. Turning it off warns that converted keywords stay broad and that AI Max, if Google enabled it, stays on. The tool description and the rules say the same.
  3. Leaving Smart Bidding. As you suggested, this is refused unless the same call passes keyword_match_type="UNSPECIFIED". When it does, the preview also warns that the converted keywords keep matching broadly under the new strategy.

Tests: new tests cover both apply-time refusals (no mutate is sent), the clean apply path, the case where turning broad off doesn't read, and the leaving-Smart-Bidding draft cases. uv run pytest: 1728 passed, 2 skipped.

Live account: validate-only dry runs against a paused Maximize clicks campaign:

  • BROAD is refused at draft;
  • UNSPECIFIED returns DRY_RUN_SUCCESS;
  • a MANUAL_CPC change runs the new apply-time read against real enum values and returns DRY_RUN_SUCCESS, because the campaign isn't broad.

Nothing was applied.

@kLOsk

kLOsk commented Oct 28, 2026

Copy link
Copy Markdown
Owner

Thanks, the update covers everything. #116 just landed and conflicts with this one in update_campaign (the same spots as before). Could you rebase once more? We'll merge right after. We're releasing the rest now, so this one goes out in the next release.

Optional keyword_match_type on update_campaign: BROAD turns campaign-level broad match on, UNSPECIFIED turns it off. Turning it on is refused unless the campaign, or the bidding_strategy in the same plan, is in _SMART_BIDDING_STRATEGIES (the set the BROAD keyword checks use), and needs a double confirmation. The preview says what Google does: every existing phrase and exact keyword is converted to broad (the originals show as Removed and are not restored by turning it off), and since September 2026 such campaigns are upgraded to AI Max. Turning it off takes a single confirmation and says converted keywords stay broad.

Moving a campaign with broad match on to a non-Smart bidding_strategy is refused unless the same call passes keyword_match_type=UNSPECIFIED. _apply_update_campaign re-reads bidding_strategy_type and keyword_match_type and refuses both unsafe combinations, so plans applied out of order or a UI change in between cannot leave broad match without Smart Bidding; the dry run runs the same check. draft_campaign is unchanged.

Fixes kLOsk#106.
@xgp
xgp force-pushed the feat/campaign-broad-match branch from dc88182 to e0926c5 Compare October 28, 2026 12:18
@xgp

xgp commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on main after #116 and #119. Every conflict was in update_campaign, and each one keeps both sides: the URL options and keyword_match_type sit side by side in the signature, the change check, the apply path and the campaign field mask. The two update_campaign rows in the rules table are merged into one, and the rules copies are regenerated with scripts/sync-rules.py. The diff against main is the same size as before, 423 additions and 3 deletions. uv run pytest: 1758 passed, 2 skipped.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

update_campaign: read and set the campaign-level broad match setting

2 participants