Six years of B2B growth marketing.Now I build the AI systems that run it.

I came to this through marketing automation. The step from building nurture flows in Dotdigital to building agents that draft their own work was smaller than it sounds. Everything below is running now, and the numbers are read off the system.

37

agents running on a schedule and on demand. Each one drafts into a review queue. None of them can publish.

94

drafts I declined, out of 1,695 raised. 677 went live. The declines are what makes the queue worth having.

$1.49

notional cost per published item across 874 logged runs, counting the ones that failed. It is list-price arithmetic on token counts rather than a bill.

The marketing years

01

Taking a live events business online

Covid closed the events. I rebuilt the audience from CRM data and moved the whole programme online: ten webinars, twelve training sessions and a virtual conference. The master email list grew to 5.72 times its starting size in the first two months. Conference attendance finished 51% over target, and the mailing list overall grew 110%. Across three days of the conference I arranged 167 meetings between sponsors and customers.

Conference attendance target +51% Mailing list overall start +110% Master email list start 5.72× Newsletter list start 1.45× 167 sponsor and customer meetings arranged across three conference days
My own campaign results. The three growth bars share one scale; the conference bar is drawn against its target.
Role
Marketing, Data & Audience Development Manager
When
Sep 2020 to Aug 2021
Where
Room151, public-finance news and events

Scoring a 200-course catalogue

The training portfolio had grown past two hundred courses and nobody could say which of them deserved the marketing budget. I built a scoring model that graded every course on commercial performance, market demand, strategic importance, growth potential and where it sat in its lifecycle. The output decided what ran, what got rebuilt and what was retired. More than twenty new courses launched off the back of it, and over 90% of those ran first time rather than being pulled for low numbers.

Invest Grow Retire Rebuild Low demand High demand Commercial performance the ones that got the budgeteverything else, scored the same way
Every course scored on five criteria, then placed; where it landed decided the budget. Illustrative spread. The axes and the four actions are the real model; individual course positions are not published.
Role
Commercial Marketing Executive
When
Aug 2021 to Jun 2024
Where
Institution of Mechanical Engineers

Turning a form fill into a qualified lead

A new professional-development programme needed a pipeline before it had an audience. I built the nurture and scoring journeys in Microsoft Dynamics and Dotdigital, with the sign-up pages in Sitefinity. Behaviour scored the contact, the score decided which sequence they got, and sales picked them up once they crossed a threshold. Within its first two months the programme had beaten the training department’s new-product revenue target for the whole year.

Sign-up Sitefinity Segment Dynamics Nurture Dotdigital Score behaviour MQL Sales Below threshold, back into nurture A continuous MQL pipeline that kept running once the rules were set.
Role
Senior Marketing Executive
When
Jun 2024 to May 2026
Stack
Microsoft Dynamics, Dotdigital, Sitefinity

That last one is why I do what I do now. A nurture flow is a set of rules that decides what happens next without me in the room. Agents are the same idea with better judgement, and the same requirement that a person checks the output before a customer sees it.

What I build now

02

Thirty-seven agents behind one approval gate

Each agent has one job, a written playbook and a slot on a schedule. They draft into a queue and stop. Nothing reaches a customer, a subscriber or a shop listing until I approve it. Approving something can also hand the work on to the next agent, so a chain advances one step per approval. A cost ledger records every run including the ones that fail, and a daily spend ceiling declines work and leaves it in the queue.

1,695drafts raised
677published
94declined by me
Departments OperationsMarketing & growthProduct & designEngineeringResearch Review queue every draft stops here nothing self-publishes I approve or decline Published677 Declined94 Waiting896 Failed to publish28 Approving a draft can also hand the work to the next agent, so a chain advances one step per approval. 1,695 raised, all four outcomes shown. Department names are grouped for this page.
Role
Built it and run it
When
Oct 2025 to present
Stack
Python, SQLite, Claude Code, cron, one small VPS

Ten lifecycle emails and an A/B test that will not call it early

The shop sends ten emails across the customer lifecycle, from order confirmation through dispatch to a win-back months later. Subscribers are double opt-in, and a buyer can turn marketing off while still getting the emails about their own order. An experiment can test the subject line, a block of copy, the call to action, the hero image or which products get cross-sold. The code holds off naming a winner until the result clears significance, and promoting it is still a click I have to make.

Triggered by something the customer did Confirm Welcome Receipt Dispatch Gettingstarted Reviewask Refund Marketing, with an opt-out that leaves the order emails running Newsletter Abandoned checkout Win-back Subject line, copy, CTA, hero image or cross-sell. A winner is promoted once it clears significance.
Role
Built it and run it
When
2026
Stack
Python, Resend, SQLite

An agent that can spend money, inside a fence it cannot climb

One agent manages the paid search accounts through a single manager account, and it is the only thing in the system holding that credential. It has real write access: it opens and pauses campaigns, moves bids and budgets and adds negatives. What it cannot do is decide how much to spend. A weekly cap is checked in code before any budget change and a change that would breach it does not happen. A stop file pauses everything. A new campaign, or a rise above the ceiling on the main account, is proposed to me first. Every write lands in a ledger with the before and the after.

The boundary The other side never holds a key asks The ad-account agent holds the only key Account it may act on Account it may not A request that reaches across is refused and written down Checked in code before every budget change A weekly cap · a stop file · a ceiling above which it has to ask me · a ledger line per write
Illustration of the boundary. Account and brand names are not shown.
Role
Built it and run it
When
2026
Stack
Google Ads API, Python, an OAuth refresh flow, JSONL ledger

A direct shop that ships to 55 countries

I moved off the marketplace and built the shop. Prices are set on the server so nothing the browser sends is trusted, Stripe and PayPal both post signed webhooks, digital products deliver on a tokenised link, and postage is priced by weight to fifty-five countries with VAT handled at the checkout.

The CockpitForge storefront, showing a 3D-printed trim wheel cover fitted to a Honeycomb Bravo throttle quadrant.
cockpitforge.com. Made to order in Britain, posted in two to four days.
Role
Built it and run it
When
2026
Stack
Python, Stripe, PayPal, Stripe Tax

Measuring it without flattering it

GA4 runs with consent mode alongside first-party request logs, so no single source has the last word. A reporting agent reads both plus the ad accounts each day, writes a summary and flags anything that moved more than it should have. It reports only. Changing a campaign stays a decision a person makes.

Server logs overcount, because ad checks, link previews and prefetching all look like visits. So each landing page gets three counts that are never blended into one: raw hits, visits a browser actually rendered, and clicks the ad platform billed. On one test page, 577 raw hits came down to 20 visits that a second signal backed up. On another, the logs showed 55 paid landings against 30 billed clicks. A test cannot be called a pass or a fail until at least 40 visitors are counted and 70% of them are backed by a second signal.

577raw hits on one page
20backed by a second signal
30 of 55paid landings billed
Sources GA4, with consent mode First-party request logs Paid search accounts Orders and email sends Reporting agent runs daily A dashboard I read A written daily digest Flags on anything odd Read only Two independent sources for traffic, so neither has the last word. Changing a campaign stays a decision a person makes. The agent cannot touch one. Illustration of the reporting layer. No account names or figures shown.
Role
Built it and run it
When
2026
Stack
GA4 Consent Mode v2, Google Ads API, Python

What AI answers say when a buyer asks

The monitor puts a bank of buying questions to Claude twice, once from what the model already knows and once with web search, then scores every answer in plain code: whether the brand is named, its share of voice against a list of rivals, and which sites get cited. Each run is compared with the last, so a change in the answers shows up as a change in the numbers.

I ran it on CockpitForge with twelve questions a flight-sim buyer would ask, none of them naming the shop. The first run said CockpitForge was named three times. All three came from the model picking up my own project files, because the questions were being asked from inside the code folder, so each question now runs from an empty folder. The clean run named CockpitForge in none of the 24 answers and Honeycomb in 9, and cited five other flight-sim sites.

0 of 24answers naming CockpitForge
9naming Honeycomb
$2.08for the whole run
Named in how many of the 24 answers All 24 Honeycomb Honeycomb: 9 of 24 9 Logitech Logitech: 6 of 24 6 Thrustmaster Thrustmaster: 4 of 24 4 Saitek Saitek: 4 of 24 4 Printables Printables: 4 of 24 4 Thingiverse Thingiverse: 4 of 24 4 Etsy Etsy: 4 of 24 4 Winwing Winwing: 3 of 24 3 Skalarki Skalarki: 2 of 24 2 CockpitForge 0 GoFlight, Turtle Beach and Virtual Fly were named once each. Twelve questions, asked twice.
Role
Built it and run it
When
2026
Stack
Python, Claude Code in headless mode

An adviser with write access to my calendar

Four areas of my life compete for the same week, so the ranking runs first in plain code with no model in it: due dates, effort, how long a thing has sat there, and how far each area has drifted from the share of time I said it should get. The adviser reads that ranked list and my real calendar, and books work blocks straight into it through the Calendar API. Every hour a second pass reads the calendar back and judges what happened. A block that has been and gone means the work got done, so the task is crossed off. A block that was deleted means it did not, so the task stays open and says so. A block that moved is judged at its new time. A due date arriving is not evidence that anything happened, and it is never allowed to tick a task off.

What it may do unattended is the whole design. Additive, reversible writes happen on their own, because a prep block or a reply task costs one click to undo. Deleting an event, moving one, replying to anyone or accepting a slot never happens without me. If I delete something it created, it does not come back on the next pass, because a deletion is an answer.

Writes on its own A prep block before a meeting that has none A task to answer a time someone proposed additive · reversible · one click to undo Waits for me Deleting an event, or moving one Replying to anyone, or accepting a slot A cancellation, surfaced and left alone Every hour it reads the calendar back A block that passed → crossed off A block deleted → the task stays open A block moved → judged at the new time A deadline passed → it asks, and never ticks
Illustration of the write rules. My own calendar is not shown.
Role
Built it and run it
When
2026
Stack
Python, Google Calendar v3, SQLite, cron, Claude Code

Triaging a legal queue without touching the judgement

A legal team’s queue is mostly repetitive low-stakes contracts that still get read end to end. I built an assistant that compares each one against a playbook the legal team owns and edits, then produces a clause-by-clause deviation table with a suggested redline and a question for the counterparty. It routes: clear, or human review with exactly what is off. It never approves and never signs, and when it is unsure it escalates. I wrote an eval before I trusted any of it, and it routes every case in the set correctly and catches every material issue in them. The set is hand-written, and a labelled set of real redacted contracts is the next step.

Contract Playbook legal owns it, I do not Deviation table redline + question Clear, a person still signs Human review, and why Biased to escalate. The expensive mistake is a wrong “fine to sign”, so uncertainty routes to a person. The playbook is the product. The team owns the file, so the behaviour is theirs to audit and edit.
Role
Built it and run it
When
2026

Testing and safety

03

A shop assistant that cannot promise anything

The shop has an AI assistant that answers questions about products, postage and a customer’s own order. Most of what keeps it safe is code around the model. Refunds, cancellations, complaints and address changes never reach the model and go straight to a scripted handover. Card numbers, email addresses and postcodes are stripped out before anything reaches a prompt or a log. The server holds the conversation history, so a visitor cannot plant lines the assistant never said. After the model writes a reply, a guard reads it: any price it mentions has to exist in the catalogue already, and discount codes, refund promises and anything binding are blocked. Its first line tells the visitor they are talking to an AI, which the EU AI Act has required since August.

A test suite attacks each of those guards without calling the model, so it costs nothing to run after every change, and it exits with a failure if any guard gives way.

93tests in the suite
8layers of guard
<1sto run all of them
The CockpitForge AI assistant open on the shop's home page. Its first message says the visitor is chatting with an AI, not a person. Asked for a discount code for the Bravo trim wheel cover, it replies that there are no discount codes and gives the listed price of £29.99.
The live assistant on cockpitforge.com, asked for a discount code. It says there isn’t one, and the price it quotes comes from the catalogue.
The path every message takes Message Input cleaning Which route? The model no tools Output guard checked in code Reply hidden text removed card, email and postcode redacted prices must exist in the catalogue; codes, refunds and promises blocked Handed to a person refunds, cancellations, complaints, address changes 93 tests attack these guards without calling the model, in under a second. Illustration of the request path, drawn without any customer data.
Role
Built it and run it
When
August 2026
Stack
Python, Claude Haiku, SQLite

Bug fixes that go in without me, inside two gates

One agent audits the system for bugs and another writes the patch. Two gates written in plain code decide whether the patch goes in without me. The first reads the finding: it has to be a confirmed bug, rated as small, in a file that is not protected. The second reads the patch itself: one or two files, at most 120 changed lines, clean syntax, and no changed line that touches logins, money or consent. The shop, the public site, the legal pages, the secrets vault and the gates themselves stay protected whatever the patch looks like. A patch that passes has its original files backed up before it is applied, and then the smoke test runs again. If anything that worked before has broken, the originals go back automatically.

In its first month it looked at 206 findings. It applied 33 on its own, handed 153 to me with the reason it stopped, and held 20 back under its daily cap. One of the fixes it applied failed the check afterwards and was rolled back without me.

206findings looked at
33applied without me
1rolled back by itself
Where 206 findings went in the first month 153 handed to me 20 held by the daily cap 33 applied without me 153handed to me, with the gate that stopped it 20held for the next day by the cap applied without me33 The 33 it applied rolled back automatically 32 kept. 1 failed the smoke test afterwards and its original files went back without me. Read from the lane’s own log, which records the decisions that changed nothing as well.
Role
Built it and run it
When
August 2026 to present
Stack
Python, Claude Code, cron

Running live code with no staging copy

Everything runs on one server with no staging copy, so a change reaches real visitors as soon as it ships. Before a change, a smoke test records a baseline. Afterwards it checks that every Python file still compiles, loads 91 live pages and endpoints, and reads the logs for new crashes. It fails only on a regression, something that worked before and does not now, so a fault that was already there is labelled and kept out of the verdict. It also compares each long-running service’s start time with the files it imports, because a service still running old code passes every other check. Errors report to Sentry. Backups are encrypted and leave the machine every night, and a monthly drill restores one and checks the database inside it.

In August too many agent runs started at once and the server ran out of CPU, then memory. I wrote the incident up. It had two separate causes, and it ended with a limit of three concurrent runs, a lock on each task and a CPU quota for the agents.

91live surfaces per check
304files compiled
3restore drills passed
Baseline what works now Change Restart services reload Smoke test under ten seconds Pass the change stays Regression fails, names the file 304 files compile 91 live surfaces load no new crashes in the logs no service on old code Restore drills 29 Jul: backup decrypted, database checked 29 Jul 1 Aug: backup decrypted, database checked 1 Aug 1 Sep: backup decrypted, database checked 1 Sep 17 Sep Each drill decrypts a real night’s backup and checks the database inside it.
Role
Built it and run it
When
2026
Stack
Python, bash, Sentry, encrypted off-site backups

Checks on what the models write

Text a model writes goes through checks in plain code before a person reads it. In the CV app, each generated cover letter is tested for the model’s own asides leaking into the letter, for passages that repeat, for stock phrases and tense slips, and for whether it names something specific to the employer and carries a fact that can be checked. A letter that fails is regenerated once against the faults it broke. The figures on this page go through a check of the same kind: a script reads them off the live system and fails when any published copy of them has drifted.

280test cases for the letter checks
5published files checked for stale figures
Invented draft Dear Hiring Team, 1 I am passionate about growth and I thrive in fast-paced environments. I built a reporting layer that saved time. 2 I built a reporting layer that saved time. 3 Sorry, let me output the corrected letter below. 4 I would love to bring my skills to your company. 1 Stock phrase 2 Repeated passage 3 The model talking to itself 4 Names nothing about the employer + no fact that can be checked A letter that trips a check is regenerated once against the faults it broke. An invented example, so no real letter or employer appears in it.
Role
Built it and run it
When
2026
Stack
Python, Claude

Search that had to beat the obvious version

Finding the right script or agent in the system used to mean matching text. I built a search that blends that text matching with embeddings computed on a home GPU and stored in SQLite, and before trusting it I wrote 35 real questions with known right answers. Text matching put the right answer first for 3 of the 35. The blended search put it first for 15 and in the top five for 23. When the GPU machine is off, it falls back to text matching.

A tuning loop holds a third of the questions back and throws out any change that only improves the questions it was tuned on. On the 12 held back, text matching got none right first time and the blended search got 4.

3 → 15right first time, of 35
23in the top five
0 → 4on the 12 held back
Text matching Blended search All 35 Right answer first Text matching: 3 of 35 3 Blended search: 15 of 35 15 In the top three Text matching: 7 of 35 7 Blended search: 22 of 35 22 In the top five Text matching: 9 of 35 9 Blended search: 23 of 35 23 The same 35 questions for both, each with a known right answer. Measured 17 September 2026.
Role
Built it and run it
When
August 2026
Stack
Python, Ollama embeddings, SQLite

Watching the terms the business runs under

The business depends on rules it did not write: Anthropic’s usage policy and commercial terms, model retirement dates, OpenAI’s data controls, the EU AI Act timeline and the ICO’s guidance on AI. Each morning a script with no model in it fetches 12 of those pages and compares each one with the copy it saved last time. An agent wakes only when a page has changed, and the change is all it reads. It has no web access, so it cannot fill a gap from memory. For each change it names the governance rule it touches, how much it matters and one thing to do, or says that nothing needs doing.

12pages watched
103changes logged since 16 August
0tokens on a day with no change
Changes found per day 4 8 16 Aug: 1 change 18 Aug: 1 change 19 Aug: 2 changes 20 Aug: 3 changes 21 Aug: 3 changes 22 Aug: 4 changes 23 Aug: 1 change 25 Aug: 4 changes 26 Aug: 4 changes 27 Aug: 5 changes 28 Aug: 5 changes 29 Aug: 3 changes 30 Aug: 3 changes 31 Aug: 3 changes 1 Sep: 8 changes 2 Sep: 8 changes 3 Sep: 2 changes 4 Sep: 5 changes 5 Sep: 4 changes 6 Sep: 2 changes 7 Sep: 1 change 9 Sep: 4 changes 10 Sep: 5 changes 11 Sep: 6 changes 12 Sep: 3 changes 13 Sep: 3 changes 14 Sep: 2 changes 15 Sep: 2 changes 16 Sep: 2 changes 17 Sep: 4 changes 16 Aug 1 Sep 17 Sep 103 changes: 69 on Anthropic’s pages, 31 on OpenAI’s, 3 on the regulation pages. The agent only runs on a day with a bar. 3 days had no change and cost nothing.
Role
Built it and run it
When
August 2026 to present
Stack
Python, cron, Claude Code

Also built

04
CV and career web app
2026

A separate product giving CV and application advice. The model runs server side so no key reaches the browser, and it is deliberately forgetful about the documents it sees.

Guardrails
2026

Agent file writes jailed to a drafts folder, a filter over what an agent may run in a shell, a secret scanner across drafts, spend ceilings that decline rather than warn, and an encrypted credential vault that survives the machine dying.

Local GPU pipeline
2026

Routine model work moved onto a home GPU over a private network, with image and speech pipelines on the same machine.

How I work

05
A generalist, deepest in growth and reporting

Email, paid, web, content, CRM and events, usually for the same funnel at the same time, with A/B tests running in most of them. Having run all of them is what stops me improving one channel at another's expense, and the depth is in growth and in the reporting that says whether it worked.

Training and events

Most of my marketing years went on filling rooms, and then on filling webinars when the rooms closed. A fixed date with a real number to hit is a good school for reporting, because attendance on the day is not a figure you can talk around.

One agent, one job

A narrow brief with a written playbook beats a clever general one. It is easier to judge, easier to fix, and when it fails it fails in a way you can read.

A person approves

Every draft waits for me. That is what makes the rest of it safe to run on a schedule, and it is why 94 of them never went anywhere.

Ceilings that refuse

A spend cap that only sends a warning is decoration. Ours declines the run and leaves the work sitting in the queue for the morning.

Measure what you actually care about

Counting drafts is easy and tells you very little. I track whether the output survived review, and what it cost to get there.

A spec before a build

On the bigger jobs the agent writes a short spec and stops. I approve it or send it back before any code exists, which is far cheaper than reviewing the finished thing.

Where I kept the model out

I use a model where a step needs judgement. The calendar ranking is arithmetic, the policy watch starts from a diff, the smoke test and the figure checks spend no tokens, and a refund request never reaches the shop assistant. Plain code is cheaper in those places and gives the same answer every time.

Get in touch