Earlier this year, Andrej Karpathy (@karpathy) pointed an agent at his own training code and let it run for two days. It ran 700 experiments, kept the 20 that beat the benchmark, and made the model train 11% faster. Then he said something quite interesting: any metric you can evaluate cheaply can be handed to an agent swarm.
Reply rate is a metric you can evaluate cheaply. I have spent some time since working out what that loop looks like pointed at outbound.
My build:
Codex reads last week's outcomes, edits the scoring and play files the outbound system runs on, runs a test, and opens a pull request. It proposes a change to the playbook with the evidence and the score attached, then waits for a human to approve it. Sending and merging stay outside the loop.
I have built the first loop a few times: sense the market, score the account, write from the signal, check the message, log the outcome, learn from the reply. This article is about the second loop, the one that edits the first.
That is the build: GTM as versioned code that improves from the market.

The repo
Start with the folder. The shape matters because Codex can only improve what it can read and edit.
1codex-self-improving-outbound/2 AGENTS.md3 README.md4 config/5 scoring.yaml6 plays.yaml7 prompts/8 improve_scoring.md9 improve_prompt.md10 pr_summary.md11 memory/12 outcomes.jsonl13 evals/14 fixtures.yaml15 score.py16 scripts/17 append_outcome.py18 run_codex_step.sh19 propose_improvement.py20 open_pr.sh21 weekly_tune.sh22 examples/23 outcomes.sample.jsonl24 weekly-pr.md
The repo is intentionally plain. config/scoring.yaml holds the rules that decide which signals matter. prompts/ holds the plays that write the messages. memory/outcomes.jsonl holds what the market did. evals/score.py is the gate that says whether a proposed change helped. AGENTS.md is the law Codex reads before it touches anything.
Run the first version offline. No CRM, no enrichment, no delivery system. The improvement loop should prove itself on local files before it gets anywhere near a real outbound machine.
Step 1. Write the law first
Before the scoring file, before the prompt files, write AGENTS.md. This is the file that keeps the agent useful and contained.
1# Self-Improving Outbound Rules23You improve an outbound system from outcomes.45Hard rules:6- Never send messages.7- Never scrape or enrich real people.8- Never self-merge.9- Edit only files in this repo.10- Change one concept at a time.11- Cite outcomes from memory/outcomes.jsonl for every proposed change.12- Improve evals/score.py before a change can become a PR.13- If the eval does not improve, revert your edit and stop.1415Allowed edits:16- config/scoring.yaml17- config/plays.yaml18- prompts/*.md1920Required output:21- changed files22- reason for each change23- before score24- after score25- pull request summary
The law has one job: narrow the work. Without it, Codex will try to help by expanding scope. It will add more data, touch more files, call more tools, or automate a step that should stay under human control. Here the job is smaller: read outcomes, propose one file change, prove it helped, then wait.
What good looks like. You can read the law before approving a PR and know exactly what Codex was allowed to do.
Where it breaks. The law becomes a compliance document. If AGENTS.md needs a table of contents, it is already too large. Keep it operational.
Step 2. Move judgment into config
Most outbound judgment lives in someone's head. Then the team buys software and expects the software to improve a decision it cannot see.
Move the judgment into a file.
1signals:2 competitor_comparison:3 weight: 84 reason: "buyer is comparing alternatives"5 implementation_page_visit:6 weight: 67 reason: "buyer is checking whether this can be installed"8 job_repost:9 weight: 510 reason: "role is still open and urgent"11 funding_event:12 weight: 513 reason: "budget or mandate may have changed"14 generic_download:15 weight: 116 reason: "content interest, weak buying intent"1718thresholds:19 draft: 620 human_review: 102122negative_signals:23 student_research: -824 vendor_pitch: -625 competitor: -10
This file starts as a visible hypothesis. If a generic download should count for zero, the team can point to the exact line and change it. If an implementation page visit is a stronger signal than you thought, Codex can propose the diff and show the outcome rows that justify it.
Do not bury this logic in a Python function. If the rule is visible, the team can review it, argue with it, and improve it without turning a sales judgment into an engineering refactor.
What good looks like. The file is small enough to argue with. Five signals is a good first version.
Where it breaks. The scoring file becomes a junk drawer. Twenty signals, six thresholds, and exception rules for every edge case will make the improver overfit. Start narrow and let the outcomes tell you where the next knob belongs.
Step 3. Write outcomes as memory
The most important file is memory/outcomes.jsonl.
One line per touch, written when the outcome is known:
1{"date":"2026-07-01","account":"Northwind Finance","signal":"competitor_comparison","play":"migration_note","score":8,"outcome":"reply","reason":"asked for migration notes"}2{"date":"2026-07-01","account":"Bluepeak Studio","signal":"generic_download","play":"resource_followup","score":1,"outcome":"no_reply","reason":"content-only intent"}3{"date":"2026-07-02","account":"KiteOps","signal":"implementation_page_visit","play":"implementation_angle","score":6,"outcome":"reply","reason":"asked about implementation timeline"}4{"date":"2026-07-02","account":"Atlas Recruiting","signal":"job_repost","play":"hiring_angle","score":5,"outcome":"bad_fit","reason":"student research request"}
The reason field is the whole point. no_reply tells you almost nothing. content-only intent tells the next run that this signal might not deserve a draft. bad_fit is useful only when the reason explains why. asked about implementation timeline is the kind of detail that can change a weight.
Build the validator before you build the improver:
1Build scripts/append_outcome.py.23It accepts:4- date5- account6- signal7- play8- score9- outcome: reply | meeting | no_reply | bad_fit | bounced10- reason1112It rejects:13- missing fields14- unknown outcomes15- empty reason16- dates in the future1718Append valid rows to memory/outcomes.jsonl.19Print the appended row.
This is where the compounding starts. A dashboard can tell you a campaign is down. A clean outcome log can tell Codex which signal, play, or phrase should change before the next run.
What good looks like. After a week, a stranger can read the file and tell which signals created replies, which plays created bad-fit conversations, and which internal favorite the market ignored.
Where it breaks. The team backfills outcomes on Friday from memory. The wins survive, the bad-fit reasons blur, and the system learns from fiction. Write the row when the outcome lands.
Step 4. Build the eval gate
Before Codex edits anything, it needs a test it cannot explain away.
Create evals/fixtures.yaml:
1cases:2 - account: Northwind Finance3 signals: [competitor_comparison, implementation_page_visit]4 expected: human_review5 note: "two strong signals on one account"67 - account: Bluepeak Studio8 signals: [generic_download]9 expected: ignore10 note: "content-only intent"1112 - account: KiteOps13 signals: [implementation_page_visit]14 expected: draft15 note: "implementation intent should clear draft threshold"1617 - account: Atlas Recruiting18 signals: [job_repost, student_research]19 expected: ignore20 note: "bad-fit marker cancels the signal"
Then create evals/score.py:
1Build evals/score.py.23Read config/scoring.yaml and evals/fixtures.yaml.45For each case:61. Sum weights for every signal.72. Add negative signal penalties.83. Route the account:9 - score >= thresholds.human_review => human_review10 - score >= thresholds.draft => draft11 - otherwise => ignore124. Compare route to expected.1314Print each prediction.15Print final accuracy as score=0.00 to score=1.00.16Exit 0 only when accuracy is 1.00.
The first gate should be small enough to understand and sharp enough to catch a real miss. In my first run, the baseline failed one case:
1Northwind Finance: predicted=human_review expected=human_review2Bluepeak Studio: predicted=ignore expected=ignore3KiteOps: predicted=ignore expected=draft4Atlas Recruiting: predicted=ignore expected=ignore5score=0.75
That was good. The system had implementation intent below the draft threshold, so it ignored an account the fixture said deserved a message. Better to catch that in a test than after a month of missed accounts.
What good looks like. One command gives one number, and every failed case is easy to inspect.
Where it breaks. The fixture only includes obvious wins. Then every reckless change passes. Put ugly cases in the gate: weak intent, bad fit, no reply, stale signals, and the accounts you wish the system had skipped.
Step 5. Let Codex propose one scoring change
Now Codex can edit.
Create prompts/improve_scoring.md:
1You improve the outbound scoring system.23Read:4- AGENTS.md5- config/scoring.yaml6- memory/outcomes.jsonl7- evals/fixtures.yaml89Your job:101. Find one scoring rule that should change.112. The reason must cite memory/outcomes.jsonl.123. Change only config/scoring.yaml.134. Run python3 evals/score.py.145. If the score improves, keep the change.156. If the score stays flat or drops, revert your change and stop.1617Output:18- the exact line changed19- the outcome rows that caused it20- before score21- after score22- whether the change should become a PR2324Do not edit prompts.25Do not add new signals.26Do not touch delivery.
Run it through the repo wrapper:
1scripts/run_codex_step.sh improve_scoring
The first version of my improver made a useful mistake. It chased the cleanest-looking reply signal. competitor_comparison had the strongest reply rate in the tiny outcome log, so the improver wanted to increase that weight. The eval stayed at 0.75, so the change was rejected.
That is exactly why the gate exists. A weaker system would have accepted the story because it sounded reasonable. This one asked a better question: did the change fix the known miss?
The second pass found the smallest edit that helped:
1- implementation_page_visit: 42+ implementation_page_visit: 6
The eval passed:
1Northwind Finance: predicted=human_review expected=human_review2Bluepeak Studio: predicted=ignore expected=ignore3KiteOps: predicted=draft expected=draft4Atlas Recruiting: predicted=ignore expected=ignore5score=1.00
That is the moment the loop becomes useful. It changed one rule, for one reason, and proved the change against a fixture.

What good looks like. The proposed diff is boring and traceable: one line changed, one outcome-backed reason attached, one eval improved.
Where it breaks. Codex changes three weights and two prompts at once. Now nobody can tell which change helped. Keep the law strict: one concept per proposal.
Step 6. Improve prompt files separately
Scoring is only half the system. The message templates decay too.
A line that worked last month starts sounding familiar. A question that earns replies in one segment gets ignored in another. A phrase that feels sharp internally gets punished by the market. Treat prompt improvement as a separate lane so Codex does not mix scoring and copy in the same PR.
Create config/plays.yaml:
1plays:2 migration_note:3 prompt_file: prompts/plays/migration_note.md4 use_when:5 - competitor_comparison6 banned_lines:7 - "thought this might be relevant"8 - "quick question"910 implementation_angle:11 prompt_file: prompts/plays/implementation_angle.md12 use_when:13 - implementation_page_visit14 banned_lines:15 - "checking out our solution"16 - "would love to chat"
Then create prompts/improve_prompt.md:
1You improve one outbound play.23Read:4- AGENTS.md5- config/plays.yaml6- memory/outcomes.jsonl7- the prompt file for the chosen play89Pick one play with at least 10 outcomes.1011Find:12- lines or structures that appear in positive outcomes13- lines or structures that appear in no_reply or bad_fit outcomes14- any phrase that should be banned1516Make one small edit to that play's prompt.1718Rules:19- Do not change scoring.20- Do not create a new play.21- Do not add a new channel.22- Cite outcome rows.23- Write the before and after instruction.2425Then run the copy eval if present.26If no copy eval exists, open the PR as review_required.
Some improvements can be scored automatically. Others still need taste. If there is no copy eval, Codex can propose the prompt edit, but it should mark the PR for review instead of pretending the edit is proven.
What good looks like. Codex says, "This phrase appeared in seven no-reply outcomes, so I added it to banned_lines," or "positive replies quoted the implementation detail in sentence one, so I tightened the play to require that."
Where it breaks. The improver rewrites the whole voice because one message got a reply. Prompt edits should be smaller than your instinct.
Step 7. Ship changes as pull requests
This is the control layer. Codex edits files, runs the eval, and writes the PR summary. A human reviews and merges.

Create prompts/pr_summary.md:
1Write a pull request summary for this outbound improvement.23Include:41. What changed.52. Why it changed, citing outcome rows.63. Before score.74. After score.85. Files changed.96. Risk.107. What the human reviewer should check.1112Keep it short.13Do not claim the change is live.
Create \scripts/open_pr.sh\:
1#!/usr/bin/env bash2set -euo pipefail34branch="codex/weekly-tune-$(date +%Y-%m-%d)"56git checkout -b "$branch"7git add config prompts evals memory8git commit -m "Codex weekly outbound tune"910body="$(cat outputs/pr-summary.md)"1112python3 scripts/create_pr.py \13 "$branch" \14 "Codex weekly outbound tune" \15 "$body"
The PR should read like a teammate wrote it:
1Changed:2- Raised implementation_page_visit from 4 to 6.34Why:5- KiteOps had implementation-page intent and replied with implementation timing.6- The previous score routed this account to ignore.78Before:9- eval score 0.751011After:12- eval score 1.001314Reviewer check:15- Make sure implementation intent is specific enough.16- Keep generic downloads low.17- Merge only if this matches actual sales judgment.
That is the safety system. Codex does the tedious work. The operator keeps the standard.
What good looks like. One PR a week, small diff, clear reason, passing eval.
Where it breaks. Someone gives Codex permission to merge because review feels like friction. That minute separates a system that improves from a system that drifts.
Step 8. Put it on a cadence
Do not run this after every reply. That is how a system overfits to one loud account.
Let the week happen, let outcomes accumulate, then tune.

Create scripts/weekly_tune.sh:
1#!/usr/bin/env bash2set -euo pipefail34cd "$(dirname "$0")/.."56python3 evals/score.py || true7scripts/run_codex_step.sh improve_scoring8python3 evals/score.py9scripts/run_codex_step.sh pr_summary > outputs/pr-summary.md10scripts/open_pr.sh
Then cron:
10 8 * * MON cd ~/codex-self-improving-outbound && scripts/weekly_tune.sh
If you use GitHub Actions, keep the same shape:
1name: weekly-outbound-tune23on:4 schedule:5 - cron: "0 8 * * 1"6 workflow_dispatch:78jobs:9 tune:10 runs-on: ubuntu-latest11 steps:12 - uses: actions/checkout@v413 - uses: actions/setup-python@v514 with:15 python-version: "3.11"16 - run: pip install -r requirements.txt17 - run: python3 evals/score.py || true18 - run: scripts/weekly_tune.sh
Run the first two tune-ups by hand. Read every diff. Watch what Codex tries to change when the sample is thin. Once the proposals are boring, put it on a schedule.
What good looks like. A weekly PR appears with the evidence, the diff, and the eval result. You merge, edit, or close it.
Where it breaks. The job runs, nobody reviews, and PRs pile up. A self-improving system still has one human habit: read the diff.
The clone-and-run version
The repo should ship with four commands:
1git clone <repo>2cd codex-self-improving-outbound3python3 -m venv .venv4source .venv/bin/activate5pip install -r requirements.txt6python3 evals/score.py7python3 scripts/propose_improvement.py
Expected first run:
1score=0.752changed config/scoring.yaml3implementation_page_visit: 4 -> 64score=1.005open PR for human review
The offline demo proves the file contracts. The Codex run proves the editing loop. After that, replace the sample outcomes with your own, rename the signals, add your plays, and build a fixture that reflects the accounts you wish the system had routed differently.
Do not start by wiring delivery. Start by proving the improvement loop.
The full version: max
This repo is the manual layer. It runs from files, public signals, and your Codex plan. It teaches the shape because every rule is exposed.
yourmax.ai is the same system with the seams hidden.
Instead of a repo you wire together yourself, max is the agent you use directly. It detects movement in the market, decides who is worth contacting and why now, drafts the outreach across email and LinkedIn for your approval, and keeps improving from the outcomes.
The repo shows the self-tuning layer most teams never build: outcomes become proposed rule changes, proposed rule changes run through a gate, and the human merge decides what becomes live. max takes that same operating logic and runs it as a managed system.
If you want the full repo, you can let me know and I will send it your way.





