[CODE]

The Gap Between Knowing and Doing: A Reviewed-YAML Layer for Live Ad Account Changes

An audit report tells you what's wrong. It doesn't change anything. This is the execution skill that turns findings into git-tracked, dry-run-previewed Google Ads mutations — and the guardrail that stops it from pausing a keyword that's actually converting.

6 min read AI-generated
google-ads automation ai-assisted typescript skills

A health report on a Google Ads account is a diagnosis. It is not a treatment. For weeks the weekly analysis pipeline for these B&B accounts had been good at the first part — flagging broad-match keywords bleeding budget, surfacing search terms that should be negatives, noticing a campaign optimizing against the wrong signal. And then nothing happened, because nothing in that pipeline touched the account. The findings sat in a report, and someone — me — had to open the Google Ads UI and translate each finding into clicks.

That translation step is where the system leaked. Not because the work was hard, but because it was the one place with no record, no review, and no replay. The report knew what to do. The account didn’t change unless I personally did it, by hand, with no diff to look at afterwards.

This piece is about closing that gap — the execution layer that sits between the audit (covered in “Teaching the Ads Auditor Not to Recommend What’s Already Done”) and the live account. The audit decides what. This decides how it actually gets applied without anyone fat-fingering a budget into the wrong customer ID.

Why not just let the agent make the API call?

The obvious shape would be: agent reads the report, agent calls the Google Ads API, done. I deliberately didn’t build that, and the reason isn’t a vague preference for caution.

An inline API mutation has no artifact. If an agent pauses eleven keywords through a live call, the only evidence is the account’s change history — buried in a UI, attributable to an OAuth token, not to a decision anyone reviewed. There is no diff to read before it happens and no file to point at when something looks wrong a week later.

So the non-negotiable rule in this skill is: no inline mutations, ever. Every change an agent wants to make gets written first into a git-tracked YAML file under sites/<slug>/ads-changes/, dated, scoped to one site. The YAML is the plan. A separate mutation runner reads the plan and applies it. The agent’s job ends at producing the file; the file is the thing I review.

This buys three things at once. The change is reviewable before it touches anything. It’s replayable and auditable after the fact. And the destructive step — the actual API write — lives in one runner with one set of guards, instead of being scattered across whatever the agent decided to call that day.

Dry-run reads the truth before anyone writes

The step I’m most glad I forced is the dry-run, and the reason is one specific failure mode: assuming you know the current state.

A report might say “this keyword is too broad, tighten it.” The naive move is to act on the keyword name as written. But a keyword’s match type isn’t in the keyword text — a phrase-match and a broad-match keyword can read identically in a report. Acting on the assumption that everything is broad is how you pause the wrong thing or add a redundant change.

The dry-run queries the account for the current state of every keyword the plan touches before anything is written. It’s not a formality that prints the plan back to you — it’s the only point where the plan gets reconciled against what’s actually live. The rule in the prompt is blunt about it: never assume match type, query first. The dry-run is where that querying happens.

The guard that earns the whole thing: don’t pause what’s converting

Here is the rule that, more than any other, is the reason this layer exists as code and not as a habit in my head:

Before adding any keyword to the pause list, check whether it has conversions in the report. If it does, do not pause it — add negatives instead.

This is the kind of mistake that’s invisible until it costs you. A keyword can look like a problem — high cost, messy search terms — and still be the keyword that produces actual bookings. The lazy fix is to pause it. The pause stops the waste and the bookings. The right fix, when a keyword converts but also catches junk, is to keep it live and carve away the junk with negative keywords, so the converting traffic survives and the wasted spend on irrelevant searches dies.

A human doing this by hand, fast, at the end of a week, across multiple accounts, makes this mistake. Not because they don’t know the rule — because the rule is one judgment call buried in a list of twenty mechanical ones, and judgment is exactly what erodes when you’re clicking through a UI on the fifth account. Encoding it as a hard precondition in the execution flow means the mistake can’t pass silently. The conversions check isn’t a suggestion; it’s a gate the keyword has to clear before it’s allowed near the pause list.

Where the tool stops, and why that’s deliberate

Not everything in an ads account is safe to mutate through an API call, and the skill draws an explicit line between what it executes and what it refuses to.

The API-executable set is narrow and reversible: adding and removing negative keywords, pausing specific keywords, adding new keywords with fixed match types. These are changes where the blast radius is small and the rollback is obvious.

Everything with real consequence stays manual — but guided. Geographic targeting and region locks, pausing or enabling whole campaigns (Performance Max included), budget changes, bid strategy changes, ad copy rewrites. The skill doesn’t try to do these through the API. Instead it produces step-by-step instructions with the exact backend paths, so the manual work is fast and unambiguous but a human is the one who pulls the trigger on anything that could move real money or take a campaign dark.

The boundary isn’t “what the API technically allows.” The API allows plenty I chose not to automate. The boundary is “what’s reversible and low-blast-radius” on one side, and “what deserves a human looking at it before it happens” on the other.

Why this exists at all

The larger system here is a digital operations stack for B&B operators who could never have afforded one. Each layer — analytics, the weekly report, the ads audit — is a piece that used to require its own headcount. Stacked, they were out of reach for a small operator’s budget. AI is what made assembling them economically possible for the first time.

The execution skill is the layer that makes the rest of it matter. An audit that nobody acts on is a report that gets read and forgotten. The point was never to produce better diagnoses — it was to make the diagnosis flow into the account reliably, reviewably, without the one weekly step that depended entirely on me not making a tired mistake on the fifth account.

The report knows what’s wrong. Now the account changes — through a file I can read first and a guard that won’t pause the keyword paying the bills.