AdCP Brief vs AAMP CampaignBrief: Two Objects, One Name

AdCP's brief is a natural-language string on the get_products request. AAMP's CampaignBrief is a Pydantic class with five declarations across two repositories, four of them in IAB Tech Lab's buyer-agent, and it is absent from the AAMP wire contract.

Only one of these two briefs is on a wire

AdCP’s brief is a string on a released schema. AAMP’s CampaignBrief is a Python class inside a reference implementation, with five declarations across two repositories, four of them in the buyer agent, field names that disagree, and no presence in the wire contract those same repositories publish. AdCP’s brief is the smaller object and the only one with a released schema behind it. Put your own campaign object on the AdCP side of that line and emit whatever AAMP shape you need out of it.

AdCP is the Ad Context Protocol, and it ships as versioned JSON Schema releases; 3.1.13 is the current stable one. AAMP is IAB Tech Lab’s Agentic Advertising Management Protocols: eight repositories, versioned independently, no protocol-wide number anywhere, and a shared wire contract that is unreleased.

One property, on one operation. get_products carries brief as a string, and the schema tells you exactly how much structure to expect: “Natural language description of campaign requirements. Required when buying_mode is ‘brief’. Must not be provided when buying_mode is ‘wholesale’ or ‘refine’.” A seller agent reads the prose and decides what it means. Both halves of that coupling live in the description text and nothing validates either one, so a brief sent in wholesale mode passes any validator you point at it and arrives at a seller that was told not to expect it. Check the pairing in your own code, because nothing downstream does.

CampaignBrief is a Pydantic class with eight enforced required fields in IAB Tech Lab’s buyer agent, an objective enum, cross-field validators, a typed brand-safety block and four human approval gates. As a model of what a planner hands over it is the better object, and AdCP has nothing close to it.

No brief schema exists in the normative spec. iab-agentic-primitives names the artifact that ought to sit above the code, in its own README:

“The spec is the normative artifact; this package is its reference implementation.”

Its spec/ directory holds 48 JSON Schemas and an OpenAPI document, and none of them is a brief. The buyer agent’s only upstream pointer is to a “Campaign Automation Strategic Plan, Sections 6.1 and 7.1” that appears in none of the eight public repositories. So whichever CampaignBrief you build against is an implementation detail with no versioning promise attached, and this one has no version to bump.

The integration cost is inside one Python package: five incompatible declarations of one class name.

Which one to build against: AdCP, with AAMP’s model vendored internally

AdCP, and take the non-determinism with it knowing that you are taking it. The same brief string sent to two seller agents comes back as two different product sets, and AdCP promises nothing about that: 3.1.13 released the field, not the interpretation.

What makes the trade worth it is where the determinism sits. The parts you can hold a counterparty to travel in filters as typed properties: a 20-value channel enum, a 36-value metric enum, a budget range with a required currency. Diff those against what came back. The prose carries the residue, the part no enum was ever going to hold. AAMP moves that problem instead of solving it, because the matching runs inside your own process where no counterparty can see it, and the only brief-derived field on its wire is an open object. The worked CTV payload is a real request you can send today.

Two conditions still make AAMP the right call: a counterparty you need has picked it, or what you want is a planning object and not a wire contract. Since CampaignBrief never touches the wire, vendoring it as your internal planning model costs you nothing on the AdCP side. If you do vendor it, pin the commit, write down which of the five definitions you mean, and own the drift, because the wire contract is unreleased and nobody is versioning the model for you. AAMP Explorer tracks the buyer agent and the wire contract commit by commit if you want to watch it move.

Supporting both is two integrations. They share no type and no endpoint, and neither corpus references the other. The generator will emit both objects from a paragraph of prose, with every field marked as stated in your text, derived from it, or assumed, and the front page writes out the seven rules the official Python SDK leaves you to check by hand.

AAMP deleted server-side product search on purpose

AdCP sends an 18-property request. AAMP sends limit and offset.

ProductListRequest in iab-agentic-primitives has exactly two properties, and its description states the decision instead of hiding it:

“Deliberately carries no filter fields: filtering is client-side over the returned Product records”

The OpenAPI document repeats it in a header comment: “There is deliberately NO POST /products/search (plan §7 amendment 3): the buyer filters client-side over the ProductListResponse fields.”

POST /products/avails is not a way around this. It requires productids, accountid, advertiserbrandid, startdate and enddate. You name the products first. QuoteRequest requires idempotency_key, product_id and deal_type before it will look at flight_start, impressions or target_cpm. And the single piece of brief-derived structure that does cross the AAMP wire is audience_plan on QuoteRequest, typed as anyOf[{object, additionalProperties: true}, null] with the description “Audience plan slot (open object; the typed model lands with the audience-plan bead)”, and it attaches after product selection.

The seller side confirms it. No file in the AAMP seller agent names CampaignBrief. The phrase “campaign brief” survives there only as a comment inside AudiencePlan, above its targeting fields: “Requirements from campaign brief.”

AdCPAAMP
Who filters inventoryThe seller, from a prose brief and 30 typed filtersThe buyer, client-side, over a paginated catalog
Server-side product searchget_products, the protocol’s first operationDeliberately absent
Discovery request shape18 top-level propertieslimit, offset
Where campaign intent entersDiscoveryQuote, per product, after selection
Brief-derived data on the wireThe brief string, filters, required_policies[]An untyped audience_plan object on QuoteRequest

AdCP runs curation on the seller. AAMP leaves it to the client. limit defaults to 50 and caps at 500, so filtering client-side means paging the whole catalog, holding it somewhere, deciding how stale you will let it get, and writing the matching logic that AdCP’s seller runs on its own inventory. Nothing in the eight repositories says how large that catalog gets or how often it changes. On AdCP’s side the same choice sets what belongs in the brief string versus the typed filters.

On the sell side the question inverts. AdCP puts the brief on your doorstep: get_products arrives carrying prose, 30 typed filters and required_policies[], and turning all of that into a product list against your own inventory is your implementation, the harder build and also the surface you compete on. AAMP asks for nothing of the kind. The seller agent’s GET /products takes limit and offset, the string “brief” doesn’t appear anywhere in its 74-path OpenAPI document, and quoting happens per product after the buyer has already chosen. Serving a paginated catalog is a smaller job than reading intent.

Where each object lives

AdCP briefAAMP CampaignBrief
What it isA string propertyA Pydantic BaseModel class
Where it is defineddist/schemas/3.1.13/media-buy/get-products-request.jsonbuyer-agent/src/ad_buyer/models/campaign_brief.py
Artifact typePublished JSON Schema, released versionPython source in a reference implementation
On the wire?Yes. It is a request field.No. No CampaignBrief schema exists in the AAMP wire contract.
Required fieldsNone inside it. The parent request requires only buying_mode.Eight, enforced: advertiser_id, campaign_name, objective, total_budget, currency, flight_start, flight_end, channels[]. A ninth value, budget_pct, is required on every ChannelAllocation by a validator rather than by the brief.
ObjectiveFree text inside the string. The 5-value enum lives on a different object, core/creative-brief.json.CampaignObjective enum: AWARENESS, CONSIDERATION, CONVERSION, REACH
BudgetSibling field filters.budget_range {min, max, currency}, currency required inside ittotal_budget: float plus currency as ISO 4217, validated by pattern
ChannelsSibling field filters.channels, a 20-value enumlist[ChannelAllocation], each {channel, budget_pct, format_prefs[]}, budget_pct values must sum to 100
Channel vocabulary20 values including retail_media, dooh, sponsored_intelligence6 values: CTV, DISPLAY, AUDIO, NATIVE, DOOH, LINEAR_TV
FlightSibling fields filters.start_date / end_dateflight_start / flight_end as date, with a validator that flight_end > flight_start
KPIsSibling field filters.required_metrics, a 36-value capability filterlist[KPI] of {metric, target_value}, metric from a 7-value enum
Brand safetyrequired_policies[] on the request: registry policy IDs, resolving to prose a governance agent judgesBrandSafety object: excluded_categories[] as IAB Content Taxonomy 3.0 IDs, excluded_keywords[] as strings
Frequency capBuy time only, packages[].targeting_overlay.frequency_capIn the brief: FrequencyCap {max_impressions, period_hours}
PacingBuy time only, packages[].pacingIn the brief: PacingModel enum
Human approvalNot modelled. Seller-side HITL only.ApprovalConfig with four gates: plan_review, booking, creative, pacing_adjustment
VersionAdCP 3.1.13, published stableNo AAMP-wide version. buyer-agent is at v2.3.0-24-g0c3a154.

The brand-safety row holds two different mechanisms. AAMP’s BrandSafety is category IDs and keyword strings: machine-checkable, and only as complete as the list. AdCP hands over registry policy IDs, and a governance agent reads the policy text and judges, which is the only one of the two routes that can carry a rule like “nothing adjacent to conflict reporting.”

Buyer-side approval is the row with an operational cost behind it. AdCP doesn’t model it, so plan sign-off, booking sign-off and creative sign-off are workflow you build, run and audit outside the protocol. AAMP names four gates in ApprovalConfig and defaults two of them on, plan_review and booking, on the model that only the demo script and the test suite ever construct. The approval pipeline imports the ApprovalConfig type and then reads its configuration off a stored campaign record, not off a typed brief. The one pipeline module that does annotate brief: CampaignBrief is the audience planner, and the only function reaching it says in its own docstring that it is the path “used by tests and demos that don’t go through the canonical flow (DealBookingFlow)”. Neither protocol exposes your sign-off chain to the counterparty. Budget for it as headcount.

Both protocols name frequency cap and pacing. AAMP takes them in the brief; AdCP only at buy time, on packages. If your planning system decides pacing, you carry that decision yourself from brief to buy, because AdCP’s discovery call has nowhere to put it.

Five objects, one name

DefinitionWhereKindWhat anchors itDoes the shipped campaign_brief.json validate?
Pipeline modelmodels/campaign_brief.pyPydantic BaseModel, 8 requiredcampaign_name, total_budget, flight_start, flight_end, channels[ChannelAllocation]No.
Flow-state modelmodels/flow_state.pyPydantic BaseModel, camelCase aliasesname, budget, start_date, end_date, channels[str]Yes
HTTP API modelinterfaces/api/main.pyPydantic BaseModel, the published onename, objectives[], budget, start_date, end_date, target_audience{}Yes
Demo dataclassexamples/buyer_demo.pydataclass, docstring “Parsed campaign brief from PDF”its own key setNot JSON-fed
Harness dataclassiab-agentic-primitives/.../harness/roles.pyfrozen dataclass, 15 fieldscampaign_name, deal_type, impressions, max_cpm, budget as MoneyDifferent repo, not JSON-fed

The five share no field name. The sharpest collision is the pair sitting in one package: models/campaign_brief.py and models/flow_state.py both declare a class called CampaignBrief, and they agree on almost nothing — campaign_name against name, total_budget against budget, flight_start / flight_end against start_date / end_date, objective as an enum against objectives as a list of bare strings, channels as typed allocations against channels as a list of strings. No adapter converts between them, and models/__init__.py re-exports only the first, so which class you get depends on which import path you typed.

Only the harness dataclass documents its own scope, in its docstring:

“Not a wire primitive: it is the buyer agent’s internal objective.”

Both campaign_brief.json files the repository ships fail the model that defines them, and they fail it identically: 12 validation errors each. Seven required fields absent, all three channels entries as bare strings where the model wants ChannelAllocation objects, target_audience missing its primary key, kpis a dict where the model wants a list. The shipped channels: ["branding", "ctv", "performance"] goes up against a six-value enum containing neither branding nor performance. A third file, data/campaign_briefs.json, carries three more demo briefs on a fourth key set, with flight_dates as the string "2026-09-01 to 2026-12-31", and those validate against none of the buyer agent’s three JSON-fed models.

None of it stops anything, because the model that validates and the model that runs are different objects. ad-buyer init writes that failing template, hard-coded in the CLI, then prints:

“Edit this file with your campaign details, then run: ad-buyer book campaign_brief.json

And book never calls the parser. It loads the JSON, reads name, budget and start_date straight off the dict, and hands the raw dict to DealBookingFlow, which keeps treating it as a dict all the way down. The documentation describes a third shape again: the CampaignBrief field table under docs/ is the API model, and the string campaign_name never appears as a brief field anywhere in the documentation. The module docstring of campaign_brief.py adds a discrepancy of its own, listing a ninth required field, target_audience, which the class defines with a default of None.

No code path in the repository connects the two. The strictest of the five, the one carrying the enums and the approval gates, is documented nowhere and reached from a single test-and-demo path. The documented shape and the annotated shape are different classes, and neither route leads to the strict one.

There is exactly one published CampaignBrief schema and it is the loosest one

buyer-agent/docs/api/openapi.json carries components.schemas.CampaignBrief, info.version 1.0.0, contact “IAB Tech Lab”, description “Campaign brief for booking.” The committed document is served live from IAB Tech Lab’s GitHub Pages site. It documents the buyer agent’s own inbound HTTP API, not anything that crosses the buyer-to-seller wire, and it is not the Pydantic class in campaign_brief.py. If you searched for a campaign brief JSON Schema and found nothing, this is what you were looking for.

Six required fields: name, objectives, budget, start_date, end_date, target_audience. Two optional: kpis, channels. It carries no objective enum at all — objectives is an array of bare strings with minItems: 1 — no channel allocation, no sum-to-100 rule, no approval gates, no brand safety. target_audience is {"type": "object", "additionalProperties": true}, an open bag, and it is required, where the pipeline model makes it optional and typed.

So the only fetchable CampaignBrief schema is the loosest of the five, and it describes what you POST to a buyer agent you are running yourself. Nothing travelling the other way carries a brief at all.

AdCP has a structured brief too, and it’s for creative

dist/schemas/3.1.13/core/creative-brief.json ships inside the release and you can validate a payload against it today. It cannot reach discovery. Title “Creative Brief”, eight properties, only name required, objective a five-value enum, tone and audience and territory as prose, messaging holding headline, tagline, cta and an ordered key_messages[], plus a compliance block of required_disclosures[] and prohibited_claims[] that AAMP’s brand-safety object has no equivalent for.

Its only route to the wire is core/assets/brief-asset.json, {"asset_type": {"const": "brief"}} plus an allOf on the creative brief, wrapped in a creative manifest. The manifest is referenced by build_creative, preview_creative, get_creative_features and validate_input, and by neither get-products-request nor create-media-buy-request. Discovery still gets a string. The release carries one more brief string, on list_transformers, ranking creative transformers by intent: prose again, creative side again.

AdCP and AAMP share three objective values and diverge on three.

AdCP core/creative-brief.jsonAAMP CampaignObjective
awarenessAWARENESS
considerationCONSIDERATION
conversionCONVERSION
retentionno counterpart
engagementno counterpart
no counterpartREACH

AAMP kept REACH; AdCP kept retention and engagement. The creative-brief crosswalk maps the agency-brief rows onto AdCP media-buy fields, and that mapping is where the loss happens.