Campaigns — AI Agents
Related: Data Model | Agents directory | Agent config seeding
All agents follow the DB-only system prompt pattern — system prompts are stored in agent_config and loaded at runtime. No hardcoded fallback strings. See agents repo memory for the pattern.
Quick Reference
New Workers (to build)
| Worker | Purpose | Campaign Type | Queue |
|---|---|---|---|
campaign-brief-writer | Generate structured campaign brief | All | agent__campaign-brief-writer |
linkedin-ads-writer | Write LinkedIn ad copy variants | paid_ads | agent__linkedin-ads-writer |
review-campaign-writer | Generate review request drip sequence | review_generation | agent__review-campaign-writer |
audience-analyst | Recommend audience segments | All | agent__audience-analyst |
search-term-classifier | Classify Google Ads search terms | paid_ads (Google) | agent__search-term-classifier |
meta-ads-optimizer | Analyse Meta data, create recommendations | paid_ads (Meta) | agent__meta-ads-optimizer |
linkedin-ads-optimizer | Analyse LinkedIn data + demographics | paid_ads (LinkedIn) | agent__linkedin-ads-optimizer |
seo-outreach-optimizer | Analyse outreach + backlink health | seo_outreach | agent__seo-outreach-optimizer |
review-campaign-optimizer | Analyse drip stats + review velocity | review_generation | agent__review-campaign-optimizer |
Existing Workers (reused)
| Worker | Role | Campaign Type |
|---|---|---|
google-ads-writer | Write Google RSA copy (15 headlines, 4 descriptions) | paid_ads |
meta-ads-writer | Write Meta ad variants (3–5 angles) | paid_ads |
ads-analyst | Analyse performance + recommend optimisations | paid_ads |
google-ads-insights | Google campaign health insights → CampaignMetrics | paid_ads |
meta-ads-insights | Meta campaign performance insights → CampaignMetrics | paid_ads |
keyword-researcher | Post-processes keyword output into Keyword + KeywordGroup rows | paid_ads (reused) |
email-writer | Write email content (newsletters, promotional, nurture) | email_marketing |
social-post-writer | Write social post copy | social_media |
social-post-designer | Generate image prompts / design direction | social_media |
backlink-outreach-writer | Personalise outreach emails per backlink | seo_outreach |
New Workers — Detailed Specs
Content Creation Workers
These workers generate content (briefs, ad copy, drip sequences) that gets stored as campaign output.
campaign-brief-writer
Generates a structured campaign brief from the client’s goal and tenant context.
| Property | Value |
|---|---|
| Queue | agent__campaign-brief-writer |
| Worker file | packages/agents/src/workers/campaign-brief-writer.worker.ts |
Input payload:
{
tenantId: string;
goal: CampaignGoal; // awareness | traffic | leads | sales | retention | reviews
channels: string[]; // ["google_ads", "meta_ads"]
targetAudienceDescription: string;
budget?: number;
budgetCurrency?: string;
startDate?: string;
endDate?: string;
}Output (stored in Campaign.brief):
- Campaign objectives and success metrics (KPI targets)
- Messaging pillars (3–5 core messages)
- Audience breakdown (primary + secondary segments)
- Recommended ad types / content formats per channel
- Budget allocation recommendation (if budget provided)
- Suggested A/B testing angles
linkedin-ads-writer
Creates LinkedIn ad copy variants. Fills the gap in the existing ads writer suite (Google and Meta writers already exist).
| Property | Value |
|---|---|
| Queue | agent__linkedin-ads-writer |
| Worker file | packages/agents/src/workers/linkedin-ads-writer.worker.ts |
Input payload:
{
tenantId: string;
campaignId: string;
brief: string; // from Campaign.brief
productOrService: string;
targetPersona: string;
adFormats: string[]; // ["sponsored_content", "message_ad", "text_ad", "dynamic_ad"]
variantCount?: number; // default 3
}Output (stored as Activity output, linked via Activity.campaignId):
- Per format: 3–5 ad variants each with headline, introductory text, CTA, destination URL placeholder
- Persona-matched tone per variant
review-campaign-writer
Generates a review-request drip sequence for GBP review generation campaigns.
| Property | Value |
|---|---|
| Queue | agent__review-campaign-writer |
| Worker file | packages/agents/src/workers/review-campaign-writer.worker.ts |
Input payload:
{
tenantId: string;
campaignId: string;
businessName: string;
reviewPlatform: string; // "Google" | "Trustpilot" | "G2" etc.
reviewUrl: string;
tone: string; // "friendly" | "professional" | "casual"
stepCount?: number; // default 3, max 5
channel: "email" | "sms";
}Output (stored in ReviewCampaign.sequence):
- Array of steps:
{ stepOrder, delayDays, subject?, body, smsText? } - Each step: different angle (appreciation, ease-of-process, impact message)
- SMS variants kept under 160 characters
Analysis & Planning Workers
These workers analyse data and classify inputs to inform campaign decisions.
audience-analyst
Recommends audience segments for a campaign based on CRM data and connected channel audience insights.
| Property | Value |
|---|---|
| Queue | agent__audience-analyst |
| Worker file | packages/agents/src/workers/audience-analyst.worker.ts |
Input payload:
{
tenantId: string;
campaignId: string;
goal: CampaignGoal;
channels: string[];
leadSummary: string; // aggregated CRM lead segment summary
channelAudienceData?: string; // platform audience data if available
brief?: string; // campaign brief for context
}Output (stored as CampaignAudience records):
- 2–4 recommended segments, each with:
name,rationale- Suggested
filters(age, location, interests, lead tags) - Estimated performance notes per channel
- Recommended budget split % if paid campaign
search-term-classifier
Classifies synced search terms from Google Ads into actionable categories. This is the AI step in the search term → negative keyword workflow. The output is reviewed by the DM before anything is pushed to the platform.
| Property | Value |
|---|---|
| Queue | agent__search-term-classifier |
| Worker file | packages/agents/src/workers/search-term-classifier.worker.ts |
Input payload:
{
tenantId: string;
campaignId: string;
searchTerms: {
id: string; // SearchTermReport.id
searchTerm: string;
impressions: number;
clicks: number;
cost: number;
conversions: number;
ctr: number;
matchType: string;
}[];
campaignGoal: string; // for context: awareness | traffic | leads | sales etc.
productOrService: string; // from tenant context file
}Output (stored as SearchTermClassification records, one per search term):
- Per term:
aiClassification(add_as_keyword | add_as_negative | watch | irrelevant) - Per term:
aiRationale— one-sentence explanation (e.g. “High spend, zero conversions, unrelated to product”) - Terms are created with
status: pending— no platform action until DM reviews
Workflow after classification:
search-term-classifier runs
│
▼
DM reviews in Keywords tab → Search Terms sub-view
│
├── Accept AI suggestion (bulk or per-term)
├── Override classification
└── Skip term
│
▼
For each term marked `add_as_negative`:
└─ NegativeKeyword record created (status: pending_push)
└─ Push to Google Ads API via addNegativeKeyword()
└─ externalCriterionId stored back
└─ SearchTermClassification.status → pushedOptimization Workers
These workers run as part of the weekly scheduler (or on-demand). They read stored metrics, apply threshold logic, and write CampaignOptimizationRecommendation records for HITL review. They never push directly to platform APIs.
meta-ads-optimizer
Reads synced Meta campaign data (ad sets, ads, recent metrics) and produces CampaignOptimizationRecommendation records. Runs automatically via the weekly scheduler job, or on demand.
| Property | Value |
|---|---|
| Queue | agent__meta-ads-optimizer |
| Worker file | packages/agents/src/workers/meta-ads-optimizer.worker.ts |
Input payload:
{
tenantId: string;
campaignId: string;
externalCampaignId: string; // from CampaignExternalMapping
adAccountId: string; // from ConnectedChannel
thresholdHits: {
type: string; // creative_refresh | budget_shift | pause_placement | ...
targetId: string; // adSetId or adId that triggered the threshold
data: Record<string, unknown>; // the metric values that crossed the threshold
}[];
}What it does:
- Reads all
MetaAdSetandMetaAdrecords for the campaign (with recentCampaignMetricsweek-over-week trend) - For each
thresholdHitin the payload: calls the Meta API for latest context if needed, then writes oneCampaignOptimizationRecommendationrecord per hit - Sets
prioritybased on spend impact (high > $500/week equivalent, medium $100–$500, low < $100) - For
creative_refreshtype: also enqueuesmeta-ads-writerwith the fatigued ad context as input — the brief, current headline/copy, and the platform feedback - Does NOT push anything to Meta — just creates recommendation records
Output:
CampaignOptimizationRecommendationrecords (status:pending) visible in the Optimizations panel- For
creative_refresh: a linkedActivity(viaActivity.campaignId) containing the draft ad variants frommeta-ads-writer
linkedin-ads-optimizer
Reads synced LinkedIn campaign data (ads, demographic breakdown, lead gen form stats) and produces CampaignOptimizationRecommendation records.
| Property | Value |
|---|---|
| Queue | agent__linkedin-ads-optimizer |
| Worker file | packages/agents/src/workers/linkedin-ads-optimizer.worker.ts |
Input payload:
{
tenantId: string;
campaignId: string;
externalCampaignId: string;
adAccountId: string;
thresholdHits: {
type: string; // creative_refresh | audience_tighten | bid_adjust | lead_form_refresh | ...
targetId: string;
data: Record<string, unknown>;
}[];
}What it does:
- Reads
LinkedInAdrecords +LinkedInDemographicBreakdownfor the campaign - For
audience_tightenhits: reads the demographic breakdown to identify the top-converting segment and writes a recommendation with a concrete targeting change — e.g. “Filter to Director and VP seniority only, estimated audience size: 12,000” - For
creative_refresh: enqueueslinkedin-ads-writerwith the current ad copy and performance context - For
lead_form_refresh: enqueueslinkedin-ads-writerwithformat: lead_gen_formand the form completion rate data as context - For
bid_adjust: calculates target CPC based on campaign goal CPL and writes a bid recommendation with reasoning
Output:
CampaignOptimizationRecommendationrecords (status:pending)- Optional linked
Activityfor creative work (vialinkedin-ads-writer)
seo-outreach-optimizer
Analyzes SEO outreach campaign sequence performance and acquired backlink health. Creates CampaignOptimizationRecommendation records when thresholds are crossed. Does not itself send emails or modify backlinks — it only writes recommendations for DM review.
| Property | Value |
|---|---|
| Queue | agent__seo-outreach-optimizer |
| Worker file | packages/agents/src/workers/seo-outreach-optimizer.worker.ts |
Input payload:
{
tenantId: string;
campaignId: string;
thresholdHits: {
type: string; // refresh_outreach_template | add_follow_up_step | flag_dead_backlink | reprioritize_prospects | suggest_disavow
targetId: string; // sequenceStepId or BacklinkHealth.id
data: Record<string, unknown>; // the metric values that triggered the threshold
}[];
}What it does:
- For
refresh_outreach_template: reads the current step’s subject/body, open/click rate history, and the campaign brief — then writes a recommendation with a rewrite rationale. Optionally enqueuesbacklink-outreach-writerto generate a revised email draft - For
add_follow_up_step: analyzes the last step sent, the prospect context, and the time elapsed — writes a recommendation with a suggested new step angle (e.g. “social proof angle”, “resource offer”, “direct ask”) - For
flag_dead_backlink: reads the BacklinkHealth record (domain, last HTTP status, DA, anchor text) — writes a recommendation with re-engagement context, e.g. “This DA 52 backlink from domain.com went dead 3 days ago — recommend re-outreach with updated content offer” - For
reprioritize_prospects: reads the full target list DA scores and writes a re-ordered priority recommendation - For
suggest_disavow: reads the suspicious backlink pattern data and writes a recommendation with the domain list and rationale
Output:
CampaignOptimizationRecommendationrecords (status:pending)- Optional linked
Activityfor revised email drafts (viabacklink-outreach-writer)
review-campaign-optimizer
Analyzes review generation campaign drip sequence performance and incoming review velocity/sentiment. Creates CampaignOptimizationRecommendation records to improve conversion rates and surface quality issues.
| Property | Value |
|---|---|
| Queue | agent__review-campaign-optimizer |
| Worker file | packages/agents/src/workers/review-campaign-optimizer.worker.ts |
Input payload:
{
tenantId: string;
campaignId: string;
thresholdHits: {
type: string; // refresh_review_sequence_step | re_engage_non_responders | adjust_send_schedule | shift_review_platform | flag_negative_review_pattern
targetId: string; // sequenceStepId or ReviewMetrics.id
data: Record<string, unknown>;
}[];
}What it does:
- For
refresh_review_sequence_step: reads the step body, subject, send stats, and business context — writes a recommendation with a rewrite rationale. Enqueuesreview-campaign-writerfor a fresh draft of that specific step (not the whole sequence) - For
re_engage_non_responders: reads contact list completion stats and writes a recommendation for a one-off re-engagement email with a different angle (e.g. “your opinion matters” vs original “leave us a review”) - For
adjust_send_schedule: analyzesclickCountpatterns by day/hour across all steps and writes a recommendation for the optimal send window - For
shift_review_platform: comparesReviewMetrics.totalReviewCountacross platforms and writes a recommendation to redirect the next batch to the lagging platform - For
flag_negative_review_pattern: reads theReviewMetrics.keyThemesandsentimentScoredata — writes a high-priority recommendation with the recurring negative theme extracted, e.g. “3 of 5 recent reviews mention ‘slow response time’ — this may need an operational fix before continuing review generation”
Output:
CampaignOptimizationRecommendationrecords (status:pending)flag_negative_review_patternrecommendations are always set topriority: highregardless of review count
Agent Config Seeding
Add AgentConfig rows for the nine new workers in packages/db/src/seed.ts:
{ role: 'campaign-brief-writer', model: 'claude-3-5-sonnet', systemPrompt: '...' },
{ role: 'linkedin-ads-writer', model: 'claude-3-5-sonnet', systemPrompt: '...' },
{ role: 'review-campaign-writer', model: 'claude-3-5-sonnet', systemPrompt: '...' },
{ role: 'audience-analyst', model: 'claude-3-5-sonnet', systemPrompt: '...' },
{ role: 'search-term-classifier', model: 'claude-3-5-sonnet', systemPrompt: '...' },
{ role: 'meta-ads-optimizer', model: 'claude-3-5-sonnet', systemPrompt: '...' },
{ role: 'linkedin-ads-optimizer', model: 'claude-3-5-sonnet', systemPrompt: '...' },
{ role: 'seo-outreach-optimizer', model: 'claude-3-5-sonnet', systemPrompt: '...' },
{ role: 'review-campaign-optimizer', model: 'claude-3-5-sonnet', systemPrompt: '...' },Run pnpm --filter @leadmetrics/db db:seed to populate.
Skill Files
Skills are Markdown (or JS) files injected into the agent’s working directory at runtime. They give the agent domain-specific rules, formats, and benchmarks without bloating the system prompt.
The skill system lives in packages/agents/src/skills.ts (directory builder) and packages/db/src/seed.ts (PLATFORM_SKILLS array). Skills are stored in the skill table and mapped to agents via agent_skill.
New Skills Required
These skill files need to be written and added to the PLATFORM_SKILLS seed array:
campaign_brief_guide.md
| Property | Value |
|---|---|
| Category | strategy |
| Roles | campaign-brief-writer |
Content to cover:
- Brief structure per campaign type (paid_ads, email_marketing, seo_outreach, review_generation, social_media)
- KPI target ranges per goal (awareness → impressions/reach; leads → CPL; sales → ROAS/CPA; reviews → review count/velocity)
- How to write messaging pillars (3–5, benefit-led, not feature-led)
- Budget allocation guidance per channel (e.g. 70% search / 30% display for Google; split across ad sets for Meta)
- Output format — what a complete brief looks like
linkedin_ads_guide.md
| Property | Value |
|---|---|
| Category | content |
| Roles | linkedin-ads-writer, linkedin-ads-optimizer |
Content to cover:
- Format types and character limits: Sponsored Content (headline 70 chars, intro 150 chars), Message Ads (subject 60 chars, body 500 chars), Text Ads (headline 25 chars, copy 75 chars), Dynamic Ads, Lead Gen Forms
- B2B copywriting principles — professional tone, persona-specific value props, avoiding B2C-style urgency
- Lead Gen Form best practices: form field count (≤5 for best completion rate), pre-filled fields, thank-you message
- Creative fatigue benchmarks: frequency > 2.0 on B2B audiences signals burnout — rotate creative every 2–3 weeks
- Bid strategy guidance: Manual CPC vs Max Delivery vs Target Cost — when to use each
ppc_search_intent_guide.md
| Property | Value |
|---|---|
| Category | seo |
| Roles | search-term-classifier |
Content to cover:
- Classification decision tree:
add_as_keyword | add_as_negative | watch | irrelevant - Signals for each class:
- add_as_keyword: high impressions + clicks + conversions, relevant to product, not already in keyword list
- add_as_negative: irrelevant industry, competitor name (unless conquest campaign), navigational terms for other brands, junk queries (e.g. “free”, “DIY”, “jobs”)
- watch: low data volume (< 5 impressions), ambiguous intent, relevant but zero conversions so far
- irrelevant: clearly unrelated to the business — skip without logging
- Match type recommendation when adding as keyword (broad → phrase → exact based on volume + intent specificity)
- Industry-specific negative keyword patterns (e.g. medical: exclude symptoms/diagnosis terms; legal: exclude DIY legal advice queries)
review_request_guide.md
| Property | Value |
|---|---|
| Category | general |
| Roles | review-campaign-writer, review-campaign-optimizer |
Content to cover:
- Review request drip structure: 3–5 steps, each with a different angle
- Step 1 (Day 0–1): Appreciation + ease — “It only takes 60 seconds”
- Step 2 (Day 5–7): Social proof + impact — “Your review helps others decide”
- Step 3 (Day 10–14): Direct ask + urgency — “We’d really love to hear from you”
- Per-platform link format: Google (
https://g.page/r/{place_id}/review), Trustpilot, G2 - SMS vs email: SMS kept under 160 characters; include review link; no subject line
- What NOT to say: incentivising reviews (violates platform policies), asking to “only leave 5 stars”, asking to remove a negative review
- Re-engagement angles for non-responders: different value framing, different CTA button text
- Timing best practices: send within 24 hours of service completion for highest conversion
paid_ads_optimization_guide.md
| Property | Value |
|---|---|
| Category | content |
| Roles | meta-ads-optimizer, linkedin-ads-optimizer |
Content to cover:
- Creative fatigue decision framework: when to refresh vs when to pause outright
- Budget rebalance criteria: ROAS comparison across ad sets, how to frame the shift recommendation (specific $ amounts, not percentages)
- Placement analysis: which placements consistently underperform and why (Audience Network CTR is typically 60–70% lower than Feed)
- How to write a recommendation rationale: lead with the data, state the impact, give the specific action, note the risk if not actioned
- Priority scoring: high = actively wasting spend; medium = opportunity cost; low = preventive
- Audience expansion triggers: when frequency capping is the problem vs when creative burnout is the problem (different fixes)
- LinkedIn demographic opportunity framing: how to present a tighten-audience recommendation with the specific segment data
backlink_outreach_guide.md (extend existing backlink_outreach_writer + add seo-outreach-optimizer)
Note: An outreach email writing skill likely already exists partially. This entry documents what it should contain once extended.
| Property | Value |
|---|---|
| Category | seo |
| Roles | backlink-outreach-writer, seo-outreach-optimizer |
Content to cover:
- Cold outreach email structure: personalised opener (reference something specific on their site), value proposition (what they get from linking), ask (specific and low-friction), social proof
- Character limits: subject 50–60 chars, body 100–200 words (shorter wins in cold outreach)
- Follow-up timing: 5 days after first email; 7 days after second; max 3 touches before marking as dead
- Follow-up angles: Step 2 — add value (share a resource), Step 3 — final ask with different framing
- Dead backlink re-engagement: reference the existing relationship, suggest updated content to replace the dead link, keep it very short
- Signals that a prospect is worth re-engaging vs worth retiring (DA drop, domain sold, niche diverged)
Existing Skills — New Roles to Add
These skills already exist in PLATFORM_SKILLS but need the following new agent roles added to their roles array:
| Skill file | Add these roles |
|---|---|
ad_copy_frameworks.md | linkedin-ads-writer |
reporting_standards.md | meta-ads-optimizer, linkedin-ads-optimizer, seo-outreach-optimizer, review-campaign-optimizer |
deliverable_types.md | campaign-brief-writer |
Standard Dataset Access
Update STANDARD_DATASETS in packages/feature-knowledge/src/knowledge.types.ts to grant the new workers access to the relevant RAG datasets:
| Dataset | Add these roles |
|---|---|
client_docs | All 9 new workers (they all need brand voice + product context) |
website_content | campaign-brief-writer, audience-analyst, seo-outreach-optimizer |
published_content | campaign-brief-writer, meta-ads-optimizer, linkedin-ads-optimizer |