Skip to main content
Content creation combines creative decisions with repeatable processes. On one hand, good content requires originality, voice, and insight that resist mechanical production. On the other hand, content operations—ideation, research, drafting, editing, publishing—follow predictable workflows that benefit from systematic execution. A content pipeline adds structure around creative work, automating repeatable steps and reserving human judgment for quality decisions. This chapter builds an autonomous content pipeline that generates ideas, researches topics, produces drafts, refines through editing, and publishes when quality standards are met. Human review gates ensure nothing goes live without approval, while performance feedback shapes what content gets created next.

15.1 The Content Challenge

Content production has characteristics that make it both amenable to and resistant to automation. Creative judgment. What makes content good? Accuracy is necessary but not sufficient. Good content has voice, angle, insight—qualities that emerge from human sensibility. Systems can generate grammatically correct text that lacks useful insight or specificity. The challenge is producing content worth reading. Quality variation. Unlike code that either works or doesn’t, content quality exists on a spectrum. A draft might be “pretty good” or “needs work” or “almost there.” Evaluation is subjective and context-dependent. What works for one audience fails for another. Process is predictable. While creative quality is hard to systematize, the content workflow is not. Ideation produces topics. Research gathers information. Drafting produces initial versions. Editing refines. Publishing distributes. These phases are the same across content types and can be orchestrated systematically. Performance is measurable. After publication, content generates signals—views, engagement, conversions. This feedback, while delayed, provides objective data about what works. Systems can learn which topics, angles, and styles perform well. In practice, you can automate the workflow steps, keep human review at explicit quality gates, and use performance feedback to prioritize future topics.

15.2 System Architecture

The content pipeline moves work items through defined stages and adds human checkpoints before publication.
┌─────────────────────────────────────────────────────────────────────────┐
│                         Content Pipeline                                 │
│                                                                         │
│   ┌────────────────────────────────────────────────────────────────┐   │
│   │                     Performance Data                            │   │
│   │               (views, engagement, conversions)                  │   │
│   └───────────────────────────┬────────────────────────────────────┘   │
│                               │                                         │
│                               ▼                                         │
│   ┌─────────┐    ┌──────────┐    ┌─────────┐    ┌─────────┐           │
│   │ Ideator │───▶│Researcher│───▶│ Drafter │───▶│ Editor  │           │
│   └─────────┘    └──────────┘    └─────────┘    └─────────┘           │
│        │                                              │                 │
│        │         Content Queue                        │                 │
│        │    ┌─────────────────────────┐              │                 │
│        └───▶│ idea → research → draft │◀─────────────┘                 │
│             │   → edit → review       │                                 │
│             └───────────┬─────────────┘                                 │
│                         │                                               │
│                         ▼                                               │
│              ┌────────────────────┐                                    │
│              │   Human Review     │                                    │
│              │      Gate          │                                    │
│              └─────────┬──────────┘                                    │
│                        │                                               │
│           ┌────────────┴────────────┐                                  │
│           │ Approved    │ Rejected  │                                  │
│           ▼             ▼           │                                  │
│    ┌────────────┐  ┌──────────┐    │                                  │
│    │  Publish   │  │  Revise  │────┘                                  │
│    └────────────┘  └──────────┘                                        │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
The Ideator generates content ideas based on performance data, trends, and content strategy. It maintains a backlog of potential topics, prioritized by expected value. The Researcher gathers information for approved topics—facts, quotes, examples, data. Research depth varies by content type. The Drafter produces initial content from research. It follows brand voice guidelines and content templates. The Editor refines drafts—improving clarity, checking facts, ensuring consistency. Multiple editing passes may occur. The Review Gate presents polished content for human approval. Humans can approve, reject with feedback, or request specific changes. Performance tracking stores post-publication metrics and exposes them to the ideation component, so future topics can be ranked using historical results.

15.3 The Content Artifact

Content pieces evolve through well-defined states, each with its own fields and operations.
interface ContentPiece {
  id: string;
  type: 'blog' | 'social' | 'email' | 'documentation';
  status: ContentStatus;

  // Ideation
  topic: string;
  angle: string;
  targetAudience: string;
  expectedValue: number;

  // Research
  research: ResearchMaterial | null;

  // Draft
  draft: ContentDraft | null;

  // Editing
  edits: Edit[];
  currentVersion: number;

  // Review
  reviews: Review[];

  // Publication
  publishedAt: number | null;
  publishedUrl: string | null;

  // Performance
  metrics: ContentMetrics | null;

  // Lifecycle
  createdAt: number;
  updatedAt: number;
}

type ContentStatus =
  | 'idea'
  | 'researching'
  | 'drafting'
  | 'editing'
  | 'review'
  | 'approved'
  | 'published'
  | 'rejected';

interface ContentDraft {
  title: string;
  body: string;
  summary: string;
  metadata: {
    [key: string]: string;
    summary?: string;
    metaDescription?: string;
    keywords?: string;
    accuracyConcerns?: string;
  };
  wordCount: number;
  version: number;
}

interface Edit {
  timestamp: number;
  type: 'clarity' | 'accuracy' | 'style' | 'structure';
  description: string;
  before: string;
  after: string;
}

interface Review {
  timestamp: number;
  reviewer: string;
  decision: 'approve' | 'reject' | 'revise';
  feedback: string;
}

interface ContentMetrics {
  views: number;
  uniqueVisitors: number;
  avgTimeOnPage: number;
  bounceRate: number;
  shares: number;
  conversions: number;
  collectedAt: number;
}
The content artifact tracks the full lifecycle with an audit trail. Status changes move through a simple state machine: idea → researching → drafting → editing → review → approved → published or rejected. Transitions are driven by specific components—ideation promotes ideas into researching, the researcher advances to drafting, the drafter to editing, the editor to review, and the review gate sets approved, rejected, or routes back to editing for revision. Each transition updates updatedAt, and edits and reviews append records to their respective arrays, so you can reconstruct who changed what and when for any piece.

15.4 Ideation with Performance Feedback

The ideator doesn’t generate ideas randomly—it uses performance data to identify what topics and angles resonate with the audience.
export default class ContentIdeator extends AgenticSystem {
  @field ideas: ContentIdea[] = [];
  @field performanceHistory: PerformanceRecord[] = [];
  @field contentCalendar: CalendarEntry[] = [];

  @schedule('every day at 6am')
  async generateIdeas() {
    // Analyze recent performance
    const insights = await this.analyzePerformance();

    // Check content gaps
    const gaps = await this.identifyGaps();

    // Generate ideas based on insights
    const newIdeas = await this.brainstormIdeas(insights, gaps);

    // Score and prioritize
    const scored = await this.scoreIdeas(newIdeas);

    // Add to backlog
    this.ideas.push(...scored);

    // Prune low-value ideas
    this.pruneBacklog();
  }

  private async analyzePerformance(): Promise<PerformanceInsights> {
    const recentContent = this.performanceHistory
      .filter(p => p.publishedAt > Date.now() - 30 * 24 * 60 * 60 * 1000) // Last 30 days
      .sort((a, b) => b.metrics.views - a.metrics.views);

    const topPerformers = recentContent.slice(0, 5);
    const underperformers = recentContent.slice(-5);

    const { text } = await llm.complete([
      { role: 'system', content: `Analyze content performance patterns.
        Identify what makes content successful or unsuccessful.
        Look for patterns in topics, angles, formats, and timing.

        Return a JSON object with keys:
        - "successPatterns": string[]
        - "failurePatterns": string[]
        - "recommendations": string[]` },
      { role: 'user', content: `Top performing content:
        ${topPerformers.map(p => `- "${p.title}" (${p.metrics.views} views, ${p.metrics.avgTimeOnPage}s avg time)`).join('\n')}

        Underperforming content:
        ${underperformers.map(p => `- "${p.title}" (${p.metrics.views} views, ${p.metrics.avgTimeOnPage}s avg time)`).join('\n')}

        Analyze and respond with strict JSON only.` }
    ]);

    let parsed: {
      successPatterns: string[];
      failurePatterns: string[];
      recommendations: string[];
    };

    try {
      parsed = JSON.parse(text);
    } catch {
      // In production, you would log this and optionally reprompt with stricter instructions.
      parsed = {
        successPatterns: [],
        failurePatterns: [],
        recommendations: []
      };
    }

    return {
      successPatterns: parsed.successPatterns || [],
      failurePatterns: parsed.failurePatterns || [],
      recommendations: parsed.recommendations || []
    };
  }

  private async identifyGaps(): Promise<ContentGap[]> {
    // Compare published content against content strategy
    const published = this.performanceHistory.map(p => p.topic);
    const strategy = this.contentStrategy; // Defined topics and themes

    const gaps: ContentGap[] = [];

    for (const theme of strategy.themes) {
      const coverage = published.filter(p =>
        p.toLowerCase().includes(theme.toLowerCase())
      ).length;

      if (coverage < strategy.minCoveragePerTheme) {
        gaps.push({
          theme,
          currentCoverage: coverage,
          targetCoverage: strategy.minCoveragePerTheme,
          priority: strategy.themePriorities[theme] || 'medium'
        });
      }
    }

    return gaps;
  }

  private async brainstormIdeas(
    insights: PerformanceInsights,
    gaps: ContentGap[]
  ): Promise<ContentIdea[]> {
    const { text } = await llm.complete([
      { role: 'system', content: `Generate content ideas that align with performance insights and fill content gaps.
        Each idea should have:
        - A specific topic
        - A unique angle
        - Target audience
        - Why it should perform well

        Return a JSON array of objects with keys:
        - "topic"
        - "angle"
        - "targetAudience"
        - "rationale"` },
      { role: 'user', content: `Performance insights:
        Success patterns: ${insights.successPatterns.join(', ')}
        Recommendations: ${insights.recommendations.join(', ')}

        Content gaps to fill:
        ${gaps.map(g => `- ${g.theme}: needs ${g.targetCoverage - g.currentCoverage} more pieces`).join('\n')}

        Generate 5 content ideas and respond with strict JSON only.` }
    ]);

    try {
      return JSON.parse(text) as ContentIdea[];
    } catch {
      // In a real system, handle invalid JSON (log, reprompt, or discard).
      return [];
    }
  }

  private async scoreIdeas(ideas: ContentIdea[]): Promise<ContentIdea[]> {
    return Promise.all(ideas.map(async idea => {
      // Score based on multiple factors
      const relevanceScore = await this.scoreRelevance(idea);
      const gapScore = this.scoreGapFill(idea);
      const effortScore = this.estimateEffort(idea);
      const timelinessScore = await this.scoreTimeliness(idea);

      const expectedValue =
        (relevanceScore * 0.4) +
        (gapScore * 0.2) +
        ((1 - effortScore) * 0.2) + // Lower effort = higher score
        (timelinessScore * 0.2);

      return {
        ...idea,
        expectedValue,
        scores: { relevanceScore, gapScore, effortScore, timelinessScore }
      };
    }));
  }

  private pruneBacklog() {
    // Remove old, low-value ideas
    const cutoff = Date.now() - 14 * 24 * 60 * 60 * 1000; // 14 days

    this.ideas = this.ideas.filter(idea =>
      idea.createdAt > cutoff || idea.expectedValue > 0.7
    );
  }
}

interface ContentIdea {
  topic: string;
  angle: string;
  targetAudience: string;
  rationale: string;
  expectedValue: number;
  scores?: {
    relevanceScore: number;
    gapScore: number;
    effortScore: number;
    timelinessScore: number;
  };
  createdAt: number;
}

interface PerformanceInsights {
  successPatterns: string[];
  failurePatterns: string[];
  recommendations: string[];
}

interface ContentGap {
  theme: string;
  currentCoverage: number;
  targetCoverage: number;
  priority: 'low' | 'medium' | 'high';
}

interface PerformanceRecord {
  id: string;
  title: string;
  topic: string;
  publishedAt: number;
  metrics: ContentMetrics;
}
The ideator demonstrates learning from performance data. Success patterns inform future ideation. Gap analysis ensures content strategy coverage. Scoring prioritizes limited production capacity. Because the ideator depends on JSON-formatted model output, the implementation must validate and parse responses, handle parsing errors, and either fall back or reprompt when the format is invalid.

15.5 Drafting with Voice

The drafter produces content following brand voice guidelines and structural templates. Voice consistency matters because readers use style and tone to recognize your brand; abrupt changes between pieces can make automated content stand out in a distracting way.
export default class ContentDrafter extends AgenticSystem {
  @field voiceGuidelines: VoiceGuidelines;
  @field templates: Map<string, ContentTemplate> = new Map();
  @field draft: stream<string>('');

  async createDraft(piece: ContentPiece, research: ResearchMaterial): Promise<ContentDraft> {
    const template = this.templates.get(piece.type);

    // Build context from research
    const researchContext = this.buildResearchContext(research);

    // Generate outline
    const outline = await this.createOutline(piece, research, template);

    // Draft each section
    let fullDraft = '';
    for (const section of outline.sections) {
      const sectionDraft = await this.draftSection(section, piece, researchContext);
      fullDraft += sectionDraft + '\n\n';
      this.draft.set(fullDraft);
    }

    // Generate metadata
    const metadata = await this.generateMetadata(piece, fullDraft);

    return {
      title: outline.title,
      body: fullDraft.trim(),
      summary: metadata.summary ?? '',
      metadata,
      wordCount: this.countWords(fullDraft),
      version: 1
    };
  }

  private async createOutline(
    piece: ContentPiece,
    research: ResearchMaterial,
    template?: ContentTemplate
  ): Promise<ContentOutline> {
    const templateGuidance = template
      ? `Follow this structure:\n${template.sections.map(s => `- ${s.name}: ${s.description}`).join('\n')}`
      : 'Create an appropriate structure for this content.';

    const { text } = await llm.complete([
      { role: 'system', content: `Create a content outline.
        ${templateGuidance}

        Voice guidelines:
        - Tone: ${this.voiceGuidelines.tone}
        - Style: ${this.voiceGuidelines.style}
        - Avoid: ${this.voiceGuidelines.avoid.join(', ')}

        Return a JSON object with:
        - "title": string
        - "sections": Array<{ "title": string, "purpose": string, "targetLength": number, "keyPoints": string[] }>` },
      { role: 'user', content: `Topic: ${piece.topic}
        Angle: ${piece.angle}
        Target audience: ${piece.targetAudience}

        Research summary:
        ${research.summary}

        Create an outline and respond with strict JSON only.` }
    ]);

    let parsed: ContentOutline;
    try {
      parsed = this.parseOutline(JSON.parse(text));
    } catch {
      // Fall back to a minimal outline if parsing fails.
      parsed = {
        title: piece.topic,
        sections: [
          {
            title: piece.topic,
            purpose: 'Fallback outline',
            targetLength: 800,
            keyPoints: []
          }
        ]
      };
    }

    return parsed;
  }

  private async draftSection(
    section: OutlineSection,
    piece: ContentPiece,
    researchContext: string
  ): Promise<string> {
    const { text } = await llm.complete([
      { role: 'system', content: `Write content following these voice guidelines:
        - Tone: ${this.voiceGuidelines.tone}
        - Style: ${this.voiceGuidelines.style}
        - Target reading level: ${this.voiceGuidelines.readingLevel}

        Things to avoid:
        ${this.voiceGuidelines.avoid.map(a => `- ${a}`).join('\n')}

        Things to embrace:
        ${this.voiceGuidelines.embrace.map(e => `- ${e}`).join('\n')}` },
      { role: 'user', content: `Section: ${section.title}
        Purpose: ${section.purpose}
        Target length: ${section.targetLength} words

        Context:
        ${researchContext}

        Write this section as continuous prose.` }
    ]);

    return text;
  }

  private async generateMetadata(
    piece: ContentPiece,
    body: string
  ): Promise<ContentDraft['metadata']> {
    const { text } = await llm.complete([
      { role: 'system', content: `Generate SEO-optimized metadata for this content.
        Return a JSON object with:
        - "summary": string          // ~150 characters
        - "metaDescription": string  // ~160 characters
        - "keywords": string         // comma-separated keywords` },
      { role: 'user', content: `Content:\n${body.slice(0, 2000)}...\n\nGenerate the metadata and respond with strict JSON only.` }
    ]);

    let parsed: any;
    try {
      parsed = JSON.parse(text);
    } catch {
      parsed = {};
    }

    const metadata: ContentDraft['metadata'] = {
      summary: parsed.summary ?? '',
      metaDescription: parsed.metaDescription ?? '',
      keywords: parsed.keywords ?? ''
    };

    return metadata;
  }
}

interface VoiceGuidelines {
  tone: string;           // e.g., "professional but approachable"
  style: string;          // e.g., "clear, direct, uses examples"
  readingLevel: string;   // e.g., "8th grade"
  avoid: string[];        // e.g., ["jargon", "passive voice", "clichés"]
  embrace: string[];      // e.g., ["concrete examples", "action verbs"]
}

interface ContentTemplate {
  type: string;
  sections: {
    name: string;
    description: string;
    targetLength: number;
    required: boolean;
  }[];
}

interface ContentOutline {
  title: string;
  sections: OutlineSection[];
}

interface OutlineSection {
  title: string;
  purpose: string;
  targetLength: number;
  keyPoints: string[];
}

interface ResearchMaterial {
  summary: string;
  facts: { content: string; source: string }[];
  quotes: { text: string; attribution: string }[];
  data: { description: string; value: string }[];
}

15.6 Editorial Refinement

Editing improves drafts through multiple passes, each focused on different quality dimensions.
export default class ContentEditor extends AgenticSystem {
  @field editHistory: Edit[] = [];

  async edit(draft: ContentDraft): Promise<ContentDraft> {
    let currentDraft = draft;

    // Pass 1: Structural editing
    currentDraft = await this.structuralEdit(currentDraft);

    // Pass 2: Clarity editing
    currentDraft = await this.clarityEdit(currentDraft);

    // Pass 3: Accuracy check
    currentDraft = await this.accuracyCheck(currentDraft);

    // Pass 4: Style polish
    currentDraft = await this.stylePolish(currentDraft);

    return {
      ...currentDraft,
      version: draft.version + 1
    };
  }

  private async structuralEdit(draft: ContentDraft): Promise<ContentDraft> {
    const { text: analysis } = await llm.complete([
      { role: 'system', content: `Analyze the structure of this content.
        Check for:
        - Logical flow between sections
        - Appropriate paragraph length
        - Clear transitions
        - Strong opening and closing

        Suggest structural improvements in plain text.` },
      { role: 'user', content: draft.body }
    ]);

    if (this.needsRevision(analysis)) {
      const { text: revised } = await llm.complete([
        { role: 'system', content: 'Revise this content to improve structure. Keep the same voice and information.' },
        { role: 'user', content: `Original:\n${draft.body}\n\nFeedback:\n${analysis}\n\nRevise for better structure.` }
      ]);

      this.recordEdit('structure', 'Improved content flow and structure', draft.body, revised);
      return { ...draft, body: revised };
    }

    return draft;
  }

  private async clarityEdit(draft: ContentDraft): Promise<ContentDraft> {
    const { text: issues } = await llm.complete([
      { role: 'system', content: `Identify clarity issues:
        - Ambiguous sentences
        - Overly complex constructions
        - Missing context
        - Unclear referents

        List specific issues with line references as plain text.` },
      { role: 'user', content: draft.body }
    ]);

    if (this.hasIssues(issues)) {
      const { text: revised } = await llm.complete([
        { role: 'system', content: 'Revise for clarity. Make every sentence clear and direct.' },
        { role: 'user', content: `Original:\n${draft.body}\n\nClarity issues:\n${issues}\n\nRevise.` }
      ]);

      this.recordEdit('clarity', 'Improved sentence clarity', draft.body, revised);
      return { ...draft, body: revised };
    }

    return draft;
  }

  private async accuracyCheck(draft: ContentDraft): Promise<ContentDraft> {
    const { text: concerns } = await llm.complete([
      { role: 'system', content: `Flag potential accuracy concerns:
        - Unverified claims
        - Statistics without sources
        - Absolute statements that might be wrong
        - Outdated information

        Be conservative—flag anything that should be verified.
        Respond in plain text listing issues, or say "no issues" if none are found.` },
      { role: 'user', content: draft.body }
    ]);

    // Accuracy issues don't auto-fix—they flag for human review
    if (this.hasIssues(concerns)) {
      return {
        ...draft,
        metadata: {
          ...draft.metadata,
          accuracyConcerns: concerns
        }
      };
    }

    return draft;
  }

  private async stylePolish(draft: ContentDraft): Promise<ContentDraft> {
    const { text: polished } = await llm.complete([
      { role: 'system', content: `Polish the writing style:
        - Vary sentence length for rhythm
        - Replace weak verbs with strong ones
        - Eliminate redundancy
        - Enhance word choices

        Improve readability and concision without changing the factual content.` },
      { role: 'user', content: draft.body }
    ]);

    this.recordEdit('style', 'Polished writing style', draft.body, polished);
    return { ...draft, body: polished };
  }

  private needsRevision(analysis: string): boolean {
    const revisionIndicators = ['should', 'could improve', 'needs', 'consider', 'suggest'];
    return revisionIndicators.some(indicator => analysis.toLowerCase().includes(indicator));
  }

  private hasIssues(analysis: string): boolean {
    return !analysis.toLowerCase().includes('no issues') &&
           !analysis.toLowerCase().includes('looks good');
  }

  private recordEdit(type: Edit['type'], description: string, before: string, after: string) {
    this.editHistory.push({
      timestamp: Date.now(),
      type,
      description,
      before: before.slice(0, 500),
      after: after.slice(0, 500)
    });
  }
}
The accuracy check relies on the model to flag potential problems; it does not validate facts on its own. In a production system you would combine these flags with external sources or human review to verify claims before publication.

15.7 Human Review Gate

Before publication, content passes through human review. The system presents content with context, collects decisions, and routes accordingly. Typically, a review UI calls submitForReview to create a review request, lists pendingReviews for human reviewers, and then invokes an API endpoint that forwards the decision to submitReview, which in turn produces a ReviewResult for the pipeline to handle.
export default class ReviewGate extends AgenticSystem {
  @field pendingReviews: ReviewRequest[] = [];
  @field reviewHistory: CompletedReview[] = [];

  async submitForReview(piece: ContentPiece): Promise<ReviewRequest> {
    // Generate review brief
    const brief = await this.generateReviewBrief(piece);

    const request: ReviewRequest = {
      id: crypto.randomUUID(),
      pieceId: piece.id,
      content: piece.draft!,
      brief,
      submittedAt: Date.now(),
      status: 'pending'
    };

    this.pendingReviews.push(request);
    return request;
  }

  private async generateReviewBrief(piece: ContentPiece): Promise<ReviewBrief> {
    return {
      topic: piece.topic,
      angle: piece.angle,
      targetAudience: piece.targetAudience,
      wordCount: piece.draft!.wordCount,
      editPasses: piece.edits.length,
      accuracyConcerns: piece.draft!.metadata.accuracyConcerns ?? 'None flagged',
      keyPoints: await this.extractKeyPoints(piece.draft!.body),
      suggestedCheckpoints: [
        'Verify accuracy of any statistics or claims',
        'Ensure tone matches brand voice',
        'Check that call-to-action is appropriate',
        'Confirm links and references are correct'
      ]
    };
  }

  @action()
  async submitReview(
    requestId: string,
    decision: 'approve' | 'reject' | 'revise',
    feedback: string,
    reviewer: string
  ): Promise<ReviewResult> {
    const request = this.pendingReviews.find(r => r.id === requestId);
    if (!request) {
      throw new Error('Review request not found');
    }

    const review: CompletedReview = {
      requestId,
      pieceId: request.pieceId,
      decision,
      feedback,
      reviewer,
      completedAt: Date.now()
    };

    // Update request status
    request.status = 'completed';

    // Record in history
    this.reviewHistory.push(review);

    // Remove from pending
    this.pendingReviews = this.pendingReviews.filter(r => r.id !== requestId);

    // Trigger appropriate action
    if (decision === 'approve') {
      return { action: 'publish', pieceId: request.pieceId };
    } else if (decision === 'revise') {
      return {
        action: 'revise',
        pieceId: request.pieceId,
        feedback,
        preserveVersion: true,
        reviewer
      };
    } else {
      return {
        action: 'archive',
        pieceId: request.pieceId,
        reason: feedback,
        reviewer
      };
    }
  }

  private async extractKeyPoints(body: string): Promise<string[]> {
    const { text } = await llm.complete([
      { role: 'system', content: `Extract 3-5 key points from this content for reviewer context.
        Return one key point per line.` },
      { role: 'user', content: body }
    ]);

    return text.split('\n').filter(line => line.trim().length > 0);
  }
}

interface ReviewRequest {
  id: string;
  pieceId: string;
  content: ContentDraft;
  brief: ReviewBrief;
  submittedAt: number;
  status: 'pending' | 'completed';
}

interface ReviewBrief {
  topic: string;
  angle: string;
  targetAudience: string;
  wordCount: number;
  editPasses: number;
  accuracyConcerns: string;
  keyPoints: string[];
  suggestedCheckpoints: string[];
}

interface CompletedReview {
  requestId: string;
  pieceId: string;
  decision: 'approve' | 'reject' | 'revise';
  feedback: string;
  reviewer: string;
  completedAt: number;
}

interface ReviewResult {
  action: 'publish' | 'revise' | 'archive';
  pieceId: string;
  feedback?: string;
  preserveVersion?: boolean;
  reason?: string;
  reviewer?: string;
}

15.8 Performance Learning Loop

After publication, performance metrics flow back to inform future content decisions.
export default class PerformanceTracker extends AgenticSystem {
  @field metrics: Map<string, ContentMetrics> = new Map();

  @schedule('every 6 hours')
  async collectMetrics() {
    const publishedContent = await this.getRecentlyPublished();

    for (const piece of publishedContent) {
      const metrics = await this.fetchMetrics(piece.publishedUrl!);

      this.metrics.set(piece.id, {
        ...metrics,
        collectedAt: Date.now()
      });

      // Check for significant performance signals
      if (this.isHighPerformer(metrics)) {
        await this.flagForAnalysis(piece.id, 'high_performer');
      } else if (this.isUnderperformer(metrics, piece)) {
        await this.flagForAnalysis(piece.id, 'underperformer');
      }
    }
  }

  @schedule('every week')
  async analyzePerformance() {
    const weeklyMetrics = this.getWeeklyMetrics();

    // Generate insights
    const { text } = await llm.complete([
      { role: 'system', content: `Analyze content performance for the week.
        Identify:
        - What content performed best and why
        - What underperformed and why
        - Patterns in successful vs unsuccessful content
        - Recommendations for future content

        Return a JSON object with:
        - "patterns": string[]
        - "recommendations": string[]` },
      { role: 'user', content: `This week's content performance:
        ${weeklyMetrics.map(m => `
          Title: ${m.title}
          Topic: ${m.topic}
          Views: ${m.views}
          Avg time: ${m.avgTimeOnPage}s
          Conversions: ${m.conversions}
        `).join('\n')}

        Respond with strict JSON only.` }
    ]);

    let insights: { patterns: string[]; recommendations: string[] };
    try {
      insights = JSON.parse(text);
    } catch {
      insights = { patterns: [], recommendations: [] };
    }

    // Store insights for ideator
    await this.storeInsights(insights);

    // Update content strategy based on learnings
    await this.updateStrategy(insights);
  }

  private isHighPerformer(metrics: ContentMetrics): boolean {
    // Define high performance thresholds
    return metrics.views > 1000 ||
           metrics.avgTimeOnPage > 180 ||
           metrics.conversions > 10;
  }

  private isUnderperformer(metrics: ContentMetrics, piece: ContentPiece): boolean {
    // Below expected performance
    return metrics.views < 100 &&
           piece.expectedValue > 0.7;
  }

  private async getRecentlyPublished(): Promise<ContentPiece[]> {
    // Implementation-specific: fetch recently published pieces.
    return [];
  }

  private async fetchMetrics(url: string): Promise<Omit<ContentMetrics, 'collectedAt'>> {
    // Implementation-specific: pull metrics from analytics provider.
    return {
      views: 0,
      uniqueVisitors: 0,
      avgTimeOnPage: 0,
      bounceRate: 0,
      shares: 0,
      conversions: 0
    };
  }

  private getWeeklyMetrics(): Array<ContentMetrics & { title: string; topic: string }> {
    // Implementation-specific: aggregate metrics for the past week with titles/topics.
    return [];
  }

  private async flagForAnalysis(pieceId: string, reason: 'high_performer' | 'underperformer') {
    // Implementation-specific: mark pieces for deeper analysis in reporting or dashboards.
  }

  private async storeInsights(insights: { patterns: string[]; recommendations: string[] }) {
    // Implementation-specific: persist insights for later retrieval by the ideator.
  }

  private async updateStrategy(insights: { patterns: string[]; recommendations: string[] }) {
    // Implementation-specific: update a ContentStrategy object that upstream components read.
  }
}

interface ContentStrategy {
  themes: string[];
  minCoveragePerTheme: number;
  themePriorities: Record<string, 'low' | 'medium' | 'high'>;
  targetMix: {
    evergreen: number;
    timely: number;
    experimental: number;
  };
  focusTopics: string[];
}
ContentStrategy gives the ideator a concrete shape for “strategy”: themes, coverage thresholds, and target mix. The updateStrategy method can adjust these fields based on observed patterns, and the ideator can read them when scoring ideas or identifying gaps.

15.9 The Complete Pipeline

Assembling all components into the autonomous content pipeline:
// Minimal interfaces for external components used in the pipeline.
// In a real system these would be full AgenticSystem classes.

interface ContentResearcher {
  research(topic: string, angle: string): Promise<ResearchMaterial>;
}

interface ContentPublisher {
  publish(piece: ContentPiece): Promise<void>;
}

export default class ContentPipeline extends AgenticSystem {
  // Components
  @field ideator: ContentIdeator;
  @field researcher: ContentResearcher;
  @field drafter: ContentDrafter;
  @field editor: ContentEditor;
  @field reviewGate: ReviewGate;
  @field publisher: ContentPublisher;
  @field tracker: PerformanceTracker;

  // Content queue
  @field pieces: Map<string, ContentPiece> = new Map();
  @field statusLog: string = '';

  // Pipeline runs continuously
  @schedule('every 1 hour')
  async runPipeline() {
    const header = `\n--- Pipeline run ${new Date().toISOString()} ---\n`;
    this.statusLog = this.statusLog + header;

    // Move pieces through stages
    await this.processIdeas();
    await this.processResearch();
    await this.processDrafts();
    await this.processEditing();
    await this.processApproved();

    this.logPipelineStatus();
  }

  private async processIdeas() {
    // Pick top ideas to develop
    const topIdeas = this.ideator.ideas
      .sort((a, b) => b.expectedValue - a.expectedValue)
      .slice(0, 3);

    for (const idea of topIdeas) {
      if (!this.hasCapacity()) break;

      const piece = this.createPiece(idea);
      this.pieces.set(piece.id, piece);

      // Move to research
      piece.status = 'researching';
      this.statusLog = this.statusLog + `Started: ${idea.topic}\n`;
    }
  }

  private async processResearch() {
    const researching = this.getPiecesByStatus('researching');

    for (const piece of researching) {
      const research = await this.researcher.research(piece.topic, piece.angle);
      piece.research = research;
      piece.status = 'drafting';
    }
  }

  private async processDrafts() {
    const drafting = this.getPiecesByStatus('drafting');

    for (const piece of drafting) {
      const draft = await this.drafter.createDraft(piece, piece.research!);
      piece.draft = draft;
      piece.status = 'editing';
    }
  }

  private async processEditing() {
    const editing = this.getPiecesByStatus('editing');

    for (const piece of editing) {
      const edited = await this.editor.edit(piece.draft!);
      piece.draft = edited;
      piece.status = 'review';

      // Submit for human review
      await this.reviewGate.submitForReview(piece);
    }
  }

  private async processApproved() {
    const approved = this.getPiecesByStatus('approved');

    for (const piece of approved) {
      await this.publisher.publish(piece);
      piece.status = 'published';
      piece.publishedAt = Date.now();

      this.statusLog = this.statusLog + `Published: ${piece.draft!.title}\n`;
    }
  }

  // Handle review decisions
  async handleReviewResult(result: ReviewResult) {
    const piece = this.pieces.get(result.pieceId);
    if (!piece) return;

    if (result.action === 'publish') {
      piece.status = 'approved';
    } else if (result.action === 'revise') {
      // Send back to editing with feedback
      piece.status = 'editing';
      piece.reviews.push({
        timestamp: Date.now(),
        reviewer: result.reviewer ?? 'human',
        decision: 'revise',
        feedback: result.feedback!
      });
    } else {
      piece.status = 'rejected';
    }
  }

  private hasCapacity(): boolean {
    const inProgress = [...this.pieces.values()].filter(p =>
      !['published', 'rejected'].includes(p.status)
    ).length;
    return inProgress < 10; // Max 10 pieces in progress
  }

  private getPiecesByStatus(status: ContentStatus): ContentPiece[] {
    return [...this.pieces.values()].filter(p => p.status === status);
  }

  private createPiece(idea: ContentIdea): ContentPiece {
    const now = Date.now();
    return {
      id: crypto.randomUUID(),
      type: 'blog',
      status: 'idea',
      topic: idea.topic,
      angle: idea.angle,
      targetAudience: idea.targetAudience,
      expectedValue: idea.expectedValue,
      research: null,
      draft: null,
      edits: [],
      currentVersion: 0,
      reviews: [],
      publishedAt: null,
      publishedUrl: null,
      metrics: null,
      createdAt: now,
      updatedAt: now
    };
  }

  private logPipelineStatus() {
    // Implementation-specific: emit statusLog to monitoring, dashboard, or logs.
  }
}
The @schedule decorator here indicates that runPipeline executes on a fixed cadence (hourly). In Idyllic, scheduled methods run in the context of the system’s Durable Object, so state is consistent across runs and you typically design methods to be idempotent in case of retries.

Key Takeaways

  • Content pipelines automate predictable steps (research, drafting, scheduling) and keep humans in charge of quality gates like editorial review and fact checking
  • Performance feedback creates a learning loop that improves ideation over time
  • Voice guidelines ensure consistency across automated drafting
  • Multiple editing passes focus on different quality dimensions
  • Human review gates prevent low-quality content from publishing
  • The pipeline runs autonomously but remains controllable

Transition

Part 2: Applications is complete. Part 2 applied the ten elements from Part 1 to systems such as virtual offices, research tools, code agents, customer service workflows, and the content pipeline described here. The Conclusion synthesizes these patterns and points toward what comes next.