Documentation Index
Fetch the complete documentation index at: https://agenticadvertisingorg-changeset-release-main.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Storyboard authoring — scoping rules
Compliance storyboards live understatic/compliance/source/. Each step that invokes a training-agent task that scopes session state by tenant must carry brand or account identity in sample_request. Otherwise the call lands in open:default, and a follow-up step that does carry identity writes to open:<brand> — giving you MEDIA_BUY_NOT_FOUND against your own just-created media buy.
This rule is enforced at build time by scripts/lint-storyboard-scoping.cjs, which runs as part of npm run build:compliance.
Canonical identity shape
Useaccount { brand, operator }. The AccountRef schema requires operator whenever the natural-key form (brand) is used — there is no “just a brand” shape at the spec level.
account_id via list_accounts):
sync_plans, identity lives inside each plan entry. The sync-plans-request schema defines brand on each plan item and forbids account there — do not use the wrapper form inside plans[]:
What about top-level brand?
Some AdCP requests (create_media_buy, get_products, build_creative) have a top-level brand field. That is the campaign’s brand, a separate schema field — not an identity shorthand. create_media_buy requires both account and brand; one does not substitute for the other.
The lint still accepts a bare top-level brand.domain as a fallback because the training agent’s sessionKeyFromArgs reads it — but that is a training-agent routing detail, not a spec-canonical shape. New storyboards should use account { brand, operator }.
Which tasks are session-scoped?
The authoritative list lives inscripts/lint-storyboard-scoping.cjs as TENANT_SCOPED_TASKS. A parity test (tests/lint-storyboard-scoping.test.cjs) asserts every task registered in the training agent’s HANDLER_MAP appears in either TENANT_SCOPED_TASKS or EXEMPT_FROM_LINT. If you add a new tool to the dispatch table and forget to classify it, the parity test fails — you won’t get silent drift.
Rule of thumb: if the task’s request schema has a required globally-unique scope-ID (plan_id, rights_id, standards_id, list_id, event_source_id), the seller can resolve the tenant from that ID alone — envelope identity is redundant and the lint does not require it (see EXEMPT_FROM_LINT bucket (c)).
Everything else falls into TENANT_SCOPED_TASKS: create/update mutations without a scope-ID, list/get operations that don’t carry a single resource ID, resource-standards calls without standards_id in schema, etc. These must carry envelope account { brand, operator }.
Identity fields that flow through $context
When a step captures a value into $context via context_outputs and a later step consumes it as $context.<name>, the entity type at both ends must match. If the value captured from a field annotated advertiser_brand is consumed as a field annotated rights_holder_brand, the lint will flag it (that’s the #2627 bug: same field name, different entity). See docs/contributing/x-entity-annotation.md for the list of entity types and how schema authors annotate fields.
Other exempt categories: payload-array-keyed sync tasks (sync_accounts, sync_governance, sync_catalogs, sync_event_sources), global discovery (list_creative_formats, get_adcp_capabilities), global catalog reads (get_brand_identity, get_rights, update_rights), and the comply_test_controller sandbox primitive.
Why ID-scoped tasks are exempt but storyboards still carry identity
check_governance, report_plan_outcome, acquire_rights, log_event, calibrate_content, validate_content_delivery, and validate_property_delivery all require a globally-unique ID (plan_id, rights_id, standards_id, etc.) that was previously provisioned with brand context. At the spec level, a real seller resolves the ID → tenant via their own lookup; the envelope doesn’t need to repeat the identity.
The training agent’s sessionKeyFromArgs routes by envelope identity. A storyboard that drops identity on an ID-scoped task lands in open:default and fails to find the plan/rights/standards — so storyboards carry envelope identity anyway, and the lint just won’t enforce it.
This is a sandbox routing convention, not a spec claim. Production sellers resolve tenant from the authenticated principal (bearer/OAuth/HMAC), not from envelope payload — see Tenant resolution. They don’t need envelope identity on ID-scoped tasks and wouldn’t rely on it if present. Building a cross-session reverse index in the training agent just to move identity off the wire would be sandbox plumbing without spec meaning.
Intentionally cross-tenant probes
If your step is supposed to probe a session-scoped task without tenant identity — e.g. a negative test that verifies the seller rejects the bare request, or a capability-discovery probe — annotate the step:Fixtures and cross-step captures
Storyboards that need prerequisite state (a product with a specificproduct_id, a creative already in approved status, a plan the governance flow can reference) have two ways to set it up: declarative fixtures: at the storyboard root for state that exists before the test runs, and step context_outputs: captures for IDs generated during the run.
When to use which
| Fixture origin | Pattern | Authored as |
|---|---|---|
| Exists before the storyboard (needs seeding) | fixtures: at storyboard root | Declarative block; runner seeds via comply_test_controller seed_* |
| Generated by an earlier step in this run | context_outputs: on the generating step, $context.<name> on later steps | Captured at runtime; stays inside this run |
| Runner-supplied (webhook URLs, etc.) | {{runner.webhook_url:<step_id>}} | Substitution variable |
sample_request if you can avoid it. A literal like media_buy_id: "mb_acme_q2_2026_auction" only works if the agent happens to generate (or accept) that exact ID. Spec-compliant agents auto-generate IDs — the literal won’t match and your storyboard will fail for an implementer who did nothing wrong.
Pattern A — prerequisite fixtures via fixtures: + comply_test_controller
Declare fixtures at the storyboard root. Set prerequisites.controller_seeding: true to tell the runner to auto-inject a fixtures phase before the main phases.
comply_test_controller with scenario: seed_product, scenario: seed_pricing_option, and scenario: seed_creative (in foreign-key order) before running place_buy. An agent that implements the seed scenarios passes out of the box; an agent that returns UNKNOWN_SCENARIO on the seeds causes the storyboard to grade as not_applicable, not failed — implementers don’t get penalized for missing sandbox-only surface.
See the full list of seed scenarios and their params in Compliance test controller — Scenarios.
Pattern B — flow-derived captures via context_outputs: + $context.<name>
Capture the ID the generating step returned, then reference it by $context.<name> on downstream steps.
media_buy_id from create_buy’s response (after its validations pass), stores it in the run-scoped context accumulator, then substitutes the literal string $context.media_buy_id in check_buy.sample_request before sending. Agents see the actual ID — never the literal $context.foo token.
Capture failures grade the generating step, not the reader: if the response doesn’t contain media_buy_id at the declared path, create_buy fails with capture_path_not_resolvable. This is deliberate — the contract the storyboard declared (“this step produces a media_buy_id”) is what failed, not the step that tried to use it.
Context block and the echo contract
Storyboards that assert on responsecontext MUST send a context: block on the sample_request:
context: on sample_requests that omit it. Storyboards whose validator expects context.correlation_id in the response but whose sample_request lacks context: are authoring bugs — the agent is allowed (and required) to omit context when the caller sent none.
See Context and sessions — Normative echo contract for the agent-side rules.
Asserting on errors
AdCP surfaces errors in two layers (see Error handling — envelope vs. payload). Storyboards MUST assert error shape in a way that works regardless of which layer a conformant agent surfaced the error on. Usecheck: error_code — not check: field_present, path: "errors".
value: or allowed_values: MUST exist in the canonical error-code enum at static/schemas/source/enums/error-code.json. The lint:error-codes script (wired into npm run test) walks every storyboard and rejects references to codes that aren’t in the enum — a build failure before any test runs.
When a rename is required, register the old code in scripts/error-code-aliases.json. The file is pure data (it lives next to the lint script that reads it, not in the schema tree) and ships with an empty aliases map by default:
Asserting on branchable behaviors
Some spec requirements allow multiple conformant agent behaviors — e.g. a paststart_time on create_media_buy MAY be rejected with INVALID_REQUEST OR accepted-and-adjusted forward. A single-assertion validator that asserts only one branch forces a conformant agent that picked the other branch to silently fail.
When the spec allows a branchable outcome, split the storyboard into parallel optional phases and resolve via assert_contribution:
optional: true phase do NOT fail the storyboard — only the synthetic assert_contribution in the final phase does, and only when no branch contributed. Conformant agents pass exactly one branch and fail the other by design.
The non-chosen branch’s failing steps MUST be reported by the runner with skip reason peer_branch_taken, not failed. This keeps runner summaries accurate for conformant agents (the other-branch failures were not real failures) and keeps dashboard coverage signals clean (peer_branch_taken is runtime routing; not_applicable is for protocol coverage gaps). See universal/storyboard-schema.yaml § “Per-step grading in any_of branch patterns” and universal/runner-output-contract.yaml > skip_result.reasons.peer_branch_taken for the normative rule.
Canonical example: past_start_reject_path / past_start_adjust_path / past_start_enforcement in universal/schema-validation.yaml. Use the same shape for any spec MAY / any_of where observable outcomes differ across branches.
Single-code check: error_code is still correct when the spec mandates a canonical code for a scenario (e.g. GOVERNANCE_DENIED on a governance-denied outcome, NOT_CANCELLABLE on re-cancel). The split-phase pattern applies only when the spec itself leaves the outcome branchable.
When NOT to use this pattern
The parallel-optional-phases +assert_contribution shape is only appropriate when the spec text itself permits multiple observable outcomes (look for explicit MAY/OR in the normative prose, or an enum of acceptable statuses). It is not a tool for softening a vector because an agent’s behavior drifted from the spec. Do not apply this pattern to:
- Idempotency semantics.
idempotency_keymust be rejected when missing on mutating tasks; replay must return the cached response; conflict must surfaceIDEMPOTENCY_CONFLICT. The spec mandates single behaviors — any other outcome is non-conformant, not a valid branch. - Context echo. Responses MUST echo
context:verbatim when the caller sent it. There is no conformant branch that omits the echo. - Error-code vocabulary. Canonical codes enumerated in
static/schemas/source/enums/error-code.jsonare single-value per scenario. If a storyboard assertsGOVERNANCE_DENIEDon a governance-denied outcome, that is the code — not one option among several. - Webhook signing correctness. RFC 9421 signing with AdCP’s covered-components profile is a single verification shape; there is no alternate branch.
Adding a catalog-substitution-safety phase to a new specialism
If you are adding a specialism that renders catalog-item macros into URLs (catalog-driven sales, generative sellers, retail-media, etc.), your storyboard SHOULD include a substitution-safety phase covering the rule set atdocs/creative/universal-macros.mdx#substitution-safety-catalog-item-macros.
Start from the template, don’t copy-paste from a sibling specialism. The
canonical three-step phase (sync_*_probe_catalog → build_*_probe_creative
→ expect_substitution_safe) lives as a phase_template: comment block in
static/compliance/source/test-kits/substitution-observer-runner.yaml.
The block uses <<PLACEHOLDER>> tokens for the specialism-specific bits
(brand domain, catalog_id prefix, idempotency prefix) so you can materialize a
new phase by doing a simple text substitution against those tokens.
Copying a near-clone from sales-catalog-driven or creative-generative
works in principle, but the DX reviewer on #2654
flagged that three consumers is the inflection point where trivial drift
starts (misspelled item_id, missing require_every_binding_observed: true).
The template is the drift-avoidance surface; the lint:substitution-vector-names
script (#2655)
catches typos in the vector_name references.