How I let an agent draft replies to my business texts without it sending one
textline is a Cloudflare Worker and a Rust CLI that lets agents read a business text line and write drafts. Nothing is sent without a person's approval, and 33 end-to-end checks, including 'the CLI has no way to send', pass.
Contents
You want an agent to read the texts your business number gets and write the replies, and you do not want a single one to go out in your name until you have read it. That is the whole design of textline: agents can draft, and only a person can send. It took six commits between 16:10 and 16:57 on 2026-10-27.
This is how it is built, the checks that show the constraint holds, and what those checks do not show.
How does a text move through it?
A provider (Telnyx or Twilio) posts each inbound text as a signed webhook to a Cloudflare Worker. One Durable Object per text holds it as a state machine with seven states: Arrived, Drafted, Approved, Sent, OptedOut, Declined and Failed. The Worker keeps its own clock for arrival and send, so every reply has a real reply time in milliseconds (replyMs), not the provider's.
The Worker never calls our machine. Our hub pulls: it reads /inbox, and a hub page called Replies shows each sender's words with the draft below them, and Approve and Decline. The provider is one seam of three functions (verify, parse, send), so Telnyx and Twilio sit behind the same interface.
What stops an agent from sending?
Three separate things, each tested.
- The state machine. A text can only be approved from the Drafted state. A text already sent cannot be approved twice, and a sender who texted STOP cannot be approved at all.
- The CLI has no send.
textline inbox --needs-draftlists texts waiting for a draft, andtextline draft <id> "words"saves one. There is no approve command. - The footer. Every outgoing reply ends with one line naming the business, the page to read more and how to opt out, for example
Web Design Vernon · webdesignvernon.com/text · STOP to opt out. We added it because Canada's anti-spam law (CASL) covers texts, and a commercial message under it has to identify who sends it and give an unsubscribe mechanism.[1]
Did it work?
bun run e2e starts the Worker under wrangler dev and posts signed fixtures at it from outside. When we ran it tonight, all 33 checks passed. Some of the checks that matter most:
- a genuine text is accepted as Arrived, and a retry is acknowledged but not duplicated;
- nothing is sent before approval, and approval sends and records the reply time;
- a forged Twilio signature is refused;
- STOP sends nothing and approval for an opted-out sender is refused; START lifts the opt-out;
- the CLI can list and draft but "has no way to send";
- deleting a number removes its threads and keeps only the opt-out, and a sweep 731 days on deletes every thread.
Did it hold on the real line?
Yes. On 2026-10-27 Mackenzie sent two test texts from a mobile to the real Telnyx number. The first, a START, arrived at 17:01:09, 1,106 ms after Telnyx stamped it. Its signature verified against Telnyx's real public key, the payload parsed, the sender was marked opted in, and nothing was sent back. The second, a short greeting, arrived at 17:01:29, 493 ms after Telnyx stamped it, and sat in Arrived.
A Claude agent drafted a reply. Mackenzie approved it at 18:47. The recorded reply time was 6,405,494 ms, which is 107 minutes, and nothing went out in that time. Once approved, the text was sent through the Telnyx API and Telnyx reported it sent 222 ms later and delivered 1,180 ms after the send. The reply carried the footer line.
Where was it tested?
The 33 checks sign their own fixtures: the Worker accepts what our fixtures sign and refuses a forged one. The live test above ran on Telnyx. The Twilio signature scheme follows Twilio's documentation and runs against our fixtures.
Next to build: pulling texts into the hub's People and Orgs as contact events, and a push to Mackenzie when a text arrives. The Worker answers none of the provider keywords itself. Telnyx answers STOP, START and HELP from its own profile, so the Worker records them and sends nothing back.
Deletion is built: textline forget <number> --yes removes every text from that sender, and a daily alarm deletes senders with nothing in 24 months. Only the opt-out list stays, so a person who texted STOP is not messaged again after their texts are removed.
What can you take from this?
- Put the constraint in the interface, then test the absence. "The CLI has no way to send" is a check that fails if someone adds a send. A rule in a prompt cannot do that.
- Keep your own clock. If you want real reply times, stamp arrival and send yourself.
- Make the provider one seam. We changed nothing outside it to add Twilio beside Telnyx.
If you are putting an agent in front of a customer channel and want to compare designs, I'd like to compare notes: mackenzie@mackenziebowes.com or @_mackenziebowes.
Sources
- Government of Canada, An Act to promote the efficiency and adaptability of the Canadian economy (CASL), section 6, Justice Laws Website. laws-lois.justice.gc.ca/eng/acts/E-1.6/page-1.html