Skip to Content
AgentsBlog Writer

Blog Writer

[Live] · agent__blog-writer · Claude Sonnet 4.6

Writes a complete, SEO-optimised long-form blog post from an approved content brief, producing publication-ready Markdown with internal links, structured headings, and a meta description.


Overview

FunctionWrite a full SEO-optimised long-form blog post from an approved content brief
TypeWorker — Content
ModelClaude Sonnet 4.6
Queueagent__blog-writer
Concurrency1
Timeout10 min
Lock duration12 min
ToolsNone (allowedTools: [])
Max turns1
Est. cost / task~$0.31
PlanFree+

Triggers

Trigger typeWhenWho initiates
Activity Planner dispatchAfter Content Brief Writer completes (and optionally Research Note Writer completes) — dependsOn both upstream jobsActivity Planner
Human on-demandUser clicks “Write post” in DM Portal content tab or Dashboard blog management screenTenant admin / DM reviewer
Scheduled / cronNot applicable — blog writing is always triggered by a brief or manual request

Input

interface BlogWriterInput { tenantId: string; campaignId?: string; contentBrief: { workingTitle: string; angle: string; primaryKeyword: string; secondaryKeywords: string[]; searchIntent: 'informational' | 'navigational' | 'transactional' | 'commercial'; recommendedOutline: string; // H2/H3 outline from Content Brief Writer internalLinkTargets?: string[]; // page URLs or slugs from Website Content RAG competitorUrls?: string[]; // top-ranking URLs to differentiate from }; researchNotes?: string; // Markdown from Research Note Writer — optional but improves quality targetWordCount: number; // from brief, typically 1200–2500 publishTarget: 'wordpress' | 'webflow' | 'other'; }

Output

interface BlogWriterOutput { title: string; metaDescription: string; // 140–160 characters slug: string; // URL-safe, keyword-first bodyMarkdown: string; // full post body — H2/H3 structure, no H1 (title handles that) internalLinksUsed: string[]; // URLs or slugs of pages linked within the body estimatedReadTime: number; // minutes, calculated at 200 wpm wordCount: number; seoScore: number; // 0–100 composite: keyword density, heading structure, meta quality, internal links }

Sample output excerpt

**Title:** Why Your Google Ads Aren't Converting (And How to Fix It in 7 Steps) **Meta description:** Most Google Ads campaigns bleed budget on the wrong keywords. Here are the 7 fixes that consistently turn underperforming campaigns around — no agency required. **Slug:** google-ads-not-converting-fixes --- ## Why Most Google Ads Campaigns Underperform The average Google Ads account wastes 76% of its budget on irrelevant clicks. That's not a fringe statistic — it's the median finding across audits of 500 SMB accounts conducted by WordStream in 2024. If your cost-per-lead has been climbing and your conversion rate hasn't budged, you're almost certainly in that majority. The good news: the causes are predictable and the fixes are systematic. Before we get into the seven steps, it's worth understanding *why* this happens. Google's default campaign settings are optimised for Google's revenue, not yours. Broad match keywords, auto-applied recommendations, and Smart Campaigns all expand your reach — and your spend — without expanding your results. ## Step 1: Audit Your Search Term Report Weekly Your search term report is the most valuable report in Google Ads, and most advertisers check it monthly at best. It shows you exactly what people typed before clicking your ad — and in most accounts, at least 30% of those terms have nothing to do with your business. **What to do:** Open Search terms → filter for the last 30 days → sort by cost → add any irrelevant term as a negative keyword immediately. Do this every Monday morning. It takes 12 minutes and pays for itself in days. > "We cut a client's wasted spend from $4,200/month to $800/month in six weeks using nothing > but weekly negative keyword management." — [Internal case study, Acme Agency, 2025]

How It Works

  1. Load brief and research context. The content brief (title, angle, keyword, outline) and optional research notes are injected into the system prompt. Research notes — if present — are treated as the primary factual source; the agent writes to the outline using those notes as scaffolding.

  2. Pre-load RAG context (worker code). Before Claude is invoked, the worker runs a single search() call with query "{activityLabel} blog post" and topK: 6. Results from client_docs, website_content, and published_content are injected into the prompt as a KNOWLEDGE BASE CONTEXT section. This is not a tool call — the context is pre-loaded before execution begins. If no chunks are found, the worker logs a warning and continues without RAG context.

  3. Draft the post. Write the full post body following the approved H2/H3 outline. The intro must hook within 100 words, each section must have a clear value statement, and the conclusion must include a CTA. Prohibited words from tenant settings are filtered at generation time.

  4. Generate metadata. After the body is complete, generate the title, metaDescription, and slug — each optimised for the primary keyword. Meta description is written to maximise click-through from SERPs, not to summarise the article.

  5. Calculate SEO score and read time. Run post-generation checks: keyword in title, keyword in first 100 words, keyword density 0.5–2.0%, ≥1 internal link, meta description 140–160 chars, H2 structure present. Score 0–100, with per-check breakdown available in logs.


System Prompt

You are an expert SEO content writer working for a digital marketing agency. Your job is to write a complete, publication-ready long-form blog post for a client based on an approved content brief and research notes. CLIENT CONTEXT: {{CLIENT_CONTEXT}} TENANT SETTINGS: {{TENANT_SETTINGS}} KNOWLEDGE BASE CONTEXT (competitor angles, internal links, brand facts): {{RAG_CONTEXT}} CONTENT BRIEF: {{BRIEF}} RESEARCH NOTES: {{RESEARCH_NOTES}} Your post must: 1. Open with a hook that earns the reader's attention within 100 words — a striking statistic, a counterintuitive claim, or a sharp problem statement. Do not start with "In today's..." 2. Follow the H2/H3 outline in the brief exactly — headings become the article's structure 3. Use the primary keyword naturally in: the title, the first 100 words, at least one H2, and the meta description. Target keyword density of 0.5–2.0% — never keyword-stuffed 4. Include facts and examples from the research notes. Cite sources inline in parentheses 5. Weave in 2–3 internal links to the pages identified in the RAG context — link naturally, never forced 6. Match the client's brand voice exactly as described in the client context 7. Write to the target word count (±10%) — do not pad with filler; every paragraph must earn its place 8. Close with a clear, low-pressure CTA that matches the post's search intent 9. Do not mention competitor brand names unless the brief explicitly calls for comparison content 10. Avoid all prohibited words listed in tenant settings After the body, output the title, meta description (140–160 chars), and slug on separate lines. Then output a JSON block with: wordCount, internalLinksUsed[], estimatedReadTime, seoScore. Write for humans first. SEO second. A post that no one reads cannot rank.

Skills Injected

Skill filePurpose
client-context-file.mdAlways injected — company, brand voice, audience, competitors
long-form-writing-guide.mdBlog post structure, intro/hook patterns, SEO writing rules, CTA formats
seo-content-rules.mdKeyword placement, heading hierarchy, meta description formula, internal linking rules

long-form-writing-guide.md — content

# Long-Form Writing Guide ## Post Structure Every blog post must follow this skeleton. Deviate only when the brief explicitly calls for it. ### Opening (100–200 words) - Lead with a hook: a specific statistic, a provocative question, or a sharp problem statement - Establish the problem or opportunity in 1–2 sentences - State what the reader will get from the article (the "payoff promise") - Do NOT start with "In today's digital world", "Are you looking for...", or any variant of "Welcome to our guide" ### Body sections (follow the H2/H3 outline) - Each H2 section = one complete idea, fully explained - Open each H2 with a 1–2 sentence orientation paragraph before diving into detail - Use numbered lists for steps/processes; bullet lists for features/options; prose for arguments - Use short paragraphs (2–4 sentences). One idea per paragraph. - Break walls of text with: subheadings, lists, pull quotes, or bold key statements - Include at least one concrete example per major section (named, specific, not hypothetical) ### Conclusion (100–150 words) - Summarise the 2–3 core takeaways — do not introduce new ideas - Close with a CTA that matches search intent: - Informational intent → "Subscribe for more guides like this" or "Download the checklist" - Commercial/transactional intent → "Book a free audit" or "Start your free trial" - Never use "Contact us today" as a standalone CTA — it is too generic ## Tone and Voice - Match the brand voice in the client context exactly - Default to: clear, direct, specific. Avoid corporate jargon and filler phrases - Passive voice: use sparingly. Active voice moves faster - Contractions: use them (they're, you'll, it's) unless brand voice is formally professional ## SEO Writing Rules - Primary keyword in: title, first 100 words, at least one H2, meta description - Do not repeat the exact primary keyword phrase more than once every 200 words - Use semantic variations: synonyms, related terms, question forms - Alt text for any images: descriptive, keyword-informed, under 125 chars - Internal links: 2–3 per post, anchor text is descriptive (never "click here") ## Prohibited Patterns - Do not open sentences with "Additionally,", "Furthermore,", or "In conclusion," - Do not use the phrase "dive deep" or "deep dive" - Do not end sections with "Let's explore..." or "Read on to find out..." - Do not use em dashes in a way that mimics AI-generated padding

RAG Usage

Mechanism: Pre-loaded (worker code runs search() before Claude starts — not a tool call).

DatasetIncludedPurpose
client_docsBrand voice, product details, case studies
website_contentExisting site context, service pages
published_contentPast posts — prevents duplication; maintains style continuity
competitor_contentNot used by blog-writer

Query: "{activityLabel} blog post" — one query, topK: 6, searched across all three permitted datasets in parallel.
Fallback: If the search fails or returns 0 chunks, execution continues without RAG context (warn logged).


HITL Gates

Blog posts pass through two sequential human approval gates before they can be published.

Status flow: dm_reviewclient_reviewclient_approved → (manual publish step)

Gate 1 — DM reviewer (POST /dm/v1/blog/:id/dm-approve)

  • DM reviewer reads the full Markdown post in the DM Portal blog screen.
  • Approve: Post moves to client_review. Client tenant admin is notified.
  • Reject: DM submits feedback text. Post is re-queued with wakeReason: "rejection" and reviewerFeedback injected into the prompt. The BlogPost version increments on the next generation.

Gate 2 — Client (Dashboard server action)

  • Client tenant admin sees the post under “Needs Approval” in Dashboard → Blog Posts.

  • Approve: The dashboard calls the clientApprovePost server action in apps/dashboard/src/app/(dashboard)/blog/actions.ts — it does NOT call the REST endpoint POST /tenant/v1/blog/:id/client-approve. The server action must update three records:

    1. BlogPost.status"client_approved"
    2. Activity.status"done"
    3. Deliverable.status"approved" via db.deliverable.updateMany({ where: { blogPostId: postId }, data: { status: "approved" } })

    Without the Deliverable sync the Deliverables page shows “Done: 0” even after the client approves.

  • Reject: Post returns to dm_review with the client’s rejection note. The DM team decides whether to re-run the agent or edit manually.


Guardrails

RuleEnforcement
Word count within ±10% of targetPost-generation check; if outside range, self-correct by expanding or trimming before returning output
Meta description 140–160 charactersHard character count check; regenerate meta if outside range
Primary keyword present in titleString match; fail SEO score check if absent
Primary keyword present in first 100 wordsString match on first 100 words of body
Minimum 1 internal link in bodyCheck internalLinksUsed[] length; warn if empty
No prohibited words from tenant settingsFilter prohibitedWords[] against full body text; flag any matches before returning output
No invented statistics or fabricated claimsResearch notes are the only factual source; any statistic not in research notes or RAG context must be flagged with “[VERIFY]” inline
Slug is URL-safe and keyword-firstRegex validation; auto-corrected if invalid characters present

Tenant Settings Used

SettingHow it’s used
brandVoiceInjected into system prompt — controls tone, formality, sentence style throughout the post
companyNameUsed in CTAs and any first-person company references
targetAudienceInforms reading level, example types, and assumed knowledge level
prohibitedWords[]Filtered from the generated body; any match triggers a warning in output
preferredKeywords[]Checked against body text — agent attempts to naturally include preferred terms
industryScopes RAG competitor research queries to the correct niche

Cost Profile

Avg input tokens~3,000 new + ~8,000 cache read (prompt caching active)
Avg output tokens~16,500 (full blog post + metadata, ~3,000–4,000 words)
Avg duration~3.5 min
Est. cost / task~$0.31

Performance & Tuning (Apr 2026)

Blog posts generate ~16.5K output tokens — one of the largest outputs in the pipeline. Tuning history:

ChangeBeforeAfterReason
timeoutSec300s600sActual runs take 3–4 min; 5-min timeout had no headroom and caused cascading retries
lockDuration360s720sMust exceed timeout to prevent stall detection mid-run
allowedTools(unset — all tools)[]Pure text generation; all context is pre-loaded; unexpected web searches add turns and latency
maxTurnsPerRun(unset)1Enforce single-turn generation; belt-and-suspenders with allowedTools: []

Why the retries piled up: The dependency resolver (packages/agents/src/lib/dependency-resolver.ts) enqueues all dependency-unblocked jobs with attempts: 6 and exponential backoff. Before the timeout was fixed, each 5-min timeout immediately triggered a retry, causing the same job to consume 30 minutes of queue time across 6 attempts.

Token counting note: The execution queue’s input-token count shows only the prompt-cache hits + new tokens combined (fixed Apr 2026 in extractUsage). The input_tokens field from Claude Code CLI reports only uncached new tokens (typically 3); the bulk of the prompt (~8K tokens) is served from cache and reported separately as cache_read_input_tokens.


Error Handling

ErrorResponse
RAG returns no Published Content resultsProceed without duplicate check; log “No published content found — duplicate check skipped”
RAG returns no Client Documents resultsProceed without brand facts; post uses only brief and research notes; note “No client documents found — verify product claims manually” in output
RAG returns no Website Content resultsProceed without internal links; set internalLinksUsed: []; SEO score penalised accordingly
Word count target is below 600 or above 5000Fail job with validation error: “Word count target out of range (600–5000)“
Post fails guardrail after 2 self-correction attemptsReturn best attempt with failing guardrail checks documented in output; create HITL record for manual review
Content brief is missing primaryKeywordFail job with validation error before any LLM call
Job exceeds 10-minute timeoutSave partial draft with note on where generation stopped; create HITL record for manual completion

© 2026 Leadmetrics — Internal use only