Skip to content

Batch approval & compliance

Nothing leaves until a person says so.

Fifty drafts at a time, each one sitting next to the evidence it cites. You read them, change them, approve or reject them. The gate isn't a setting we recommend leaving on — it's a boundary in the system, and there's no switch for it.

The review queue

Fifty at a time, evidence on the right

Batches are small on purpose. Fifty is enough to see a pattern in your own copy and few enough that you actually read them — which is the entire point of the gate.

Batch 03 · UK & US SEO agencies · 50 drafts · in review

Awaiting you · 18 of 50 read

Draft 19 · northbeamdigital.co.uk · sam@northbeamdigital.co.uk

Subject: the SEMrush widget on your /reports page

Sam —

You're still running SEMrush's reporting widget on /reports. Forty-one agencies reviewing it publicly this year said the same thing: the reports are technically correct and unreadable to the client.

We build the client-facing layer on top of the same data. Worth fifteen minutes?

ApproveEditRejectRegenerate

Evidence

Cited signal

SEMrush reporting widget detected on the public /reports page.

northbeamdigital.co.uk/reports · 22 Jul 2026

Verbatim quote · 41× theme

“Technically correct and completely unreadable to my client.”

Trustpilot · Mar 2026

Compliance

  • Country GB · CASL not engaged
  • Postal address present
  • Unsubscribe injected
  • No suppression match · last contacted never

What you can do

Six actions, one of which doesn't exist

Everything here is per-draft or rule-based. Nothing lets you clear a batch without having looked at it.

Read

The draft, its cited signal and the verbatim quote sit on one screen. Both citations carry their source link, so checking a claim is a click rather than an investigation.

Edit inline

Rewrite any line in place. The edit is stored beside the original, so the learning loop can later tell you which of your rewrites actually improved reply rate.

Approve

Marks that one draft as ready. Approving is per-draft by default; a batch is only approved once you've been through it, and the approver and timestamp are recorded.

Reject

With a reason. Rejections are the highest-value data in the system — they're what retires a losing angle before it reaches another two hundred people.

Regenerate

Send it back to be rewritten against the same signal, or against a different pain theme from the dossier if the angle was the problem rather than the wording.

Bulk reject by rule

“Reject anything citing the pricing theme.” One pass clears a whole angle out of the batch. There is a bulk reject; there is deliberately no bulk approve.

The human gate

There is no “send all”, and no setting that removes the gate.

Every outreach tool says a human is in the loop. Most of them mean a checkbox that a hurried operator turns off in month two. Ours is enforced in three places that a preference screen can't reach.

In the pipeline

The graph ends at the queue

Draft generation is a separate workflow from sending, and it contains no dispatch code at all. Its final state is a batch sitting in review. You cannot misconfigure your way into an automatic blast, because there is nothing downstream to misconfigure.

In the interface

Approval is per draft

Bulk rejection exists because clearing a bad angle quickly is good for everyone. Bulk approval doesn't, because it's the one control whose only real use is skipping the reading. A batch records who approved it and when.

In the database

No fact, no row

A draft's citation is a NOT NULL foreign key to a real, dated, sourced signal. A draft with nothing citable about that specific company cannot be written to the database — never mind approved or sent.

-- drafts
cited_signal_id  UUID NOT NULL
    REFERENCES signals(id) ON DELETE RESTRICT

Prospects with no citable signal aren't padded into the batch with something vague. They're routed to a fallback list and left there until a scan finds them a reason.

Duplicate protection

Emailing the same person twice is structurally impossible

Not “unlikely if the sync ran”. Three independent mechanisms, all of them in the database, all of them enforced at the moment of the send rather than the moment of the plan.

Checked in the send transaction

Suppression isn't consulted when the batch is planned and then trusted. It's re-read inside the same transaction that writes the send, because someone can unsubscribe in the hours between drafting and dispatch.

A unique index, not a rule of thumb

One live send per contact per batch, enforced by a partial unique index. A retry, a double-click or a replayed webhook collides with the database rather than with your prospect's inbox.

An org-wide cooldown

Contacts carry the date they were last approached, and the workspace sets a cooldown window — ninety days by default. Two campaigns run by two people cannot land on the same person inside it.

The send path, in full

BEGIN;
  -- suppression check and send insert, one transaction
  SELECT 1 FROM suppressions
   WHERE org_id = $1 AND lower(email) = lower($2);
  INSERT INTO sends (batch_id, draft_id, contact_id, …);
COMMIT;
-- uq_send_campaign_contact: (contact_id, batch_id) WHERE status <> 'failed'

Jurisdiction gates

The gate rejects. It never rewrites.

Compliance checks are deterministic code, not a model asked to be careful. When a batch fails one, it stops and tells you which rule and which drafts — it doesn't quietly patch the message and carry on.

CAN-SPAM

Blocks the batch until it's satisfied

A physical postal address and a working opt-out are required fields on the campaign. Without both, the batch cannot leave review — the gate refuses the batch rather than quietly sending a non-compliant version of it.

CASL

Hard block on Canadian prospects

Canada's consent regime does not accommodate cold bulk email, so prospects resolving to a Canadian country code are excluded from bulk batches entirely. Not warned about — excluded, and told you why.

Unsubscribe

Injected, and not removable

Every approved message carries an opt-out link. It's added by the gate, not by the copywriting model, so no prompt, template or edit can produce a message without one.

None of this is legal advice, and it isn't a substitute for knowing the rules in the markets you sell into. It's the floor, enforced consistently, so the failure mode is a blocked batch rather than a complaint.

The audit defence

Why this person, and why now

Every contact carries a stored record of why we believed they were contactable at all. It's the difference between a defensible answer and an apology.

Where the address came from

The public page it was found on, stored with the contact. Provenance, not a purchased row from a list broker you can't name.

What kind of address it is

Role inbox or a named individual — the distinction that decides your posture under GDPR, recorded rather than assumed.

Whether it verified

Valid, risky or invalid at the time of enrichment. Risky addresses stay out of batches instead of quietly costing you deliverability.

Who approved it, and when

Batches record their approver and timestamp. If anyone ever asks why a specific person received a specific email, the answer is one query long.

Anyone can mark a contact as do-not-contact, and unsubscribes, bounces and complaints write to suppression automatically. Once an address is there, it is there permanently — the list only ever grows.

Start with the dossier.

Tell us your product and we'll mine your market's public complaints — ranked, quoted and sourced. Useful whether or not you ever send an email.

Free · no card · yours to keep