n8n's Guardrails node masks your PII. It doesn't give it back. Here's why that matters.
n8n shipped a native Guardrails node back in November. PII detection, jailbreak protection, content filtering - all built into the platform, zero external services. It's a genuinely good default, and if you're building AI workflows in n8n and not using it yet, you should be.
But we kept hitting the same wall with it, and once you see the wall, you can't unsee it.
The wall
Here's a workflow almost everyone building support automation eventually writes:
[Webhook: support ticket] β [Guardrails: Sanitize Text] β [LLM: summarize] β [write summary to CRM]
A ticket comes in: "Hi, I'm Sarah Chen, my account email is sarah.chen@acme.com, and I was charged twice for my subscription."
Guardrails' Sanitize Text mode catches the PII before it hits the LLM: "Hi, I'm [NAME], my account email is [EMAIL], and I was charged twice for my subscription."
Good. The LLM never sees Sarah's real email. It summarizes: "[NAME] reports a duplicate subscription charge tied to [EMAIL]."
Now write that to the CRM. And here's the wall: you can't. [NAME] and [EMAIL] are gone. Not encrypted, not hashed - gone. Guardrails redacts, and redaction is a one-way door. There's no way to ask "what was [EMAIL] actually?" because the node never kept a record. It just deleted the substring and moved on.
So now you're stuck writing "[NAME] reports a duplicate charge tied to [EMAIL]" into a customer record that's supposed to say Sarah Chen. You either accept a useless CRM entry, or you bypass Guardrails for this step and send the raw ticket to the LLM anyway - which defeats the entire point of having a guardrail in the first place.
Why this isn't a Guardrails bug
I want to be fair to n8n here, because this isn't a flaw in their implementation - it's the design goal. Guardrails is a content moderation primitive. Its job is: does this text violate a policy, and if so, neutralize it.
Jailbreak detection, NSFW filtering, keyword blocking - for all of those, throwing the violating content away is exactly correct. You don't want the jailbreak attempt preserved anywhere. PII just happens to share a node with those other checks, and inherits the same throw-it-away behavior.
That's fine for compliance scanning ("did this text contain a credit card number, yes/no"). It's not fine the moment your workflow needs the value of that PII downstream - which, if you're doing anything beyond pure logging, is most real workflows.
The piece that was missing: reversibility
This is the exact gap we built Privent to close. Same idea as Guardrails' sanitize mode - mask sensitive values before they reach an LLM - but with one architectural difference: the mapping between a token and its original value is kept, not destroyed.
[Webhook] β [Privent: Tokenize] β [LLM: summarize] β [Privent: Detokenize] β [write to CRM]
The ticket becomes: "Hi, I'm [NAME_001], my account email is [EMAIL_002], and I was charged twice for my subscription."
The LLM summarizes using the tokens, same as before - it still never sees Sarah Chen or her real email. But at the trusted egress point (right before you write to the CRM), Detokenize swaps the tokens back: "Sarah Chen reports a duplicate subscription charge tied to sarah.chen@acme.com."
That's the whole difference. Mask going in, restore coming out, and the LLM is never in the loop for either direction.
"Isn't that just... storing the PII somewhere else?"
Fair question, and worth answering directly instead of hand-waving it. Yes - somewhere, a token has to map back to a real value, or detokenization is impossible by definition. The question that actually matters is where that mapping lives and who can reach it.
We built two modes specifically because the answer to that question depends on your workflow:
Tokenless mode - no API key, no account. The tokenβvalue map lives in n8n's own workflow static data, scoped to a single execution's
sessionId. Nothing leaves your n8n instance. This is the mode for "I just need tokenizeβLLMβdetokenize inside one workflow," which covers the support-ticket example above completely.npm install n8n-nodes-privent # set Authentication = Tokenless (Visitor) on the node, doneCloud mode - for when you need the mapping to survive across multiple workflow runs, or across different workflows entirely (multi-agent handoffs, async processes where tokenize happens in one execution and detokenize happens in another, hours later). That needs a real persisted vault, which means a backend, which means an API key.
If your workflow is "one webhook in, one response out," you don't need Cloud mode. Tokenless mode covers it, and the mapping never crosses your network boundary.
Where Guardrails is still the right tool
I don't want to oversell this. If your use case is purely "block this if it contains a jailbreak attempt" or "don't let credit card numbers reach this LLM, full stop" - Guardrails is simpler, it's zero-install, and you should use it. We're not trying to replace it.
The line is pretty clean once you draw it out:
| Need | Use |
|---|---|
| Block/flag content that violates a policy | Guardrails |
| Mask PII and never need it again | Guardrails (Sanitize mode) |
| Mask PII, then restore the real value downstream | Privent |
| Jailbreak / prompt injection detection | Guardrails |
| Audit trail of which agent touched which token | Privent (Cloud mode) |
A lot of workflows genuinely only need the left column. Some - anything that writes an LLM-processed result back into a system of record with the subject's real identity - need the right one, and that's the gap nobody else in the n8n ecosystem is covering yet.
Try it
cd ~/.n8n
npm install n8n-nodes-privent
Tokenless mode, no signup: github.com/privent-ai/n8n-nodes-privent
If you've hit this same wall with Guardrails - or solved it some other way - I'd genuinely like to hear how. Drop it in the comments.
Comments
No comments yet. Start the discussion.