Autonomous · shipping weekly since Feb 2026

Eight AI agents that run a technical blog with nobody watching.

Find what buyers search for. Write about it credibly. Refuse to publish it if it isn't good enough. Ship it, syndicate it, then measure what happened — and feed that back in. Every week. No CMS logins, no editors, no calendar reminders.

pipeline_runner · run_id a3f9c2e1
$ python -m src.pipeline_runner
keyword_research 12 stored · 8.4s
topic_generation 1 brief · 11.2s
content_writer 2,418 w · 96.7s
seo_optimization score 74 · reject
  ↺ cycle 1/2 — returning to writer
content_writer 2,690 w · 84.1s
seo_optimization score 87 · pass
internal_linking 3 links · 0.3s
publishing live · 14.9s
distribution 3 targets· 6.2s
─────────────────────────────
run complete · 7 agents · 0 humans
01

Overview

A content supply chain, rebuilt as a chain of agents

Each one has a single job, a written contract, and a quality gate behind it. None of them can call each other.

8
Specialized agents — seven in the publishing chain, one measuring what happened afterwards. They coordinate exclusively through SQLite rows and a status column.
1 of 7–17
LLM calls per post that write the first draft. Every other call expands, reviews, rewrites or judges it — that ratio is the architecture.
121
Deterministic anti-AI-detection rewrite rules
495
Lines of versioned prompt specification
10
SQLite tables backing coordination
~6,400
Lines of production Python
$2 / $10
Hard spend ceiling per run / per day, checked before each API call
80
Blended quality score required to publish — 60% LLM judgment, 40% programmatic
0
Human touchpoints between a keyword and a published URL
The thesis

Getting an LLM to produce a plausible blog post takes an afternoon. Getting it to produce one that is long enough, structurally complete, keyword-correct, stylistically human, internally linked, correctly illustrated and verifiably published takes everything else. The generation call is the small part.The interesting engineering is routing each task to the right model, giving every agent a narrow contract, scoring output against something the generator doesn't control — and actually rejecting work that fails.

Why this had to exist

PROBLEM 01

Research was the bottleneck

Finding which long-tail variants deserve an article means hours in keyword tools per topic — and paid SEO suites bill per seat, per month, forever.

PROBLEM 02

Cadence collapsed under client work

An agency's writers are its engineers. One client deadline and publishing stops — but SEO only compounds if you never stop.

PROBLEM 03

Naive AI content is worse than none

Generic LLM output is legible to readers and to search engines. Publishing it under the banner of an AI agency is actively self-harming.

PROBLEM 04

Nothing closed the loop

Content shipped and was forgotten. Nobody went back for the page sitting at position 11 that needed one revision to reach page one.

02

Architecture

Seven agents, one feedback loop

Click any agent to inspect its contract, its use of AI, and how it fails.

↺  Rejection sends the post back to Agent 3 — max 2 cycles

Keyword Research

Agent 1 · Discovery · gpt-4o

Expands seed keywords through Google Autocomplete — direct suggestions plus a 26-letter alphabet expansion per seed — then hands the deduplicated pool to GPT-4o, which returns structured JSON scoring each keyword on relevance, commercial intent and feasibility, assigning a topical cluster, search intent and difficulty.

Only keywords clearing a composite score of 6.0 are stored.

AI roleScoring, clustering, intent classification — structured JSON
Skips whenQueue already holds 30 unused keywords
Guardrails50 requests/run, 0.8–1.5s jitter, rotating UAs, stop-on-429
Data cost$0 — Autocomplete replaces a paid SEO suite

The orchestrator is deliberately thin

pipeline_runner.py acquires an exclusive flock so a manual trigger can never race the cron job, fetches the sitemap once and injects it into shared config (three downstream agents need internal-link candidates; one HTTP call serves all of them), assigns a run ID, then invokes each agent inside an error-isolating wrapper. One quiet behaviour matters more than it looks: before running the writer it checks queue depth, and if fewer than five keywords remain it forces a research pass first. The pipeline refills its own inputs.

# The chain — and the loop that makes it more than a conveyor belt
KeywordResearch → TopicGeneration → ContentWriting
    → SEOOptimization ⇄ ContentWriting   # max 2 revision cycles
    → InternalLinking → Publishing → ContentDistribution

# Any subset runs standalone — the status column makes it resumable
$ python -m src.pipeline_runner writer seo linker publisher

The post lifecycle

State lives in a column, not in memory. A run that dies halfway through resumes exactly where it stopped — the next invocation simply finds rows in the appropriate status.

draftneeds_revisiondraftseo_passreadypublishingpublisheddistributed|failed
TableRoleWritten by
keywordsScored queue with cluster, search intent, difficultyAgent 1
blog_topicsEditorial briefs — outline JSON, LSI terms, meta, CTAAgent 2
postsContent plus quality, SEO, density and revision metricsAgents 3–6
pipeline_runsPer-agent execution log: status, duration, tokens, cost, errorsBaseAgent
cost_trackingDaily per-model token and spend ledgerLLM wrapper
distribution_logsPer-platform syndication attempts and outcomesAgent 7
post_performanceGSC impressions, clicks, CTR, position, top queriesAgent 8
striking_distanceQueries ranking 5–20 — the optimization backlogAgent 8
content_decayPages losing impressions — the refresh backlogAgent 8
schema_versionMigration trackingInit
Why SQLite, not a queue

A single-writer, cron-driven pipeline handling one post per run has no need for a database server. SQLite gives transactional handoffs, zero operational surface, and — critically — a state file small enough to persist between stateless GitHub Actions runs through the cache API. A status column bought resumability, inspectability, selective re-runs and crash recovery for a fraction of the complexity of a workflow engine.

03

AI Engineering

Where the difficulty actually lives

Model routing, prompt architecture, multi-pass generation chains — and convincing an LLM to reliably reject its own kind of work.

3.1  Every call declares a task. The task picks the model.

All eight agents share one OpenAI wrapper, but no call names a model directly — it names a task, and the task resolves to a model, a token ceiling and a temperature. Creative work goes to GPT-4o at high temperature. Evaluative and formatting work goes to GPT-4o-mini at low temperature and roughly a sixteenth of the price.

TaskModelMax tokensTempRationale
Keyword researchgpt-4o4,0960.5Nuanced commercial-intent judgment across ~100 keywords in one call
Topic generationgpt-4o4,0960.5Structured editorial planning, JSON mode enforced
Blog draftinggpt-4o12,0000.8Long-form generation needs headroom and creative latitude
Draft expansiongpt-4o12,0000.85Higher temp so it doesn't just regurgitate the short draft
Quality reviewgpt-4o12,0000.4Editorial correction wants precision, not invention
Humanize rewritegpt-4o12,0000.9Highest temperature in the system — variance is the goal
SEO scoringgpt-4o-mini4,0960.5Rubric-following against a fixed schema; cheap enough to run on every draft
Distribution copygpt-4o-mini2,0480.5Short-form reformatting of content that already exists

When one model both writes and grades, it reliably approves its own tics. Splitting them is the single highest-leverage decision in the system.

3.2  The four-step writing chain

No single call produces a 2,500-word article that is simultaneously long enough, structurally complete, editorially sharp and stylistically human. Each property gets a dedicated pass with its own prompt, persona and temperature — and a verification step behind it.

t 0.8
STEP 1 · AI
Draft
Full article from the outline, with live sitemap URLs supplied for natural in-paragraph linking.
t 0.85
STEP 1b · CONDITIONAL
Expand
Fires only if measured word count is under the floor: “make each section 2–3× longer, do not summarize.”
t 0.4
STEP 2 · AI
Editorial review
A skeptical senior editor persona hunts and rewrites AI tells, generic claims and structural monotony.
t 0.9
STEP 3 · AI
Humanize rewrite
A ghostwriter persona rebuilds rhythm: fragments, asides, opinions, deliberate imperfection.
code
STEP 4 · DETERMINISTIC
Post-processing
121 regex rewrites, paragraph-starter variation, contraction injection, block-breaking, HTML cleaning, burstiness scoring, keyword-placement repair — then re-expansion if humanization shrank the post.
Step 4 is where the system stops trusting the model

Word count is measured, not assumed. Keyword presence in the title, the first 100 words and the meta description is verified and repaired in code if the model drifted. Every pass that can shorten an article is followed by a re-measurement, and expansion re-fires if needed. An LLM will confidently misreport anything countable.

3.3  Prompts are source code

Not one prompt is embedded in Python. All eight live in config/prompts/*.txt — 495 lines of specification that diff, review and roll back like any other source file. Runtime context (keyword, outline, word targets, sitemap URLs, prior rejection reasons) is injected as the user message; the file is always the system message. Most quality improvements in this project were prompt commits, not code commits.

# config/prompts/blog_writer_draft.txt — excerpt

STRICT WRITING RULES — FOLLOW THESE EXACTLY:

1. BANNED WORDS AND PHRASES — Never use any of these:
   "delve", "leverage", "utilize", "facilitate", "paradigm shift"
   "in today's rapidly evolving", "in the ever-changing landscape"
   "game-changer", "revolutionary", "cutting-edge"
   "unlock the power", "harness the potential", "supercharge"
   ...

MANDATORY SECTIONS: Introduction, Problem, Architecture, Implementation,
Code Example, Tech Stack, Performance, Common Mistakes, Use Cases,
FAQ (3-5 Q&A), Conclusion, CTA.
04

Live Demo

The Humanizer Lab

This runs the real thing. All 121 production regex rules and the actual burstiness formula, extracted from src/utils/text_humanizer.py and executed in your browser. Type anything, or load a sample.

Anti-AI-detection pipeline

121 ruleslive

Struck-through red = pattern detected. Green = the deterministic replacement.

Input
After deterministic pass
In today's rapidly evolvingin the current landscape of software development, it's important to note that AI agents are a game-changerbig improvement. Let's dive in. Organizations looking to leverageuse cutting-edgemodern technology must utilizeuse robustsolid, scalableflexible solutions to facilitateenable seamlesssmooth integration. Furthermore, Also,this comprehensive guideguide will delveexplore into everything you need to know aboutunderstanding building state-of-the-artmodern AI systems. Moreover, Andit is important to note thatnote that a holisticcomplete approach can unlock the power ofuse your existing infrastructure. Additionally, Also,these actionablepractical insights will empowerenable your team to harness the potential ofuse this revolutionarynew paradigmapproach shiftmajor change.
27
AI patterns caught
0.60
Burstiness (warn < 0.30)
88
Words
6
Sentences analysed
0.0
Quality score (0–10)

✗ 27 patterns rewritten deterministically
✓ Burstiness 0.60 — sentence rhythm is within human range
quality_score = 10 − (27 × 0.5) − 0.00 = 0.0

Why burstiness?

Burstiness is the coefficient of variation of sentence lengths. Human prose swings — a nine-word sentence followed by a forty-word one. LLM prose settles into a metronome around 15–25 words and stays there. That uniformity is one of the most reliable machine signatures there is, and unlike vocabulary it survives paraphrasing.

# src/utils/text_humanizer.py — measured, not asserted
cv = std_dev(sentence_word_counts) / mean(sentence_word_counts)
score = min(1.0, cv / 0.6)      # 0.0 = metronome, 1.0 = human

if burstiness < 0.3:
    logger.warning("Low burstiness (%.2f) — may sound AI-generated", burstiness)

quality_score = 10 - (ai_patterns_remaining * 0.5) - burstiness_penalty

Three defenses stack, in increasing order of trustworthiness: instruction (the banned-words prompt), LLM rewriting (the temperature-0.9 humanize pass, which is told to add at least five sentences under eight words and two over forty), and deterministic enforcement(what you just ran). Only the last one can't be ignored by a model having an off day.

05

Live Demo

The Quality Gate

How a machine decides that machine-written content isn't good enough to publish. This simulator uses the exact scoring logic from seo_optimization_agent.py — toggle failures and watch the verdict flip.

SEO Optimization Agent — verdict simulator

40% programmatic60% LLM

Toggle defects the deterministic checks would find, and set what the LLM judge scored.

91
BLENDED SCORE · THRESHOLD 80
PASS

programmatic = 100 (no deductions)
programmatic = 100
overall = int(85 × 0.6 + 100 × 0.4) = 91

Two rules override the arithmetic

Missing mandatory sections are an automatic rejection, whatever the blended score says — you cannot buy your way past a missing FAQ with good prose. And after two failed revision cycles the post ships with a logged warning, because an unbounded quality loop is a pipeline that never publishes. Bounded autonomy over perfect autonomy.

CheckEnforced byThreshold
Word countCode — at 3 points in the chain2,000–3,000 target · 1,500 hard floor
Keyword densityCode (BeautifulSoup text extraction)0.5% – 2.5%
Keyword in title / intro / metaCode — auto-repaired if missingMandatory
Mandatory sectionsCode (marker matching) + LLM11 sections, all required
Heading countCode (regex over H2–H6)≥ 5
HTML validityCodeNo malformed structure
AI pattern countCode (121 compiled patterns)Scored into quality metric
BurstinessCode (sentence-length CV)Warn below 0.30
Overall SEO scoreHybrid 60 / 40≥ 80 to publish
Internal linksCode — pillar-first, cluster-awareUp to 3
06

Live Demo

The economics of autonomy

Autonomous LLM systems fail expensively before they fail loudly. Every call is costed from live token usage, attributed to a model, written to a daily ledger — and limits are checked before the next call is made.

Cost model

estimate

Real published pricing, transparent token assumptions. Adjust and see where the money goes.

Assumes 3 long-form gpt-4o passes per post (draft · review · humanize),
2 short gpt-4o calls (research · topic), 2 gpt-4o-mini calls (judge · distribution).
Each revision cycle adds 2 long-form 4o passes + 1 mini re-judge.
Estimate — the ledger in cost_tracking holds the real figures.

$0.23
Per published post
$0.98
Per month, all-in
8.5
LLM calls per post
OK
vs. $2 per-run ceiling

gpt-4o  $2.50 in / $10.00 out per 1M
gpt-4o-mini  $0.15 in / $0.60 out per 1M

Pre-call limit checks

Exceeding $2 in a run or $10 in a day raises CostLimitExceeded and halts generation — checked before the request goes out, not after the bill arrives.

Bounded retries

Rate limits and 5xx back off exponentially (2s → 4s → 8s, capped at 60s) for at most three attempts. Client errors are never retried — a malformed request retried three times is three times the bill for the same failure.

Attributed ledger

Every call writes model, task, input/output token split and estimated cost to cost_tracking, keyed by date and model. Spend is queryable per agent, per day.

Free where it counts

Keyword discovery runs on Google Autocomplete plus LLM scoring rather than a subscription SEO suite. The data source costs nothing; the intelligence layer costs cents.

07

Reliability

An unattended system is defined by how it fails

Nobody is watching at 06:00 UTC on a Monday. Every failure mode has to have a decided-in-advance answer.

🔒 Exclusive run lock

fcntl.flock on a lock file. A second concurrent run exits cleanly rather than corrupting shared state — the manual-trigger-during-cron scenario.

🛡 Per-agent isolation

Every agent runs inside a wrapper that catches everything, records crashed with the exception to pipeline_runs, and lets the rest of the chain continue.

📡 Pre-flight checks

If WordPress is unreachable the publisher refuses to start, leaving posts in ready. Nothing is lost; the next run picks them up.

⚡ Transient vs. terminal

Connection errors and timeouts revert a post to ready for automatic retry. Genuine failures mark it failed so it stops consuming attempts.

✓ Publication verification

After creating a post the client re-fetches it by ID. A 201 that didn't actually persist is treated as a failure, not a success.

↓ Graceful degradation

No image? Publish without it. Link injection failed? Mark ready anyway. Platform down? Log per-platform and continue. Nothing optional blocks the critical path.

↩ Rollback on partial work

If topic generation fails mid-flight the keyword reverts from in_progress to new. Failed work never silently consumes inputs.

👁 Full run observability

Every execution records start, finish, status, records processed, tokens, cost and error text — queryable history, not scrollback in a log file.

Deployment

The pipeline runs on GitHub Actions — Mondays at 06:00 UTC, capped at 15 minutes, with a workflow_dispatch trigger exposing each agent and useful combinations (writer,seo,linker,publisher) as dropdown options. The SQLite file is restored from the Actions cache before the run and saved afterwards with if: always(), so state survives even when the pipeline fails. Logs upload as artifacts with 30-day retention. Secrets are injected as environment variables and resolved through *_env key indirection in YAML, so no credential ever touches the repository. A Dockerfile, Procfile, Railway config and APScheduler-based in-process scheduler exist for running the same pipeline as a long-lived service instead.

08

War Stories

Nine problems that only appear at 3am

Every one of these was found in production, by a run nobody was watching.

01 Posts kept coming back short

Asking for 2,000–3,000 words reliably produced 1,200. Worse: the humanization pass shrank posts further while “tightening” them — the fix for one problem was quietly causing another.

FixWord count measured after every pass, with conditional expansion re-firing at three separate points in the chain. Explicit per-section length budgets in the prompt. And an instruction in the humanize step to never go below the current count.
02 Content read as AI-written

Uniform sentence rhythm, identical paragraph openers and a recognizable stock vocabulary survived every prompt-level instruction not to use them. Telling a model to avoid its own defaults only partly works.

FixThree stacked defenses — banned-vocabulary prompting, a dedicated temperature-0.9 humanize rewrite, and 121 deterministic regex rewrites plus structural passes. Burstiness measured and scored so style became a number rather than an opinion.
03 The keyword kept falling out

Each rewrite pass had a chance of dropping the primary keyword from the title, the intro or the meta description — the three places it matters most. Four passes meant four chances to lose it.

Fix_enforce_keyword_placement() verifies all three after generation and repairs them in code: injecting into the title, prepending a natural mention to the first paragraph, appending to the meta description inside the 160-character limit.
04 WordPress was intermittently unreachable

Posts that had cost real money to generate were being marked failed because of transient network conditions on an ephemeral CI runner.

FixPre-flight connectivity check before publishing starts. Connection errors and timeouts revert posts to ready rather than failed. Post-creation verification by re-fetch, so a phantom success is caught.
05 Google rate-limited the scraper

Alphabet expansion issues up to 27 requests per seed keyword. With nineteen seeds, aggressive crawling drew 429s fast.

FixRandomized 0.8–1.5s delays, rotating user agents, a hard 50-request-per-run ceiling, and immediate stop-on-429 rather than retry — backing off from a rate limit by retrying is how you get blocked.
06 Quality loops could run forever

A post that keeps failing review keeps getting rewritten. The article may simply be at a local maximum — and the loop will happily burn the daily budget discovering that.

FixHard cap of two revision cycles, after which the post publishes with a logged warning. The failure mode of an unattended agent isn't stopping — it's never stopping.
07 Cost could run away silently

A retry storm or an unusually long generation chain multiplies spend without anything visibly breaking. The pipeline looks healthy right up until the invoice.

FixPer-call cost estimation from live token usage, a daily per-model ledger, pre-call limit checks, hard $2/run and $10/day ceilings, capped exponential backoff and no retries on client errors.
08 Internal links looked machine-placed

Early versions appended link lists at the end of posts — an obvious footprint that adds no topical value and reads exactly like what it is.

FixSitemap URLs injected into the writing prompt for natural in-paragraph weaving, plus a deterministic pillar-first, cluster-aware injector capped at three links that only wraps the first natural occurrence in body text.
09 Concurrent runs corrupted state

A manual trigger fired during a scheduled run left two processes writing the same SQLite file.

FixExclusive flock at process start. The second run exits cleanly with a message instead of racing.
09

Evolution

Prototype to production in four weeks

Three agents became eight. The pivotal change wasn't adding capability — it was granting one agent the authority to say no.

Week 1
Close the loop
Get something — anything — from keyword to published URL without a human. Quality comes later; proof that the chain holds comes first.
Days 1–4
v1 — three agents, end to end
SEO Manager → Blog Writer → Publisher. Autocomplete plus LLM scoring, a three-step writing pipeline, WordPress publishing, scheduled on Actions. By day four the complete loop worked. The output wasn't good enough.
Days 5–7
First quality crisis: length
Posts consistently landed under target — 1,200 words against a 2,000 floor. Prompts were rewritten with per-section word budgets and an explicit expansion pass was added.
Week 2
Make the output publishable
The loop worked but the writing was recognizably machine-made and technically bare. Both had to change before anything could carry the agency's name.
Days 8–11
Anti-AI-detection, inline images, 3×/week
The humanization layer landed alongside inline image placeholders and an increased publishing cadence — the first attempt at making generated prose read like it came from a person.
Days 12–14
SEO infrastructure, phases 1 & 2
Google indexing notifications, internal linking and Article schema — then FAQ schema, table of contents, Open Graph tags, alt-text enrichment, snippet optimization and LSI keyword injection.
Week 3
Make quality measurable
“Does this sound like AI?” is unanswerable and unshippable. Week three turned every subjective judgment into a number with a threshold behind it.
Days 15–18
4-step humanizer + burstiness scoring
Humanization graduated from a single regex pass to a four-stage pipeline with quantitative measurement. Style stopped being a matter of taste and became a threshold.
Days 19–21
The measurement loop
Search Console integration turned a publishing pipeline into a learning one. Striking-distance detection and content decay tracking gave the system a view of its own results.
Week 4
Make it an agent system
Everything so far was one big writer with helpers bolted on. The final week broke it apart — and gave one of the pieces a veto.
Days 22–25
v2 — the multi-agent refactor
Three monolithic agents decomposed into seven specialized ones. The pivotal change: SEO review became a separate agent with authority to reject the writer's work and send it back. A genuine feedback loop, not a conveyor belt.
Days 26–27
Distribution & sitemap-aware linking
LinkedIn copy via Slack webhook, Dev.to syndication with canonical URLs, and live sitemap fetching so the writer weaves real internal links into prose while drafting.
Day 28
Cadence deliberately cut to 1×/week
A reversal, and an improvement. Three posts a week outran the site's ability to build authority per post. Weekly publishing concentrates quality, cost and internal-link equity where they compound. The constraint was never throughput.
What four weeks bought

Week 1 proved the chain. Week 2 made the output publishable. Week 3 made quality measurable. Week 4 made it a system. Roughly half the elapsed time went into rejecting and rewriting rather than generating — which is the same ratio the finished pipeline runs at, and not a coincidence.

10

Lessons

What building this changed

LESSON 01

Generation is the easy 20%

A plausible blog post takes an afternoon. Long enough, structurally complete, keyword-correct, human-sounding, linked, illustrated and verifiably published takes everything else.

LESSON 02

Never let the generator grade itself

Separate agent, different model, explicit rubric it didn't author. Self-review in the same context approves nearly everything.

LESSON 03

Use code for anything countable

Word counts, density, heading counts, keyword placement, HTML validity — an LLM will confidently misreport all of them. Ask models for judgment, not arithmetic.

LESSON 04

Autonomy needs hard stops

Two revision cycles. Fifty requests. Three retries. $2 per run. Every loop has a bound, because the failure mode of an unattended agent is never stopping.

LESSON 05

Status columns beat orchestrators

Resumability, inspectability, selective re-runs and crash recovery — for a fraction of the complexity of a workflow engine, and it made every agent runnable from the CLI.

LESSON 06

Prompts are source code

495 lines in versioned text files, not Python literals. Every change to editorial standards became a reviewable diff.

LESSON 07

Cadence is a quality decision

Cutting from three posts a week to one was an improvement. More output from the same pipeline dilutes link equity and topical focus.

LESSON 08

Measure the thing you're worried about

“Does this sound like AI?” is unanswerable. “Is the sentence-length coefficient of variation below 0.3?” is a check you can put in a pipeline.

What ships next

Close the optimization loop

striking_distance already identifies pages ranking 5–20. The next agent takes that queue as input and generates targeted revisions — optimizing what exists rather than only adding to it.

Automated content refresh

Same pattern for content_decay: declining pages get regenerated sections, updated examples and a republication.

Performance-weighted scoring

Feed real ranking outcomes back into the keyword agent's scoring prompt, so topic selection learns which clusters actually performed.

Multi-model consensus review

Route the quality gate through more than one model family and require agreement, reducing single-model bias in what gets approved.

Native LinkedIn publishing

LinkedIn currently routes through Slack for a human tap — the last manual touchpoint anywhere in the system.

Prompt regression tests

A held-out topic set scored on every prompt change, so editorial standards get tuned with evidence instead of vibes.

On the numbers in this case study

Every figure here describes architecture, configuration or enforced guardrails — all verifiable in the codebase. The cost model is an explicit estimate with its assumptions exposed. Traffic and ranking outcomes are tracked continuously in post_performance, striking_distance and content_decay via the daily Search Console sync, and should be read from live data rather than quoted from a document.

Idea in your head? Let’s
bring it to life.

Got a project? A wild idea? Or just want to say hey?
We're here for all of it — reach out anytime.

I’m looking for a help with:

I’m hoping to stay around of (in USD):