Ein n8n Workflow, der der Rechtsabteilung eine einzige Eingangstür gibt. Anfragen kommen über ein Formular oder ein gemeinsames Postfach herein, werden gegen eine Taxonomie aus zwölf Typen klassifiziert und landen in einer von fünf Spuren: Self-Service-Vorlage, Playbook-Prüfung, Anwalt, blockiert beim Anfragenden oder GC-Eskalation. Ein stündlicher Sweep verfolgt die SLA-Uhr in Geschäftsstunden; ein Report am Montagmorgen sagt, was die Abteilung tatsächlich gefragt wurde. Kostet rund 0,011 bis 0,017 USD pro Anfrage an Claude Inferenz.
Der Workflow liegt in apps/web/public/artifacts/legal-request-intake-router-n8n/legal-request-intake-router-n8n.json — 26 Nodes verteilt auf drei Zweige mit jeweils eigenem Trigger. Die drei Postgres-Tabellen, die er braucht, stehen in der danebenliegenden schema.sql, und das Credential-Setup samt einer sechsstufigen Verifikationssequenz steht in der _README.md.
Wann Sie ihn einsetzen
Ihr Rechtsteam bearbeitet deutlich mehr als 30 Anfragen im Monat und kann ohne Tabellenkalkulation nicht sagen, wie viele letzte Woche eingingen oder worum es dabei ging. Anfragen erreichen Sie über den Kanal, an den sich der Anfragende gerade erinnert hat: 87% der Rechtsanfragen kommen per E-Mail, laut der Intake-Untersuchung von Checkbox, der Rest telefonisch oder persönlich. Ein relevanter Anteil dessen, was hereinkommt, ist Arbeit, die der Anfragende allein erledigen könnte, wenn ihn jemand auf eine Vorlage hinweisen würde.
Der Gewinn ist nicht die Klassifikation. Der Gewinn ist, dass eine Anfrage in unter zehn Minuten eine erste Rückmeldung mit benannter Spur und erklärter Frist bekommt, und dass Sie am Ende der Woche eine belastbare Antwort auf die Frage haben, wofür die Rechtsabteilung ihre Zeit aufwendet. Diese zweite Hälfte ist der eigentliche Grund, das zu bauen. Der CLOC State of the Industry Report 2026, gestützt auf 135 Rechtsabteilungen mit einem Medianumsatz von 13 Milliarden USD, fand steigende Arbeitslast bei Regulatory Compliance (63% der Abteilungen) und Cybersecurity (58%), während sich die beiden üblichen Entlastungsventile verengten: nur 47% erwarten wachsende interne Rechtsausgaben (nach 65% im Vorjahr) und 32% erwarten Wachstum bei der Anwaltszahl. Wenn Sie sich nicht aus dem Problem herausrekrutieren können, muss das Argument für mehr Ressourcen aus Nachfragedaten kommen, die Sie derzeit nicht erheben.
Dieser Workflow ist die Schicht über der Automatisierung je Vertragstyp. Er entscheidet, in welche Pipeline eine Anfrage gehört; der NDA-Triage-Workflow macht die Arbeit auf Klauselebene, sobald ein NDA als solches erkannt wurde.
Wann Sie ihn NICHT einsetzen
Lassen Sie es unter etwa 30 Anfragen im Monat. Die zwei Stunden Setup sind der kleine Aufwand; einen Servicekatalog zu kodifizieren und das Anfragendenverzeichnis zu befüllen ist der eigentliche, und der rechnet sich bei diesem Volumen nicht. Ein gemeinsames Postfach und eine wöchentliche Rotation ist die bessere Antwort.
Lassen Sie es, wenn Sie keine geschriebenen Vorlagen oder Playbooks haben. Die Self-Service-Spur zeigt auf eine Vorlagen-URL, und die Playbook-Spur setzt voraus, dass ein Prüfer eine geschriebene Position zum Abgleich hat. Ohne das landet jede Anfrage bei einem Anwalt und Sie haben eine teure Warteschlange gebaut. Schreiben Sie zuerst den Katalog: das ist das eigentliche Projekt, und dieser Workflow ist das, was ihn danach sichtbar macht.
Lassen Sie es für Eingänge, die vom ersten Kontakt an dem Anwaltsgeheimnis unterliegen müssen: interne Untersuchungen, Whistleblower-Meldungen und alles, was bereits unter einem Litigation Hold steht. Das braucht einen separaten Kanal, der die Automatisierung vollständig umgeht. Das Privileg-Gate des Workflows fängt diejenigen ab, die versehentlich an der Haupttür ankommen, aber ein Kanal, den Sie bewusst durch einen Klassifikator geleitet haben, ist ein Kanal, den Sie später verteidigen müssen.
Lassen Sie es, wenn das Problem der Rechtsabteilung Kapazität ist und nicht Routing. Der Router macht die Warteschlange lesbar und die Nachfrage messbar. Er macht keine Anwälte, und ein Team bei 100% Auslastung sieht denselben Backlog mit besseren Etiketten.
Setup
Führen Sie zuerst schema.sql aus. Es legt requester_directory an (wer fragt und auf wessen Papier), legal_request_log (die Audit-Spur und die SLA-Uhr) und legal_sla_policy (eine Zeile je Spur, vorbefüllt mit den vier Standard-Uhren). Importieren Sie dann das JSON, binden Sie die fünf Platzhalter-Credentials gemäß README, und stellen Sie vor allem anderen die Zeitzone des Workflows ein. Der Export kommt mit Europe/London, und jeder Cron-Ausdruck sowie die Geschäftsstunden-Arithmetik in Compute Breach Tier lesen aus dieser einen Einstellung. Sie falsch zu setzen wirft keinen Fehler; es verschiebt lautlos jede Frist.
Die Konfiguration, die das Verhalten bestimmt, sind eine Handvoll Konstanten in zwei Code-Nodes. In Apply Routing Policy: CONFIDENCE_FLOOR steht auf 0,75, VALUE_ESCALATION_USD auf 250.000 USD, und WALK_AWAY_FLAGS listet die sechs Kategorien, die eine GC-Eskalation erzwingen, unabhängig davon, was das Modell geschlossen hat. Setzen Sie VALUE_ESCALATION_USD auf den Wert, den Ihre Unterschriftenrichtlinie ohnehin nennt, nicht auf eine runde Zahl. In Normalize Request sind PRIVILEGE_PATTERNS acht Regex-Ausdrücke, die eine Anfrage ganz ohne API-Aufruf in den GC-Kanal umleiten.
Rechnen Sie damit, die SLA-Stunden einmal nachzujustieren. Sie liegen in der Tabelle legal_sla_policy und nicht in einem Node, also ist eine Änderung ein SQL-UPDATE und keine Workflow-Bearbeitung.
Was der Workflow tut
Intake Form Webhook und Intake Mailbox Poll — legal@ sind die beiden Eingänge; Normalize Request ist der einzige Node, der weiß, dass es zwei gibt. Er flacht beide Formen zu einem Envelope ab, deckelt den Body bei 4.000 Zeichen und setzt ein Boolean privilege_hit. Eine Anfrage ohne identifizierbaren Absender wird zwangsmarkiert statt geraten — es gibt niemanden, an den eine Antwort gehen könnte.
Privileged-Content Gate handelt auf Basis dieses Booleans. Sein True-Zweig geht direkt nach #legal-gc-escalations, und das Modell sieht den Inhalt nie. Diese Reihenfolge ist der Punkt: bei einer Vorladung oder einer Untersuchung ist ein Klassifikations-Roundtrip ein Privilegproblem und keine Latenzersparnis.
Requester Context schlägt den Absender zuerst über die exakte Adresse und danach über die Domain nach, sodass eine benannte Ausnahme den organisationsweiten Standard schlägt. Merge Requester Context setzt bei fehlender Zeile risk_posture: 'unknown' statt 'standard' — dieses eine Wort verhindert, dass ein nicht erkannter Absender eine automatisierte Rechtsauskunft erhält.
Claude — Classify + Route schickt den Envelope an Sonnet 5, mit einem System Prompt, der zwölf Anfragetypen, drei Kandidatenspuren, neun Risk Flags und eine feste Liste von Feldern benennt, denen ein Prüfer sonst hinterherlaufen müsste. Zurück kommt striktes JSON mit lane, confidence und einer Begründung unter 300 Zeichen. Sonnet 5 statt Haiku, weil der teure Fehler eine anwaltsreife Anfrage in einer Self-Service-Autoantwort ist und nicht der Preisunterschied je Aufruf.
Apply Routing Policy ist der Sicherheitsgurt, und bewusst gewöhnliches JavaScript statt eines zweiten Prompts. Fünf Overrides greifen in Prioritätsreihenfolge: ein Walk-away-Risk-Flag erzwingt gc_escalation; ein angegebener Wert auf oder über der Eskalationsschwelle erzwingt lawyer; eine Confidence unter 0,75 stuft self_serve auf playbook zurück; eine nicht standardmäßige Risikoeinstufung tut dasselbe; und benannte fehlende Felder leiten nach awaiting_requester. Jeder Override stempelt seinen Grund in die Audit-Zeile, sodass Sie messen können, welche Schutzmechanismen ihren Platz verdienen. Ein Parse-Fehler — der klassische Rückfall nach jeder Prompt-Änderung ist, dass das Modell sein JSON in Markdown-Fences packt — wird abgefangen und ungelesen an einen Menschen eskaliert. Die Schutzmechanismen liegen hier und nicht im Prompt, weil ein Schutz, der nur im Prompt existiert, durch beliebigen Text umgangen werden kann, den ein Anfragender ins Formular kopiert.
Lane Switch fächert in fünf Zweige auf, mit gc_escalation als Fallback-Ausgang statt eines stillen Verwerfens. Alle fünf laufen in Write Intake Log zusammen, das eine Zeile mit Schlüssel source_message_id und ON CONFLICT DO NOTHING einfügt: n8n wiederholt bei transienten Postgres-Fehlern, und eine doppelte Zeile würde jede Zahl im Wochenreport doppelt zählen. Die Self-Service-Spur schreibt ebenfalls ihre Zeile. Eine nicht protokollierte Self-Service-Antwort ist unsichtbare Arbeit, und unsichtbare Arbeit ist genau das, was dieser Workflow abstellen soll.
Der zweite Zweig läuft stündlich an Werktagen, berechnet die verstrichenen Geschäftsstunden gegen das SLA je Spur und eskaliert bei 50%, 100% und 150% der Uhr, wobei er auf der dritten Stufe in den GC-Kanal wechselt. Record Escalation Tier schreibt die Stufe nach dem Slack-Post zurück, sodass ein fehlgeschlagener Post in der nächsten Stunde eine doppelte Erinnerung erzeugt statt eines stillen Ausfalls. Der dritte Zweig aggregiert die Woche und postet einen Report, der zu jeder Zahl sagt, was zu tun ist, und nicht nur, wie die Zahl lautet.
Die Kostenrealität
Claude Inferenz ist der dominante variable Kostenblock. Eine Anfrage serialisiert zu etwa 2.400–3.800 Input-Tokens — die Taxonomie aus zwölf Typen und die Spurregeln dominieren, der Freitext des Anfragenden steuert 200–800 bei — und die strukturierte Antwort landet bei 250–400 Output-Tokens. Zum Sonnet-5-Listenpreis von 3 USD je Million Input-Tokens und 15 USD je Million Output-Tokens sind das 0,011 bis 0,017 USD je Anfrage. Bei 400 Anfragen im Monat 4,40–6,80 USD. Bei 2.000 sind es 22–34 USD.
Die n8n Executions sind die zweite Position. Der Intake-Zweig ist eine Execution je Anfrage; der SLA-Sweep mit zehn Läufen je Werktag ergibt rund 220 im Monat; der Report rund vier. 400 Anfragen im Monat sind also etwa 620 Executions und liegen im Starter-Plan von n8n Cloud (20 € im Monat, 2.500 Executions). Bei 2.000 Anfragen im Monat sind Sie bei rund 2.220 und sollten wegen Reserve und Nebenläufigkeit auf Pro wechseln (50 € im Monat, 10.000 Executions). Selbst gehostet auf einem kleinen VPS trägt beides ohne Execution-Deckel.
Dagegengerechnet: die manuelle Variante dieser Arbeit ist ein Koordinator, der jede Anfrage liest, entscheidet wohin sie geht, und ihr hinterherläuft. Bei geschätzten 6–10 Minuten je Anfrage für Lesen-Klassifizieren-Weiterleiten-Bestätigen und einem vollkostenbelasteten Koordinatorensatz von 60–90 USD je Stunde sind das 6–15 USD an Personenzeit je Anfrage — Werte, die ausdrücklich als Schätzungen und nicht als gemessene Benchmarks markiert sind. Das Verhältnis ist nicht der interessante Teil. Interessant ist, dass das Urteil des Koordinators für die 20–30% der Anfragen aufgewendet wird, bei denen das Routing wirklich mehrdeutig ist, statt für das NDA, das zum vierzigsten Mal hereinkommt.
Erfolgskennzahlen
Verfolgen Sie drei Zahlen wöchentlich, alle drei stehen bereits im Report.
Intake-Abdeckung — der Anteil der Rechtsarbeit, der überhaupt durch die Eingangstür kam. Nähern Sie sich ihm über das Verhältnis von source = 'form'- zu source = 'email'-Zeilen, und zielen Sie bis Tag 90 auf unter 30% E-Mail. Abdeckung ist die Kennzahl, die entscheidet, ob irgendetwas anderes hier real ist; ein Router, der die Hälfte der Arbeit sieht, produziert einen Nachfragereport, der sich selbstsicher irrt.
Haltbarkeit des Self-Service — von den mit einer Vorlage beantworteten Anfragen der Prozentsatz, bei dem dieselbe Person nicht innerhalb von sieben Tagen zurückkam. Zielen Sie auf 85% oder besser. Das ist das ehrliche Gegengewicht zu einer Deflection-Rate, die nur misst, dass Sie schnell Nein gesagt haben.
Erstkontakt unter zehn Minuten, in jeder Spur. Driftet er, liegt es an einem langsamen API-Aufruf oder an eingereihten n8n Executions, nicht an der Routing-Logik. Prüfen Sie die Execution-Liste, bevor Sie an einem Schwellwert drehen.
Gegenüber den Alternativen
Gegenüber dem gemeinsamen Postfach und der Tabelle. Der Status quo kostet nichts im Betrieb und produziert keine Daten. Er funktioniert unter etwa 30 Anfragen im Monat gut und verfällt oberhalb auf eine bestimmte Weise: die Warteschlange bleibt beherrschbar, während das Reporting zur Fiktion wird, weil in einer vollen Woche niemand eine Tabelle nachpflegt. Wenn Sie nur schnelleres Routing wollen, bringt eine Rotation plus ein guter Autoresponder Sie fast ans Ziel. Bauen Sie dies, wenn jemand die Rechtsabteilung auffordert, Stellen zu begründen, und die ehrliche Antwort lautet, dass Sie nicht wissen, was Sie letztes Quartal getan haben.
Gegenüber einem Legal-Front-Door-Produkt.Checkbox und Streamline AI verkaufen genau das als Produkt, mit Formularbaukasten, Workflow-Designer und Reporting ab Werk. Beide sind angebotsgebunden — keines veröffentlichte zum Preis-Check im August 2026 einen Einstiegspreis —, was sie in einen Enterprise-Beschaffungszyklus setzt statt in einen Nachmittag. Sie sind die bessere Wahl, wenn Legal Ops Budget und keine Engineering-Unterstützung hat, und wenn eine anbieterseitig geschnittene Taxonomie zu Ihrem Anfragenmix passt. Dieser Workflow ist die bessere Wahl, wenn Ihre Routing-Regeln etwas Spezifisches über Ihr Geschäft kodieren — eine Unterschriftenschwelle, eine Geschäftseinheit, die immer einen Anwalt braucht, ein Typ von Gegenpartei, den Sie nicht per Self-Service bedienen wollen — weil diese Regeln in einem Code-Node liegen, der Ihnen gehört, und nicht in einer Konfigurationsmaske, die Sie erst verhandeln müssen.
Gegenüber einer Ticket-Queue, die Sie schon haben. Jira Service Management, ServiceNow oder Zendesk können ab heute ein Formular für Rechtsanfragen entgegennehmen, ohne neue Lizenzkosten, wenn die IT ohnehin eines betreibt. Was sie routen, sind Formularfelder: der Anfragende wählt „Vertragsprüfung“, und das Ticket geht in die Vertragsprüfungs-Queue. Das funktioniert exakt so gut wie die Selbsteinordnung Ihrer Anfragenden, und genau die erweist sich in Intake-Daten durchgängig als unzuverlässig — der Typ-Hinweis in diesem Workflow wird dem Modell ausdrücklich als selbst angegeben und möglicherweise falsch übergeben. Nehmen Sie das Ticketsystem, wenn Ihr Anfragenmix schmal ist und Ihr Formular ihn aufzählen kann. Nehmen Sie dies, wenn die entscheidende Information in einem Absatz Freitext steht.
Worauf Sie achten müssen
Self-Service wird zur Abwehrmauer. Fehlerbild: Anfragende bekommen einen Vorlagen-Link, die Vorlage beantwortet ihre eigentliche Frage nicht, und sie umgehen die Rechtsabteilung per Direktnachricht — wo die Arbeit weiterhin anfällt, aber nicht mehr gezählt wird. Schutz: die Recontact-Abfrage im Wochenreport zählt Self-Service-Antworten, bei denen dieselbe Person innerhalb von sieben Tagen zurückkam, und markiert über 15%. Die Self-Service-Antwort in Slack endet zudem mit einem ausdrücklichen Notausgang: im Thread antworten, und es geht in die Prüfwarteschlange, ohne neues Formular.
Taxonomie-Drift leitet neue Arbeit an Anwälte. Fehlerbild: eine neue Regulierung oder Produktlinie erzeugt Anfragen, die in keinen der zwölf Typen passen, als other landen und per Default in die Anwaltsspur fallen, sodass die Warteschlange wächst, während das Modell zu funktionieren scheint. Schutz: der Report rankt request_type = 'other' und markiert über 10% des Volumens, mit dem Hinweis, dass dies ein fehlender Typ in der Taxonomie ist und kein Modellfehler. Ergänzen Sie ihn im System Prompt in Claude — Classify + Route.
Privilegierter Inhalt erreicht die API. Fehlerbild: jemand leitet einen Thread mit Prozesskontext und einem angehängten Routinevertrag weiter, und der gesamte Thread geht in eine Inferenzanfrage. Schutz: PRIVILEGE_PATTERNS leitet acht Kategorien vor jedem API-Aufruf um, und Normalize Request deckelt den Body bei 4.000 Zeichen, sodass ein langer weitergeleiteter Thread ohnehin abgeschnitten wird. Kombinieren Sie den Workflow mit einer geschriebenen KI-Richtlinie für Rechtsteams, die den Datenfluss ausdrücklich autorisiert, und wiederholen Sie Verifikationstest 4 aus der README nach jeder Änderung an diesen Mustern.
Die SLA-Uhr zählt die falschen Stunden. Fehlerbild: die verstrichene Zeit wird in Kalenderstunden gerechnet, Verletzungsalarme feuern nachts und am Wochenende für Anfragen, die bequem im Fenster liegen, und das Team schaltet den Kanal binnen zwei Wochen stumm. Schutz: Compute Breach Tier zählt nur Montag bis Freitag zwischen BUSINESS_START und BUSINESS_END, und der Sweep selbst ist per Cron auf werktägliche Geschäftsstunden begrenzt. Verifikationstest 6 in der README existiert genau dafür, eine Zeitzonen-Abweichung zwischen Workflow-Einstellung und diesen Konstanten aufzudecken.
Die Kanalnorm bildet sich nie heraus. Fehlerbild: das Formular existiert, und die Leute schreiben trotzdem direkt an den GC, weil die erste Anfrage immer so verschickt wird, wie sie immer verschickt wurde. Schutz: der Trigger auf dem Postfach legal-intake@ fängt sie ab, und der E-Mail-Anteilsschwellwert im Report macht die Lücke als Zahl sichtbar statt als Gefühl. Kombinieren Sie das in den ersten 30 Tagen mit Autorespondern auf den persönlichen Postfächern der Anwälte. Liegt der E-Mail-Anteil an Tag 90 noch über 30%, ist das Problem organisatorisch und keine Node-Änderung behebt es.
Stack
n8n für die Orchestrierung, Claude Sonnet 5 für die Klassifikation, Slack für jede Warteschlange und den Wochenreport, Ironclad oder Ihr eigenes CLM für den Matter-Datensatz der Playbook-Spur, Postgres für Verzeichnis, Log und SLA-Policy, und Gmail für das Auffangpostfach. Die Konzepte hinter den Routing-Regeln stehen in Legal Intake, und wo das auf der Fähigkeitskurve liegt, steht im Legal-Ops-Reifegradmodell. Sobald eine Anfrage geroutet ist, regelt die Contract-Review-SOP, was die Playbook-Spur tatsächlich tut.
# Legal Request Intake Router — n8n
One front door for every legal request. Two entry points (a form webhook and a `legal-intake@` mailbox) converge into one normalized envelope, get classified against a twelve-type taxonomy, and route into one of five lanes — self-serve, playbook review, lawyer, awaiting-requester, or GC escalation. An hourly sweep chases the SLA clock; a Monday report tells you what the department was actually asked for last week.
**Files**
| File | What it is |
|---|---|
| `legal-request-intake-router-n8n.json` | The workflow export. 26 nodes, three trigger-rooted branches. |
| `schema.sql` | Three Postgres tables. Run this first. |
| `_README.md` | This file. |
---
## 1. Import
1. Run `schema.sql` against your Postgres database. It is idempotent — `CREATE TABLE IF NOT EXISTS` throughout, and the five `legal_sla_policy` rows use `ON CONFLICT DO NOTHING`.
2. In n8n: **Workflows → Import from File →** `legal-request-intake-router-n8n.json`.
3. Open **Workflow settings** and set the timezone. The export ships `Europe/London`. Every cron expression in the file (`0 9-18 * * 1-5` for the SLA sweep, `0 8 * * 1` for the report) reads that setting, and so does the business-hours arithmetic in `Compute Breach Tier`. Setting it wrong does not throw — it silently shifts every SLA deadline.
4. Bind the five credentials by name (next section). The export references them as `PLACEHOLDER_*` ids, which n8n shows as unbound until you map them.
5. Leave the workflow **inactive** until you have run the verification sequence in section 3.
---
## 2. Credentials
Five, one section each.
### `PLACEHOLDER_POSTGRES_CRED_ID` — Postgres (type: Postgres)
The database holding the three tables from `schema.sql`. Used by five nodes. The workflow needs `SELECT`, `INSERT`, and `UPDATE` on `legal_request_log`, `SELECT` on `requester_directory` and `legal_sla_policy`. It never needs `DELETE` or DDL — grant accordingly.
### `PLACEHOLDER_ANTHROPIC_CRED_ID` — Anthropic (type: Header Auth)
- **Name:** `x-api-key`
- **Value:** your Anthropic API key, from [console.anthropic.com](https://console.anthropic.com) → API Keys.
Used only by `Claude — Classify + Route`. The `anthropic-version: 2023-06-01` header is set on the node itself, not in the credential.
### `PLACEHOLDER_SLACK_CRED_ID` — Slack (type: Header Auth)
- **Name:** `Authorization`
- **Value:** `Bearer xoxb-...` — a bot token from your Slack app's **OAuth & Permissions** page.
Scopes required: `chat:write` and `chat:write.public`. Invite the bot into `#legal-ops`, `#legal-queue`, `#legal-lawyer-queue`, and `#legal-gc-escalations` before the first run; a post to a channel the bot is not in returns `not_in_channel` with HTTP 200, so it fails quietly.
The two requester-facing nodes (`Slack — Self-Serve Reply`, `Slack — Ask For Missing Fields`) derive a Slack handle from the email local part. If your handles do not match your email prefixes, replace that expression with a `users.lookupByEmail` call and add the `users:read.email` scope.
### `PLACEHOLDER_CLM_CRED_ID` — Ironclad (type: Header Auth)
- **Name:** `Authorization`
- **Value:** `Bearer ...` — an Ironclad API token with workflow-create permission.
Used by `CLM — Open Playbook Matter` only. The node's `template` value (`legal-playbook-review`) and its attribute names are **per-tenant** — read them off your own workflow designer and edit the node body. If you do not have a CLM, disable this node; the playbook lane still posts to Slack and still writes its audit row.
### `PLACEHOLDER_GMAIL_CRED_ID` — Gmail (type: Gmail OAuth2)
The dedicated `legal-intake@` mailbox — a shared mailbox, never an individual lawyer's inbox. Used by `Intake Mailbox Poll — legal@` and `Mark Email Processed`.
### `PLACEHOLDER_WEBHOOK_ID_LEGAL_INTAKE`
Not a credential. n8n assigns a real webhook id on import; copy the production URL from the `Intake Form Webhook` node and point your intake form at it. Expected JSON body:
```json
{
"submission_id": "form-2026-08-18-0042",
"requester_email": "jane@acme.com",
"business_unit": "EMEA Sales",
"request_type_hint": "vendor_contract",
"summary": "Renewal of the Datadog MSA",
"detail": "Free-text description of what they need and by when.",
"counterparty": "Datadog Inc.",
"claimed_value_usd": 84000,
"needed_by": "2026-09-05"
}
```
Only `submission_id` and `requester_email` are load-bearing. `Normalize Request` defaults everything else, and a submission with no requester email is force-routed to the GC channel rather than guessed at.
---
## 3. First-run verification
Run these six in order, with the workflow **inactive**, using **Execute Workflow** and pinned test data on the trigger node. Each one proves a different branch. Do not activate until all six pass.
### Test 1 — the happy path, self-serve lane
Pin a webhook body for a standard NDA from a requester you have inserted into `requester_directory` with `risk_posture = 'standard'`. Expect: `Lane Switch` takes output 1, the requester gets a Slack DM with a template link, and one row lands in `legal_request_log` with `lane = 'self_serve'` and `override_reason IS NULL`.
This is the only test where an automated answer goes out. If it routes anywhere else, check that your directory row actually matched — `SELECT * FROM requester_directory WHERE lower(match_value) = lower('jane@acme.com')`.
### Test 2 — the unknown requester is not self-served
Same body, but change `requester_email` to an address with no directory row and no matching domain. Expect: `lane = 'playbook'` and `override_reason = 'risk_posture_unknown'`. This proves that `Merge Requester Context` defaults a missing row to `unknown` rather than `standard` — the guard that stops an unrecognised sender receiving an automated legal answer.
### Test 3 — the walk-away override beats the model
Pin a body describing an employment matter with the word `termination` in the detail (but not in the subject, so the privilege gate does not catch it first). Expect: whatever Claude proposed, `lane = 'gc_escalation'` and `override_reason` starts with `walk_away_flag:`. Check the `#legal-gc-escalations` post arrived.
### Test 4 — the privilege gate skips the model entirely
Pin a body with `"summary": "Subpoena received from the state AG"`. Expect: `Privileged-Content Gate` takes its TRUE branch, `Claude — Classify + Route` **never executes** (confirm in the execution view — the node should be untouched, not merely fast), and the GC channel post says explicitly that no classification was run.
This is the test that proves privileged content cannot reach the Anthropic API through the normal path. Re-run it after every edit to `PRIVILEGE_PATTERNS`.
### Test 5 — parse failure escalates rather than failing open
Temporarily edit `Claude — Classify + Route` to point at `https://api.anthropic.com/v1/messages-broken` so the call returns an error body. Execute. Expect: `Apply Routing Policy` catches it, emits `lane = 'gc_escalation'` with `override_reason` starting `parser_error:`, and a human gets the request unread. **Restore the URL afterwards.**
This is the test most teams skip and most regret. A classifier that fails open is worse than no classifier, because the failure is invisible.
### Test 6 — the SLA sweep counts business hours, not calendar hours
Insert a row directly:
```sql
INSERT INTO legal_request_log (source_message_id, source, requester_email, request_type, lane, sla_business_hours, received_at)
VALUES ('sla-test-1', 'form', 'jane@acme.com', 'vendor_contract', 'playbook', 16, now() - interval '3 days');
```
Execute the `SLA Sweep — Hourly Weekdays` branch. Expect one escalation post naming an elapsed figure **lower** than 72, because weekends and nights are excluded. If it reports something close to 72, your workflow timezone and `BUSINESS_START`/`BUSINESS_END` in `Compute Breach Tier` disagree. Then re-execute immediately: the second run must post **nothing**, because `Record Escalation Tier` wrote the tier back. Delete the test row when done.
### Optional — the weekly report on an empty week
Execute the `Weekly Demand Report — Mon 08:00` branch against an empty table. It should post the "no requests logged" message rather than dividing by zero.
---
## 4. What to tune, and when
Ship with the defaults. Change them after a quarter of real traffic, not before.
| Setting | Node | Default | Change it when |
|---|---|---|---|
| `CONFIDENCE_FLOOR` | Apply Routing Policy | `0.75` | The weekly report shows more than ~25% low-confidence and sampling proves they were genuinely routable. |
| `VALUE_ESCALATION_USD` | Apply Routing Policy | `250000` | Your signature-authority matrix says a different number. It should match that document, not a round figure. |
| `WALK_AWAY_FLAGS` | Apply Routing Policy | 6 flags | Never shrink this list to reduce escalation volume. Fix the taxonomy or the intake form instead. |
| `PRIVILEGE_PATTERNS` | Normalize Request | 8 patterns | Add to it freely. Every addition costs you one unclassified request and buys certainty about a category of content. |
| `BUSINESS_START` / `BUSINESS_END` | Compute Breach Tier | `9` / `18` | Your team is not on a single working day — split by `region` from the directory if you support follow-the-sun. |
| SLA hours per lane | `legal_sla_policy` table | 16 / 40 / 8 / 8 | Your published service catalog says otherwise. The table is the right place to change it; no node edit needed. |
| Report thresholds | Format Demand Report | 15 / 10 / 30 / 20 / 25 % | After a quarter, set each to the level your team actually treats as a problem. |
---
## 5. Known limits
1. **The classification is a routing decision, never legal advice.** The system prompt states this and the self-serve reply points at a template rather than answering. Do not extend the prompt to answer the underlying question.
2. **Attachments are not read.** `has_attachment` is a boolean the classifier can use as a signal; the file itself is never sent. For clause-level review of an attached contract, this router hands off to a per-contract-type flow — the NDA triage flow is the worked example.
3. **The recontact metric is a proxy.** It counts any later request from the same person within seven days, so a requester with two unrelated matters registers as a recontact. It is directionally right at the volumes this report is read at; treat a spike as a prompt to read five threads, not as a measurement.
4. **`Mark Email Processed` uses `markAsRead`.** If your `legal-intake@` mailbox has other readers, switch it to `addLabels` with a dedicated `legal-intake-processed` label and rely on the trigger filter (already set to `-label:legal-intake-processed`) for deduplication.
5. **Not runtime-tested against a live Anthropic, Slack, Ironclad, or Gmail tenant.** The workflow JSON is complete and every Code node's logic has been exercised against the routing cases in section 3, but the HTTP node bodies are written from published API shapes and should be verified against your own tenant during the first-run sequence.
-- legal-request-intake-router-n8n — schema
-- Run this once against the Postgres database bound to PLACEHOLDER_POSTGRES_CRED_ID
-- before importing the workflow. Three tables: who is allowed to ask, what was
-- asked, and how fast each lane is expected to answer.
-- ---------------------------------------------------------------------------
-- 1. requester_directory
-- Maps a requester's email domain or exact address to their business unit and
-- the unit's standing risk posture. The router degrades gracefully when a
-- requester is missing (posture defaults to 'unknown', which blocks the
-- self-serve lane), so an empty table is safe on day one — but every row you
-- add moves requests out of the lawyer queue.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS requester_directory (
id BIGSERIAL PRIMARY KEY,
match_value TEXT NOT NULL, -- 'jane@acme.com' or '@acme-emea.com'
match_type TEXT NOT NULL -- 'email' | 'domain'
CHECK (match_type IN ('email', 'domain')),
business_unit TEXT NOT NULL,
region TEXT,
risk_posture TEXT NOT NULL DEFAULT 'standard'
CHECK (risk_posture IN ('standard', 'elevated', 'restricted')),
default_assignee TEXT, -- Slack member ID of the unit's named lawyer
notes TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (match_value, match_type)
);
CREATE INDEX IF NOT EXISTS requester_directory_match_idx
ON requester_directory (match_type, lower(match_value));
-- ---------------------------------------------------------------------------
-- 2. legal_sla_policy
-- One row per lane. The SLA sweep reads these; the router stamps the tier onto
-- each logged request. Hours are BUSINESS hours, not calendar hours — the
-- Compute Breach Tier code node converts using BUSINESS_HOURS.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS legal_sla_policy (
lane TEXT PRIMARY KEY
CHECK (lane IN ('self_serve', 'playbook', 'lawyer',
'awaiting_requester', 'gc_escalation')),
sla_business_hours INTEGER, -- NULL = no clock (self-serve is instant)
escalation_channel TEXT NOT NULL,
description TEXT
);
INSERT INTO legal_sla_policy (lane, sla_business_hours, escalation_channel, description) VALUES
('self_serve', NULL, '#legal-ops', 'Template or policy answer returned at intake. No clock.'),
('playbook', 16, '#legal-queue', 'Standard-paper review against a written playbook. 2 business days.'),
('lawyer', 40, '#legal-lawyer-queue', 'Needs a lawyer''s judgment. 5 business days.'),
('awaiting_requester', 8, '#legal-ops', 'Blocked on the requester supplying named missing fields.'),
('gc_escalation', 8, '#legal-gc-escalations', 'Privileged, litigation, or regulator-facing. 1 business day, GC-visible.')
ON CONFLICT (lane) DO NOTHING;
-- ---------------------------------------------------------------------------
-- 3. legal_request_log
-- The audit trail, the SLA clock, and the only honest source for the weekly
-- demand report. source_message_id is the idempotency key: n8n retries on
-- transient Postgres errors and you do not want a duplicate row (or a duplicate
-- Slack post) for one request.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS legal_request_log (
id BIGSERIAL PRIMARY KEY,
source_message_id TEXT NOT NULL UNIQUE, -- Gmail message id, or form submission id
source TEXT NOT NULL -- where it actually arrived from
CHECK (source IN ('form', 'email', 'backfill')),
requester_email TEXT NOT NULL,
business_unit TEXT,
risk_posture TEXT,
request_type TEXT NOT NULL, -- from the taxonomy in the Claude prompt
lane TEXT NOT NULL
REFERENCES legal_sla_policy (lane),
model_lane TEXT, -- what Claude said, before overrides
override_reason TEXT, -- why the policy node disagreed, if it did
confidence NUMERIC(4,3),
sla_business_hours INTEGER,
risk_flags TEXT[] NOT NULL DEFAULT '{}',
missing_fields TEXT[] NOT NULL DEFAULT '{}',
claimed_value_usd NUMERIC(14,2),
assignee TEXT, -- Slack member ID
status TEXT NOT NULL DEFAULT 'open'
CHECK (status IN ('open', 'awaiting_requester', 'closed')),
last_escalated_tier INTEGER NOT NULL DEFAULT 0, -- 0 none, 1 = 50%, 2 = 100%, 3 = 150%
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
first_touch_at TIMESTAMPTZ,
closed_at TIMESTAMPTZ,
recontacted_within_7d BOOLEAN -- backfilled by the weekly report job
);
CREATE INDEX IF NOT EXISTS legal_request_log_open_idx
ON legal_request_log (status, lane, received_at)
WHERE status <> 'closed';
CREATE INDEX IF NOT EXISTS legal_request_log_received_idx
ON legal_request_log (received_at DESC);
CREATE INDEX IF NOT EXISTS legal_request_log_requester_idx
ON legal_request_log (lower(requester_email), received_at DESC);