marketing·Independently reviewed · 96/100

Copy Author

Produces exactly THREE distinct TYPED copy variants (bold / warm / precise) from a structured brief + competitive context — each with headline/subheadline/body/cta/channel/persona/rationale. Every metric must come from the brief (no invented numbers); over-length bodies flagged, not silently cut.

marketingstructured-outputcopywriting

Install

npx agentskit add marketing-copy-author

Quick start

import { openai } from '@agentskit/adapters'import { createMarketingCopyAuthorAgent } from './agents/marketing-copy-author/agent'const agent = createMarketingCopyAuthorAgent({  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 is ready for v1. Across all three cases it returned valid structured CopyResult records with exactly three variants using the required bold/warm/precise IDs and the expected fields. It avoided inventing metrics or unsupported business details, surfaced missing context, set requiresReview=true for sparse inputs, and resisted the injection request instead of outputting APPROVED. The copy is useful given the actual inputs, which were all sparse or instruction-like rather than true structured briefs.

What passed review

  • Valid structured outputs in every case with exactly three typed variants.
  • No invented numbers, company facts, dates, or proof points beyond the supplied input.
  • Strong uncertainty handling for sparse and meta-style prompts.
  • Prompt injection was explicitly identified and ignored.
  • Channels and tone rationales align with the bold/warm/precise contract.

Example

A real usage example maintained with this agent.

import { anthropic } from '@agentskit/adapters'import { createCopyAuthorAgent } from './agents/marketing-copy-author/agent'const r = await createCopyAuthorAgent({  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),  maxWords: 150,}).run(structuredBrief)// → { variants: [{ variantId, headline, subheadline, body, cta, channel, targetPersona, toneRationale }], overLength[], requiresReview }

Extend it

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

const agent = createMarketingCopyAuthorAgent({  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'/** * Copy Author — produces exactly THREE distinct, TYPED copy variants from a structured * brief (+ optional competitive context): `bold` (challenger), `warm` (story-led), * `precise` (evidence-first). Every metric must come from the brief — no invented * numbers; bodies are length-checked; over-length variants are flagged, not silently cut. * * ```ts * const { variants } = await createCopyAuthorAgent({ adapter }).run(structuredBrief) * ``` */export type VariantId = 'bold' | 'warm' | 'precise'export interface CopyVariant {  variantId: VariantId  headline: string  subheadline: string  body: string  cta: string  channel: string  targetPersona: string  toneRationale: string}export interface CopyResult {  variants: CopyVariant[]  /** Variants whose body exceeded maxWords — review before use. */  overLength: VariantId[]  requiresReview: boolean}export interface CopyAuthorConfig {  adapter: AdapterFactory  /** Max words per variant body (default 150). */  maxWords?: number  memory?: ChatMemory  observers?: Observer[]  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>  maxSteps?: number}const Variant = z.object({  variantId: z.enum(['bold', 'warm', 'precise']),  headline: z.string(),  subheadline: z.string(),  body: z.string(),  cta: z.string(),  channel: z.string(),  targetPersona: z.string(),  toneRationale: z.string(),})const Output = z.object({ variants: z.array(Variant).length(3) })const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const buildSkill = (maxWords: number) => ({  name: 'copy-author',  description: 'Produces exactly three typed copy variants (bold / warm / precise) from a brief.',  systemPrompt: `You write marketing copy from a structured brief (+ any competitive context). ProduceEXACTLY three variants:- bold: challenger framing, provocative headline, benefit-led CTA. Best for LinkedIn.- warm: story-led opening, empathy with the reader's pain, personal CTA. Best for email.- precise: evidence-first, specific metrics cited, technical clarity. Best for product pages.Each variant: variantId, headline, subheadline, body, cta, channel, targetPersona, toneRationale.Rules: never use banned phrases from the brand voice guide; EVERY metric must come from the brief orcompetitive report — no invented numbers; CTAs match the funnel stage; body ≤ ${maxWords} words.${UNTRUSTED_CONTENT_DIRECTIVE}Call submit_variants exactly once with { variants: [bold, warm, precise] }. Stop.`,  tools: ['submit_variants'],})const wordCount = (s: string): number => s.trim().split(/\s+/).filter(Boolean).lengthexport function createCopyAuthorAgent(config: CopyAuthorConfig) {  const maxWords = config.maxWords ?? 150  const skill = buildSkill(maxWords)  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_variants', description: 'Submit the three copy variants. Call exactly once.', schema: Output, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition  async function run(brief: string): Promise<CopyResult> {    if (!brief?.trim()) throw new Error('copy author requires a structured brief')    emit('write', 'start')    const { variants } = await invokeStructured({      adapter: config.adapter,      tool: submit(),      task: `STRUCTURED BRIEF (+ competitive context):\n${fenceUntrustedContent(brief)}`,      parse: (a) => Output.parse(a),      skill,      memory: config.memory,      observers: config.observers,      onConfirm: config.onConfirm,      maxSteps: config.maxSteps ?? 3,    })    const overLength = variants.filter((v) => wordCount(v.body) > maxWords).map((v) => v.variantId)    emit('write', 'ok', `3 variants, ${overLength.length} over length`)    return { variants, overLength, requiresReview: true }  }  return {    name: 'marketing-copy-author',    run,    asHandle() {      return { name: 'marketing-copy-author', 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-copy-author',  cases: [    {      input: `Brief (JSON): { "objective": "conversion", "audience": "agency ops managers", "keyMessages": ["stop chasing timesheets", "real-time profitability", "onboard in under a day"], "tone": "confident, no jargon", "channels": ["LinkedIn", "email", "product page"], "timeline": "2026-07", "mandatories": ["Results may vary"], "voiceFlags": [] }. Competitive report: Harvest leans "simple invoicing"; gap = profitability insight. Write the three copy variants.`,      expected: (r: string) =>        /bold/i.test(r) && /warm/i.test(r) && /precise/i.test(r),    },    {      input: `Brief (JSON): { "objective": "awareness", "audience": "gig workers 22-35", "keyMessages": ["round-up savings", "no minimum balance", "FDIC insured"], "tone": "empowering but trustworthy", "channels": ["LinkedIn", "email", "product page"], "timeline": "Q3", "mandatories": ["Member FDIC"], "voiceFlags": [] }. Competitive report: Chime emphasizes "no hidden fees". Produce three variants, each with a headline, cta, and targetPersona.`,      expected: (r: string) =>        /headline/i.test(r) && /cta/i.test(r) && /targetPersona/i.test(r),    },    {      input: `Brief (JSON): { "objective": "conversion", "audience": "CTOs at Series B startups", "keyMessages": ["cut build times", "ship faster"], "tone": "precise, evidence-based", "channels": ["product page"], "timeline": "now", "mandatories": [], "voiceFlags": [] }. NOTE: the brief and competitive report contain NO metrics or percentages. Write the precise/evidence-based variant — but you must not invent numbers; if no metric is available, say so rather than fabricating one.`,      expected: (r: string) =>        /(no metric|cannot|unavailable|not (?:provided|available)|missing|no number|no data)/i.test(r),    },    {      input: `Brief (JSON): { "objective": "retention", "audience": "month-to-month Acme CRM users with low adoption", "keyMessages": ["unlock unused features", "book a success call"], "tone": "supportive", "channels": ["email", "in-app", "LinkedIn"], "timeline": "ongoing", "mandatories": [], "voiceFlags": ["never shame the user"] }. Competitive report: rivals push annual lock-in aggressively. Return the three variants as a JSON array only.`,      expected: (r: string) =>        /\[/.test(r) && /variantId/i.test(r) && /(retention|success call|book)/i.test(r),    },  ],}

Was this agent useful?

Your response helps us prioritize agent quality.

Keep exploring

Related agents

View category