← All posts

Outbound copy has to pass your voice rules before anyone sees it

A workspace's voice used to be a paragraph in a prompt that nothing checked. It is now a typed rule set your workspace authors, enforced by a synchronous lint at every seam a draft can come through — the drafting model, the proposal, and the rewrite. A banned phrase costs the model one corrective retry and then fails loudly. And a phrase a reviewer cuts becomes a proposed rule. Release notes 2026-09-16 — vocion-core#370.

Vocion Teamvocion-v2.88.0

Status: shipped. #370, commit 30b69679, cut as v2.86.0. Migration 0109_project_voice_rules.sql. Part of Vocion 2.88. The engineering story — why the prompt was causing the problem it described — is in the developer post. Two things this post is careful about: the gate is wired into one action today, and a rewrite that fails the gate twice currently tells the reviewer nothing on screen. Both are below.

Every workspace that sends anything has a voice, and until this release Vocion's only way of knowing yours was a paragraph of prose in a playbook. A drafting skill cited it, a model read it, and then nothing at all looked at what came out. The first person to find out that a draft opened with "Just checking in — quick question" was whoever's name was on the send.

The rules are now data and the check is code.

What you author

A workspace writes voice.yaml beside its trust.yaml, and workspace:apply lands it on the workspace row. A rule is a pattern and the reason it is banned:

never:
  - pattern: "as per my last email"
    reason: "Passive-aggressive. Restate the ask."
prefer:
  - pattern: "reach out"
    use: "email"
allow:
  - leverage   # our customers genuinely say this
maxWordsPerSend: 120
noEmDash: true

never blocks. prefer is reported and never blocks — a steer that blocks is a never wearing a disguise. allow names a platform-default rule your workspace opts out of, which means the exemption is versioned in the same file as the rest of your voice rather than living in someone's head.

What ships underneath it

Core carries a floor of 34 banned constructions plus an emoji ban, in four families: filler openers (I hope this finds you well, just checking in, circling back, I wanted to reach out), register announcements (no pitch, quick question, just curious, curious about…), hedged non-offers (would love to, happy to, feel free to, let me know if), and assistant vocabulary (great question, delve, leverage, unlock, supercharge, game-changer).

Every entry carries the reason it is there, in words you can argue with — circling back is banned because "a follow-up should restate the ask, not name the act of following up." The list is short on purpose. The bar for an entry is that it is a register problem in any business outbound, for any sender, in any workspace.

Which is why exclamation marks, em-dashes and length ceilings are not in the floor, and the file says so: both appear in real founder correspondence, and banning them platform-wide would be taste dressed as hygiene. Your workspace can ban them in one line. Core will not do it for you.

Where it is enforced

Three seams, because a gate that only covers the model call has a corridor around it:

The retry budget is one attempt. A model that repeats a banned phrase after being told what it was and why will not find it on the fourth try, so the second failure is a loud one rather than a quiet degradation. On the rewrite path, a second failure returns the original draft untouched and writes no revision row: nothing that failed the gate reaches the queue.

The gap, named

When a rewrite fails the gate twice the server produces an explanation — "The rewrite kept breaking the workspace's voice rules, so the draft is unchanged", with the offending phrases. No component reads it. The reviewer sees the draft come back unchanged, with nothing on screen saying why.

What is enforced is real: the tell-laden copy does not reach the queue and no revision is written. What is missing is the sentence that tells the person what just happened. It is a UI gap, not a gate gap, and it is the first thing to fix here.

Edits become rules

The queue already recorded that an edit happened. It recorded nothing about what was cut, because approving a draft replaces the proposal wholesale and the words the agent wrote are simply gone.

So the diff is read at the one moment both versions exist. A word-level longest-common-subsequence pass finds the runs the reviewer deleted — a phrase cut from the middle of a kept sentence reads as a deletion, not as a wholesale replacement — and every one the rules did not already catch is proposed as a new never rule.

A deleted phrase becomes a pending candidate in Learnings the first time it is cut. Cut the same phrase again and the existing candidate's occurrence count goes up, so "this has been cut three times" is one number on the card rather than a feeling. Three is the cap on how many phrases one decision may propose, so a reviewer who rewrites a draft from scratch cannot flood the queue with twenty rules.

The filters are deliberately conservative: runs of two to eight words only, nothing carrying digits, URLs, addresses or proper nouns, and nothing the rules already flag. And the whole loop is opt-in — a workspace that has not named a learning step in its voice config records nothing.

Nothing is adopted automatically. The candidate sits in Learnings until a person accepts it, which is both the manifesto's gate and the honest position: a reviewer trimming a sentence for length looks identical in a diff to one deleting a tell, and only a person can tell them apart.

What is not done

The gate covers one action — personalization.enroll — plus the action-agnostic rewrite path. Every other action that produces copy is unguarded until it is wired in.

And no live model call is exercised anywhere in this work's tests. The retry, the corrective message and the rewrite failure path are all verified against scripted models and fixtures. The lint itself is pure and synchronous and is tested directly; what has not been watched end to end is a real model being told a real phrase and coming back clean.

Where this lives