An autonomous blog agent is not a fancier writing tool. It is a loop. The model decides what to do next, calls tools, reads the results, and decides again until the post is done or it hits an error it cannot recover from. I built one of these for paying clients through Elev8 Growth Solutions, running on Claude API and Supabase. This post shows the full architecture: schema DDL, the tool-call loop, the quality gate, the approval flow, and the real costs after weeks in production.

What the autonomous blog agent actually does, step by step

Most "AI writing" products are single-shot: you give them a prompt and they hand you a draft. An agent is different because it has a loop with tool use. Here is what mine does on each run:

  1. Keyword intake. A row is inserted into a blog_jobs table in Supabase with a target keyword, a pillar label, and a status of queued.
  2. Research phase. The agent calls a web-search tool, pulls the top results for the keyword, and stores a compressed research summary back to the job row.
  3. Outline phase. Claude reads the research summary and produces a structured outline. The outline is written to a separate outlines table, keyed to the job.
  4. Draft phase. Claude expands the outline into a full post, section by section. Each section is appended to a drafts table as it completes, so a crash does not wipe the whole run.
  5. Quality gate. A second Claude call scores the draft against a rubric: keyword presence, word count, factual hedging, brand voice markers. If the score is below threshold, it flags the draft for human review rather than proceeding.
  6. Human approval. I get a Slack message with a link to a simple review UI. One button approves, one requests a revision with a note, one rejects. No code editor needed.
  7. Publish. On approval, a Supabase Edge Function fires, formats the HTML, and calls the CMS API to create a published post.

That full cycle, from queued to published, takes about four to seven minutes of wall-clock time on a normal post. The model is doing real work across multiple calls, and the state lives in Supabase the whole time so I can inspect any step that misbehaved.

Supabase schema: what I actually store

The schema is the part most write-ups skip. Here is the DDL I use, simplified slightly for readability:

-- Jobs table: one row per blog post request
create table blog_jobs (
  id           uuid primary key default gen_random_uuid(),
  keyword      text not null,
  pillar       text not null,
  status       text not null default 'queued',
  research     text,
  outline_id   uuid references outlines(id),
  draft_id     uuid references drafts(id),
  quality_score numeric,
  created_at   timestamptz default now(),
  updated_at   timestamptz default now()
);

-- Outlines table
create table outlines (
  id         uuid primary key default gen_random_uuid(),
  job_id     uuid references blog_jobs(id),
  content    jsonb not null,
  created_at timestamptz default now()
);

-- Drafts table: sections stored as ordered JSONB array
create table drafts (
  id         uuid primary key default gen_random_uuid(),
  job_id     uuid references blog_jobs(id),
  sections   jsonb not null default '[]',
  full_html  text,
  word_count integer,
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- Approval events
create table approval_events (
  id         uuid primary key default gen_random_uuid(),
  job_id     uuid references blog_jobs(id),
  decision   text not null, -- 'approved', 'revision', 'rejected'
  note       text,
  created_at timestamptz default now()
);

The status column on blog_jobs drives everything. It moves through: queued, researching, outlining, drafting, quality_check, pending_approval, approved, publishing, published, failed. Any process can query the status and know exactly where the job is. If the agent crashes mid-draft, I can see the last committed status and resume from that point without rerunning the expensive research call.

The Claude tool-call loop in Python

Here is the core of the agentic loop. I am using the anthropic Python SDK. This is condensed but structurally accurate:

import anthropic
import json

client = anthropic.Anthropic()

tools = [
    {
        "name": "web_search",
        "description": "Search the web for current information on a topic.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string"}
            },
            "required": ["query"]
        }
    },
    {
        "name": "save_outline",
        "description": "Save the post outline to the database.",
        "input_schema": {
            "type": "object",
            "properties": {
                "job_id": {"type": "string"},
                "outline": {"type": "object"}
            },
            "required": ["job_id", "outline"]
        }
    },
    {
        "name": "append_section",
        "description": "Append a completed draft section to the database.",
        "input_schema": {
            "type": "object",
            "properties": {
                "job_id": {"type": "string"},
                "section_html": {"type": "string"},
                "section_index": {"type": "integer"}
            },
            "required": ["job_id", "section_html", "section_index"]
        }
    }
]

def run_agent_loop(job_id: str, keyword: str, system_prompt: str):
    messages = [
        {"role": "user", "content": f"Write a complete blog post targeting the keyword: {keyword}. Job ID: {job_id}"}
    ]

    while True:
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=4096,
            system=system_prompt,
            tools=tools,
            messages=messages
        )

        # Append assistant turn
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "end_turn":
            break

        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    result = dispatch_tool(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps(result)
                    })
            messages.append({"role": "user", "content": tool_results})
            continue

        break  # unexpected stop reason, exit loop

    return messages

The dispatch_tool function is a plain Python dict lookup that routes tool names to actual functions: a Serper API call for web search, a Supabase upsert for save_outline, and an array append for append_section. The loop keeps running until Claude issues an end_turn, which it does after it has called append_section for every section in the outline.

What I would actually do: build the autonomous blog agent with a hard quality gate

The quality gate is the most important part of the system and the part I underbuilt in the first version. My first deployment had no gate. The agent published whatever it drafted. Inside two weeks I had three posts live with keyword stuffing errors and one that cited a study that does not exist. I pulled them manually and rewrote the gate.

The gate is a separate Claude call, not a self-review by the same context window. It takes the finished draft and a rubric, and returns a JSON score object:

{
  "keyword_present_in_first_100_words": true,
  "keyword_in_at_least_one_h2": true,
  "word_count": 2634,
  "word_count_passes": true,
  "citation_hallucination_risk": "low",
  "voice_consistency_score": 0.84,
  "overall_pass": true
}

If overall_pass is false, the job moves to pending_approval with a flag that shows the reviewer exactly which checks failed. The reviewer can fix the draft in the UI or send it back to the agent with a note. I chose not to let the agent auto-retry more than once because the token cost of a full retry is real money, and a second hallucination on the same post usually means the source material is the problem, not the draft.

The tradeoff I accepted: the gate adds about 2,000 to 3,000 input tokens per post. On claude-opus-4-5 pricing, that is a small fraction of the total run cost but it is not zero. For most SMB use cases, that cost is trivially justified by avoiding a manual fix that takes thirty minutes.

Scheduling, triggering, and the approval UI

I trigger runs in two ways. The first is a cron job in a Supabase Edge Function that polls blog_jobs for status = 'queued' every hour and fires the agent if it finds a row. The second is a Slack slash command: /blog keyword="autonomous blog agent" pillar="ai-agents". That command inserts a row into blog_jobs and the next cron tick picks it up. No code editor, no terminal.

The approval UI is a Next.js page deployed on Vercel. It pulls all pending_approval jobs from Supabase via a server component, renders the draft HTML in a preview pane, and exposes three buttons wired to a Supabase Edge Function that writes an approval_events row and updates the job status. The whole UI took about three hours to build and has needed zero changes since.

One thing I got wrong early: I sent approval links directly in Slack with no auth. Anyone with the link could approve a post. I added Supabase Row Level Security so the approve endpoint only works for authenticated users. That is a five-minute fix that I should have done on day one.

Real costs: what a post actually costs to generate

Here are the token counts from actual production runs on a 2,500-word post targeting a moderately competitive keyword. These are averages across several runs, not best-case numbers.

Phase Model Input tokens Output tokens
Research + outline claude-opus-4-5 ~8,000 ~1,200
Draft (all sections) claude-opus-4-5 ~12,000 ~3,800
Quality gate claude-opus-4-5 ~3,000 ~300
Total ~23,000 ~5,300

At current public Claude API pricing, that works out to roughly $0.40 to $0.60 per post on the Opus tier. If I drop research and outline to claude-haiku-3-5 and only use Opus for the draft, I cut that closer to $0.20. For a client publishing eight posts a month, the LLM cost is under $5. The Supabase free tier covers the database at that volume. Vercel's free tier covers the approval UI. The real cost is the time I spent building the system, which was about forty hours across two weeks.

For a deeper look at how I think about total agent cost for small business deployments, I wrote a separate breakdown at AI Agent Cost for Small Business: What I Actually Paid.

What broke in production and what I fixed

A few failures worth naming:

Loop runaway. On one run, a tool-call response came back malformed and the loop did not hit end_turn. It kept calling the search tool in a tight loop until I hit the rate limit. I added a max-iterations counter. If the loop exceeds fifteen iterations without completing, it writes a failed status and pages me.

Draft truncation. Early on I was assembling the full draft in a single Claude call with a high max_tokens. On longer posts it would truncate mid-sentence because I hit the output limit. Moving to the section-by-section append model fixed this. Each section call targets around 400 to 600 words, well inside output limits.

Supabase connection pooling. The Edge Function running the agent loop was opening a new Postgres connection on every tool call. Under load this exhausted the connection pool. I switched to using the Supabase REST API for writes inside the loop and reserved the direct Postgres connection for the heavier reads at job start. Connection count dropped by about 80 percent.

Voice drift. Without a strong system prompt, Claude drifts toward generic blog prose after a few sections. I added explicit voice constraints to the system prompt and a voice-consistency check in the quality gate. The gate now flags any section that scores below 0.75 on a simple rubric I defined as a few dozen example phrases and anti-patterns.

Who should build this and who should not

If you are a developer comfortable with Python, async functions, and a basic understanding of how Supabase RLS works, this is a two-week project. If you are an operator who wants content without touching code, you should not build this yourself. The maintenance surface is real: the web-search API will change, Claude pricing and model names will change, and the quality gate needs tuning as your brand voice evolves.

For operators who want the output without the build, my agency runs this system for clients. If that is you, reach out through Elev8 Growth Solutions and I can tell you what it costs to run it for your site.

For builders who want to go deeper or talk architecture: I am on LinkedIn and I respond to people who ask specific questions.