In one paragraph
A tool that writes to live ad accounts is a tool that spends money on your behalf. The only defensible design is one where a person approves a specific, complete, immutable description of what will happen, and the system refuses to act if reality has moved away from that description. Everything else in Outfitter follows from that sentence.
1. The shape of the problem
Paid media work produces a steady stream of changes that are individually small and collectively large. Add forty negatives. Pause the eleven ad groups that stopped converting. Lift six budgets by ten percent going into the season. Roll a new headline across thirty ad groups.
Each of those is boring. The danger is not in any one edit. It is in the fact that someone is going to do forty of them in a row, in a spreadsheet, at four in the afternoon, and one of the rows is going to have an extra zero in it.
The industry's answer has been faster editors. Bulk upload, find and replace, offline editing and one big sync. Those tools are good at what they do, and what they do is reduce the time between deciding and changing. That is the correct goal for someone working in an account they own, on their own money, where the cost of a mistake lands on the person who made it.
It is the wrong goal for the far more common case: an agency operating a client's account, a junior running a task a senior scoped, a contractor with credentials, or one person on a bad day. In those cases the time between deciding and changing is not overhead. It is where review happens.
2. Why most previews are not evidence
Nearly every tool that mutates something important shows you a confirmation screen. Very few of those screens are evidence, because of how they are built. The summary is generated by presentation code that reads the same inputs the execution code will read, and then execution runs independently against live state some seconds or minutes later.
Three things go wrong with that arrangement, and all three are common:
- The preview and the execution disagree. They are different code paths over the same inputs. A rule applied in one and not the other produces a screen that was honest and an outcome that was not.
- The world moves in between. Someone else edits the account. A campaign ends. A budget changes. The preview described a world that no longer exists, and nothing checks.
- The preview is incomplete. It shows what will happen and stays quiet about what was dropped. The user approves nine changes without learning that a tenth was silently discarded.
A preview that can disagree with execution is not a safety mechanism. It is a reassurance mechanism, and reassurance is worse than nothing, because it converts caution into confidence without earning it.
3. Compilation as the primitive
The fix is to stop treating the preview as a rendering of intent and start treating it as an artifact that execution consumes. Outfitter compiles a batch into a plan, and the plan is the only thing the publisher will ever act on.
Compilation is a pure function. It takes the proposed items and a snapshot of account state, and returns a plan. It reads no database, consults no clock and makes no network call. That constraint is what buys everything else: the same inputs always produce the same plan, so the plan can be recomputed later and compared, and the comparison means something.
Each item leaves compilation in one of three conditions:
- A candidate, legal to publish. Money candidates carry the old value, the new value and the percentage move.
- An exclusion, carrying a machine-readable reason. It will not publish, and there is no interface affordance to make it publish.
- A warning, which will publish but has something attached worth reading first, such as a budget that turns out to be shared across several campaigns.
The critical detail is that exclusions are part of the plan. The plan is not a list of what will happen. It is a complete account of what was asked for and what the system decided about each piece. Approving it means approving the refusals too.
Four exclusions, four reason codes. The detail column names the specific entity or number that failed, so the fix does not require guessing.
4. Approving a hash
Once the plan is a deterministic artifact, it can be fingerprinted. Outfitter computes two digests over canonical JSON: one over the plan including its exclusions, and one over the configuration that produced it.
An approval carries both. This is a small change with a large consequence. The approval no longer says "this person approved this changeset". It says "this person approved this exact plan, produced under these exact rules". Those are different claims, and only the second one survives contact with a system where things change.
It also makes rule changes safe by construction. Tightening a cap changes the configuration digest, which invalidates every approval issued under the looser rule. Nobody has to remember to go re-approve things. The approvals stop being valid on their own.
The same reasoning applies to the approval requirement itself, which is why Outfitter re-evaluates it as a live query at publish time rather than reading a stored status flag. If a workspace had one eligible approver, that person approved, and a second eligible approver is added before the publish runs, the plan quietly stops being sufficiently approved. Storing "approved: true" would have missed that. Asking the question again catches it.
5. Refusing on drift
At publish time, the plan is compiled again from current account state and compared to the plan that was approved. If they differ, the publish is refused.
There is no override. This is the part people push back on, and it is the part worth keeping. An override on a drift check makes the check decorative: the one time it fires is the one time someone is in a hurry, and a button that exists to be pressed under pressure will be pressed under pressure. Re-running a dry run costs seconds. Publishing against an account that moved underneath you costs whatever the account was worth.
The refusal is also specific. It names what changed, so the operator's next move is to look at a new dry run and decide again, which is exactly the behavior the design wants.
6. A closed vocabulary
Outfitter can perform 27 operations. Not 27 by coincidence, but 27 by allowlist. Each one declares its required parameters, its optional parameters, the entity type it targets, whether it moves money, whether it creates something, and which platforms support it. A parameter that is not declared is rejected at compile time rather than passed through.
The vocabulary is versioned as a unit, and a version bump invalidates plans compiled under the previous version. This is deliberately inconvenient. The alternative, letting old approvals ride across a semantic change to what an operation means, is how a system ends up executing something nobody approved.
The closure buys three properties that are hard to get any other way:
- The blast radius is enumerable. The complete set of things this software can do to an ad account fits on one page of a website. A reviewer can read it.
- Malformed input fails early. A typo in a field name is a compile error, not a partially applied change.
- Capability grows on purpose. New power requires editing the allowlist, which is a reviewable act, rather than emerging from a permissive schema.
The scope decisions are as important as the operations. Outfitter creates nothing above the ad level, so campaigns and ad groups must already exist. It cannot touch geo, audiences, placements or schedules. It can move a bid target's value but cannot switch a campaign from one bidding strategy to another. Each of those is a category of change where the damage is slow, quiet, and expensive to reverse, and none of them is a thing anyone needs to do forty times in a row.
7. Money is a different category
Six of the 27 operations move money: campaign budgets, ad set budgets and bids, keyword bids and bid strategy targets. Treating them like the other twenty-one would be a category error, so they carry their own rules.
| Control | Rule | Why this one |
|---|---|---|
| Approver role | Owner, rather than admin | Spend authority and operational authority are different things. |
| Typed confirmation | The aggregate delta, typed by hand | A number you have to type is a number you have to read. A checkbox is not. |
| Two-person rule | Two distinct approvers whenever two are eligible | Degrades honestly on a one-person workspace, and records that it did. |
| Per-item cap | 20% by default, 50% hard ceiling | Matches bid-strategy doctrine. Bigger single steps discard platform learning. |
| Per-batch cap | Aggregate daily-budget delta, capped per workspace | Twenty legal changes can add up to one illegal one. |
| Freshness | Inventory synced within the hour, or the item is blocked | A percentage computed against a stale baseline is a fiction. |
The freshness rule deserves a note, because it is the one most tools get backwards. When the baseline is old, the honest response is to refuse, not to warn. A warning on a stale baseline asks the operator to evaluate a percentage that was computed from a number that might be wrong, which is not a decision anyone can actually make.
8. Defaults and kill switches
Publishing is off when Outfitter is installed. Four independent layers have to be on before anything reaches a platform, and they are checked in order: a deployment-wide flag, a workspace toggle, a production authorization on the specific connection, and a per-platform allowlist. The first failure refuses the whole publish.
Four layers is not redundancy for its own sake. Each one belongs to a different person making a different decision. The deployment flag is the operator of the software. The workspace toggle is the account owner. The connection authorization is whoever decided this particular ad account is ready. The platform allowlist is us, saying which integrations we are willing to stand behind. Collapsing them into one switch would mean one of those four people is implicitly deciding for the other three.
Newly created entities land paused unless activation was explicitly requested and approved. Connections start in sandbox mode, where the adapter records the exact payload it would have sent and sends nothing. Reads, inventory sync, compilation and validation keep working when publishing is off, so turning the gate off does not turn the product into a brick.
9. Journal-first execution
Publishing is where a system finds out whether its bookkeeping is real. A process can die between the request and the response, and when it comes back it has to answer a question with real consequences: did that change happen or not?
Outfitter answers it by writing first. A pending journal row is inserted and committed before the network call is made. An item with no row provably never reached the platform. An item with a row and no result is the case a human has to look at, and it is labelled as exactly that rather than being guessed either way.
Retries carry deterministic idempotency keys, so a repeat cannot become a duplicate. Retry is limited to transient failures; a rejection is not retried, because retrying a rejection is just asking the same question louder. A platform that keeps failing trips a circuit breaker, and the trip is an audited event rather than a log line.
Resuming an interrupted publish re-runs the entire authorization chain from the start. It does not inherit the authorization of the run it is continuing. A resume is a new act, and the account may have moved since the original run began.
10. Undo, honestly
The obvious feature request for a tool like this is a big undo button. Outfitter does not have one, and the reason is structural: a fast undo path is a second way to write to a live account. A second write path needs its own preview, its own approval and its own caps, at which point it is not fast, or it skips them, at which point it is the least safe part of a system built for safety.
Instead, every mutating operation snapshots prior state before it runs, and reverting drafts a new changeset that inverts the items that provably landed. That changeset compiles, gets approved and publishes like any other.
Rollback is also explicit about the cases it cannot handle, which is the part that matters most. Three operations create things the vocabulary has no operation to remove: creating a responsive search ad, a Meta ad creative or a Meta ad. Those appear in their own section of the rollback preview, described as needing a person, rather than being dropped from a list that then looks complete.
The general principle. A system that reports partial success as success teaches its users to stop reading. Naming the four outcomes separately, landed, never sent, failed, and sent with unknown outcome, costs a little interface complexity and buys the only thing that makes an audit trail worth keeping, which is that people believe it.
11. What this design does not solve
A paper that only lists strengths is a brochure. These are the real edges of the design as it stands.
- There is no cross-changeset rolling money cap. The aggregate delta inside one batch is bounded. The total across many batches in a day is not. Approvals are the control there, and approvals are people. This is a known gap rather than an oversight, and it is the next cap worth building.
- Not everything is reversible. See section 10. The honest version is that creates need a human, and the interface says so rather than implying otherwise.
- The gate does not know whether a change is a good idea. It checks that a change is legal, bounded, reviewed and recorded. Whether raising that budget was smart is a judgment the tool deliberately does not make, which is why it sits beside analysis tools rather than replacing them.
- Coverage is uneven across platforms. Google Ads publishes today, with ten of its fourteen implemented operations verified end to end against a live account. Meta's nine operations are built and switched off pending an adversarial review. Microsoft and OpenAI Ads have no write adapter. The availability table is kept current for exactly this reason.
The argument in one line
Make the preview an artifact, bind the approval to it, and refuse to act when reality has moved. Everything else is detail, and every piece of detail in this paper exists because one of those three would otherwise leak.