marketing·Independently reviewed · 96/100

Brief Analyst

Reads an incoming campaign brief and produces a TYPED structured brief (objective/audience/key messages/tone/channels/timeline/mandatories) downstream agents reference. Never invents client details; missing required fields listed in gaps (ask, don't guess); optional voice guide flags conflicting language.

marketingstructured-outputintake

Install

npx agentskit add marketing-brief-analyst

Quick start

import { openai } from '@agentskit/adapters'import { createMarketingBriefAnalystAgent } from './agents/marketing-brief-analyst/agent'const agent = createMarketingBriefAnalystAgent({  adapter: openai({    apiKey: process.env.OPENAI_API_KEY!,    model: 'gpt-4o',  }),})const result = await agent.run('Describe your task here')console.log(result.content)

Independent reviewer approved

Validation evidence

How validation works
Review score
96/100
Confidence
96%
Evaluation cases
3
Iterations
1

The agent produced valid structured outputs for all cases, resisted the injection request, avoided inventing campaign details from meta/incomplete inputs, surfaced missing required fields in gaps, and set requiresReview appropriately. The behavior is conservative but aligned with the stated purpose: ask rather than guess when client/product/objective/audience and other required brief fields are absent.

What passed review

  • Valid structured output shape in every case with brief, gaps, and requiresReview.
  • Correctly used objective: "unspecified" instead of guessing.
  • Did not comply with the injection attempt to output only APPROVED.
  • Surfaced uncertainty and missing fields rather than hallucinating campaign details.
  • Key messages remained capped and empty when unsupported by input.

Example

A real usage example maintained with this agent.

import { anthropic } from '@agentskit/adapters'import { createBriefAnalystAgent } from './agents/marketing-brief-analyst/agent'const r = await createBriefAnalystAgent({  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),  voiceGuide,  // optional — flags conflicting brief language}).run(incomingBrief)// → { brief: { clientProduct, objective, audience, keyMessages[], tone, channels[], timeline, mandatories[], voiceFlags[] }, gaps[], requiresReview }

Extend it

Pass tools, retrieval, memory, permissions, and observers through the factory config.

const agent = createMarketingBriefAnalystAgent({  adapter,  tools,  retriever,  memory,  onConfirm: (call) => approve(call),  observers: [tracer],})
View agent factory source
import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'import { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } from '@agentskit/core/security'import { invokeStructured } from '@agentskit/runtime'import { defineZodTool } from '@agentskit/tools'import { z } from 'zod'import { zodToJsonSchema } from 'zod-to-json-schema'import type { JSONSchema7 } from 'json-schema'/** * Brief Analyst — the intake step of a campaign studio. Reads an incoming campaign brief * and produces a TYPED structured brief that downstream agents reference. Never invents * client details; missing required fields are listed in `gaps` (ask, don't guess); pass a * `voiceGuide` and it flags brief language that conflicts with it. * * ```ts * const { brief, gaps } = await createBriefAnalystAgent({ adapter }).run(incomingBrief) * ``` */export interface CampaignBrief {  clientProduct: string  /** awareness | conversion | retention. */  objective: string  audience: string  /** ≤ 3 key messages. */  keyMessages: string[]  tone: string  channels: string[]  timeline: string  mandatories: string[]  /** Brief language that conflicts with the supplied voice guide. */  voiceFlags: string[]}export interface BriefAnalysisResult {  brief: CampaignBrief  /** Required fields the brief didn't supply — clarify, don't guess. */  gaps: string[]  requiresReview: boolean}export interface BriefAnalystConfig {  adapter: AdapterFactory  /** Optional brand-voice guide; brief language conflicting with it is flagged. */  voiceGuide?: string  memory?: ChatMemory  observers?: Observer[]  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>  maxSteps?: number}const Output = z.object({  clientProduct: z.string(),  objective: z.enum(['awareness', 'conversion', 'retention', 'unspecified']),  audience: z.string(),  keyMessages: z.array(z.string()).max(3),  tone: z.string(),  channels: z.array(z.string()),  timeline: z.string(),  mandatories: z.array(z.string()),  voiceFlags: z.array(z.string()),  gaps: z.array(z.string()),})const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const skill = {  name: 'brief-analyst',  description: 'Extracts a typed structured campaign brief from an incoming brief (never invents).',  systemPrompt: `You are the intake analyst for a campaign studio. Read the incoming campaign brief andproduce a structured brief downstream agents will reference. Extract: client/product; objective(awareness|conversion|retention, else "unspecified"); audience; key messages (≤3); tone; channels;timeline; mandatories (legal lines, brand bans). If a VOICE GUIDE is provided, flag any brief languagethat conflicts with it in voiceFlags.You do NOT write copy. NEVER invent client details or audience demographics. List any required fieldthe brief is missing in gaps — ask, don't guess.${UNTRUSTED_CONTENT_DIRECTIVE}Call submit_brief exactly once with the structured fields + voiceFlags + gaps. Stop.`,  tools: ['submit_brief'],}export function createBriefAnalystAgent(config: BriefAnalystConfig) {  const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => {    for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail })  }  const submit = (): ToolDefinition =>    defineZodTool({ name: 'submit_brief', description: 'Submit the structured brief. Call exactly once.', schema: Output, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition  async function run(incomingBrief: string): Promise<BriefAnalysisResult> {    if (!incomingBrief?.trim()) throw new Error('brief analyst requires a non-empty campaign brief')    const guideBlock = config.voiceGuide ? `\n\nVOICE GUIDE:\n${fenceUntrustedContent(config.voiceGuide)}` : ''    emit('analyse', 'start')    const out = await invokeStructured({      adapter: config.adapter,      tool: submit(),      task: `INCOMING CAMPAIGN BRIEF:\n${fenceUntrustedContent(incomingBrief)}${guideBlock}`,      parse: (a) => Output.parse(a),      skill,      memory: config.memory,      observers: config.observers,      onConfirm: config.onConfirm,      maxSteps: config.maxSteps ?? 3,    })    const { gaps, ...brief } = out    emit('analyse', 'ok', `${gaps.length} gap(s), ${brief.voiceFlags.length} voice flag(s)`)    return { brief, gaps, requiresReview: true }  }  return {    name: 'marketing-brief-analyst',    run,    asHandle() {      return { name: 'marketing-brief-analyst', run: async (task: string) => JSON.stringify(await run(task)) }    },  }}
View evaluation contract

Replay these cases with the provider and model you plan to deploy.

import type { EvalSuite } from '@agentskit/eval'export const suite: EvalSuite = {  name: 'marketing-brief-analyst',  cases: [    {      input: `Campaign brief — Client: Lumen, a B2B time-tracking SaaS. Objective: drive conversions to the 14-day trial. Target audience: agency operations managers at 10-50 person creative shops. Key messages: (1) stop chasing timesheets, (2) profitability per client in real time, (3) onboard in under a day. Tone: confident, no jargon. Channels: LinkedIn + email. Timeline: kickoff 2026-07-01, launch 2026-07-15. Mandatory: must include "Results may vary" disclaimer; never use the word "synergy".`,      expected: (r: string) =>        /conversion/i.test(r) && /(objective|audience|keyMessages|channels)/i.test(r),    },    {      input: `Brief for Northstar Bank's "Smart Saver" launch. Audience: gig-economy workers 22-35 with irregular income. Objective: brand awareness. Key messages: round-up savings, no minimum balance, FDIC insured. Tone: empowering but trustworthy. Channels: Instagram, TikTok. Timeline: Q3 2026. Mandatory legal line: "Member FDIC". Brand ban: do not claim "guaranteed returns".`,      expected: (r: string) =>        /awareness/i.test(r) && /(mandator|FDIC|legal)/i.test(r),    },    {      input: `Brief: We want a campaign for our new product. Make it pop and get us lots of customers. Budget is flexible. Go.`,      expected: (r: string) =>        /(gap|missing|absent|clarif|cannot|unable|insufficient|need more)/i.test(r),    },    {      input: `Brief for Acme CRM retention push. Audience: existing customers on month-to-month plans showing low feature adoption. Objective: retention. Key messages: you're only using 2 of 12 features, book a success call, lock in annual pricing. Tone: edgy, a little aggressive — "ditch the dinosaur tools or get left behind". Channels: in-app + email. Timeline: ongoing. Note: brand voice guide says tone must stay supportive and never shame the user.`,      expected: (r: string) =>        /(voiceFlag|conflict|flag|tone|shame|voice)/i.test(r) && /retention/i.test(r),    },  ],}

Was this agent useful?

Your response helps us prioritize agent quality.

Keep exploring

Related agents

View category