feat(media-buy): allowed_actions + available_actions for update_media_buy (#4480)#4514
Conversation
…ys (#4480) structured action vocabulary for update_media_buy capability discovery so buyers can pre-flight which mutations are valid instead of failing mid-flight. - extends media-buy-valid-action enum with finer-grained values; legacy coarse values retained for 3.x backwards compat (removed in 4.0) - adds allowed_actions[] on Product (advisory template, modes[], allowed_statuses[]) - adds available_actions[] on get_media_buys, create_media_buy, update_media_buy responses (authoritative per-buy, singular mode, optional sla, optional terms_ref) - adds enums: media-buy-action-mode, action-not-allowed-reason - adds core types: sla-window, product-allowed-action, media-buy-available-action - adds ACTION_NOT_ALLOWED error code + typed error-details/action-not-allowed.json - adds enumMetadata on media-buy-valid-action with update_fields per action plus deprecated+rollup on legacy coarse values so SDKs can dispatch and hide legacy values when finer rollup targets are present - deprecates valid_actions[] in favor of available_actions[] (removed in 4.0) - documents normative action -> field mapping in update_media_buy.mdx composes with #4425's requires predicate grammar. refs #4480, #4425 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…owed-actions-rfc-4480
…#4480) audit-pass cleanups from final review before WG submission: - align legacy-value deprecation with repo convention: x-deprecated-enum-values at top level (matches specialism.json pattern); drop deprecated: true from enumMetadata, keep rollup - restore "Media Buy Valid Action" title (was renamed inadvertently) - add packages[].start_time / packages[].end_time to flight-date actions' update_fields; previously only buy-level dates were mapped, which left package-level date updates without a mapped action - expand update_targeting field paths in the action -> field mapping table to fully qualify the keyword/negative-keyword incremental fields - add explicit MUST-prefer guidance in the doc body so SDK authors reading only the docs page see the consumer rule, not just the schema descriptions refs #4480
bokelley
left a comment
There was a problem hiding this comment.
Solid RFC. Strong shape overall — flagging items I'd want addressed (or explicitly acknowledged) before WG submission.
What works
- Two-tier shape (product template / buy-authoritative) is correct. Mirrors
cancellation-policyand reporting capabilities; divergence between productallowed_actions[]and resolvedavailable_actions[]is expected and called out. enumMetadata.update_fieldsis the right move. SDK dispatch reading dotted paths from the enum entry — not parsing the prose table — kills a class of doc-drift bugs. Same pattern asrecovery/suggestiononerror-code.json.ACTION_NOT_ALLOWEDwithcurrently_available_actionsinerror.detailssaves the round-trip, matches the typederror-details/*.jsonconvention, andadditionalProperties: trueon the details object is right (extension surface).- Rollup metadata for legacy values is clean — SDKs hide
update_budgetwhen finer-grained values are present in the same payload. Migration path is explicit. x-deprecated-enum-values+ 4.0 removal target matches repo convention; AHEAD-set disposition (held-for-next-minor, 3.1) is correct for an enum extension on a wire-stable surface.- Scope discipline. Holding
swap_pre_approved_creativeuntil a real consumer distinguishes it fromreplace_creativeis right — easier to extend than retract.
Items to address
-
uniqueItems: truedoes not enforce action-keying. The changeset/PR text says "uniquely keyed by `action`; sellers MUST NOT emit two entries with the same `action`," but `uniqueItems` only catches structurally identical objects. `[{action:"pause",mode:"self_serve"},{action:"pause",mode:"requires_approval"}]` passes validation, and predicate evaluators indexing by action will silently pick one. Either add a conformance/contract test that catches duplicate-action arrays, or note in the schema `description` that the MUST is enforced by validators, not by the schema. -
Direction-of-change isn't expressible from `update_fields` alone. `increase_budget` / `decrease_budget` / `reallocate_budget` all dispatch to `packages[].budget`; `extend_flight` / `shorten_flight` / `update_flight_dates` all touch the same date fields. Server-side dispatch enforcement (described in the follow-up plan as "near-mechanical") actually needs to diff request-vs-current state to pick the right action. Worth surfacing this in the field-mapping table so the next implementer doesn't assume the schema is self-sufficient and re-derive the comparison badly.
-
`mode_mismatch` recovery is a flow switch, not a retry. Buyer SDK can't re-call `update_media_buy` for a `requires_proposal` action — they need `create_proposal` / `finalize_proposal`. The current `suggestion` prose says "re-issue via the flow named" but I'd sharpen it: this is a flow switch, not a retry against the same task.
-
`conditional_self_serve` ships as a forward-decl. The mode lands in 3.1 but the tolerance grammar belongs on #4425. Until #4425 ships, sellers declaring this mode have no schema-level way to communicate tolerances. Either acknowledge that explicitly in `enumDescriptions[conditional_self_serve]` ("tolerance expression composes with #4425; until that lands, tolerances are declared out-of-band") or hold the enum value until the paired grammar is ready.
-
`terms_ref` is a free-string forward ref. Schema accepts anything until the buy-terms RFC lands. Worth a one-liner saying the schema will tighten when the buy-terms namespace ships, so reviewers don't pattern-match this as a permanently loose pointer.
-
TOCTOU on mode resolution. `available_actions[].mode` is read at one moment; buy state can change before the mutation arrives. The `mode_mismatch` error covers it, but a sentence in `media-buy-available-action.json` description — "advisory at moment of emission; sellers MAY resolve to a different mode by mutation time" — would close the loop.
-
`remove_creative` field path. The mapping table says "Creative removed from `packages[].creatives[]` or `creative_assignments`," but `update_media_buy` is PATCH semantics. Worth confirming the request schema actually supports a removal shape — is removal expressed by omitting the creative from a replacement array, or via an explicit delete primitive? Today's request schema doesn't obviously support either; that ambiguity would land in SDKs.
Follow-up plan
The rollout sequence (SDK consumer to validate the schema surface, server-side enforcement as a small per-server fast-follow, drift telemetry tied to the same rot pattern as `supports_proposals`) is the right shape. Drift telemetry in particular is worth landing sooner rather than later — defensive over-declaration is the predictable failure mode here.
response to PR #4514 review feedback: - action-uniqueness invariant moved from uniqueItems (catches only structurally-identical objects) into the schema description on both product-allowed-action and media-buy-available-action, with explicit validator-MUST language - direction-of-change note in update_media_buy.mdx field-mapping table: increase/decrease/reallocate and extend/shorten share update_fields paths; the action resolves from request-vs-current diff, which server-side enforcement MUST perform - mode_mismatch recovery prose sharpened to "flow switch, not a retry against update_media_buy" in both action-not-allowed-reason.json and error-code.json's enumMetadata suggestion - conditional_self_serve enumDescription explicitly notes tolerances are declared out-of-band until #4425's requires grammar lands; buyers cannot statically predict auto-approval from this surface alone today - terms_ref description tightened: schema accepts any string for now and will tighten to a structured reference when the buy-terms RFC ships - TOCTOU note on media-buy-available-action: mode/sla advisory at emission; state can change before the mutation arrives; seller MAY resolve to a different mode at mutation time and reject with ACTION_NOT_ALLOWED - remove_creative mapping clarified: removal is "omit from replacement array" via PATCH semantics (no explicit delete primitive in 3.x) refs #4480
bokelley
left a comment
There was a problem hiding this comment.
Re-reviewed against ff81fe5 — all seven items addressed cleanly.
| # | Item | Resolution |
|---|---|---|
| 1 | uniqueItems action-keying | Validator-MUST language added to both core schemas |
| 2 | Direction-of-change dispatch | Field-mapping table now states server-side enforcement MUST diff request-vs-current |
| 3 | mode_mismatch flow switch | Sharpened in both enum description and recovery suggestion, with explicit create_proposal/finalize_proposal + webhook callouts |
| 4 | conditional_self_serve forward-decl | Honest framing: buyers cannot statically predict auto-approval from this surface until #4425 lands |
| 5 | terms_ref forward ref | Both schemas note tightening when buy-terms RFC ships |
| 6 | TOCTOU on mode resolution | mode/sla called out as advisory-at-emission |
| 7 | remove_creative removal shape | Clarified as replacement-semantics; no explicit delete primitive in 3.x |
One render-only nit (not blocking): the new direction-of-change paragraph in update_media_buy.mdx sits mid-table between the budget rows and the targeting rows. The blank lines on either side should let the table re-open, but worth eyeballing the rendered MDX output before WG submission to confirm the table doesn't break visually. Content is right either way.
Approving — ready for WG. Looking forward to the SDK consumer PR exercising the schema surface end-to-end.
What this implements
Spec changes for #4480 —
allowed_actionson products,available_actionson buys, structuredACTION_NOT_ALLOWEDrejection. Composes with #4425'srequirespredicate grammar.This is PR #1 of the RFC rollout — schema + docs only. SDK and consumer work follows. Wire-compat: additive on the response side; existing
valid_actions[]callers keep working through 3.x.In scope (this PR)
media-buy-valid-actionenum extended with finer-grained values; legacy coarse values marked viax-deprecated-enum-values(removed in 4.0)enumMetadata.<action>.update_fieldsper action (dotted paths intoupdate_media_buybody) so SDKs and codegen dispatch from schema metadata;enumMetadata.<legacy>.rolluplets SDKs hide legacy values when finer rollup targets are presentallowed_actions[]on Product (advisory template:modes[], optionalallowed_statuses[], optionalsla, optionalterms_ref)available_actions[]onget_media_buys/create_media_buy/update_media_buyresponses (authoritative per-buy: singularmode, optionalsla, optionalterms_ref; uniquely keyed byaction)media-buy-action-modeenum:self_serve|conditional_self_serve|requires_proposal|requires_approvalACTION_NOT_ALLOWEDerror code + typederror-details/action-not-allowed.json(attempted_action,reason,currently_available_actions)update_media_buy.mdxvalid_actions[]deprecated in favor ofavailable_actions[](removed in 4.0); sellers SHOULD populate both during 3.xFollow-up PRs I'll drive
@adcp/clientSDK consumer: pre-flight helpers (canExtendFlight(buy),canRemoveCreative(buy), etc.) driven byavailable_actions[]andenumMetadata.update_fields; structured handling ofACTION_NOT_ALLOWED. Validates the schema surface end-to-end.enumMetadata.update_fieldsmakes the server-side check near-mechanical (action → covered fields → reject mismatches before reaching the handler), so this should be a small PR per server SDK rather than a full re-implementation.swap_pre_approved_creativevsadd_new_creative. Partial coverage in this PR (replace_creative,update_creative_assignments,remove_creative). Holding the rest until a real consumer workflow distinguishes them fromreplace_creative— the enum is easier to extend than to retract, and shipping speculative splits risks two values that overlap in practice.last_verifiedtimestamp onallowed_actions[]entries and/or honored-rate expectations, to prevent defensive over-declaration the waysupports_proposalshas rotted. Likely lands alongside constraint metadata once the shape is clearer.Depends on other RFCs
requirespredicate grammar plus alteduration operator — tracked on that RFC.terms_reftarget namespace — referenced as opaque string here; resolved when the buy-terms RFC lands.Reviews
Schema / protocol / DX angles all covered before WG submission. Audit fixes folded in:
frequency_capsingular, package-level flight-date paths added, typederror-detailsschema,x-deprecated-enum-valuesto match repo convention, MUST-prefer guidance in the docs body.Test plan
node scripts/build-schemas.cjspasses cleannode scripts/lint-error-code-drift.cjspasses (ACTION_NOT_ALLOWED→held-for-next-minor, target3.1)refs #4480, #4425