Campaign brief agent
This page explains how a campaign brief agent runs public market research under a silent grant, combines app-injected product materials, target audiences, and past campaigns with sourced market facts into a brief draft, and streams research progress back as typed events. After reading it, you will understand when this scenario actually needs interactive consent, why every external fact must carry a source, and why data like product materials belongs in explicit inputs rather than Memory.
Use case
Marketing teams preparing a campaign need to summarize goals, audience, messaging, competitor context, channel ideas, and risk boundaries. Internal materials are scattered across docs, CRM, and campaign workspaces, while public market information needs source-by-source verification — assembling one brief by hand means repeated searches across several systems.
Typical triggers:
- Quarterly campaign planning kicks off, and a first brief is due within a week.
- A product enters a new market or audience segment, and competitor context and channel ideas must be summarized.
- A campaign retrospective ends, and its lessons must feed into the next brief.
Engineering challenges
- Facts and assumptions share one document: a brief mixes sourced market facts, internal judgment, and unverified assumptions. A brief that does not separate the three passes guesses downstream as facts, so every item must carry a source or be marked as an assumption.
- Public information goes stale fast and contradicts itself: market sizing and competitor moves differ between sources, so every external data point needs a collection time and source URL, and conflicting sources must be presented side by side rather than silently arbitrated.
- Internal inputs and public queries must stay separated: product materials, audience profiles, and unreleased information are injected explicitly by your app; the moment the task carries them into a public search query, that is a leak.
Module composition
| Module | Role | Notes |
|---|---|---|
| GenAuth | Core | Silent delegation issues the short-lived runtime credential every product call requires; interactive consent is only needed when login state or write actions enter the picture. Credentials remain short-lived, revocable, and audited. |
| Web Agent | Core | Scans market trends and public reports with WebSearch, then researches competitor pages one by one, keeping a source URL and collection time for every external fact. |
| GUMem | Not used | Product materials, target audiences, and past campaign lessons are business data — your app reads them from docs, CRM, and campaign records and injects them into the task explicitly. They are not Memory. |
When interactive consent is needed
Every product call requires a GenAuth delegate token; public read-only scenarios are covered by silent delegation. The default path here only does public web research: your app exchanges the GenAuth user ID bound to your Qoni credentials for a runtime credential, with no user redirect. The credential is explicit, short-lived, and revocable, covering only public reads and task execution; editing internal materials, exporting customer records, and publishing externally sit outside every grant.
Upgrade to interactive consent (mode: 'interactive') when:
- The Agent should read authorized pages of an internal docs library in a controlled session, instead of receiving app-injected summaries.
- Research needs signed-in pages or paid data sources.
The upgrade works the same as in other scenarios: mode: 'interactive' plus a redirectUri, with the user confirming in Qoni Console and your server exchanging the grant via completeDelegateToken — see the Quickstart for the full flow, and the Product launch messaging agent for a complete controlled-internal-reads example.
Workflow
The user selects a product, audience, and campaign goal.
Your app obtains a runtime credential through a silent grant (public research, no user redirect).
Your app reads product materials, audience profiles, and past lessons from docs, CRM, and campaign records, injecting them into the task as explicit inputs.
Checkpoint: Internal materials are input only; unreleased product information must not appear in the query content of any subsequent public web task.
Web Agent scans market trends and public reports through WebSearch, keeping a source URL and publish date per result.
Web Agent researches competitor pages one by one and cross-checks market information; research progress streams back as events.
The Agent combines internal and external inputs into a brief draft: goals, audience, messaging, channel ideas, assumptions, and risk boundaries.
The Agent returns the brief draft, a source list, and open questions with an audit id; after app-side validation they go to the marketing lead.
Checkpoint: Every external fact in the brief should trace back to a concrete source; conclusions without supporting evidence should be marked as assumptions, not stated as facts.
Example code
The example below wires this scenario into your backend with the official Qoni SDK (@qoniai/qoni): silent delegation → a webSearch.run() market scan → one doAnything.run() that digs into competitor pages and assembles the brief, streaming research progress through run.events() → parse and validate facts versus assumptions.
import { Qoni, QoniScopes } from '@qoniai/qoni'
const qoni = new Qoni({
accessKey: process.env.QONI_ACCESS_KEY!,
secretKey: process.env.QONI_SECRET_KEY!,
})
export async function draftCampaignBrief(
userId: string,
input: { marketTopic: string; competitorPages: string[] },
) {
// 1. Silent delegation: public web research only, no site sign-in involved
// (upgrade to interactive for controlled internal-doc reads)
const { data: grant } = await qoni.delegateToken({
user: { id: userId },
agent: 'campaign-brief',
products: ['webSearch'],
scopes: [QoniScopes.DO_ANYTHING_READ, QoniScopes.DO_ANYTHING_MANAGE],
})
// 2. Explicit inputs: read internal context from docs, CRM and campaign
// records (business data, not Memory)
const internalInputs = await loadCampaignInputs(userId) // e.g. { product: ..., segments: [...], pastCampaigns: [...] }
// 3. First, a quick WebSearch scan of market trends and public reports
const scan = await qoni.webSearch.run({
token: grant.token,
prompt: `
Find recent market trends and public reports about
${input.marketTopic}. Record the source URL and publish date
for every result.
`,
maxResultsPerQuery: 8,
})
const marketScan = await scan.wait()
// 4. Then one doAnything run digs into competitor pages and assembles the brief
const run = await qoni.doAnything.run({
token: grant.token,
prompt: `
Research these competitor pages: ${input.competitorPages.join(', ')}.
Cross-check the market scan below and draft a campaign brief:
goals, audience, messaging, channel ideas, assumptions and risks.
Return brief items as a JSON array of
{ section, statement, kind: "fact" | "assumption", sourceUrl } objects —
facts must carry a source URL, unsourced items are assumptions.
Never include unreleased product details in any search query or
page visit. Do not edit or publish anything.
Internal inputs: ${JSON.stringify(internalInputs)}
Market scan: ${JSON.stringify(marketScan.output)}
`,
capture: { screenshots: true },
})
// 5. Event stream: forward research progress, screenshots, and
// human-in-the-loop steps to your frontend in real time
let output: unknown
for await (const event of run.events()) {
if (event.type === 'progress') appendTrace(event.data) // which competitor page is under research
if (event.type === 'screenshot') renderScreenshot(event.image) // evidence per page
if (event.type === 'interaction') handleInteraction(event.data) // risk pages / confirmations → human
if (event.type === 'done') output = event.data.output
}
// 6. Parse and validate the output contract on the app side:
// items labelled as facts without a source are demoted or dropped
const briefItems = parseBriefItems(output).filter(
(item) => item.kind === 'assumption' || item.sourceUrl,
)
return {
briefItems,
audit: { auditId: grant.auditId, permissionBoundary: grant.grantedScopes },
}
}The output structure is a contract set by the task prompt: here it is an array of { section, statement, kind, sourceUrl }, parsed and validated by parseBriefItems on the app side, and any item labelled kind: "fact" without a sourceUrl is dropped or demoted to an assumption. When the event stream disconnects, the SDK reconnects and resumes with Last-Event-ID; event type constants live in QoniEventTypes.
Data and memory boundaries
This scenario touches four kinds of data; GUMem is not used here:
- Versioned rules: if brand voice and forbidden language need to constrain the brief's wording, keep them in your policy store and inject them per version.
- Business state: product materials, audience profiles, past campaign lessons, and this brief's conclusions — read from docs, CRM, and campaign records by your app, injected explicitly, with conclusions written back to the same business stores.
- Audit records: the delegation and behavior chain formed by
grantIdandauditId— maintained by GenAuth. - User Memory (optional): only long-term personal preferences a user has explicitly confirmed belong in GUMem; audience profiles and campaign lessons are team-level business data, not personal memory, so this scenario neither recalls nor writes back by default.
Failure handling
| Situation | Recommended handling |
|---|---|
| Internal inputs fail to load | Treat it as missing input and prompt the user to supply it; never fall back to guessing. |
| Public sources contradict each other | List the conflicting sources and timestamps side by side and mark them as open questions instead of arbitrating silently. |
| The event stream disconnects | The SDK reconnects and resumes per sseMaxRetries; beyond the limit, treat it as a failure and replay the events received so far. |
| An item is labelled a fact but lacks a source | App-side validation demotes it to an assumption or drops it, and the brief notes how many were handled. |
Production notes
Do not send unreleased product information into public web tasks. External claims should be marked for human confirmation. External data in the brief — market sizing, competitor moves — should retain collection timestamps so stale information is never cited as current. Limit internal-input reads to what this campaign needs; never inject a full CRM export into the task.
Next steps
- Read the Quickstart to run the shortest path for Agent identity and delegation.
- Read Authorization and browser sandbox for the security boundaries of controlled sessions.
- Continue with the Product launch messaging agent for an adjacent scenario.