Spec Document ⚠ No changes implemented 2026-05-11

Campaign Orchestrator
Upgrade Spec

AskDavid Supervisor-Agent Improvements — V4 Multi-Agent Campaign Orchestrator

Workflow V4 Multi-Agent Campaign Orchestrator
n8n ID 0SU1PNP8iz6eJQed
Status SPEC ONLY — Confirm before building
Section 01

Current Workflow Structure

Baseline architecture before any upgrades are applied.

Campaign Trigger (Webhook POST /campaign-orchestrator)
  └─► Extract Campaign Data (Set node — normalizes fields)
        └─► Porter Strategy (Anthropic — Claude 3.5 Sonnet)
              ├─► Ogilvy Copy (Anthropic)
              ├─► Warhol Creative (Anthropic)
              ├─► Vee Social (Anthropic)
              ├─► Burnett Ads (Anthropic)
              └─► Merge All Outputs (5-input Merge/append)
                    └─► Hemingway QA (Anthropic — 6-point QA + 35-pt Conversion Score)
                          └─► Compile Final Output (Set node — assembles package)
                                └─► Save to Supabase (table: campaign_orchestrations)
                                      └─► Telegram Notification

Input Fields at Extract Campaign Data

Field
Type
Description
campaignBrief
string
Full brief text
clientName
string
Who the campaign is for
campaignType
string
e.g. "general" — free text, currently underused
budget
number
Numeric budget value
timeline
string
e.g. "30 days"
timestamp
datetime
$now

Hemingway QA Scoring System

6-Point Checklist: Brand Voice, Accuracy, Formatting, CTA Clarity, Completeness, Conversion Effectiveness

Conversion Score: 7 dimensions × 5 pts = 35 pts total

  • 30–35: Ship
  • 25–29: Minor fixes
  • 20–24: Revision required
  • <20: Rewrite / Escalate

Upgrade 01

LLM-as-Judge Retry Loop

Enforce QA verdicts structurally — not just advisorily.

Problem Being Solved

Currently, Hemingway QA runs once and its output flows directly to Compile Final Output regardless of what verdict it returns. If Hemingway scores the package at 18/35 (Rewrite) or flags REJECT on a critical lens, the system still compiles and saves.

The QA score is advisory, not structural. There is no enforcement mechanism.

Design: Where the Retry Loop Goes

The retry loop inserts two new nodes between Hemingway QA and Compile Final Output:

Hemingway QA
  └─► [NEW] QA Score Parser  (Code node)
        ├─► IF score >= 25 AND status == APPROVED → Compile Final Output  (existing path)
        └─► IF score < 25 OR status == REVISE/REJECT → [NEW] Revision Dispatcher (Code node)
              ├─► Re-runs failing agent(s) with injected QA critique
              └─► Loops back to Merge All Outputs → Hemingway QA (up to N retries)
              └─► After max retries → Compile Final Output with qa_escalated: true flag

Node 1: QA Score Parser (Code — JavaScript)

Parses Hemingway's raw text output to extract structured judgment fields. Position: canvas [1232, 200] — immediately after Hemingway QA.

const qaText = $('Hemingway QA').item.json.content;

// Extract OVERALL QUALITY SCORE (1-10)
const scoreMatch = qaText.match(/OVERALL QUALITY SCORE[:\s]+(\d+)/i);
const qualityScore = scoreMatch ? parseInt(scoreMatch[1]) : 0;

// Extract APPROVAL STATUS
const statusMatch = qaText.match(/APPROVAL STATUS[:\s]+(APPROVED|REVISE|REJECT)/i);
const approvalStatus = statusMatch ? statusMatch[1] : 'REJECT';

// Extract Conversion Score (X/35)
const conversionMatch = qaText.match(/Conversion Score[:\s]+(\d+)\/35/i);
const conversionScore = conversionMatch ? parseInt(conversionMatch[1]) : 0;

// Extract which agents were flagged for revision
const failingAgents = [];
if (qaText.includes('Porter') && qaText.toLowerCase().includes('revise')) failingAgents.push('porter');
if (qaText.includes('Ogilvy') && qaText.toLowerCase().includes('revise')) failingAgents.push('ogilvy');
if (qaText.includes('Warhol') && qaText.toLowerCase().includes('revise')) failingAgents.push('warhol');
if (qaText.includes('Vee') && qaText.toLowerCase().includes('revise')) failingAgents.push('vee');
if (qaText.includes('Burnett') && qaText.toLowerCase().includes('revise')) failingAgents.push('burnett');

// Retry counter
const retryCount = ($('QA Score Parser').item?.json?.retryCount ?? 0);

return [{ json: {
  qualityScore,
  approvalStatus,
  conversionScore,
  failingAgents,
  passes: approvalStatus === 'APPROVED' && conversionScore >= 25,
  retryCount,
  qaFeedback: qaText,
  ...($('Hemingway QA').item.json)
}}];

Output Fields

Field
Type
Purpose
qualityScore
number (1-10)
Overall package quality
approvalStatus
string
APPROVED / REVISE / REJECT
conversionScore
number (0-35)
Conversion effectiveness total
failingAgents
string[]
Which agents to re-run
passes
boolean
Gate decision — true = advance to compile
retryCount
number
Loop counter for max-retry guard
qaFeedback
string
Full critique to inject into retry prompts

Node 2: Retry Gate (IF node)

Position: [1344, 200] — immediately after QA Score Parser.

Condition Logic:

IF ($json.passes === true) OR ($json.retryCount >= 2)
  • TRUE branch → Compile Final Output (proceed normally)
  • FALSE branch → Revision Dispatcher (re-run failing agents)

Max 2 retries prevents infinite loops. After 2 cycles, compile with qa_escalated: true flag and send a distinct Telegram alert.

Node 3: Revision Dispatcher (Code — JavaScript)

On the FALSE branch of Retry Gate. Re-invokes only the specific failing agents (not all agents) with QA critique injected as context.

Revised Prompt Injection Pattern

REVISION CONTEXT — Round {{ retryCount + 1 }}:

Your previous output was reviewed by Hemingway QA and requires revision.

QA CRITIQUE:
{{ qaFeedback }}

FAILING LENS(ES): {{ failingAgents.join(', ') }}

---
ORIGINAL BRIEF:
{{ campaignBrief }}

[... rest of original prompt ...]

INSTRUCTION: Address every issue flagged by QA above. Do not repeat
previous output — produce a revised version that resolves each cited problem.

Loop-Back Path

Revision Dispatcher
  → (re-runs failing agent nodes with augmented prompts)
  → Merge All Outputs  (only new outputs replace old)
  → Hemingway QA  (re-evaluates)
  → QA Score Parser  (retryCount incremented)
  → Retry Gate  (checks passes OR retryCount >= 2)

Escalation Handling

When retryCount >= 2 and passes === false, Compile Final Output receives:

{
  "qa_escalated": true,
  "qa_escalation_reason": "Failed to reach score ≥25/35 after 2 retry cycles",
  "final_conversion_score": 18,
  "final_approval_status": "REJECT"
}

Telegram update needed: Detect qa_escalated: true and send: "⚠️ Campaign package escalated — QA score below threshold after 2 retries. Manual review required."

Canvas Positioning Summary

Node
Canvas Position
Connection
QA Score Parser
[1232, 200]
After Hemingway QA
Retry Gate
[1344, 200]
After QA Score Parser
Revision Dispatcher
[1344, 50]
FALSE branch of Retry Gate
Compile Final Output
[1344, 384]
TRUE branch of Retry Gate (existing node)

The existing Hemingway QA → Compile Final Output direct connection is severed and replaced with Hemingway QA → QA Score Parser → Retry Gate → [branch].


Upgrade 02

Intent-Based Routing

Stop running all 5 agents on every request — route by campaign intent.

Problem Being Solved

Currently, every request hits the same 5-agent parallel pipeline regardless of campaign type. This is wasteful:

  • A research run doesn't need Burnett to write ad copy
  • An email campaign doesn't need Warhol for creative direction
  • An SEO brief doesn't need Vee Social for Instagram captions

Routing by intent saves tokens, reduces latency, and improves quality by eliminating off-topic agent noise.

Design: Where the Intent Classifier Goes

Extract Campaign Data
  └─► [NEW] Intent Classifier  (Anthropic — claude-3-5-haiku, fast + cheap)
        └─► [NEW] Intent Router  (Switch node)
              ├─► social_content  → Social Content Sub-Workflow
              ├─► email_campaign  → Email Campaign Sub-Workflow
              ├─► ad_creative     → Ad Creative Sub-Workflow
              ├─► seo_brief       → SEO Brief Sub-Workflow
              ├─► research_run    → Research Run Sub-Workflow
              └─► general         → Existing Full Pipeline (fallback)

Node 1: Intent Classifier (Anthropic — Haiku)

Model: claude-3-5-haiku-20241022 — classification call, not generative. Fast and cheap (~$0.0002/call).

Canvas position: [336, 384] — between Extract Campaign Data and Porter Strategy.

System Prompt

You are a campaign intent classifier. Your only job is to read a campaign
brief and return a single classification label.

Return ONLY one of these exact strings — nothing else:
- social_content
- email_campaign
- ad_creative
- seo_brief
- research_run
- general

Classification rules:
- social_content: Social media posts, platform content, organic social strategy,
  LinkedIn/Instagram/Facebook/TikTok content creation
- email_campaign: Email newsletters, drip sequences, nurture flows,
  subject lines, email copy
- ad_creative: Paid media — Facebook Ads, Google Ads, display creative,
  ad copy, landing page copy for ad campaigns
- seo_brief: Blog posts, article writing, keyword strategy, content for search,
  organic search optimization
- research_run: Market research, competitor analysis, audience analysis,
  industry reports, discovery work (no content deliverables)
- general: Spans multiple channels, full campaign packages, or doesn't
  clearly fit one of the above

Do not explain your reasoning. Return only the label.

Alternative: Code-Node Regex Fallback (faster / cheaper)

const brief = $json.campaignBrief.toLowerCase();
if (/\b(instagram|linkedin|facebook|tiktok|social|post|reel|story)\b/.test(brief)) return 'social_content';
if (/\b(email|newsletter|drip|nurture|subject line|open rate)\b/.test(brief)) return 'email_campaign';
if (/\b(ad|ads|paid|ppc|google ads|facebook ads|meta ads|cpc|cpm|landing page)\b/.test(brief)) return 'ad_creative';
if (/\b(seo|blog|article|keyword|search|rank|organic|serp)\b/.test(brief)) return 'seo_brief';
if (/\b(research|analyze|competitor|market|discovery|report|audit)\b/.test(brief)) return 'research_run';
return 'general';

Recommendation: Use LLM classifier as primary; add Code fallback if LLM call times out.

Node 2: Intent Router (Switch node)

Canvas position: [448, 384] — Porter Strategy shifts right. Match expression: {{ $('Intent Classifier').item.json.content.trim().toLowerCase() }}

Intent-to-Agent Mapping

social_content
Social Content Run
Porter Vee Social Hemingway QA Ogilvy Warhol Burnett
email_campaign
Email Campaign
Porter Ogilvy (email-mode) Hemingway QA Warhol Vee Social Burnett
ad_creative
Ad Creative Sprint
Porter Ogilvy Warhol Burnett Hemingway QA Vee Social
seo_brief
SEO Brief
Porter Ogilvy (SEO-mode) Hemingway QA Warhol Vee Social Burnett
research_run
Research Run
Porter (research-mode) Hemingway QA Ogilvy Warhol Vee Social Burnett
general
Full Pipeline (Fallback)
Porter Ogilvy Warhol Vee Social Burnett Hemingway QA

Implementation Options

Option B
Branching Paths in Same Workflow
Single workflow to manage
Canvas becomes very wide and hard to read
Shared node names create expression ambiguity

Standardized Sub-Workflow Output Contract

Every sub-workflow must return this JSON structure so the main workflow's terminal nodes work unchanged:

{
  "campaign_id": "uuid",
  "client_name": "string",
  "original_brief": "string",
  "intent": "social_content | email_campaign | ad_creative | seo_brief | research_run | general",
  "porter_strategy": "string | null",
  "ogilvy_copy": "string | null",
  "warhol_creative": "string | null",
  "vee_social": "string | null",
  "burnett_advertising": "string | null",
  "hemingway_qa": "string",
  "qa_score": "number",
  "qa_status": "APPROVED | REVISE | REJECT",
  "created_at": "ISO8601 timestamp",
  "budget": "number"
}

Fields not produced by a given intent are set to null — Supabase handles nullable columns.


Section 04

Combined Future State

Both upgrades applied — full target architecture.

Campaign Trigger (Webhook POST)
  └─► Extract Campaign Data
        └─► [NEW] Intent Classifier (Haiku)
              └─► [NEW] Intent Router (Switch)
                    ├─► social_content  → Execute: Social Content Sub-Workflow
                    │                         (Porter → Vee → QA Score Parser → Retry Gate ↺)
                    ├─► email_campaign  → Execute: Email Campaign Sub-Workflow
                    │                         (Porter → Ogilvy[email] → QA Score Parser → Retry Gate ↺)
                    ├─► ad_creative     → Execute: Ad Creative Sub-Workflow
                    │                         (Porter → Ogilvy → Warhol → Burnett → QA Score Parser → Retry Gate ↺)
                    ├─► seo_brief       → Execute: SEO Brief Sub-Workflow
                    │                         (Porter → Ogilvy[seo] → QA Score Parser → Retry Gate ↺)
                    ├─► research_run    → Execute: Research Sub-Workflow
                    │                         (Porter[research] → QA Score Parser → Retry Gate ↺)
                    └─► general         → Existing Full Pipeline
                                              (Porter → All 4 Agents → Merge → Hemingway QA → QA Score Parser → Retry Gate ↺)
                                                                                                                        ↓
                                                                                               Compile Final Output
                                                                                                        ↓
                                                                                               Save to Supabase
                                                                                                        ↓
                                                                                               Telegram Notification

Section 05

Implementation Sequence

When Robert gives the go-ahead — build in this order.

1
Add QA Score Parser + Retry Gate to main workflow
Lowest risk change — purely additive. Doesn't affect existing paths.
2
Create Revision Dispatcher logic
Needs careful testing with loop-back wiring to avoid infinite loops.
3
Create Intent Classifier node
Can be tested in isolation without disrupting any existing routing.
4
Build Intent Router (Switch node)
Wire to Intent Classifier output. Keep existing Porter connection as fallback.
5
Build each sub-workflow
Start with social_content (most common) and general (existing pipeline extracts cleanly).
6
Wire Execute Workflow nodes
Intent Router → each sub-workflow. Standardize input/output contract.
7
Update Telegram notification template
Handle qa_escalated flag and intent field for context-aware alerts.

⛔ Do NOT implement without Robert's explicit go-ahead on each step.


Section 06

Open Questions for Robert

Decisions needed before implementation begins.

Question 01
What's the right max retry count?
The spec uses 2 as the default. More retries = higher quality floor, but also higher latency and token cost. Could make this configurable via the webhook payload (max_retries: 3).
Question 02
Classifier model: Haiku LLM or Code regex fallback first?
Use Haiku (~$0.0002/call) as primary, or route through Code node regex first and only call LLM when regex returns null? The hybrid approach saves cost at scale — regex covers ~80% of cases.
Question 03
Sub-workflow structure: standalone or Execute-only?
Should each sub-workflow have its own webhook trigger (fully standalone) or be callable only via Execute Workflow? Standalone is more flexible for direct testing. Execute-only keeps the entry point cleaner.
Question 04
Agent prompt parameterization: one Ogilvy or separate variants?
Ogilvy Copy currently has a single prompt. For email and SEO intents, it needs different output structure (subject lines vs. article structure). Should we maintain one Ogilvy node with conditional prompt logic, or separate Ogilvy variants per sub-workflow?
Question 05
QA escalation path after 2 failed retries?
When a campaign fails QA after 2 retries, should it: (a) still save to Supabase with a flag, (b) send a different Telegram alert only and NOT save, or (c) open a new Notion task for manual review?