I gave Codex terminal access to my cold email stack. Full setup below, plus the one flag that stopped it from nuking a live campaign.

Tools & Stack

Most "AI SDR" talk is about writing copy. That is the least interesting thing an agent can do for you. Copy you can already generate in a chat window and paste.

What actually eats your week is the operations layer. Checking which campaign stalled overnight. Pulling reply counts before a Monday call. Re-importing 40 contacts a CRM sync dropped. Reading an inbox thread, deciding it is a real reply and not an out-of-office, and drafting the response. Verifying a tracking domain that quietly failed. None of that is hard. It is just constant, and it does not batch well.

That work is scriptable, which means it is agent-able, as long as your sending tool has a real command line or API and not just a web dashboard.

I have been running this with Codex for about six weeks. Below is the setup, the parts that broke, and the guardrail I would not run without.

Why the terminal and not a chat window

Codex is good in a shell. It reads the output of a command, decides what to do next, runs the next one. A chat window makes you the clipboard between the agent and your tool, and you get bored around step four.

For that loop to work, the tool has to give the agent three things:

  1. Machine-readable output. JSON, not a rendered table with box-drawing characters.
  2. Stable exit codes, so the agent can tell "no results" from "your key is dead" without parsing English error text.
  3. A refusal path on anything destructive, so a confused agent hits a wall instead of a delete.

If your sending platform only has a web app, you are stuck with browser automation, which breaks every time they ship a CSS change. Check whether yours has a public API first. Several do and never advertise it.

I use FirstSales, so the examples below are its CLI. If you are on something else the shape of the workflow still holds, just swap the commands.

Step 1: install and prove the connection

npm install -g @firstsales.io/cli

Then a scoped key, not your main one. This matters more than it sounds, and I will come back to it.

export FIRSTSALES_API_KEY="your_key"
export FIRSTSALES_BASE_URL="https://api.app.firstsales.io"

firstsales whoami --json
firstsales doctor --pretty

whoami returns the user, key, org and workspace context. It does not print the key back, raw or hashed, which is the behavior you want when an agent might paste output into a log you forget to clean up.

Get your ids once and write them down, because almost every command needs them:

firstsales orgs list --json
firstsales workspaces list --org org_123 --json

Step 2: give Codex the map

Codex reads an AGENTS.md in your working directory. This is where the whole thing lives or dies. Without it the agent guesses at commands, guesses wrong, and you spend the afternoon correcting it.

Mine is short:

# Outreach operations

Tool: firstsales CLI. Always pass --json.
Org: org_123   Workspace: ws_123

## Rules
- Read before write. Run whoami, then list/get, before any mutation.
- Never run a command with --confirm unless I asked for that exact deletion in this session.
- Use --dry-run first on anything that writes.
- Every retryable write gets --idempotency-key.
- Exit codes: 0 ok, 1 runtime, 2 usage, 3 auth, 4 not found, 5 rate limited.
  On 5, stop and tell me. Do not retry in a loop.
- Do not pause or resume a campaign without asking.

## Commands I use
firstsales campaigns list --org org_123 --workspace ws_123 --json
firstsales campaigns analytics --org ... --workspace ... --campaign camp_123 --json
firstsales campaigns progress --org ... --workspace ... --campaign camp_123 --json
firstsales inbox threads --org ... --workspace ... --json
firstsales connectors test --org ... --workspace ... --connector conn_123 --json

That last section saves more tokens and more wrong turns than anything else in the file. An agent that has seen the exact command shape once will reproduce it. An agent working from a doc site will improvise flags that do not exist.

Step 3: the morning read

This is the prompt I run most days.

Check every campaign in ws_123. For each, pull analytics and progress. Tell me: which ones sent less than 60% of expected volume yesterday, which have replies I have not read, and which connectors fail a test. Do not change anything.

Codex runs campaigns list, loops analytics and progress per campaign, hits connectors list then connectors test on each, and comes back with a summary. Two minutes, no tabs, and it catches the boring failures I used to find on Thursday.

The failure it caught that paid for the whole setup: one sending account had silently disconnected after a Google token refresh. Volume dropped about a fifth. In the dashboard that shows up as a campaign that looks basically fine.

Step 4: contact imports that survive being interrupted

Imports are where agents do real damage, because the natural failure mode is a half-finished run that you restart and now everything is duplicated.

firstsales contacts create \
  --org org_123 --workspace ws_123 \
  --data '{"email":"alex@example.com","firstName":"Alex"}' \
  --idempotency-key crm-import-alex@example.com \
  --json

The idempotency key is the whole trick. Key it off something stable and unique to the row, like the email, not off a counter. Then a crashed import re-run is safe, and you can tell the agent to just start over instead of reasoning about where it stopped.

Prompt shape that works:

Read leads.csv. For each row, create the contact with an idempotency key of "csv-<the email>". If any row exits non-zero, log the email and keep going. Report failures at the end.

The "keep going and report" instruction matters. The default agent instinct on an error is to stop and ask, and then you are babysitting a 400-row import.

Step 5: inbox triage, with a human in the middle

This is the part I was most nervous about and it turned out to be the most useful.

firstsales inbox threads --org ... --workspace ... --json
firstsales inbox thread --org ... --workspace ... --thread thread_123 --json
firstsales inbox approve-draft --org ... --workspace ... --email email_123 --idempotency-key approve-email-123
firstsales inbox reject-draft --org ... --workspace ... --email email_123 --json

My rule: the agent reads and classifies, I approve. It sorts threads into positive, question, referral, not-now, and auto-reply, and it can pull product context to ground an answer:

firstsales kb query --org ... --workspace ... --kb kb_123 --data '{"query":"what do we do about SOC2"}'

Then it stops and shows me the queue. I approve or reject. I have not found a version of full auto-reply I trust, and I would not suggest one. The classification alone removes most of the work, because the actual bottleneck was never typing, it was reading forty threads to find the six that mattered.

Genuine caveat since I know someone will ask: classification is not free of mistakes. It has miscategorised a sarcastic "sure, send it over" as positive at least twice. That is fine when a human approves. It would not be fine on autopilot.

The guardrail

Destructive commands refuse to run without --confirm. That is a property of the tool, not a habit I have to maintain, and it is the reason I let an agent near a production workspace at all.

There is a second layer worth doing. Cut the agent its own key, scoped down:

firstsales api-keys create \
  --org org_123 \
  --data '{"name":"codex automation","scopes":["campaigns:read","contacts:write"],"accessLevel":"workspace"}' \
  --json

Read on campaigns, write on contacts, nothing else. Now the worst outcome of a bad prompt is some junk contacts, not a deleted campaign. Rotate it when the project ends. Raw keys are shown once at creation and never retrievable, so store it properly the first time.

And --dry-run prints the method, URL and body without sending anything. When I am testing a new prompt I have Codex run every mutation dry first and show me the request. It has caught wrong workspace ids twice.

What did not work

--auto-approve on the copilot command is best-effort. The server decides, not the CLI, and if a turn is still pending it exits 1 rather than polling forever. Sensible design, but do not build a pipeline that assumes it went through.

Signals and webhooks return unsupported_operation. There is no public surface for them yet, so event-driven triggers are out. My scheduling is a cron running a Codex prompt, which is cruder than I would like.

Exit codes changed in 0.1.1. If you find a script from an older version that branches on them, it is wrong now.

What this is actually worth

Rough numbers from my own week. Campaign checks, import fixes, inbox sorting and connector debugging ran somewhere around six to eight hours. It is now closer to two, and most of that two is me approving replies, which is the part I should be doing.

The bigger change is not the hours. It is that the daily check now happens daily, because it costs one prompt instead of forty minutes of clicking. The disconnected sending account would have run broken for a week under my old habits.

Start with the read-only loop. Get the morning check working, live with it for a week, and only then let the agent write anything. If you skip straight to imports and approvals you will have a bad afternoon and blame the agent, when the actual problem was that you never made it prove it could read your account correctly.

Happy to answer setup questions in the comments. If you are running something similar on a different platform, post what your equivalent commands look like, since half the value here is knowing which tools expose enough surface to be worth automating at all.

Posted Aug 5, 2026, 1:34 PM UTC

13
Loading comments…