MDX Limo
ContentEngine — Technical Addendum #3

ContentEngine — Technical Addendum #3

Agentic System Architecture, Tool Inventory, Workflow Orchestration & Observability

FieldValue
AuthorAlton Wells
DateMarch 2026
StatusDraft for Review
Parent SpecContentEngine Technical Specification v3
Depends OnAddendum #1 (LangExtract), Addendum #2 (Content Types & SEO Validation)
ScopeAgent definitions, model configuration, tool specifications, workflow orchestration, human gate contracts, error handling, audit trail

1. Purpose & Scope

This addendum defines the complete agentic system for ContentEngine: how agents are configured, what tools they use, how workflows are orchestrated across the Strategy, Content, and Production layers, how human review gates work mechanically, what happens when things fail, and how every agent decision is recorded for auditability.

1.1 What This Addendum Covers

  • Agent configuration system with per-agent model selection and runtime settings
  • Complete tool inventory: 28 tools across shared, strategy, content, and production categories
  • Strategy Layer workflow: orchestration of Competitive Intelligence, Search Landscape, and Content Strategy agents
  • Content Layer workflow: Writer → Editor revision loop with step budget management
  • Production Layer workflow: human review gate, cleanup, SEO validation, publishing
  • Human gate contracts: suspend/resume event schemas for all three human touchpoints
  • Post-publish pipeline: event-driven job chain specification
  • Error handling: maxSteps policy, failure modes, escalation paths
  • Audit trail schema and logging strategy
  • Brand voice and prompt architecture

1.2 What This Addendum Does Not Cover

  • Filesystem-as-context architecture (deferred per parent spec Section 11)
  • Agent prompt engineering details (separate addendum — this addendum defines the structure, not the prose)
  • Hierarchical summary generation logic (separate addendum)
  • Graph relationship builder logic (separate addendum)
  • UI/UX specifications for the calendar, editor, or dashboard views

1.3 Architectural Principle: Deterministic Orchestration, Agentic Execution

ContentEngine uses workflows for orchestration and agents for execution. The workflow defines the fixed sequence: always run Competitive Intel, then Search Landscape, then feed both to Strategy. The agents within each step decide how to accomplish their task — which tools to call, in what order, how many times. This is the correct separation. Workflows own the "what happens next." Agents own the "how do I do this step."

This means we do not use a supervisor/sub-agent pattern for the Strategy Layer. The Strategy Agent does not dynamically decide whether to run competitive analysis — it always receives competitive analysis output as input. The workflow guarantees this. Agent autonomy is scoped to tool selection and reasoning within a step, not to workflow routing.


2. Agent Configuration System

2.1 Design Principle

Every agent's model is configurable at runtime via a settings table. No model ID is hardcoded in agent definitions. This allows the team to swap models without redeploying code — useful for cost optimization, testing new model releases, and per-agent tuning.

2.2 Configuration Schema

1CREATE TABLE agent_config ( 2 id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 3 agent_id VARCHAR(50) NOT NULL UNIQUE, 4 display_name VARCHAR(100) NOT NULL, 5 model_id VARCHAR(100) NOT NULL, 6 max_steps INTEGER NOT NULL DEFAULT 10, 7 temperature FLOAT DEFAULT 0.7, 8 category VARCHAR(30) NOT NULL, -- 'strategy', 'content', 'production' 9 is_active BOOLEAN DEFAULT true, 10 notes TEXT, 11 updated_at TIMESTAMP DEFAULT now(), 12 updated_by VARCHAR(50) DEFAULT 'system' 13); 14 15CREATE INDEX idx_agent_config_id ON agent_config(agent_id);

2.3 Default Configuration

Agent IDDisplay NameDefault ModelDefault maxStepsCategoryTemperature
competitive-intelligenceCompetitive Intelligenceanthropic/claude-sonnet-4-2025051412strategy0.5
search-landscapeSearch Landscapeanthropic/claude-sonnet-4-2025051410strategy0.5
content-strategyContent Strategyanthropic/claude-sonnet-4-2025051420strategy0.7
content-briefContent Briefanthropic/claude-sonnet-4-2025051415content0.6
writerWriteranthropic/claude-sonnet-4-2025051412content0.8
editorEditoranthropic/claude-sonnet-4-2025051410content0.4
final-cleanupFinal Cleanupanthropic/claude-sonnet-4-202505146production0.2
publishingPublishinganthropic/claude-sonnet-4-2025051415production0.1
seo-autofixSEO Auto-Fixanthropic/claude-sonnet-4-202505148production0.3

Temperature rationale: Strategy agents are moderate (need creative planning but grounded in data). The Writer is highest (creative generation). The Editor is low (precision, not creativity). Production agents are near-zero (mechanical execution).

2.4 Agent Instantiation Pattern

Every agent resolves its model and parameters from the config table at invocation time, not at import time. This ensures config changes take effect immediately without restart.

1import { Agent } from "@mastra/core/agent"; 2 3async function createConfiguredAgent( 4 agentId: string, 5 instructions: string | (() => Promise<string>), 6 tools: Record<string, Tool> 7): Promise<Agent> { 8 const config = await db.getAgentConfig(agentId); 9 10 return new Agent({ 11 id: agentId, 12 model: config.model_id, 13 instructions, 14 tools, 15 maxSteps: config.max_steps, 16 }); 17}

2.5 Settings UI Requirements

The agent configuration is exposed in the Strategy Settings view (parent spec Section 7.3). The UI provides a table of all agents with editable model, maxSteps, and temperature fields. Changes are saved to agent_config and take effect on the next agent invocation. The UI also displays last-run metadata per agent (timestamp, token usage, step count) pulled from the audit trail to inform tuning decisions.


3. Tool Inventory

Tools are the interface between agents and the outside world. Each tool has a defined input schema (Zod), output schema, and a single clear purpose. Tools are grouped into four categories: shared (used by multiple agents), strategy-specific, content-specific, and production-specific.

3.1 Shared Tools

These tools are available to multiple agents. The underlying implementation is shared; each agent receives the same tool instance.

Tool S1: readDomainSummary

Used by: Competitive Intelligence, Content Strategy

FieldValue
PurposeReturns the Level 0 domain summary from the hierarchical summary tree. ~500 tokens of high-level competitive landscape.
Input{ scope: "ours" | "competitor" | "combined" }
Output{ summary_text: string, metrics_snapshot: object, generated_at: Date }
Data Sourcedomain_summaries table
NotesIf no summary exists for the requested scope, returns a structured error. Agent should not retry — missing summaries indicate the summary generation pipeline hasn't run yet.

Tool S2: readClusterSummaries

Used by: Competitive Intelligence, Content Strategy

FieldValue
PurposeReturns Level 1 cluster summaries. Each ~300 tokens with performance data, gap analysis, and competitor comparison.
Input{ clusterId?: string, scope?: "ours" | "competitor" | "combined", limit?: number }
Output{ summaries: Array<{ cluster_id, cluster_name, summary_text, page_count, performance, gap_analysis, competitor_comparison, generated_at }> }
Data Sourcecluster_summaries table
NotesWithout clusterId, returns all clusters for the given scope. Default limit: 20.

Tool S3: readPageSummaries

Used by: Content Strategy, Content Brief

FieldValue
PurposeReturns Level 2 page summaries. Each ~150 tokens with topic coverage, keywords, and quality score.
Input{ clusterId?: string, pageType?: "ours" | "competitor", keyword?: string, limit?: number }
Output{ summaries: Array<{ page_id, url, page_type, summary_text, topics, keywords, quality_score, generated_at }> }
Data Sourcepage_summaries table
NotesThe keyword filter does a text search within the keywords JSONB field. Default limit: 30.

Tool S4: queryExtractions

Used by: Content Strategy, Content Brief, Writer, Editor

FieldValue
PurposeQueries Level 3 entity data from any extraction table. The universal structured data access tool.
Input{ source: "competitor" | "ours" | "serp" | "ai_overview" | "brand_voice", extractionClass: string, keyword?: string, pageId?: string, since?: Date, limit?: number }
Output{ extractions: Array<{ id, extraction_class, extraction_text, attributes, source_location, page_url?, created_at }>, total_count: number }
Data Sourcecompetitor_extractions, our_page_extractions, serp_extractions, brand_voice_extractions (routed by source)
NotesThe keyword filter searches within extraction_text and attributes JSONB. Default limit: 50. This is the most frequently called tool across all agents.

Tool S5: traverseContentGraph

Used by: Competitive Intelligence, Content Strategy, Content Brief, Writer

FieldValue
PurposeWalks the content relationship graph following specified edge types. Returns connected nodes with relationship metadata.
Input{ startNodeType: string, startNodeId?: string, edgeTypes: string[], direction?: "outbound" | "inbound" | "both", maxDepth?: number, limit?: number }
Output{ nodes: Array<{ node_type, node_id, relationship_type, relationship_metadata, depth, connected_to }>, edge_count: number }
Data Sourcecontent_relationships table
NotesstartNodeId is optional — when omitted with a startNodeType, the tool returns all edges of the specified types for that node type (e.g., all gap edges). maxDepth defaults to 2. limit defaults to 100. Results are sorted by confidence score descending.

While this is a single flexible tool, agents receive curated guidance in their instructions about which edge types are relevant to their task. The Competitive Intelligence agent is told to use competes_with, outperforms, gap, and same_topic_as. The Writer is told to use links_to, should_link_to, and covers_topic. This constrains behavior without requiring separate tool implementations.

Tool S6: webSearch

Used by: Competitive Intelligence, Search Landscape, Content Strategy, Content Brief, Writer, Editor

FieldValue
PurposeLive web search for validation, research, and fresh intelligence.
Input{ query: string, maxResults?: number }
Output{ results: Array<{ title, url, snippet, published_date? }> }
Data SourceFirecrawl search API
NotesmaxResults defaults to 5, maximum 10. Rate limited to 30 requests per minute across all agents. The tool wrapper handles rate limiting and returns a structured error if the limit is hit. Agents should not retry immediately — the rate limit resets within 2 minutes.

Tool S7: queryContentPlan

Used by: Content Strategy, Content Brief

FieldValue
PurposeReturns content plan items from the calendar, filtered by status, type, and date range.
Input{ status?: string[], contentType?: string[], dateRange?: { start: Date, end: Date }, source?: "ai_generated" | "human_added", limit?: number }
Output{ items: Array<ContentPlanItem>, total_count: number }
Data Sourcecontent_plan_items table
NotesWithout filters, returns all non-published items. Default limit: 50.

Tool S8: readContentStrategy

Used by: Content Strategy, Content Brief, Writer (via brief context)

FieldValue
PurposeReturns the active content strategy directives.
Input{} (no parameters — always returns the active strategy)
Output{ id, name, description, target_audience, brand_voice_guidelines, content_pillars, priorities, active }
Data Sourcecontent_strategy table (filtered by active: true)
NotesIf no active strategy exists, returns structured error. System should always have exactly one active strategy.

3.2 Strategy Layer Tools

These tools are specific to Strategy Layer agents and not shared with Content or Production agents.

Tool ST1: queryCompetitorChanges

Used by: Competitive Intelligence

FieldValue
PurposeReturns recent content changes detected across competitor sites.
Input{ competitorId?: string, since: Date, changeType?: "new" | "updated" | "removed", limit?: number }
Output{ changes: Array<{ id, competitor_name, page_url, change_type, detected_at, diff_summary }>, total_count: number }
Data Sourcecompetitor_changes table joined with competitors
NotesDefault limit: 50. since is required — no unbounded historical queries. For the daily digest, since is set to 24 hours ago. For the weekly full analysis, since is set to 7 days ago.

Tool ST2: queryKeywordPerformance

Used by: Search Landscape

FieldValue
PurposeReturns our ranking data with change deltas over a specified period.
Input{ keywordId?: string, clusterId?: string, minPositionChange?: number, dateRange: { start: Date, end: Date }, limit?: number }
Output{ rankings: Array<{ keyword, url, current_position, previous_position, position_change, impressions, clicks, ctr }> }
Data Sourceour_page_performance joined with keywords
NotesminPositionChange filters for significant movers only (e.g., minPositionChange: 3 returns only keywords that moved 3+ positions). Default limit: 100.

Tool ST3: querySerpExtractions

Used by: Search Landscape

FieldValue
PurposeReturns structured SERP feature data for tracked keywords.
Input{ keywordId: string, extractionClass?: string, snapshotDate?: Date }
Output{ extractions: Array<{ extraction_class, extraction_text, attributes, snapshot_date }> }
Data Sourceserp_extractions joined with serp_snapshots
NotesWithout snapshotDate, returns the most recent snapshot.

Tool ST4: queryAiOverviewTracking

Used by: Search Landscape

FieldValue
PurposeReturns AI Overview presence and citation status for tracked keywords.
Input{ keywordId?: string, ourSiteCited?: boolean, since?: Date, limit?: number }
Output{ overviews: Array<{ keyword, detected_at, our_site_cited, cited_sources, summary_text }> }
Data Sourceai_overview_tracking
NotesourSiteCited filter is the primary alert mechanism — the Search Landscape agent checks for keywords where AI Overviews exist but don't cite us.

Tool ST5: semrushKeywordResearch

Used by: Search Landscape, Content Strategy

FieldValue
PurposeFetches fresh keyword data from the Semrush API.
Input{ keywords: string[], market?: string }
Output{ keywords: Array<{ keyword, search_volume, difficulty, cpc, intent, trend }> }
Data SourceSemrush REST API
NotesMarket defaults to "us". Rate limited by Semrush plan (Business: 5,000 requests/day). Results are cached in the keywords table — the tool checks cache first and only calls the API for stale or missing data (stale = older than 7 days).

Tool ST6: gscPerformanceQuery

Used by: Search Landscape

FieldValue
PurposeQueries real-time data from Google Search Console.
Input{ urls?: string[], queries?: string[], dateRange: { start: Date, end: Date }, dimensions?: string[] }
Output{ rows: Array<{ url?, query?, impressions, clicks, ctr, avg_position, date? }> }
Data SourceGoogle Search Console API (OAuth2)
Notesdimensions defaults to ["query", "page"]. Maximum date range: 16 months (GSC API limit). Results are written to our_page_performance table after retrieval — this tool both queries and caches.

Tool ST7: addContentPlanItem

Used by: Content Strategy

FieldValue
PurposeWrites a new item to the content calendar.
InputContentPlanItem schema (title, targetKeyword, contentType, priority, scheduledDate, rationale, etc.)
Output{ id: string, created: boolean }
Data Sourcecontent_plan_items table (INSERT)
NotesAll items created by this tool are automatically tagged with source: "ai_generated". The tool validates that the targetKeyword exists in the keywords table and that no duplicate item exists for the same keyword + content type combination.

Tool ST8: updateContentPlanItem

Used by: Content Strategy

FieldValue
PurposeModifies an existing content plan item.
Input{ id: string, updates: Partial<ContentPlanItem> }
Output{ id: string, updated: boolean }
Data Sourcecontent_plan_items table (UPDATE)
NotesCannot update items with status published. Cannot change source field. All updates are logged in the audit trail.

3.3 Content Layer Tools

Tool C1: loadApprovedBrief

Used by: Writer (via workflow step, not direct agent tool)

FieldValue
PurposeLoads an approved content brief with all associated data for the Writer agent's context.
Input{ briefId: string }
Output{ brief: ContentBrief, brandVoice: BrandVoiceExtractions, strategy: ContentStrategy }
Data Sourcecontent_briefs + brand_voice_extractions + content_strategy
NotesThis is a workflow step tool, not an agent-callable tool. It assembles the complete context package that becomes the Writer agent's dynamic instructions. The brief, brand voice, and strategy are loaded together in a single operation to minimize database round-trips.

Tool C2: queryOurPages

Used by: Editor, Content Brief

FieldValue
PurposeQueries our published content inventory.
Input{ url?: string, keyword?: string, contentType?: string, status?: string, limit?: number }
Output{ pages: Array<{ id, url, title, content_type, status, published_at, word_count }> }
Data Sourceour_pages table
NotesUsed by the Editor to verify internal link targets resolve to real published pages. Used by the Content Brief agent to check what we already cover.

Used by: Editor, Final Cleanup

FieldValue
PurposeValidates that all internal links in a content body resolve to published pages.
Input{ contentBody: string }
Output{ links: Array<{ url, anchor_text, exists: boolean, page_title?: string }>, broken_count: number, valid_count: number }
Data SourceParses markdown links from contentBody, checks each against our_pages table
NotesReturns the full link inventory with validation status. The Editor uses this to flag broken links as critical edits. The Final Cleanup agent uses it for the post-human-edit verification pass.

3.4 Production Layer Tools

Tool P1: formatForCms

Used by: Publishing

FieldValue
PurposeConverts the final markdown draft into the format expected by the Supabase content tables.
Input{ contentBody: string, metadata: { title, slug, contentType, authorId, targetKeyword, secondaryKeywords } }
Output{ formatted: { html_body: string, raw_text: string, meta_title: string, meta_description: string, schema_json: string, slug: string } }
Data SourceNone (transformation logic)
NotesGenerates JSON-LD schema markup per the content type mapping defined in Addendum #2 Section 4. Converts markdown to HTML. Extracts raw text for the raw_text field. Validates slug format.

Tool P2: uploadToCms

Used by: Publishing

FieldValue
PurposeWrites the formatted content to the Supabase content tables.
Input{ formatted: FormattedContent (output of formatForCms), planItemId: string, draftId: string }
Output{ pageId: string, url: string, published: boolean, publishedAt: Date }
Data Sourceour_pages table (INSERT or UPDATE for refreshes) + our_page_seo table
NotesFor new content, creates a new row. For refreshes, updates the existing row and preserves published_at (original date) while updating last_updated. Updates content_plan_items status to published. Updates content_drafts status to published.

Tool P3: setMetadata

Used by: Publishing

FieldValue
PurposeSets SEO metadata fields on the published page record.
Input{ pageId: string, metaTitle: string, metaDescription: string, canonicalUrl: string, schemaMarkup: string, openGraph: object, twitterCard: object }
Output{ pageId: string, updated: boolean }
Data Sourceour_page_seo table (UPDATE)
NotesSeparated from uploadToCms because metadata may be adjusted by the SEO auto-fix agent independently of the content body.

Tool P4: pingIndexingApi

Used by: Publishing

FieldValue
PurposeSubmits the published URL to Google's Indexing API and/or IndexNow for fast crawling.
Input{ url: string, action: "URL_UPDATED" | "URL_DELETED" }
Output{ indexed: boolean, response: string }
Data SourceGoogle Indexing API / IndexNow API
NotesTries IndexNow first (simpler, broader search engine coverage). Falls back to Google Indexing API if available. Logs the response for the post-publish monitoring check at 24 hours.

Tool P5: triggerPostPublishPipeline

Used by: Publishing

FieldValue
PurposeFires the post-publish event that kicks off the extraction → summary → graph → linking chain.
Input{ pageId: string, url: string, isRefresh: boolean }
Output{ triggered: boolean, jobId: string }
Data SourceEvent emitter → Trigger.dev job queue
NotesThis tool does not wait for the pipeline to complete. It fires the event and returns immediately. The pipeline runs asynchronously. See Section 8 for pipeline specification.

Tool P6: schedulePostPublishMonitoring

Used by: Publishing

FieldValue
PurposeSchedules the 24h/7d/30d monitoring checks for a newly published page.
Input{ pageId: string, url: string, targetKeyword: string }
Output{ scheduled: boolean, monitoringJobs: Array<{ checkType, scheduledAt }> }
Data SourceTrigger.dev scheduled jobs
NotesCreates three scheduled jobs: 24h (index verification via GSC), 7d (initial ranking check), 30d (full performance review). Each job writes results to our_page_performance and flags underperformers for the refresh queue.

Tool P7: logToAuditTrail

Used by: Publishing (and available to all agents via workflow context)

FieldValue
PurposeWrites a structured entry to the audit trail.
Input{ agentId: string, workflowRunId: string, action: string, details: object }
Output{ logged: boolean }
Data Sourceagent_audit_log table (INSERT)
NotesSee Section 10 for the full audit trail specification. This tool is called explicitly by the Publishing Agent at each pipeline step. Other agents have their tool calls logged automatically by the workflow engine (see Section 10.2).

3.5 Tool Count Summary

CategoryCountTools
Shared8S1–S8
Strategy8ST1–ST8
Content3C1–C3
Production7P1–P7
Total26

3.6 Tool-to-Agent Assignment Matrix

ToolComp. IntelSearch Land.Content Strat.BriefWriterEditorCleanupPublishingSEO Fix
S1 readDomainSummary
S2 readClusterSummaries
S3 readPageSummaries
S4 queryExtractions
S5 traverseContentGraph
S6 webSearch
S7 queryContentPlan
S8 readContentStrategy
ST1 queryCompetitorChanges
ST2 queryKeywordPerformance
ST3 querySerpExtractions
ST4 queryAiOverviewTracking
ST5 semrushKeywordResearch
ST6 gscPerformanceQuery
ST7 addContentPlanItem
ST8 updateContentPlanItem
C2 queryOurPages
C3 verifyInternalLinks
P1 formatForCms
P2 uploadToCms
P3 setMetadata
P4 pingIndexingApi
P5 triggerPostPublishPipeline
P6 scheduleMonitoring
P7 logToAuditTrail

Design principle: each agent receives only the tools it needs. A smaller tool surface means fewer irrelevant tool calls, lower token usage, and easier debugging. The Writer agent has 4 tools. The Publishing agent has 7 tools. The Content Strategy agent has 10 tools (the most complex decision-maker).


4. Strategy Layer Workflow

4.1 Workflow Definition

The Strategy Layer is a single Mastra workflow containing three sequential agent steps plus a human review gate. The workflow is triggered either by a scheduled job (weekly) or by a human clicking "Generate Plan" in the calendar UI.

1const strategyLayerWorkflow = createWorkflow({ 2 id: "strategy-layer", 3 inputSchema: z.object({ 4 triggerType: z.enum(["scheduled", "manual"]), 5 triggeredBy: z.string().optional(), 6 }), 7 outputSchema: z.object({ 8 planItemIds: z.array(z.string()), 9 strategyNotes: z.string(), 10 }), 11}) 12 .then(competitiveIntelStep) 13 .then(searchLandscapeStep) 14 .then(contentStrategyStep) 15 .then(saveAndSuspendForReviewStep) 16 .commit();

4.2 Step 1: Competitive Intelligence Agent

The Competitive Intelligence agent runs first. Its output becomes part of the Content Strategy agent's input context.

Agent invocation within step:

1const competitiveIntelStep = createStep({ 2 id: "competitive-intel", 3 inputSchema: z.object({ 4 triggerType: z.string(), 5 }), 6 outputSchema: CompetitiveIntelOutput, 7 execute: async ({ inputData }) => { 8 const agent = await createConfiguredAgent( 9 "competitive-intelligence", 10 competitiveIntelInstructions, 11 { readDomainSummary, readClusterSummaries, queryCompetitorChanges, 12 queryExtractions, traverseContentGraph, webSearch } 13 ); 14 const result = await agent.generate([ 15 { role: "user", content: buildCompetitiveIntelPrompt(inputData.triggerType) } 16 ]); 17 return result.object; // structured output via Zod 18 }, 19});

The agent's tool call pattern (expected, not enforced):

  1. readDomainSummary({ scope: "combined" }) — get the big picture (~500 tokens)
  2. queryCompetitorChanges({ since: 7daysAgo }) — what's new (~variable)
  3. readClusterSummaries({ scope: "competitor" }) — competitor coverage depth (~3,000 tokens)
  4. queryExtractions({ source: "competitor", ... }) — drill into specific changes (~variable)
  5. traverseContentGraph({ edgeTypes: ["gap", "outperforms"] }) — structural threats (~variable)
  6. webSearch(...) — validate and enrich findings as needed

Output: CompetitiveIntelOutput as defined in parent spec Section 4.1. Stored in workflow state for the Content Strategy step.

4.3 Step 2: Search Landscape Agent

Runs after Competitive Intelligence. Independent analysis — does not consume CompetitiveIntel output.

Tool call pattern:

  1. queryKeywordPerformance({ dateRange: last30days, minPositionChange: 3 }) — significant movers
  2. queryAiOverviewTracking({ since: 7daysAgo }) — AI Overview changes
  3. querySerpExtractions(...) — SERP feature details for priority keywords
  4. gscPerformanceQuery(...) — real-time performance data
  5. semrushKeywordResearch(...) — fresh data for emerging keywords

Output: SearchLandscapeOutput as defined in parent spec Section 4.2. Stored in workflow state.

4.4 Step 3: Content Strategy Agent

The brain. Receives both prior agent outputs via workflow state plus direct tool access for deeper investigation.

Input context assembly (performed in the workflow step before agent invocation):

1const contentStrategyStep = createStep({ 2 id: "content-strategy", 3 inputSchema: z.object({ 4 competitiveIntel: CompetitiveIntelOutput, 5 searchLandscape: SearchLandscapeOutput, 6 }), 7 outputSchema: ContentPlanOutput, 8 execute: async ({ inputData }) => { 9 const agent = await createConfiguredAgent( 10 "content-strategy", 11 await buildStrategyInstructions(), 12 { readContentStrategy, readDomainSummary, readClusterSummaries, 13 readPageSummaries, queryExtractions, queryContentPlan, 14 traverseContentGraph, webSearch, addContentPlanItem, 15 updateContentPlanItem } 16 ); 17 18 const result = await agent.generate([ 19 { 20 role: "user", 21 content: `COMPETITIVE INTELLIGENCE REPORT:\n${JSON.stringify(inputData.competitiveIntel)}\n\n` + 22 `SEARCH LANDSCAPE REPORT:\n${JSON.stringify(inputData.searchLandscape)}\n\n` + 23 `Generate a prioritized content plan for the next production cycle.` 24 } 25 ]); 26 return result.object; 27 }, 28});

The Content Strategy agent's expected context budget (~14,000 tokens total, as specified in parent spec Section 4.3) is consumed as:

SourceTokensMethod
Active strategy directives~500readContentStrategy()
Domain summary~500readDomainSummary()
Competitive Intel output~2,000Passed via workflow state
Search Landscape output~2,000Passed via workflow state
Cluster summaries~3,000readClusterSummaries()
Current calendar~1,000queryContentPlan()
Graph edges~1,500traverseContentGraph()
Drill-down page summaries~2,000readPageSummaries() (selective)
Entity-level extractions~1,500queryExtractions() (selective)

4.5 Step 4: Save & Suspend for Human Review

After the Content Strategy agent runs, its output (new plan items) is persisted to content_plan_items and the workflow suspends.

1const saveAndSuspendForReviewStep = createStep({ 2 id: "save-and-suspend", 3 inputSchema: ContentPlanOutput, 4 outputSchema: z.object({ planItemIds: z.array(z.string()) }), 5 execute: async ({ inputData, suspend }) => { 6 // Write plan items to database 7 const ids = await db.batchInsertContentPlanItems( 8 inputData.planItems.map(item => ({ 9 ...item, 10 status: "planned", 11 source: "ai_generated", 12 })) 13 ); 14 15 // Notify human 16 await notifications.send({ 17 channel: "slack", 18 message: `ContentEngine generated ${ids.length} new content plan items. Review at [calendar URL]`, 19 }); 20 21 // Suspend workflow — waits for human to resume via API 22 const reviewResult = await suspend({ 23 type: "calendar-review", 24 planItemIds: ids, 25 message: "Awaiting human review of content calendar", 26 }); 27 28 // Human has approved — reviewResult contains approved item IDs 29 return { planItemIds: reviewResult.approvedItemIds }; 30 }, 31});

See Section 7.1 for the full human gate contract.


5. Content Layer Workflow

5.1 Workflow Definition

The Content Layer workflow runs once per approved content plan item. It is triggered when a brief is approved by the human reviewer. The workflow manages the Writer → Editor → revision loop with a maximum of 2 revision cycles.

1const contentLayerWorkflow = createWorkflow({ 2 id: "content-layer", 3 inputSchema: z.object({ 4 briefId: z.string(), 5 planItemId: z.string(), 6 }), 7 outputSchema: z.object({ 8 draftId: z.string(), 9 seoScore: z.string(), 10 }), 11}) 12 .then(loadBriefStep) 13 .then(writerStep) 14 .then(editorStep) 15 .then(revisionDecisionStep) // branches based on editor output 16 .then(saveDraftStep) 17 .commit();

5.2 Brief Generation Sub-Workflow

Before the Content Layer runs, each approved plan item goes through brief generation. This is a separate workflow that suspends for human brief approval.

1const briefGenerationWorkflow = createWorkflow({ 2 id: "brief-generation", 3 inputSchema: z.object({ planItemId: z.string() }), 4 outputSchema: z.object({ briefId: z.string() }), 5}) 6 .then(generateBriefStep) 7 .then(saveBriefAndSuspendStep) // suspends for human approval 8 .commit();

The Content Brief agent receives: approved plan item data, competitor extractions for the target keyword, graph edges, page summaries, brand voice extractions, and web search results. Output is the ContentBriefOutput schema defined in parent spec Section 4.4.

5.3 Writer Agent Step

The Writer receives the full context package assembled by loadBriefStep:

  • Approved brief (outline, word counts, keyword targets, competitor gaps)
  • Brand voice extractions (tone markers, vocabulary, patterns)
  • Active strategy directives (pillars, audience, priorities)

This context is injected into the agent's dynamic instructions at invocation time. The Writer agent's tools are deliberately minimal — most of its context comes from the brief, not from live queries:

ToolPurpose
webSearchReal-time fact verification during writing
queryExtractionsCheck consistency with existing content
traverseContentGraphFind additional internal linking opportunities

maxSteps: 12. The Writer typically uses 2-4 tool calls for research/verification plus 1 final generation call. The buffer accounts for longer pieces that require multiple fact-check cycles.

Output: Full markdown content body with frontmatter, internal links, external links, and [IMAGE: ...] placement markers.

5.4 Editor Agent Step

The Editor receives the Writer's draft plus the original brief and brand voice data.

ToolPurpose
queryOurPagesVerify internal link targets resolve
verifyInternalLinksBatch link validation
webSearchFact-check claims and statistics

maxSteps: 10. The Editor typically uses 2-3 tool calls (link verification, fact-checking) plus 1 analysis/output call.

Output: EditorOutput as defined in parent spec Section 5.2 — overall assessment, edit list, voice consistency score, readability score, and optionally a revised draft.

5.5 Revision Loop

The revision decision step implements a bounded loop:

1const revisionDecisionStep = createStep({ 2 id: "revision-decision", 3 inputSchema: z.object({ 4 editorOutput: EditorOutput, 5 revisionCount: z.number(), 6 originalBrief: ContentBrief, 7 brandVoice: BrandVoiceExtractions, 8 }), 9 outputSchema: z.object({ 10 finalDraft: z.string(), 11 totalRevisions: z.number(), 12 }), 13 execute: async ({ inputData }) => { 14 const { editorOutput, revisionCount, originalBrief, brandVoice } = inputData; 15 16 if (editorOutput.overallAssessment === "pass" || revisionCount >= 2) { 17 // Accept the draft — either it passed or we've hit max revisions 18 return { 19 finalDraft: editorOutput.revisedContent || inputData.currentDraft, 20 totalRevisions: revisionCount, 21 }; 22 } 23 24 // Revision needed — send back to Writer with edit context 25 const writerAgent = await createConfiguredAgent("writer", ...); 26 const revisedDraft = await writerAgent.generate([ 27 { role: "user", content: buildRevisionPrompt(editorOutput.edits, originalBrief) } 28 ]); 29 30 // Run through Editor again 31 const editorAgent = await createConfiguredAgent("editor", ...); 32 const newEditorOutput = await editorAgent.generate([ 33 { role: "user", content: buildEditorPrompt(revisedDraft.text, originalBrief, brandVoice) } 34 ]); 35 36 // Recursive check (bounded by revisionCount) 37 return executeRevisionDecision({ 38 editorOutput: newEditorOutput.object, 39 revisionCount: revisionCount + 1, 40 originalBrief, 41 brandVoice, 42 currentDraft: revisedDraft.text, 43 }); 44 }, 45});

Maximum 2 revision cycles. After 2 cycles, the draft proceeds to human review regardless. The human reviewer sees the Editor's notes and can make corrections that the agents missed. This prevents infinite loops and respects the design principle that humans catch what agents miss.


6. Production Layer Workflow

6.1 Workflow Definition

The Production Layer handles human review, cleanup, SEO validation, and publishing. It contains the third and final human gate.

1const productionLayerWorkflow = createWorkflow({ 2 id: "production-layer", 3 inputSchema: z.object({ 4 draftId: z.string(), 5 planItemId: z.string(), 6 briefId: z.string(), 7 }), 8 outputSchema: z.object({ 9 pageId: z.string().optional(), 10 published: z.boolean(), 11 failureReason: z.string().optional(), 12 }), 13}) 14 .then(suspendForHumanReviewStep) 15 .then(finalCleanupStep) 16 .then(seoValidationStep) 17 .then(seoFixLoopStep) 18 .then(publishStep) 19 .commit();

6.2 Human Review Gate

The workflow suspends and presents the draft in the editor interface (parent spec Section 7.2). The human reviewer performs all actions specified in parent spec Section 6.1: reading, adding experience, placing images, editing voice, fact-checking, and approving or rejecting.

1const suspendForHumanReviewStep = createStep({ 2 id: "human-review", 3 execute: async ({ inputData, suspend }) => { 4 const draft = await db.getDraft(inputData.draftId); 5 const brief = await db.getBrief(inputData.briefId); 6 7 await notifications.send({ 8 channel: "slack", 9 message: `Draft ready for review: "${draft.title}". Review at [editor URL]`, 10 }); 11 12 const reviewResult = await suspend({ 13 type: "draft-review", 14 draftId: inputData.draftId, 15 briefId: inputData.briefId, 16 message: "Awaiting human review of draft", 17 }); 18 19 if (reviewResult.action === "rejected") { 20 // Send back to Content Layer with rejection notes 21 return { rejected: true, rejectionNotes: reviewResult.notes }; 22 } 23 24 // Human approved — return the human-edited content 25 return { 26 rejected: false, 27 editedContent: reviewResult.editedContent, 28 imagesPlaced: reviewResult.imagesPlaced, 29 }; 30 }, 31});

6.3 Final Cleanup Agent

After human edits, the Final Cleanup agent makes a technical-only pass. It does not change tone, wording, or content. It fixes formatting issues introduced during human editing.

Tools: verifyInternalLinks only. maxSteps: 6. Typically 1-2 tool calls + 1 cleanup pass.

6.4 SEO Validation & Auto-Fix Loop

The SEO validation engine (Addendum #2, Section 6) runs as deterministic code, not an LLM. If any BLOCKING check fails, the SEO Auto-Fix agent attempts repair.

1const seoFixLoopStep = createStep({ 2 id: "seo-fix-loop", 3 execute: async ({ inputData }) => { 4 let content = inputData.content; 5 let validation = runSeoValidation(content); 6 let fixCycles = 0; 7 8 while (!validation.passed && fixCycles < 3) { 9 const failures = validation.checks 10 .filter(c => c.tier === "BLOCKING" && !c.passed && c.autoFixable); 11 12 if (failures.length === 0) break; // non-fixable failures remain 13 14 const fixAgent = await createConfiguredAgent("seo-autofix", ...); 15 const fixResult = await fixAgent.generate([ 16 { role: "user", content: buildAutoFixPrompt(content, failures) } 17 ]); 18 19 content = fixResult.text; 20 validation = runSeoValidation(content); 21 fixCycles++; 22 } 23 24 if (!validation.passed) { 25 // Escalate to human 26 await notifications.send({ 27 channel: "slack", 28 message: `SEO validation failed after ${fixCycles} fix cycles. Manual correction needed.`, 29 details: validation.checks.filter(c => !c.passed), 30 }); 31 } 32 33 return { content, validation, fixCycles }; 34 }, 35});

6.5 Publishing Step

If SEO validation passes (10/10 BLOCKING score), the Publishing agent executes the publication pipeline using its 7 tools in sequence: format → upload → set metadata → ping indexing → trigger post-publish → schedule monitoring → log to audit trail.

The Publishing agent has maxSteps: 15 because it must call each tool in sequence, and some tools may require retry on transient failures. The agent is instructed to execute tools in a specific order and to halt if any step fails.


7. Human Gate Contracts

Each human touchpoint is a workflow suspension. The workflow suspends and provides a payload describing what the human needs to do. The Next.js app reads this payload, renders the appropriate UI, and calls the Mastra server's resume endpoint when the human completes their action.

7.1 Gate 1: Calendar Review

FieldValue
Suspend Event Typecalendar-review
TriggerContent Strategy agent completes plan generation
UI SurfaceCalendar view (parent spec Section 7.1)

Suspend payload:

1{ 2 type: "calendar-review", 3 workflowRunId: string, 4 planItemIds: string[], 5 generatedAt: Date, 6 message: string, 7}

Resume payload (sent by Next.js app when human completes review):

1{ 2 approvedItemIds: string[], // items human approved 3 rejectedItemIds: string[], // items human rejected 4 editedItems: Array<{ // items human modified before approving 5 id: string, 6 changes: Partial<ContentPlanItem>, 7 }>, 8 humanAddedItems: string[], // IDs of items human created during review 9}

Resume API call:

1POST /api/workflows/strategy-layer/runs/{runId}/resume 2Content-Type: application/json 3Body: { resumePayload: { ... } }

7.2 Gate 2: Brief Approval

FieldValue
Suspend Event Typebrief-approval
TriggerContent Brief agent completes brief generation
UI SurfaceBrief review panel (inline in calendar item detail view)

Suspend payload:

1{ 2 type: "brief-approval", 3 workflowRunId: string, 4 briefId: string, 5 planItemId: string, 6 message: string, 7}

Resume payload:

1{ 2 action: "approved" | "revision_requested", 3 revisionNotes?: string, // if revision_requested 4 editedOutline?: ContentBrief, // if human modified the brief before approving 5}

7.3 Gate 3: Draft Review

FieldValue
Suspend Event Typedraft-review
TriggerContent Layer workflow completes (draft + edits saved)
UI SurfaceContent editor (parent spec Section 7.2)

Suspend payload:

1{ 2 type: "draft-review", 3 workflowRunId: string, 4 draftId: string, 5 briefId: string, 6 planItemId: string, 7 editorNotes: EditorOutput, // Editor's assessment for human context 8 message: string, 9}

Resume payload:

1{ 2 action: "approved" | "rejected", 3 editedContent: string, // the human-edited markdown (with images placed) 4 imagesPlaced: Array<{ 5 marker: string, // original [IMAGE: ...] marker text 6 imageUrl: string, 7 altText: string, 8 width: number, 9 height: number, 10 }>, 11 rejectionNotes?: string, // if rejected — sent back to Content Layer 12}

7.4 Resume Mechanism

The Next.js application calls the Mastra server's workflow resume endpoint. Mastra's built-in suspend/resume handles the state management — the workflow run is persisted to storage while suspended and rehydrated on resume.

The Next.js app needs a mapping of active workflow runs to their current suspend state. This is queried via:

1GET /api/workflows/{workflowId}/runs?status=suspended

Returns all suspended runs with their suspend payloads, allowing the UI to render the correct review interface per item.


8. Post-Publish Pipeline

8.1 Design Decision: Trigger.dev Job Chain

The post-publish pipeline has no human gates — it is fully automated. This makes it a better fit for a Trigger.dev job chain than a Mastra workflow. Trigger.dev provides retry-on-failure, job status monitoring, and scheduled execution, which are the primary requirements here.

8.2 Pipeline Specification

The pipeline is triggered by Tool P5 (triggerPostPublishPipeline) when the Publishing agent completes.

1Event: page.published 2Payload: { pageId, url, isRefresh } 3 4Job 1: extract-new-page 5 → POST /extract to LangExtract sidecar with document_type=our_page 6 → Write results to our_page_extractions 7 → Emit: extraction.complete { pageId } 8 9Job 2: generate-page-summary (triggered by extraction.complete) 10 → Generate Level 2 page summary from extraction entities 11 → Write to page_summaries 12 → Emit: summary.page.complete { pageId, clusterId } 13 14Job 3: regenerate-cluster-summary (triggered by summary.page.complete) 15 → Regenerate Level 1 cluster summary for the affected cluster 16 → Write to cluster_summaries 17 → Emit: summary.cluster.complete { clusterId } 18 19Job 4: regenerate-domain-summary (triggered by summary.cluster.complete) 20 → Regenerate Level 0 domain summary 21 → Write to domain_summaries 22 23Job 5: build-graph-edges (triggered by extraction.complete) 24 → Runs in parallel with Jobs 2-4 25 → Build covers_topic, targets_keyword, links_to edges from extraction data 26 → Run should_link_to analysis: find existing pages that should link to new page 27 → Write all edges to content_relationships 28 29Job 6: execute-bidirectional-linking (triggered by build-graph-edges.complete) 30 → For each should_link_to edge: queue the source page for a light content update 31 that adds an internal link to the new page 32 → This is a lightweight, automated edit — not a full refresh cycle

8.3 Failure Handling

Each job has a retry policy: 3 retries with exponential backoff (10s, 30s, 90s). If a job fails after all retries, it is marked as failed and an alert is sent to Slack. Downstream jobs that depend on the failed job do not run. The pipeline can be manually re-triggered from the dashboard for the failed job onward.

Critical: The publishing action is NOT rolled back on pipeline failure. The content is live. The pipeline failure means the system's internal data (summaries, graph, extractions) is temporarily out of sync with the published content. This is acceptable because the next scheduled data job will catch up, and the content itself is not affected.


9. Error Handling & Step Limits

9.1 maxSteps Policy

maxSteps is a safety boundary, not a performance target. When an agent hits its step limit, Mastra forces the agent to stop and return whatever it has. This is a partial result.

What happens when an agent hits maxSteps:

  1. The workflow step captures the partial output.
  2. The step status is set to failed_max_steps.
  3. The partial output is logged to the audit trail with the full tool call history (which tools were called, in what order, how many tokens consumed).
  4. A Slack notification is sent: "Agent [name] hit step limit ([N] steps). Partial output saved. Review required."
  5. The workflow marks the current run as failed and does not proceed to the next step.

This is not a silent failure. The human sees it in the dashboard and can either re-run the workflow (the agent may succeed on retry with different reasoning) or manually complete the task.

9.2 Why maxSteps Matters

Consider the Content Strategy agent with maxSteps: 20. Its expected tool call pattern uses ~9 tool calls. The 20-step budget provides margin for:

  • Retrying a tool call that returned empty results with different parameters
  • Making additional readPageSummaries calls to drill into specific clusters
  • Running extra webSearch calls to validate opportunities
  • The final structured output generation call

If the agent wastes steps (e.g., calling readDomainSummary multiple times with the same parameters, or searching for irrelevant topics), it may exhaust its budget before completing. This is a prompt engineering problem, not a system problem. The audit trail (Section 10) makes it visible which tools were called and in what order, enabling prompt iteration.

After the first 50 workflow runs, review the audit trail to determine actual step usage per agent. If the median step count for an agent is less than 50% of its maxSteps budget, the budget can likely be reduced. If any agent hits maxSteps more than 5% of the time, either the budget needs to increase or the agent's instructions need refinement.

9.4 Other Failure Modes

FailureHandling
Tool returns error (API down, rate limit, etc.)Agent sees the error message and can decide to retry or use alternative approach. If the tool is critical and the agent cannot proceed, it should indicate this in its output.
LLM returns malformed structured outputZod validation catches this. The workflow step retries the agent call once with a note appended: "Your previous response did not match the required output schema. Please ensure your response exactly matches the schema." If retry fails, step fails.
Workflow step timeoutSteps have a 5-minute timeout (configurable per step). On timeout, same handling as maxSteps — partial output saved, step marked failed, notification sent.
LangExtract sidecar unavailableCircuit breaker pattern (Addendum #1, Section 6.2). Post-publish pipeline jobs queue for retry. No data loss.
Supabase connection failureStandard retry with backoff. If persistent, all workflows pause and alert fires.

10. Audit Trail

10.1 Design Purpose

The audit trail serves three functions: debugging (why did the agent make that decision?), cost tracking (how many tokens did this workflow run consume?), and quality improvement (which agents waste steps or make poor tool choices?).

10.2 Schema

1CREATE TABLE agent_audit_log ( 2 id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 3 4 -- Context 5 workflow_id VARCHAR(50) NOT NULL, 6 workflow_run_id VARCHAR(100) NOT NULL, 7 step_id VARCHAR(50) NOT NULL, 8 agent_id VARCHAR(50) NOT NULL, 9 model_id VARCHAR(100) NOT NULL, 10 11 -- Timing 12 started_at TIMESTAMP NOT NULL, 13 completed_at TIMESTAMP, 14 duration_ms INTEGER, 15 16 -- Result 17 status VARCHAR(20) NOT NULL, -- 'success', 'failed', 'failed_max_steps', 'timeout' 18 steps_used INTEGER NOT NULL, 19 max_steps_configured INTEGER NOT NULL, 20 21 -- Token Usage 22 input_tokens INTEGER NOT NULL DEFAULT 0, 23 output_tokens INTEGER NOT NULL DEFAULT 0, 24 total_tokens INTEGER NOT NULL DEFAULT 0, 25 estimated_cost_usd FLOAT DEFAULT 0.0, 26 27 -- Tool Calls (ordered sequence of tool invocations) 28 tool_calls JSONB NOT NULL DEFAULT '[]', 29 -- Structure: [{ tool_name, input_summary, output_summary, 30 -- duration_ms, tokens_used, step_number }] 31 32 -- Output 33 output_summary TEXT, -- truncated agent output for quick review 34 full_output_ref VARCHAR(200), -- S3/storage reference for full output if large 35 36 -- Error 37 error_message TEXT, 38 error_type VARCHAR(50), 39 40 created_at TIMESTAMP DEFAULT now() 41); 42 43CREATE INDEX idx_audit_workflow_run ON agent_audit_log(workflow_run_id); 44CREATE INDEX idx_audit_agent ON agent_audit_log(agent_id); 45CREATE INDEX idx_audit_status ON agent_audit_log(status); 46CREATE INDEX idx_audit_date ON agent_audit_log(created_at); 47CREATE INDEX idx_audit_cost ON agent_audit_log(estimated_cost_usd);

10.3 What Gets Logged

Every agent invocation produces exactly one agent_audit_log row. The tool_calls JSONB array captures the full sequence of tool invocations within that agent run:

1[ 2 { 3 "step_number": 1, 4 "tool_name": "readDomainSummary", 5 "input_summary": "{ scope: 'combined' }", 6 "output_summary": "Returned domain summary, 487 tokens", 7 "duration_ms": 120, 8 "tokens_used": 487 9 }, 10 { 11 "step_number": 2, 12 "tool_name": "queryCompetitorChanges", 13 "input_summary": "{ since: '2026-02-27', changeType: 'new' }", 14 "output_summary": "Returned 12 changes across 4 competitors", 15 "duration_ms": 85, 16 "tokens_used": 1240 17 } 18]

input_summary and output_summary are truncated representations (max 500 characters each) to keep the JSONB field manageable. Full tool inputs and outputs are available in the Mastra observability logs for detailed debugging.

10.4 Cost Tracking

estimated_cost_usd is calculated at log time using the model ID and token counts. The calculation uses a cost lookup table:

1CREATE TABLE model_pricing ( 2 model_id VARCHAR(100) PRIMARY KEY, 3 input_cost_per_1m FLOAT NOT NULL, -- USD per 1M input tokens 4 output_cost_per_1m FLOAT NOT NULL, -- USD per 1M output tokens 5 effective_date DATE NOT NULL, 6 notes TEXT 7);

This table is manually maintained when pricing changes. The audit trail cost is an estimate — actual billing may differ due to caching, batching, or pricing tier changes.

10.5 Dashboard Queries

The audit trail supports the following dashboard views:

Cost by period: SELECT SUM(estimated_cost_usd), date_trunc('day', created_at) FROM agent_audit_log GROUP BY 2

Agent efficiency: SELECT agent_id, AVG(steps_used), AVG(max_steps_configured), COUNT(*) FILTER (WHERE status = 'failed_max_steps') FROM agent_audit_log GROUP BY agent_id

Workflow run detail: SELECT * FROM agent_audit_log WHERE workflow_run_id = ? ORDER BY started_at

Slowest agents: SELECT agent_id, AVG(duration_ms), MAX(duration_ms) FROM agent_audit_log GROUP BY agent_id ORDER BY 2 DESC


11. Brand Voice & Prompt Architecture

11.1 Prompt Assembly

Agent instructions are assembled at invocation time from multiple sources. The assembly is performed in the workflow step before the agent is created, not inside the agent definition itself.

1┌──────────────────────────────────────────────┐ 2│ Final Agent Prompt │ 3│ │ 4│ ┌─────────────────────────────────────────┐ │ 5│ │ 1. Base Instructions (static) │ │ 6│ │ Role definition, behavioral rules, │ │ 7│ │ output format requirements │ │ 8│ └─────────────────────────────────────────┘ │ 9│ ┌─────────────────────────────────────────┐ │ 10│ │ 2. Brand Voice (from DB, ~500 tokens) │ │ 11│ │ Tone markers, vocabulary, patterns, │ │ 12│ │ words to avoid │ │ 13│ └─────────────────────────────────────────┘ │ 14│ ┌─────────────────────────────────────────┐ │ 15│ │ 3. Strategy Context (from DB, ~500 tok) │ │ 16│ │ Pillars, priorities, audience │ │ 17│ └─────────────────────────────────────────┘ │ 18│ ┌─────────────────────────────────────────┐ │ 19│ │ 4. Task-Specific Context (variable) │ │ 20│ │ Brief, intel reports, etc. │ │ 21│ └─────────────────────────────────────────┘ │ 22└──────────────────────────────────────────────┘

11.2 Brand Voice Settings

Brand voice configuration is managed in the Strategy Settings UI (parent spec Section 7.3). The settings capture two types of data:

Human-entered settings (stored in content_strategy):

  • Brand voice guidelines (free text)
  • Target audience description
  • Content pillars
  • Words/phrases to always avoid
  • Preferred terminology mappings (e.g., "always say 'team members' not 'employees'")

LangExtract-derived voice data (stored in brand_voice_extractions):

  • Tone markers extracted from sample content
  • Vocabulary preferences extracted from sample content
  • Sentence patterns extracted from sample content

Both are loaded and combined when assembling prompts for the Writer and Editor agents. The human-entered settings take precedence — if a human says "never use the word 'leverage'" but the extracted samples contain it, the human rule wins.

11.3 Sample Content Management

The Strategy Settings UI includes a "Voice Samples" section where the content lead uploads 5-10 representative pieces that define the target voice. When samples are uploaded or changed, the system triggers a LangExtract brand_voice extraction (Addendum #1, Section 3.5) and stores the results.

The UI displays the extracted tone markers, vocabulary, and patterns so the content lead can verify the extraction captured the intended voice. If the extraction misses something, the human adds it manually in the settings.

11.4 Prompt Size Budget

All agents operate well within Sonnet's 200K context window. The largest prompt is the Writer agent for a comprehensive guide:

ComponentEstimated Tokens
Base instructions~800
Brand voice data~500
Strategy context~500
Full content brief (guide, 5,000 words target)~3,000
Tool results accumulated during execution~4,000
Total~8,800

This is under 5% of the context window. Prompt size is not a constraint for any current agent.


12. Open Questions for Review

#QuestionImpactRecommended Default
1Should the audit trail store full tool inputs/outputs or only summaries? Full storage enables replay but increases storage cost.Debugging depth vs. storageSummaries in Postgres, full data in Mastra's built-in observability logs (time-limited retention).
2The post-publish pipeline uses Trigger.dev job chains. Should we use Mastra workflows instead for consistency, even though there are no human gates?Architectural consistency vs. simplicityTrigger.dev. No human gates = no need for suspend/resume = simpler job chain.
3Should agents be allowed to call other agents directly (supervisor pattern) for any use case, or should all inter-agent communication go through workflow state?Flexibility vs. traceabilityWorkflow state only for MVP. Supervisor pattern is a future consideration if we need dynamic task decomposition.
4The revision loop is bounded at 2 cycles. Should this be configurable per content type (e.g., guides get 3 cycles, glossary gets 1)?Quality vs. costFixed at 2 for MVP. Review after 50 pieces to see if any content type consistently needs more.
5Agent temperature is set per agent in the config table. Should temperature also be adjustable per content type (e.g., Writer at 0.8 for blog posts but 0.6 for case studies)?Granularity vs. complexityPer-agent only for MVP. Per content-type temperature adds schema and UI complexity with uncertain benefit.
6The SEO Auto-Fix agent currently has access to setMetadata only. Should it also be able to make minor content body edits (e.g., inserting a keyword in the first paragraph)?Auto-fix scopeYes — give it setMetadata plus a patchContentBody tool that can make targeted string replacements (not full rewrites). Define the patch tool in implementation.
7When a human rejects a draft at Gate 3, should the content go back to the Content Layer (Writer + Editor) or to a simpler "revision" workflow that only involves the Writer?Pipeline complexityBack to the full Content Layer. The Editor acts as quality gate on revisions too. The human's rejection notes are passed as additional context.

End of Addendum #3

Next addendum: Hierarchical Summary Generation — defining how LangExtract extraction outputs are aggregated into navigable Level 0–3 summaries for agent consumption.