How to Build a Self-Improving Outbound System on Codex

@nifinet
ENGLISH2 days ago · Jul 19, 2026
227K
358
22
13
1.7K

TL;DR

Nicolas Finet details a technical framework for building a self-improving outbound sales system. The system uses AI agents to analyze reply rates and propose messaging improvements via pull requests.

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.

Nicolas Finet - inline image

The repo

Start with the folder. The shape matters because Codex can only improve what it can read and edit.

text
1codex-self-improving-outbound/
2 AGENTS.md
3 README.md
4 config/
5 scoring.yaml
6 plays.yaml
7 prompts/
8 improve_scoring.md
9 improve_prompt.md
10 pr_summary.md
11 memory/
12 outcomes.jsonl
13 evals/
14 fixtures.yaml
15 score.py
16 scripts/
17 append_outcome.py
18 run_codex_step.sh
19 propose_improvement.py
20 open_pr.sh
21 weekly_tune.sh
22 examples/
23 outcomes.sample.jsonl
24 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.

markdown
1# Self-Improving Outbound Rules
2
3You improve an outbound system from outcomes.
4
5Hard 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.
14
15Allowed edits:
16- config/scoring.yaml
17- config/plays.yaml
18- prompts/*.md
19
20Required output:
21- changed files
22- reason for each change
23- before score
24- after score
25- 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.

yaml
1signals:
2 competitor_comparison:
3 weight: 8
4 reason: "buyer is comparing alternatives"
5 implementation_page_visit:
6 weight: 6
7 reason: "buyer is checking whether this can be installed"
8 job_repost:
9 weight: 5
10 reason: "role is still open and urgent"
11 funding_event:
12 weight: 5
13 reason: "budget or mandate may have changed"
14 generic_download:
15 weight: 1
16 reason: "content interest, weak buying intent"
17
18thresholds:
19 draft: 6
20 human_review: 10
21
22negative_signals:
23 student_research: -8
24 vendor_pitch: -6
25 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:

javascript
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:

text
1Build scripts/append_outcome.py.
2
3It accepts:
4- date
5- account
6- signal
7- play
8- score
9- outcome: reply | meeting | no_reply | bad_fit | bounced
10- reason
11
12It rejects:
13- missing fields
14- unknown outcomes
15- empty reason
16- dates in the future
17
18Append 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:

yaml
1cases:
2 - account: Northwind Finance
3 signals: [competitor_comparison, implementation_page_visit]
4 expected: human_review
5 note: "two strong signals on one account"
6
7 - account: Bluepeak Studio
8 signals: [generic_download]
9 expected: ignore
10 note: "content-only intent"
11
12 - account: KiteOps
13 signals: [implementation_page_visit]
14 expected: draft
15 note: "implementation intent should clear draft threshold"
16
17 - account: Atlas Recruiting
18 signals: [job_repost, student_research]
19 expected: ignore
20 note: "bad-fit marker cancels the signal"

Then create evals/score.py:

text
1Build evals/score.py.
2
3Read config/scoring.yaml and evals/fixtures.yaml.
4
5For each case:
61. Sum weights for every signal.
72. Add negative signal penalties.
83. Route the account:
9 - score >= thresholds.human_review => human_review
10 - score >= thresholds.draft => draft
11 - otherwise => ignore
124. Compare route to expected.
13
14Print 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:

text
1Northwind Finance: predicted=human_review expected=human_review
2Bluepeak Studio: predicted=ignore expected=ignore
3KiteOps: predicted=ignore expected=draft
4Atlas Recruiting: predicted=ignore expected=ignore
5score=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:

markdown
1You improve the outbound scoring system.
2
3Read:
4- AGENTS.md
5- config/scoring.yaml
6- memory/outcomes.jsonl
7- evals/fixtures.yaml
8
9Your 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.
16
17Output:
18- the exact line changed
19- the outcome rows that caused it
20- before score
21- after score
22- whether the change should become a PR
23
24Do not edit prompts.
25Do not add new signals.
26Do not touch delivery.

Run it through the repo wrapper:

bash
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:

text
1- implementation_page_visit: 4
2+ implementation_page_visit: 6

The eval passed:

text
1Northwind Finance: predicted=human_review expected=human_review
2Bluepeak Studio: predicted=ignore expected=ignore
3KiteOps: predicted=draft expected=draft
4Atlas Recruiting: predicted=ignore expected=ignore
5score=1.00

That is the moment the loop becomes useful. It changed one rule, for one reason, and proved the change against a fixture.

Nicolas Finet - inline image

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:

yaml
1plays:
2 migration_note:
3 prompt_file: prompts/plays/migration_note.md
4 use_when:
5 - competitor_comparison
6 banned_lines:
7 - "thought this might be relevant"
8 - "quick question"
9
10 implementation_angle:
11 prompt_file: prompts/plays/implementation_angle.md
12 use_when:
13 - implementation_page_visit
14 banned_lines:
15 - "checking out our solution"
16 - "would love to chat"

Then create prompts/improve_prompt.md:

markdown
1You improve one outbound play.
2
3Read:
4- AGENTS.md
5- config/plays.yaml
6- memory/outcomes.jsonl
7- the prompt file for the chosen play
8
9Pick one play with at least 10 outcomes.
10
11Find:
12- lines or structures that appear in positive outcomes
13- lines or structures that appear in no_reply or bad_fit outcomes
14- any phrase that should be banned
15
16Make one small edit to that play's prompt.
17
18Rules:
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.
24
25Then 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.

Nicolas Finet - inline image

Create prompts/pr_summary.md:

markdown
1Write a pull request summary for this outbound improvement.
2
3Include:
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.
11
12Keep it short.
13Do not claim the change is live.

Create \scripts/open_pr.sh\:

bash
1#!/usr/bin/env bash
2set -euo pipefail
3
4branch="codex/weekly-tune-$(date +%Y-%m-%d)"
5
6git checkout -b "$branch"
7git add config prompts evals memory
8git commit -m "Codex weekly outbound tune"
9
10body="$(cat outputs/pr-summary.md)"
11
12python3 scripts/create_pr.py \
13 "$branch" \
14 "Codex weekly outbound tune" \
15 "$body"

The PR should read like a teammate wrote it:

text
1Changed:
2- Raised implementation_page_visit from 4 to 6.
3
4Why:
5- KiteOps had implementation-page intent and replied with implementation timing.
6- The previous score routed this account to ignore.
7
8Before:
9- eval score 0.75
10
11After:
12- eval score 1.00
13
14Reviewer 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.

Nicolas Finet - inline image

Create scripts/weekly_tune.sh:

bash
1#!/usr/bin/env bash
2set -euo pipefail
3
4cd "$(dirname "$0")/.."
5
6python3 evals/score.py || true
7scripts/run_codex_step.sh improve_scoring
8python3 evals/score.py
9scripts/run_codex_step.sh pr_summary > outputs/pr-summary.md
10scripts/open_pr.sh

Then cron:

bash
10 8 * * MON cd ~/codex-self-improving-outbound && scripts/weekly_tune.sh

If you use GitHub Actions, keep the same shape:

yaml
1name: weekly-outbound-tune
2
3on:
4 schedule:
5 - cron: "0 8 * * 1"
6 workflow_dispatch:
7
8jobs:
9 tune:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v4
13 - uses: actions/setup-python@v5
14 with:
15 python-version: "3.11"
16 - run: pip install -r requirements.txt
17 - run: python3 evals/score.py || true
18 - 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:

bash
1git clone <repo>
2cd codex-self-improving-outbound
3python3 -m venv .venv
4source .venv/bin/activate
5pip install -r requirements.txt
6python3 evals/score.py
7python3 scripts/propose_improvement.py

Expected first run:

text
1score=0.75
2changed config/scoring.yaml
3implementation_page_visit: 4 -> 6
4score=1.00
5open 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.

Remix in YouMind

Turn one viral article into a full content workflow

Collect the source, decode the pattern, create assets, draft the story, and distribute from one AI workspace.

Explore YouMind
For creators

Turn your Markdown into a clean 𝕏 article

When you publish your own long-form writing, images, tables, and code blocks make 𝕏 formatting painful. YouMind turns a full Markdown draft into a clean, ready-to-post 𝕏 article.

Try Markdown to 𝕏

More patterns to decode

Recent viral articles

Explore more viral articles