BRUTAL. Solution Story Engine

Building an outreach sequencer for managers who don't write copy

examples/second-post.md · 2546 words · L0 · 49/50

Export articleFrontmatter JSONJSON-LDFull bundle

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:

  1. 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.
  2. 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.
  3. The band hides behind a disclosure. In the approval screen it sits collapsed, so seeing it is a deliberate act rather than ambient exposure.
  4. 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 <> instead of the canonical form, so a small alias map absorbs the near-misses. We keep the map narrow and review additions by hand, because every alias weakens the closed-set guarantee a little.

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:

  1. Let the model do the research and the drafting, where breadth beats judgement.
  2. 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.
  3. 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.

Working artifacts (fact map, review, rubric) — internal, never published

gap report

None blocking. Open enrichment items remain in the intake record (not required by this angle): outcome of the 10-sample sign-off, first real usage and spend figures from the built-in logging, and a client-side quote. Each would upgrade a future revision from design-truth to result-truth.

fact map

# claim (compressed) record field confidence ok?
1 internal tool for GoStudent; 100+ centres; managers non-marketers; tried and abandoned channel §1, §2 stated ok
2 three brands: Studienkreis (DE), LernQuadrat (AT), GoCampus §1 client.name stated ok
3 WhatsApp-Web persona shorthand (builder's characterisation, framed as internal shorthand) §1 who_felt_pain stated ok
4 channel demands (strategy, persona, copy, structure, tokens); Smartlead/Instantly/Lemlist/Apollo/Clay assume competent operator; infrastructure vs expertise §2 previous_attempts stated ok
5 non-goals: no sending/scheduling/warm-up/deliverability; managers keep senders §3, §7 stated ok
6 allowlist because many domains; wizard captures centre + sender variables; generation hard-blocked until both §3, §4 stated ok
7 five-stage pipeline incl. durable gate; laptop-lid resilience; gate holds days (7-day park is record fact, "days" used) §4, §5 stated ok
8 stack: Next.js/Vercel, Supabase Frankfurt, Prisma, Auth.js JWT, Inngest §4 stack stated ok
9 7 languages; six until Italian added mid-project §6/§8 stated ok
10 per-brand server-side theming, no skin choice §4 custom stated ok
11 Sonnet-only operator instruction overrode architecture's research-model recommendation §3, §6 stated ok
12 caching ~90% off cached input; projected spend $50–300/mo (framed as projection) §8 metrics stated/projected ok
13 triggers: ~$2/sequence sustained, ~$300/mo trajectory → halt + escalate §3 ceilings stated (design) ok
14 risk-first headless M1 with cost logging from first call; days-not-months + 3-week extension; tests 302 → 344 + 1 E2E §5, §8, §12 stated ok
15 Munich fixture: maths/German, 29 €/45 min, parents before mid-year report cards §11 evidence (fixture, labelled as fixture) stated ok
16 income field → hedged band; two-boundary schema validation; disclosure; gate-only §5 hard part 1 stated ok
17 automation-bias research informed gate; button after four fields; inline-editable; research cheap / generation expensive §5 resolution stated ok
18 canonical markers; pure injection function; throw on unknown; <> alias; narrow alias map; "contract instead of a hope" (builder quote) §5 hard part 2 stated ok
19 fan-out beat schema migration; separately tunable output §6 stated ok
20 Web Speech + Sonnet extraction preserved Sonnet-only §6 stated ok
21 "campaign" rename after real usage; Studienkreis red-not-blue correction §6 stated ok
22 four speeds: ~60 s / ~10 s / faster / <100 ms single transaction §4, §8 stated/estimated ("about/roughly") ok
23 concurrency 5 (plan) vs 10–30 architecture target; enqueue-only cap check; 1.5 s polling; admin read-only by construction + runtime test §8 not_improved, §4 stated ok
24 no business results; N=10 sign-off across 7 languages; UsageLog built for measurement §8 stated (design) ok
25 closing framework (research/gate/deterministic last mile) derived §4/§5 opinion, grounded ok

rubric — outreach-sequencer — v2 rewrite — loop 1

# criterion score evidence line (quoted)
1 specificity density 5 "302 unit tests at the baseline to 344 plus a critical-path end-to-end test" + fixtures, five named tools, four-speed table, $ triggers, 12+ earned specifics
2 hard part explained 5 two design problems with full mechanism: "the set of tokens that can reach an exported document is closed, enumerated, and tested"
3 stakes in reader units 4 "silent corruption is the worst failure class there is" + dormant channel across 100+ centres; still no euro figure (open NEEDS)
4 readability mechanics 5 checks.py PASS (v2 thresholds)
5 voice match 5 hedged-authoritative register throughout: "Design truths today, measurements next."
6 structural momentum 5 two-problem spine declared in the opening and paid off in order; every section teaches
7 transferable insight 5 the three-step encode-the-expert framework, imperative, Brutal-free
8 honest limitations 5 "concurrency sits at 5… that cap is a billing tier, not a design property, but today it is the number"
9 differentiation 5 swap test fails at fixtures, alias-map reasoning, override honesty, N=10 gate
10 factual traceability 5 fact map 25/25; 0 orphans

TOTAL: 49/50 · gates: PASS VERDICT: ship (register v2; operator review of the new register requested)

verification sign-off (stage 2, operator, 2026-07-24)

  • GoStudent + brands cleared to L0 by the operator; client naming applied above.
  • KTM and e-motion remain descriptors (L2) pending their own clearance.
  • Compliance-descoping omission and undated timeline stand as accepted defaults.
  • Register v2 rewrite per operator's exemplar corpus (voice-calibration.md), 2026-07-24.

stage-8 notes (working)

  • Accepted softs (readability-check.md override protocol): the 7-sentence model-layer paragraph earns the room (two constraints + their reasoning); the transferable-part question is a genuine diagnostic the reader must answer, not a rhetorical setup; passive share 11% is the EN heuristic reading legitimate constructions ("gets checked", "is enforced").