A programmatic SEO agent
Pages generated from data, checked before they ship and tested after.

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.
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.
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.