We recently built an internal tool for GoStudent, the tutoring group, that writes cold-email campaigns for the managers of its 100-plus physical centres. The users are not marketers. Most of them had already tried cold email, produced sequences they could not judge, and quietly stopped. The tool's job is to be the copywriter they do not have: it researches the audience, drafts the sequence in the manager's language, and hands back something that drops cleanly into whatever sending tool that manager already uses.
The build produced two design problems we think are worth writing up properly. One is about safety: the research step wanted to estimate what parents can afford, and an AI writing wealth assumptions about families is a newspaper headline waiting to happen. The other is about reliability: merge fields have to come out in each manager's exact sending-tool syntax every single time, in a tool operated by people who cannot debug a broken campaign. The two problems pulled in opposite directions, one toward more human judgement and one toward less, and the shape of the final system falls directly out of how we resolved each. This post walks through both, plus the smaller decisions and surprises along the way.
The channel was dormant, not broken
GoStudent's offline business runs tutoring centres under three brands: Studienkreis in Germany, LernQuadrat in Austria, and GoCampus. Each centre has a manager, and for that manager, outbound acquisition is one duty among many. The internal persona shorthand during the build was blunt: someone who struggles to connect WhatsApp Web to a phone. The description sounds unkind, but it set the design constraint everything else had to survive.
Now look at what the channel actually demands of that person. A working cold-email operation needs audience selection, persona research, copy that lands in the local market's register, a sequence structure with sensible timing, and merge-field tokens in the exact syntax of the sending tool. The managers had access to the standard stack: Smartlead, Instantly, Lemlist, Apollo, Clay. Every one of those products assumes a competent operator. They provide infrastructure. The missing layer was expertise, and a manager who cannot evaluate the output cannot improve it with any amount of tooling.
This diagnosis inverted the usual build. We did not build sending, scheduling, warm-up, or deliverability, and the tool does not touch inboxes at all. Every manager keeps their existing sender. What we built is the layer the market does not sell: the strategist and copywriter, encoded as a pipeline. The manager's job shrinks to editing.
The shape of the system
The tool is an internal web app behind Google sign-in. Managers span many email domains, so a single-workspace rule was impossible; a database allowlist is the gate instead. A one-time setup wizard captures two things: the centre (location, offering, context) and the manager's sender variables, meaning how their particular sending tool writes its merge fields. Generation is hard-blocked until both exist, because every downstream guarantee depends on them.
After setup, a campaign runs through five stages:
research Sonnet + live web search builds an audience persona
↓
approval gate a human reads and edits the research ← the judgement step
↓
generate Sonnet drafts the sequence from the approved persona
↓
inject a pure function substitutes the manager's exact
merge-field syntax; unknown token → throw
↓
export a Google Doc lands in the manager's own Drive
All model work runs inside a durable background-job engine (Inngest). That sounds like plumbing but is a product decision: a manager who closes a laptop lid mid-generation loses nothing, and the approval gate can hold for days without a process hanging anywhere. The stack around it is deliberately boring: Next.js on Vercel, Postgres on Supabase in Frankfurt, Prisma, Auth.js with JWT sessions. The app speaks 7 languages; the launch set was six until the stakeholder added Italian mid-project. Per-brand theming resolves server-side, so a Studienkreis manager and a LernQuadrat manager each see their own brand from first login without ever choosing a skin.
Two constraints shaped the model layer. First, an operator instruction fixed the model choice, verbatim from the build log: "Pls only ever use Sonnet models." Our own architecture review had recommended a stronger model for the research step, so the override is worth being honest about. It is the kind of trade real projects make: one model everywhere is simpler to reason about and cheaper, and the quality bar gets checked at an acceptance gate anyway. Second, prompt caching does real work here. The static scaffolding of the generation prompts is cached, which takes roughly 90 percent off those input tokens, and the projected model spend for the whole estate came out at 50 to 300 dollars a month. We also wired a stop-and-escalate trigger into the design. A sustained cost above about 2 dollars per sequence, or a trajectory past about 300 dollars a month, halts the pipeline and pages a human rather than quietly burning budget.
We sequenced the build risk-first. Before any interface existed, the generation pipeline ran headless behind a script with cost logging from the very first call. Model quality, live-web-research reliability, and cost at volume were the three unknowns that could have killed the project, so they were surfaced in week one rather than discovered under a finished UI. The initial arc took days rather than months, with a three-week extension following, and the test suite grew from 302 unit tests at the baseline to 344 plus a critical-path end-to-end test by the end.
To make this concrete, here is what a manager actually types, taken from one of our launch fixtures. A Munich centre offers maths and German tutoring at 29 euros per 45 minutes, and wants to reach parents in the weeks before mid-year report cards. That is the entire input. Audience strategy, persona, tone, structure, and tokens all come out the other end.
Design problem one: the field that could have ended up in a newspaper
The research stage originally wanted a number for likely monthly household income. The reasoning is legitimate: affordability genuinely changes what a good sequence should say, how it should frame pricing, and which offer leads. The risk is also obvious. An AI system publishing wealth estimates about specific audiences, on behalf of a tutoring company that serves families, is one screenshot away from a stereotyping story. We considered dropping the field entirely and rejected that too, because the information does real work.
The fix was structural, and we want to stress that word, because the reflex in most AI products is a politer prompt. Prompts are requests. We wanted a fence. Four decisions built it:
- The number became a band. No numeric income estimate exists anywhere in the system. The field is a hedged, qualitative affordability band, and the hedging is not a stylistic habit but a validated format.
- Schema validation enforces the hedge at two boundaries. The research layer validates the band's shape when the model produces it, and the approval route validates it again before anything is stored. A model that emits a numeric guess fails validation twice, which is belt and braces by design.
- The band hides behind a disclosure. In the approval screen it sits collapsed, so seeing it is a deliberate act rather than ambient exposure.
- It only exists inside the human gate. The band never reaches generated copy directly; it informs the sequence through the persona a human has consciously accepted or edited.
The gate itself is designed against approval theatre. Research on automation bias finds that users who like automation tend to over-approve it (Bias in the Loop, arXiv:2509.08514). We took that seriously rather than treating the human step as a legal fig leaf. The primary button stays disabled until all four research fields have actually been seen. Everything on the screen is inline-editable, so approve-with-edits is the path of least resistance rather than a power-user trick. The gate is also, not coincidentally, the cheap place to fail: research is the inexpensive call and generation the expensive one, so a persona rejected at the gate saves the money as well as the embarrassment.
Design problem two: zero wrong tokens as a contract
The second problem sounds small until you sit with it. Merge fields must come out in the manager's exact sending-tool syntax, every time. One malformed token does not fail loudly at send time; it mail-merges garbage into a prospect's inbox, inside the one tool the manager cannot debug. For a user who cannot evaluate output, silent corruption is the worst failure class there is.
So the model is never allowed to write tool syntax at all. It emits canonical markers only, and a pure function performs the substitution at the end of the pipeline. The shape of the mechanism, with illustrative tokens:
model writes: "Sie erreichen uns hier: {{canonical:CALENDAR}}"
manager config: CALENDAR → the syntax their sending tool expects
injected output: the same sentence, exact tool syntax substituted
unknown token: injection throws; the campaign never exports
Because the injector fails loudly on anything it does not recognise, "zero wrong tokens" stops being a hope about model behaviour and becomes a property of the system. The set of tokens that can reach an exported document is closed and tested. Our build notes phrased it in a way we still like: the fail-loud guarantee is what makes zero wrong tokens a contract instead of a hope.
There is one deliberate softening. Models and template authors both drift toward natural-language tokens, writing things like <
What surprised us
The cheap decision was the better product. Multi-audience campaigns were specced as a schema migration: turn the sequence's single audience column into an array. The cheaper path was to leave the column singular and fan out N parallel single-audience jobs grouped under one campaign. We took it for cost reasons and discovered the output was better: one sequence per audience, each separately editable and separately tunable, instead of a single blended sequence trying to speak to everyone. We went in expecting a compromise and came out with the design we would now recommend.
A constraint routed around itself. Voice input was requested after the Sonnet-only rule was set, and speech-to-text would normally mean adding a transcription model. Instead the browser's own Web Speech API does the transcription and Sonnet only does structured extraction from the transcript. The constraint held and the feature shipped.
The users renamed the product. Managers never said "sequence" in real usage. They said "campaign", so a full terminology sweep renamed the concept across the product.
Embedded work finds small truths. Studienkreis's brand colour had been cited wrong in our own design documents for a week. Red, not blue. Not a profound error, just a typical one.
Speeds, ceilings, and honest numbers
A manager gets four speeds, and the spread matters more than any single figure:
| mode | wall clock | what it skips |
|---|---|---|
| full research | ~60 s of model time | nothing: live web research + human gate |
| quick, from scratch | ~10 s | the human gate (research still runs) |
| quick + fast mode | faster still | web search too; the model works from its own knowledge |
| template path | <100 ms | the model entirely: one database transaction |
The template path is the one we point at in design discussions: when a manager picks a pre-baked campaign, no model is called at all, and the whole operation is a single atomic transaction. Sometimes the right amount of AI in a pipeline is none. The zero-model path also happens to make the tool feel instant in the place where beginners start.
The ceilings, stated plainly because they are real. Background concurrency sits at 5 on the current vendor plan, where the architecture had targeted 10 to 30; that cap is a billing tier, not a design property, but today it is the number. The spend cap is checked when a job is enqueued, not mid-flight, so a manager standing at the cap can overshoot it on a single multi-audience campaign. Generation progress reaches the browser by polling every 1.5 seconds rather than streaming. And admins can observe everything while mutating nothing, which is enforced the boring, reliable way: the write methods do not exist in the admin data-access layer, and a runtime test asserts they never appear.
We cannot show business results yet. The acceptance gate for launch is stakeholder sign-off on 10 sample sequences spanning the audiences and all 7 languages. Usage logging is built into every generation precisely so the next version of this write-up can report real reply rates, real per-sequence cost, and which audiences the fan-out design actually helps.
The transferable part
If a channel sits dormant in your company even though the tools are licensed and the will exists, run the diagnosis before buying anything else: is the missing layer infrastructure, or expertise? The vendors sell the first. If your people cannot evaluate the output, the gap is the second, and a tool that assumes a skill your people do not have is a cost, not a capability.
And if you do encode the expert into a pipeline, the shape that worked here generalises to most operational AI we build:
- Let the model do the research and the drafting, where breadth beats judgement.
- Put one human gate exactly where judgement matters, and design it for active review: reveal the risky field deliberately, keep everything editable, make approve-with-edits the easy path.
- Make the last mile deterministic. Whatever step must never be wrong should not be probabilistic at all: closed token sets, validation at every boundary, and a function that throws rather than guesses.
Common questions
Why not use the AI writer built into Smartlead or Instantly? Those writers assume the operator can already judge audience strategy, structure and copy quality. GoStudent's centre managers could not, which was the actual gap. This tool owns strategy and drafting end to end, then exports to whatever sender each manager already uses, so no sending infrastructure gets replaced.
What does the human gate actually approve? The persona research: audience description, buying context, the hedged affordability band, and tone. The approve button stays disabled until all four fields have been seen, every field is editable inline, and generation only runs from a persona a human has accepted or corrected.
Could this pattern work outside tutoring? The shape is not tutoring-specific: model research, one human gate where judgement matters, deterministic injection for the step that must never be wrong. Prompts, languages, audiences and sender-variable mappings live in configuration rather than code, so adapting it is mostly configuration work.
A system built this way is quiet. It sends nothing, and its best moment is a centre manager exporting a campaign they could not have written and did not have to. Most of the operational AI we build ends up with this shape, and we have come to treat that as a good sign.
antonio@brutal.ai reads every reply.