A programmatic SEO agent

Pages generated from data, checked before they ship and tested after.

Drawers of wooden letterpress type

A programmatic SEO agent is useful for three jobs: turning rows of data into pages that each answer a real query, checking every page against rules before it ships, and running small, dated experiments once pages are live. It is dangerous for one: deciding on its own that a page is good. Generating ten thousand pages has been cheap for years. What costs is shipping pages that are wrong, thin or identical, and agents make that mistake faster than any intern.

I have done programmatic SEO with and without agents. On Derrick, which I co-founded, I built more than 100 page variations in Webflow from a Google Sheet before usable AI existed, and two years later, with no further SEO work, they still brought 88 clicks a day. Since then the site moved to Astro and agents took over much of the recurring work under written rules, which is the setup I describe here. The numbers are in the Derrick case study.

Split the roles before you write a prompt

One agent that plans, writes, checks and measures will grade its own homework. I split the work into roles with separate instructions, and the lead role is the only one allowed to decide.

Role Does Never does
Head of SEO agent Plans the week from the data, gives verdicts on tests Writes pages
Writer agent Turns a data row and a brief into a page draft Decides what gets published
Optimiser agent Improves existing pages: titles, internal links, missing sections Touches pages under test
Experimenter agent Runs bounded tests and writes dated verdicts Changes its success criteria mid-test
Checker (script plus judge) Runs the pre-ship checks below Fixes what it flags

The lead and the experts run on the strongest model available, because their work is judgement. Purely mechanical sub-tasks, such as a find and replace across templates, go to a small fast model. One constraint shapes the setup in Claude Code: a sub-agent cannot spawn another sub-agent, so all delegation is organised from the top.

Every page starts from a data row with sources

The agent writes from data, never from memory. Each row carries the facts the page will state and where they come from, so the checker can verify them later. Say you sell a data tool and publish one page per use case:

{
  "slug": "enrich-companies-recruitment-agencies",
  "query_target": "enrich company list for recruitment agencies",
  "facts": {
    "feature_name": "Enrich Companies",
    "cost": "1 credit per company",
    "source": "/features/enrich-companies"
  },
  "unique_content": ["columns a recruiter needs and why", "two questions from support tickets"],
  "status": "draft"
}

The unique_content field is the line that matters. A page that only swaps the use case name in a template is the near-duplicate content Google’s spam policies describe as scaled content abuse. If a row has nothing specific to say, the page should not exist. How to design that database is covered in the database chapter of the programmatic SEO guide.

On Derrick, product facts live in one file, and a script checks that a feature exists before any page cites it. Counts are never typed into a page; a script computes them. That removes a whole class of errors, such as a page still quoting an old feature count after new features ship.

Block of metal type ready to print

Check every page before it ships

The checker runs two kinds of test. Mechanical checks are a script: same input, same verdict. Subjective checks go to a judge model in a blank context that sees only the page and the data row, never the writer’s reasoning.

Check Type Fails when
Every stated fact matches a field in the row Script A price, count or feature name is not in the source
Feature exists Script The cited feature has no page on the site
Near-duplicate Script Text similarity with another page above your threshold
Banned words and competitor names Script Any match
Internal links resolve Script Any link returns an error
Title and meta length, one H1 Script Out of bounds
Answers the target query in the first screen Judge The judge cannot quote the answer
Says something a template could not Judge The unique content could apply to any row

Pages that fail go back with the reason; nothing is published by the agent itself. It opens a pull request, a person merges. After any change to the writer’s instructions, a set of frozen briefs is re-run and compared with the previous outputs, so a fix in one place does not quietly break pages elsewhere.

Type case seen from above

Test on a few pages, with a verdict date

Once pages are live, the experimenter agent proposes changes and tests them on a bounded set, typically 10 pages, never the whole family. Every test is written down before it starts:

# illustrative test spec
test_id: title-with-price
hypothesis: Putting the credit cost in the title raises CTR on feature pages
pages: [list of 10 slugs]
control: [10 comparable slugs, unchanged]
metric: CTR at comparable positions, Search Console
start: 2026-11-10
verdict_on: 2026-12-08
decision_rule: keep if CTR gap above 20% relative and positions stable
pages_frozen: true

The verdict date is four weeks out and fixed in advance. The pages_frozen flag only works if every other routine reads it: a routine that ignored such a declaration spoiled one of our tests (chapter 12).

Distrust your own measurement first

The most instructive incident on our SEO system was a measurement error that the Head of SEO agent found in its own exports in November 2026, told in full in the measurement chapter. Two rules came out of it:

  • Before combining two exports, check that their totals match.
  • When a number drives a decision, confirm it with a second independent instrument rather than a third look at the first one.

Which metrics to follow per stage of the funnel is in the performance tracking chapter.

Your next step: pick one page family, write the data row schema with a source for every fact and a unique_content field, and run the mechanical checks on ten existing pages before you let an agent write a new one.