Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/large-lemons-bet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
188 changes: 188 additions & 0 deletions docs/storyboards/audience_sync.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
id: audience_sync
version: "1.0.0"
title: "Audience sync"
category: audience_sync
summary: "Full audience lifecycle: account discovery, audience creation with hashed identifiers, and audience deletion."
track: audiences
required_tools:
- sync_audiences
platform_types:
- display_ad_server
- social_platform
- retail_media
- audio_platform
- dsp
- pmax_platform

narrative: |
You support audience syncing via the sync_audiences task. Buyers push first-party
audience segments to your platform using hashed identifiers (emails, phones). Your
platform matches those identifiers against your user graph and returns match rates.

This storyboard verifies the full audience lifecycle: discover an account to sync
against, create a test audience with hashed identifiers, and clean up by deleting it.

agent:
interaction_model: media_buy_seller
capabilities:
- sells_media
- accepts_audiences
examples:
- "Retail media networks"
- "Social platforms"
- "DSPs with audience onboarding"

caller:
role: buyer_agent
example: "Scope3 (DSP)"

prerequisites:
description: |
The caller needs an active account on the seller platform. The account_setup phase
discovers or creates the account relationship before syncing audiences.
test_kit: "test-kits/acme-outdoor.yaml"

phases:
- id: account_setup
title: "Account setup"
narrative: |
Before syncing audiences, the buyer needs an account on the seller platform.
This phase discovers an existing account via list_accounts or establishes one
via sync_accounts.

steps:
- id: discover_account
title: "Discover or create account"
narrative: |
List existing accounts to find one suitable for audience sync. The account
must already exist and be active. The account_id is captured for use in
subsequent audience operations.
task: list_accounts
schema_ref: "account/list-accounts-request.json"
response_schema_ref: "account/list-accounts-response.json"
doc_ref: "/accounts/tasks/list_accounts"
stateful: false
expected: |
Return at least one account with:
- account_id: platform-assigned identifier
- status: active (required for audience sync)

sample_request: {}

validations:
- check: response_schema
description: "Response matches list-accounts-response.json schema"
- check: field_present
path: "accounts[0].account_id"
description: "At least one account exists with an account_id"

- id: audience_sync
title: "Audience sync"
narrative: |
The buyer syncs a test audience containing hashed email and phone identifiers.
The seller platform matches those identifiers and returns per-audience results
including action (created/updated), status, and match rates.

After verifying creation, the test audience is deleted to clean up.

steps:
- id: discover_audiences
title: "Discover existing audiences"
narrative: |
Call sync_audiences with no audiences array to discover what audiences
already exist on the platform for this account. This is a read-only
discovery call.
task: sync_audiences
schema_ref: "media-buy/sync-audiences-request.json"
response_schema_ref: "media-buy/sync-audiences-response.json"
doc_ref: "/media-buy/task-reference/sync_audiences"
comply_scenario: sync_audiences
stateful: false
expected: |
Return existing audiences for the account (may be empty).
Each audience should have an audience_id and status.

sample_request:
account:
brand:
domain: "acmeoutdoor.com"
operator: "pinnacle-agency.com"

validations:
- check: response_schema
description: "Response matches sync-audiences-response.json schema"

- id: create_audience
title: "Create test audience"
narrative: |
Push a test audience with hashed email and phone identifiers. The seller
matches identifiers against their user graph and returns the audience with
action: created, match counts, and match rate.
task: sync_audiences
schema_ref: "media-buy/sync-audiences-request.json"
response_schema_ref: "media-buy/sync-audiences-response.json"
doc_ref: "/media-buy/task-reference/sync_audiences"
comply_scenario: sync_audiences
stateful: true
expected: |
Accept the audience and return:
- audience_id: matches the submitted ID
- action: created
- status: active or processing
- uploaded_count: number of identifiers received
- matched_count: number of identifiers matched
- effective_match_rate: ratio of matched to uploaded

sample_request:
account:
brand:
domain: "acmeoutdoor.com"
operator: "pinnacle-agency.com"
audiences:
- audience_id: "adcp-test-audience-001"
name: "AdCP test audience"
add:
- hashed_email: "a000000000000000000000000000000000000000000000000000000000000000"
- hashed_phone: "b000000000000000000000000000000000000000000000000000000000000000"

validations:
- check: response_schema
description: "Response matches sync-audiences-response.json schema"
- check: field_present
path: "audiences[0].audience_id"
description: "Audience has an audience_id"
- check: field_present
path: "audiences[0].action"
description: "Audience has an action (created or updated)"

- id: delete_audience
title: "Delete test audience"
narrative: |
Clean up by deleting the test audience. The seller should acknowledge the
deletion with action: deleted.
task: sync_audiences
schema_ref: "media-buy/sync-audiences-request.json"
response_schema_ref: "media-buy/sync-audiences-response.json"
doc_ref: "/media-buy/task-reference/sync_audiences"
comply_scenario: sync_audiences
stateful: true
expected: |
Delete the test audience and return:
- audience_id: matches the test audience
- action: deleted

sample_request:
account:
brand:
domain: "acmeoutdoor.com"
operator: "pinnacle-agency.com"
audiences:
- audience_id: "adcp-test-audience-001"
delete: true

validations:
- check: response_schema
description: "Response matches sync-audiences-response.json schema"
- check: field_present
path: "audiences[0].action"
description: "Audience deletion acknowledged with action field"
2 changes: 1 addition & 1 deletion docs/storyboards/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# id: string (unique identifier, e.g., "creative_template")
# version: string (semver, e.g., "1.0.0")
# title: string (human-readable title)
# category: enum (capability_discovery | schema_validation | behavioral_analysis | error_compliance | creative_template | creative_ad_server | creative_sales_agent | creative_generative | creative_lifecycle | media_buy_seller | media_buy_guaranteed_approval | media_buy_non_guaranteed | media_buy_proposal_mode | media_buy_governance_escalation | media_buy_catalog_creative | media_buy_state_machine | campaign_governance_denied | campaign_governance_conditions | campaign_governance_delivery | signal_marketplace | signal_owned | social_platform | si_session | brand_rights | property_governance | content_standards)
# category: enum (capability_discovery | schema_validation | behavioral_analysis | error_compliance | creative_template | creative_ad_server | creative_sales_agent | creative_generative | creative_lifecycle | media_buy_seller | media_buy_guaranteed_approval | media_buy_non_guaranteed | media_buy_proposal_mode | media_buy_governance_escalation | media_buy_catalog_creative | media_buy_state_machine | campaign_governance_denied | campaign_governance_conditions | campaign_governance_delivery | signal_marketplace | signal_owned | social_platform | si_session | brand_rights | property_governance | content_standards | audience_sync)
# summary: string (one-line description for listings)
# narrative: string (paragraph explaining the overall flow)
#
Expand Down
22 changes: 21 additions & 1 deletion server/tests/unit/storyboards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
describe('listStoryboards', () => {
it('returns all storyboards when no category filter', () => {
const results = listStoryboards();
expect(results.length).toBeGreaterThanOrEqual(25);
expect(results.length).toBeGreaterThanOrEqual(26);

const ids = results.map((s) => s.id);
expect(ids).toContain('capability_discovery');
Expand Down Expand Up @@ -40,6 +40,7 @@ describe('listStoryboards', () => {
expect(ids).toContain('brand_rights');
expect(ids).toContain('property_governance');
expect(ids).toContain('content_standards');
expect(ids).toContain('audience_sync');
});

it('each summary has required fields', () => {
Expand Down Expand Up @@ -719,3 +720,22 @@ describe('media_buy_state_machine storyboard', () => {
expect(kit!.id).toBe('acme_outdoor');
});
});

describe('audience_sync storyboard', () => {
it('covers account discovery, audience creation, and deletion', () => {
const sb = getStoryboard('audience_sync')!;
expect(sb).toBeDefined();
const phaseIds = sb.phases.map((p) => p.id);
expect(phaseIds).toContain('account_setup');
expect(phaseIds).toContain('audience_sync');
const tasks = sb.phases.flatMap((p) => p.steps.map((s) => s.task));
expect(tasks).toContain('list_accounts');
expect(tasks).toContain('sync_audiences');
});

it('resolves acme_outdoor test kit', () => {
const kit = getTestKitForStoryboard('audience_sync');
expect(kit).toBeDefined();
expect(kit!.id).toBe('acme_outdoor');
});
});
Loading