Intermediate

Updated · 8 min read

Redacting PII before it reaches your AI tools

This week a review of our own site found the chat widget forwarding raw visitor questions, verbatim, to a third-party analytics tool — and writing the same text into a database with no redaction and no retention limit. The redactor existed. It ran on three other paths. Nobody had wired it into the newest surface. That is the ordinary shape of this failure: not a missing control, a control with a gap in it. This guide is what we changed and why, written as the operator's version rather than the legal one.

Justin Williames

By Justin Williames

Founder, Orbit · 10+ years in lifecycle marketing

SharePostPost

What people actually paste into a marketing AI box

Open a text field in front of someone trying to solve a problem and they will paste the problem. All of it. Not a sanitised description of the problem — the error, the credential, the path, the customer.

When we pressure-tested our own chat widget we wrote two questions in the shape real ones arrive in. The first was somebody asking why their ESP key kept failing, with the live key inline and their own work email address at the end so we could reply. The second was somebody asking why an export broke, with the full filesystem path pasted in — which carried an employee username in one segment and a client's company name in the next. Neither person would describe what they did as "submitting personal data". Both had.

That is the working list for any marketing AI surface: credentials (API keys, bearer tokens, session tokens, anything with an sk- or xox prefix), contact details (their own email, and frequently a customer's), file paths and URLs (which leak usernames, client names, and internal hostnames without anyone noticing), and long digit runs (account numbers, phone numbers, card fragments). Every one of those arrives inside an otherwise unremarkable support question.

The people typing sensitive strings into your product are not being careless. They are debugging. Pasting the whole thing is the correct instinct everywhere except the one place you built.

Clean the text at the earliest boundary you control

The principle is that text gets cleaned at the earliest point you control, and the cleaned version is the only one that moves. Not cleaned on the way into the dashboard. Not cleaned when someone asks. Cleaned before it crosses the first boundary — the browser, the extension, the client SDK — so what leaves the user's machine is already the version you would be comfortable reading aloud.

Ours is deterministic regex, deliberately. No model, no heuristic anyone can argue with in a review: pattern in, placeholder out. Keys become [key], addresses become [email], paths become [path], digit runs become [number]. An LLM-based redactor is the wrong tool here — it is non-deterministic on exactly the inputs you most need it to be reliable on, and you cannot write a test that pins its behaviour.

Two details in that redactor took real debugging and generalise to anyone building one.

Order the patterns, deliberately. Keys run before URLs, and URLs before emails. A token sitting inside a query string has to die as a key; if the URL pattern fires first it swallows the whole string and the fragment can survive somewhere else in the pipeline. Whichever pattern is most destructive should go first.

Strip invisible characters before you match anything. Control characters and zero-width joiners can split a token across a pattern boundary, so a key survives the regex and reassembles perfectly wherever the value is read later. Ours removes that class of character first, then runs the rules, then caps the length.

The honest limit is worth stating out loud: names cannot be caught deterministically. Ours does not try, and our privacy page does not claim it — it names emails, URLs, paths, keys and numbers, which is exactly the set the code handles. A promise slightly narrower than the code is fine. A promise wider than the code is the thing that costs you.

Before storage, not before display

The tempting version is to store the raw text and clean it on the way out — into the dashboard, the export, the LLM prompt. It reads as reversible and flexible. It is neither.

Once raw text lands in a table it also lands in the nightly backup, the read replica, the search index, and the query logs of whatever ran the INSERT. Redacting the display path leaves every one of those untouched. And the moment someone asks you to delete their data, you are hunting a string across four systems instead of confirming one column never held it.

Derived data is where this gets subtle. We wanted a normalised fingerprint of each question so themes could be clustered without an LLM pass, which raised a real question: is a derived column new collection, or does it inherit the original? The answer we settled on has three conditions, and they are the ones to copy. Derive from the stored, redacted value and never from the raw input — deriving from the pre-redaction string is new collection wearing a derived-column costume. Write the derived column in the same statement as its source, and null it in the same statement that purges the source, so it cannot outlive what it came from. And store normalised plaintext rather than a hash: a hash survives deletion of its input, functions as a membership oracle for anyone holding a guess, and cannot be inspected when someone exercises a subject-access request.

Pair the redaction with a retention window and a purge that actually runs. Ours is 90 days on chat rows, called from the same routes that write them, so the deletion path cannot rot separately from the write path. Retention discipline is the same muscle as list hygiene — data you keep past its usefulness is a liability with a storage bill attached.

Double redaction, and why the server pass is not paranoia

We run the same rules twice: once client-side, before anything leaves the machine, and again server-side at ingest. The two implementations are kept in lockstep — same patterns, same order, same placeholders.

They do different jobs. Client-side is for the user: it is what makes "the sensitive string never left your laptop" a true sentence rather than a policy. Server-side is for you: it is what makes the guarantee hold when a client is out of date, when someone posts to the endpoint directly, or when a new surface ships without the client library wired in. Never trust the client to have run its own redactor — that is not distrust of the user, it is an acknowledgement that the client is the one component in the system whose version you do not control.

Belt-and-braces has an unglamorous second benefit. Because the server pass exists, a missed client integration degrades to "redacted a bit later than we wanted" instead of "stored a live credential". That is the difference between a bug and an incident.

Telemetry wants a vocabulary, not a transcript

Most of what marketing teams call product telemetry does not need free text at all. It needs to know that something failed, which thing, and how often.

Our tool telemetry carries an error_class validated against a closed set — timeout, auth_failed, rate_limited, not_found and a handful more — checked at the route and re-checked at the writer. It never carries the error message body, because message bodies are where the payload lives: the failing URL, the record that broke, the credential in the stack trace. A class name answers every question we actually ask of that event.

A rule that saved us a debugging session later: reject unknown values, never coerce them. The obvious shortcut is to map anything unrecognised to error and move on. Don't — an unknown string is a version-skew signal telling you a client is newer than the server, and coercion destroys the only evidence of that.

Exactly one of our event types is permitted free text, the one carrying a bad-experience description, and it is the one redacted on both sides. Everything else is enums, counts and durations. Applied to the analytics event that started this whole review, the fix was the same shape: the chat event now sends query_length, result count, and which result ranked first. Length answers "are people writing sentences or keywords". The words themselves answered nothing we were asking and put a visitor's email address in a third party's hands, in breach of their terms as much as ours.

The same instinct belongs in your customer data model. Store the fact, not the sentence — a boolean, a bucket, a count. Custom attribute design covers where that line sits in an ESP, and the CRM vs CDP decision covers who ends up holding the raw layer when you have both.

“We don’t send your content”: what that has to mean mechanically

Every AI vendor writes some version of that sentence. Most mean it. It is worth nothing until it is mechanical, and mechanical means four things.

The claim is greppable.If "we never send prompt content" is true, there is a single function every outbound payload passes through and you can read it in one sitting. If the answer is "several services handle it carefully", the claim is a hope.

The schema enforces it. A field typed as an enum of twelve values cannot carry a customer's email. A field typed as TEXT can, and eventually will. Design the shape so the bad thing is unrepresentable rather than merely discouraged.

A test asserts it. Ours asserts an email address and a filesystem path do not survive the insert. Without that, the guarantee is a comment, and comments do not fail builds.

The disclosure matches the code today.This is the one that caught us. Our extension's telemetry doc said plainly that user prompts and conversation content are never sent verbatim. The extension kept that promise exactly. The website's own chat box, on the same domain that receives the extension's telemetry, stored exactly that — and the privacy page described analytics as receiving page views and device type, with free text nowhere on the list. Two surfaces, one brand, one promise, and a regulator reads the product as a whole.

The rewritten privacy copy now says the chat events carry the question's length and never its words. That sentence became true the afternoon the redactor shipped. It was not true that morning, and the gap between those two states is the entire lesson. The AI Personalisation skill covers the wider guardrail set for deploying AI inside lifecycle programs, including where model output needs a human gate before it reaches a customer.

The questions to ask any AI marketing tool

Vendor security pages are written to survive procurement, not to answer these. Ask them directly, in a shared document, and treat a vague answer as an answer.

Ask thisGood answerRed flag
What exactly is in the analytics event when I use the AI feature?A field list, with types. Lengths, enums, counts.“Standard usage analytics.”
Is my prompt text stored, and where?Named store, named region, named retention window.“We don't train on your data” (a different question).
Do you redact before storage or before display?Before storage, with the pattern list shown.Any answer describing a dashboard.
Which sub-processors receive the text?A current list, with what each one gets.A list that hasn't changed in two years.
What happens to derived data on deletion?Derived columns purge with their source.“Aggregates are anonymous.”
Can you show me a test that proves the redaction?Yes, here it is.A policy document.

The second row deserves its own note, because it is where most marketing evaluations go wrong. "We don't train on your data" is a true and reassuring sentence that answers a question you did not ask. Training is one downstream use. Storage, retention, sub-processor access, support-team visibility and breach exposure are the others, and they are unaffected by a no-training commitment. Ask about storage separately, every time. The same discipline applies when you compare ESPs — the AI features shipped in 2025 and 2026 were bolted onto data models designed years before anyone was thinking about prompt logs.

None of this is a reason to keep AI out of the marketing stack. We run our own AI surfaces and intend to keep running them; the review that started this guide made ours defensible rather than shutting it down. The discipline is the same one behind personalisation that doesn't feel creepy — hold less than you could, be precise about what you hold, and make the claim you publish match what the code does today. If you want a picture of how much of this a tool handles for you before it becomes your problem, the with-and-without comparison is the honest version.

Take this into Monday: find the newest text field in your marketing stack and read the code path from that field to storage. The redactor you already own probably isn't on it.

Read to the end

Scroll to the bottom of the guide — we'll tick it on your reading path automatically.

Frequently asked questions

Isn't regex redaction too crude? Shouldn't we use an AI model to detect PII?
For this job, no. A regex redactor is deterministic — the same input always produces the same output, you can write tests that pin it, and you can show the pattern list to a reviewer. A model-based redactor is non-deterministic on exactly the inputs you most need reliability on, and 'usually catches API keys' is not a control. Use regex for the deterministic classes (keys, emails, URLs, paths, digit runs), and be explicit in your privacy copy that names are not among them, because names cannot be caught deterministically.
We already have a DPA with our analytics vendor. Doesn't that cover it?
A data processing agreement governs what the vendor may do with data you send them. It does not authorise you to send data you never told your users you were collecting, and most analytics terms of service explicitly prohibit sending personal data into the product at all. Forwarding free text a visitor typed can breach the vendor's terms and your own privacy notice at the same time. The contract is downstream of the decision about what leaves your machine.
How do we analyse chat themes if the text is redacted?
Redaction removes credentials, addresses, paths and digit runs — it leaves the sentence structure and the vocabulary that themes are actually built from. 'my [key] keeps failing on export' clusters exactly as well as the unredacted version. Derive any clustering column from the stored redacted value rather than the raw input, write it in the same statement as its source, and purge it in the same statement. The themes come out the same and the analysis is defensible.
If we can't run a full engineering project, what's the minimum?
A single function, applied at every write path for user-typed text, plus a retention window with a purge that actually runs. That is a day of work and it removes the category of incident where a live credential sits in your database. The client-side pass, the closed vocabularies and the derived-column rules are the second wave. Start with the write path, because everything downstream inherits from it.
Should we tell users their input is redacted?
Yes, in one line under the input and one bullet on the privacy page — and be specific about what is stripped rather than making a broad promise. A narrow, accurate claim ('we remove emails, URLs, file paths, keys and long number sequences before storing anything') survives scrutiny. A broad one ('we remove all personal information') fails the first time someone types a name.
Does this apply to the AI features inside our ESP, or only to tools we build?
Both, and the ESP case is harder because you can't read the code. Ask what the AI feature sends when it generates a subject line: the whole audience payload, a sample, or a schema description? Ask where the generated content and its inputs are stored and for how long. Vendors who have thought about it answer with field lists. Vendors who haven't answer with adjectives.
Aren't error messages safe to send?
Error message bodies are one of the leakiest surfaces in a stack. They routinely contain the failing URL with its query string, the record that broke, and occasionally a credential in a stack trace. Send an error class from a closed set instead, validated at ingest, and reject unknown values rather than coercing them to a generic 'error' — an unrecognised class is a version-skew signal, and coercing it destroys the evidence.

This guide is backed by an Orbit skill

Related guides

Browse all
Strategy9 min

AI personalisation at scale: the architecture that actually works

Every ESP now sells an AI personalisation layer. Most teams turn it on and quietly notice the lift is smaller than the sales deck promised. The model isn't the problem — the plumbing underneath is. Here's the data, content and activation stack that decides whether AI personalisation moves revenue or just moves dashboards.

Strategy14 min

Building a personal chief-of-staff AI on Claude Routines

A real chief of staff used to mean a salary line on an exec's budget. Anthropic's Routines feature — Claude running on a schedule with access to your work tools — pulls the job inside reach of one operator. This is the architecture: morning brief, hourly interactive layer, midday drift check, evening debrief with end-of-day reconciliation, Sunday weekly review. Plus the draft-react protocol that lets the assistant act without auto-sending, calendar work blocks that double as the task tracker, and memory files the system writes into. Brain in a GitHub repo, runtime in claude.ai, no servers.

Strategy9 min

Predictive models in lifecycle: churn, propensity, and recommendations without the magic

Predictive models in lifecycle are mostly three things: churn risk, conversion propensity, and product recommendations. Each one earns or loses its place based on whether its score actually changes a decision. Here's the operator view of what's worth deploying, what to expect from ESP-native suites, and when to build your own.

Strategy10 min

Segmentation strategy: beyond RFM

RFM is the floor of audience segmentation, not the ceiling. Every program that stops there ends up describing what users already did without ever predicting what they'll do next. Here's the segmentation stack that actually drives lifecycle decisions — and how to build it in Braze without ending up with 400 segments nobody understands.

Craft8 min

Generative AI for lifecycle content: where it earns its place and where it embarrasses you

Generative AI inside lifecycle ESPs has moved from novelty to default in 18 months. BrazeAI (formerly Sage AI), Iterable Copy Assist, Klaviyo's subject line generator — they all promise per-message copy at scale. Some uses are genuinely useful. Others are a fast path to brand drift, factual errors, and reputational damage. Here's the line.

Strategy9 min

Switching ESP without losing your lifecycle programs

Two questions land here constantly: "does it only work with Braze?" and "so it doesn't work with other CRMs?". Underneath both is the same worry — how much of the last two years dies if we move platforms. Here's what survives a migration, what doesn't, and how to build so the answer stays in your favour.

Found this useful? Share it with your team.

SharePostPost

You finished the playbook. Get the next.

New guides and product updates land in your inbox when they ship. One list, real lifecycle work, unsubscribe the second it stops being useful.

Guides and Orbit updates only. No sequences, no selling your address.

Use this in Claude

Claude can run this playbook for you.

Orbit is a free extension for Claude Desktop — no licence key, no card — that runs the lifecycle work you just read about. You've read how it works; Orbit hands Claude the same playbook as a skill it can execute: discovery, build, QA, push, on your own ESP.

Download Orbit — free