How a change gets made in Tempio
Every write to an ad entity is a change_request row before it is a change on a platform. This note is the lifecycle — the seven statuses, who may approve, what no rule may ever approve, and what an agent should do at each step.
Last reviewed 2026-09-13.
Nothing in Tempio writes to a campaign, an ad group or an ad directly. Every
such write becomes a change_request row first, and the row is what an agent
creates, edits, approves and reverts. That is an architecture rule, not a
promise that a person reads every one: a change is approved by you, or by a
rule you wrote, and both paths go through the same row.
Planning
Before drafting, know which shape you are asking for. CHANGE_OPERATIONS
names every operation a row may carry — the creates (create_campaign,
create_ad_group, create_ad, create_creative, create_keyword,
create_negative_list, create_asset_group, create_lead_form), the updates
(update_budget, update_status, update_name, update_targeting,
update_bid, update_schedule, update_creative, update_keyword,
update_negative_list, update_asset_group, update_account_defaults), and
the two that are neither (upload_media, remove_entity), plus the escape
hatch's platform_api_request.
An operation the connected platform's adapter does not list in
capabilities.operations is refused at draft time, before a reviewer ever
sees it. Check what a connection can express rather than drafting hopefully.
Doing
createDraft runs in a fixed order, and every step of it is a refusal you can
plan around: the connection must belong to the workspace; the adapter must
support the operation; the payload is parsed against its versioned Zod schema;
the target account or campaign must belong to this workspace and this
connection, so a draft cannot steer another connection's spend; the payload's
currency is stamped from the owning ad account and a client-supplied value is
overwritten without comment; and the budget guardrails run. The row is inserted
as pending_approval with the mirror's pre-image stamped on it, which is what
makes the before-and-after panel and the undo possible later.
Several operations submitted together become one batch through
createDraftBatch: the rows share a correlationId, keep their order in
batchSeq, and a child create that depends on a parent carries dependsOnId.
A batch is one reviewable unit. It is approved whole or not at all — if a row
is refused during the claim, the rows already claimed are returned to the queue
with the reason on their audit rows.
updateDraft edits a row in place. It is allowed while a draft is still
pending_approval, and also once it has failed, which is how a stopped batch
is fixed and resumed. The edited payload is re-parsed, re-stamped with the
account's currency, re-guarded and re-validated against the adapter; nothing
about having been drafted once exempts it from a check.
Approving is approveDrafts, over either a list of draftIds or one
correlationId. Each row is claimed atomically — an UPDATE … WHERE status = 'pending_approval' moving it to applying — so a second caller gets an
invalid_state rather than a double apply. The claim re-runs the kill switch
and the budget check against current state, because the world moves between
drafting and approving. Only then is the apply dispatched. rejectDrafts is
the same shape in the other direction and applies nothing.
The seven statuses of CHANGE_STATUSES are draft, pending_approval,
applying, applied, partial, failed and rejected. partial means the
operation half-landed — Meta's create_ad is two Graph calls, so a created
creative with a failed ad is honestly neither applied nor failed. A row left in
applying after an adapter threw is deliberate: the outcome is unknown, and
resolveStuckDraft is the manual path for a row stuck there longer than
STUCK_APPLYING_MINUTES (15), which marks it failed with a note saying so.
Every result carries a review deep-link: reviewUrlFor points at the batch
when there is a correlationId, at the single draft when there is not, and at
the queue when a write named no row at all. Hand a person that URL — it is
absolute, because a bare path is not something anyone can open.
Who may approve
APPROVER_KINDS is human, mcp_key and policy. A person approving in the
app records their user. An MCP key minted with the approve scope may approve,
including a draft it wrote itself, and the record says which — a key approving
its own work is a different fact from a person approving it. The in-app chat
agent never holds that scope. A rule the workspace wrote is the third
authority, and the row names the rule.
What no rule may ever approve is fixed in POLICY_NEVER_OPERATIONS: every
create_* operation, upload_media, remove_entity,
update_account_defaults, and platform_api_request. A rule is written
against a shape, and a raw platform request has none. A rule also refuses a
budget or bid increase, an amount over its own stated cap, an amount in a
currency other than its own, a change whose direction cannot be read for want
of a pre-image, a draft the adapter's dry run refused, and anything on a
connection whose kill switch is on. Because a batch is one unit, a rule clears
a batch only when it clears every row in it.
Undo
revertDraft builds a NEW draft from the pre-image and sends it to the same
gate. Nothing is reversed behind anyone's back. It is refused for
remove_entity — the entity is gone on the platform and nothing here brings it
back — and for a row that is not applied or partial, because there is
nothing to put back.
So: never describe a Tempio write as having been made. Say that it is drafted, name what will change, give the review link, and say who or what can clear it.
Sources
- createDraft, approveDrafts, rejectDrafts, updateDraft, resolveStuckDraft —
repo://apps/app/lib/ads/drafts.service.ts(read 2026-09-13) - createDraftBatch, reviewUrlFor, revertDraft —
repo://apps/app/lib/ads/drafts/batch.ts(read 2026-09-13) - The policy evaluator and POLICY_NEVER_OPERATIONS —
repo://apps/app/lib/ads/policy/auto-approve.ts(read 2026-09-13) - CHANGE_STATUSES, CHANGE_OPERATIONS, APPROVER_KINDS —
repo://packages/db/schema.ts(read 2026-09-13) - Drafts — the product page for the same lifecycle —
repo://apps/docs/content/docs/drafts/index.mdx(read 2026-09-13)