Skip to main content
Three people said yes to a conversation last month and nobody has written to them yet. The notes are in the CRM: who they are, what they asked, what would make them want to talk again. Each first note has to sound like it came from the person signing it, and none of them should go out unread. You want the drafts written for you, one per contact, from the notes you already have. You want to read each one and tap send or say no, with the reason kept, so the next drafts are better. And you want to be sure that nothing reaches a contact without your yes, however many drafts there are. Obversa makes the send a step that belongs to you. A Claude seat drafts each note from the brief and the contact’s notes. Your decision on each is a step in the run: a yes sends the note through the CRM’s command, a no records your reason, and a note you haven’t decided on stays stopped on its question. This file is one three-step run per contact, a draft, a decision and a send, with the record of all three kept beside the drafts.

Run it

Set the project up as Installation describes. Copy the file with briefs/, contacts/ and decisions.json beside it, sign in to Claude Code, and run it from that directory. decisions.json carries what the person decided on the last pass; set OUTREACH_SEND_URL to your CRM’s endpoint, or leave the fictional default:
Terminal
The output below is the proof’s offline run, with scripted seats standing in for the models, so the words are the script’s and the shape is the run’s.
Output, from the offline proof
Three contacts, three drafts, one send. The first note was approved and went to the CRM. The second was refused, with the reason on the record. The third has no decision yet, so its run stopped on the question and nothing was sent. history/ holds one line per contact in sent.jsonl, refused.jsonl or waiting.jsonl, and each contact’s run has its own record under records/.

The file

The brief says what a first note is and what it never does, and a person reads every draft:
briefs/outreach.md
Each contact is one dag() run of three steps. The draft seat writes outbox/<contact>.md; the person’s decision is an approval step; the send is a command that runs only when the decision was yes:
examples/use-cases/sales/draft-then-send.ts (excerpt)
A decision the person has already made is the step’s answer, so the run carries on from it. Without one, approval has no answer to give: the step pauses with the question on the record, and the send never runs. A no fails the step with the person’s note as its summary, and the send never runs either. The host then files each contact by what happened:
examples/use-cases/sales/draft-then-send.ts (excerpt)
Each run records to records/<contact>.jsonl under its own run id, so a later pass can reopen exactly the run that is waiting when the person decides, and the sent and refused ones are untouched.
examples/use-cases/sales/draft-then-send.ts

The team’s shape

What the run did

The proof runs the file against a scripted seat and a stand-in for curl, and checks what the page says: three drafts on disk, one call to the CRM and only for the approved note, the refusal’s note on the record, and the third run stopped on its question. Obversa recorded each contact’s run as its own event log, so the record for the refused note shows the draft, the question, and the person’s reason in order, and the record for the waiting note ends on the question. The person’s decisions here are read from a file, because the file has to run offline. In use, the answer comes through the callbacks client from a page that asks one question, and a run started again with resume: true finds it, as A person decides shows. Either way the send is a step the person owns, and the drafts that never got a yes are still on disk to read.

Next steps