Un flow de n8n que le da al departamento legal una sola puerta de entrada. Las solicitudes llegan desde un formulario o un buzón compartido, se clasifican contra una taxonomía de doce tipos y aterrizan en uno de cinco carriles: plantilla de autoservicio, revisión por playbook, abogado, bloqueada por el solicitante o escalamiento al GC. Un barrido cada hora persigue el reloj del SLA en horas hábiles; un informe cada lunes por la mañana dice qué le pidieron realmente al departamento. Cuesta entre 0,011 y 0,017 USD por solicitud en inferencia de Claude.
El workflow se entrega en apps/web/public/artifacts/legal-request-intake-router-n8n/legal-request-intake-router-n8n.json — 26 nodos repartidos en tres ramas con su propio trigger. Las tres tablas de Postgres que necesita están en el schema.sql adyacente, y la configuración de credenciales más una secuencia de verificación de seis pasos están en _README.md.
Cuándo usarlo
Tu equipo legal atiende más de 30 solicitudes al mes y no puede decir, sin abrir una hoja de cálculo, cuántas llegaron la semana pasada ni de qué trataban. Las solicitudes te llegan por el canal que el solicitante recordó en ese momento: el 87% de las solicitudes legales llega por email, según la investigación de intake de Checkbox, y el resto por teléfono o en persona. Una parte relevante de lo que aterriza es trabajo que el propio solicitante podría terminar solo si alguien le señalara una plantilla.
La ganancia no es la clasificación. La ganancia es que una solicitud recibe un primer contacto en menos de diez minutos con un carril asignado y una fecha límite declarada, y que al final de la semana tienes una respuesta defendible a “¿en qué está gastando su tiempo el equipo legal?”. Esa segunda mitad es lo que justifica construirlo. El State of the Industry Report 2026 de CLOC, basado en 135 departamentos legales con ingresos medianos de 13.000 millones de USD, encontró carga de trabajo creciente en cumplimiento regulatorio (63% de los departamentos) y ciberseguridad (58%), mientras las dos válvulas de escape habituales se estrechaban: solo el 47% espera que crezca el gasto legal interno (contra el 65% anterior) y el 32% espera crecimiento de plantilla de abogados. Cuando no puedes contratar para salir del problema, el argumento por más recursos tiene que construirse con datos de demanda que hoy no recolectas.
Este flow es la capa que va por encima de la automatización por tipo de contrato. Decide a qué pipeline pertenece una solicitud; el flow de triaje de NDAs hace el trabajo a nivel de cláusula una vez que un NDA fue identificado como tal.
Cuándo NO usarlo
Sáltatelo por debajo de unas 30 solicitudes al mes. Las dos horas de configuración son el costo pequeño; codificar un catálogo de servicios y poblar el directorio de solicitantes es el costo real, y no se paga a ese volumen. Un buzón compartido y una rotación semanal es la mejor respuesta.
Sáltatelo si no tienes plantillas ni playbook escritos. El carril de autoservicio apunta a la URL de una plantilla, y el carril de playbook asume que un revisor tiene una posición escrita contra la cual contrastar. Sin eso, cada solicitud se enruta a un abogado y habrás construido una cola cara. Escribe primero el catálogo: ese es el proyecto real, y este flow es lo que lo hace visible después.
Sáltatelo para intake que deba estar amparado por secreto profesional desde el primer contacto: investigaciones internas, denuncias de whistleblower y cualquier cosa ya bajo una retención por litigio. Eso necesita un canal separado que evite la automatización por completo. La compuerta de privilegio del flow atrapa las que llegan a la puerta principal por error, pero un canal que decidiste enrutar a través de un clasificador es un canal que después tendrás que defender.
Sáltatelo si el problema del equipo legal es capacidad y no enrutamiento. El router hace la cola legible y la demanda medible. No fabrica abogados, y un equipo que ya está al 100% de utilización verá el mismo backlog con mejores etiquetas.
Configuración
Ejecuta schema.sql primero. Crea requester_directory (quién pregunta y sobre el papel de quién), legal_request_log (el registro de auditoría y el reloj del SLA) y legal_sla_policy (una fila por carril, precargada con los cuatro relojes por defecto). Después importa el JSON, vincula las cinco credenciales placeholder según el README y, antes que nada, configura la zona horaria del workflow. La exportación viene con Europe/London, y cada expresión cron más la aritmética de horas hábiles en Compute Breach Tier leen de ese único ajuste. Equivocarse no lanza ningún error; simplemente desplaza cada fecha límite en silencio.
La configuración que decide el comportamiento son unas pocas constantes en dos nodos Code. En Apply Routing Policy: CONFIDENCE_FLOOR está en 0,75, VALUE_ESCALATION_USD en 250.000 USD, y WALK_AWAY_FLAGS lista las seis categorías que fuerzan un escalamiento al GC sin importar lo que concluyó el modelo. Pon VALUE_ESCALATION_USD en lo que ya diga tu matriz de facultades de firma, no en una cifra redonda. En Normalize Request, PRIVILEGE_PATTERNS son ocho regex que desvían una solicitud al canal del GC sin ninguna llamada a la API.
Cuenta con ajustar las horas del SLA una vez. Viven en la tabla legal_sla_policy y no en un nodo, así que cambiarlas es un UPDATE de SQL y no una edición del workflow.
Qué hace el flow
Intake Form Webhook e Intake Mailbox Poll — legal@ son los dos puntos de entrada; Normalize Request es el único nodo que sabe que hay dos. Aplana ambas formas en un solo envelope, corta el cuerpo a 4.000 caracteres y fija un booleano privilege_hit. Una solicitud sin solicitante identificable se marca a la fuerza en vez de adivinarse: no hay a quién enviarle una respuesta.
Privileged-Content Gate actúa sobre ese booleano. Su rama verdadera va directo a #legal-gc-escalations y el modelo nunca ve el contenido. Ese orden es el punto: para una citación judicial o una investigación, un viaje de ida y vuelta de clasificación es un problema de privilegio, no un ahorro de latencia.
Requester Context busca al remitente primero por dirección exacta y después por dominio, de modo que una excepción nombrada gana sobre el valor por defecto de la organización. Merge Requester Context asigna a una fila faltante risk_posture: 'unknown' en lugar de 'standard': esa sola palabra es lo que evita que un remitente no reconocido reciba una respuesta legal automatizada.
Claude — Classify + Route envía el envelope a Sonnet 5 con un system prompt que nombra doce tipos de solicitud, tres carriles candidatos, nueve banderas de riesgo y una lista fija de campos que un revisor tendría que perseguir. Devuelve JSON estricto con un lane, un confidence y un razonamiento de menos de 300 caracteres. Sonnet 5 en vez de Haiku porque el error caro es una solicitud de nivel abogado sentada en una autorrespuesta de autoservicio, no la diferencia de precio por llamada.
Apply Routing Policy es el cinturón de seguridad, y es deliberadamente JavaScript común y no un segundo prompt. Cinco overrides se disparan en orden de prioridad: una bandera de riesgo de la lista de walk-away fuerza gc_escalation; un valor declarado igual o superior al piso de escalamiento fuerza lawyer; una confianza por debajo de 0,75 degrada self_serve a playbook; una postura de riesgo no estándar hace lo mismo; y campos faltantes nombrados enrutan a awaiting_requester. Cada override estampa su razón en la fila de auditoría, así puedes medir qué guardas se ganan su lugar. Un fallo de parseo —la regresión clásica después de cualquier edición del prompt es que el modelo envuelva su JSON en fences de markdown— se captura y escala a un humano sin leer. Las guardas viven aquí y no en el prompt porque una guarda que solo existe en el prompt es evadible con el texto que el solicitante pegue en el formulario.
Lane Switch abre en cinco ramas, con gc_escalation como salida de fallback en vez de un descarte silencioso. Las cinco convergen en Write Intake Log, que inserta una fila con clave source_message_id y ON CONFLICT DO NOTHING: n8n reintenta ante errores transitorios de Postgres, y una fila duplicada contaría doble cada número del informe semanal. El carril de autoservicio también escribe su fila. Una respuesta de autoservicio sin registrar es trabajo invisible, y el trabajo invisible es exactamente lo que este flow existe para eliminar.
La segunda rama barre cada hora en días hábiles, calcula las horas hábiles transcurridas contra el SLA de cada carril y escala al 50%, 100% y 150% del reloj, saltando al canal del GC en el tercer nivel. Record Escalation Tier escribe el nivel después del post de Slack, así que un post fallido produce un recordatorio duplicado a la hora siguiente en lugar de un silencio. La tercera rama agrega la semana y publica un informe que dice qué hacer con cada número, no solo cuál es el número.
La realidad del costo
La inferencia de Claude es el costo variable dominante. Una solicitud se serializa en unos 2.400–3.800 tokens de entrada —la taxonomía de doce tipos y las reglas de carril dominan, con el texto libre del solicitante sumando 200–800— y la respuesta estructurada aterriza en 250–400 tokens de salida. Al precio de lista de Sonnet 5 de 3 USD por millón de tokens de entrada y 15 USD por millón de salida, eso son 0,011 a 0,017 USD por solicitud. Con 400 solicitudes al mes, 4,40–6,80 USD. Con 2.000, 22–34 USD.
Las ejecuciones de n8n son la otra línea. La rama de intake es una ejecución por solicitud; el barrido de SLA con diez corridas por día hábil son unas 220 al mes; el informe son unas cuatro. Así que 400 solicitudes al mes son unas 620 ejecuciones, dentro del plan Starter de n8n Cloud (20 € al mes, 2.500 ejecuciones). Con 2.000 solicitudes al mes estás en unas 2.220 y deberías pasar a Pro (50 € al mes, 10.000 ejecuciones) por el margen y la concurrencia. Autohospedarlo en un VPS pequeño soporta cualquiera de los dos sin tope de ejecuciones.
Contra eso: la versión manual de este trabajo es un coordinador leyendo cada solicitud, decidiendo a dónde va y persiguiéndola. Con un estimado de 6–10 minutos por solicitud para leer-clasificar-enrutar-acusar recibo y una tarifa cargada de coordinador de 60–90 USD la hora, eso son 6–15 USD de tiempo humano por solicitud —cifras marcadas como estimaciones, no como benchmarks medidos—. La razón no es lo interesante. Lo interesante es que el criterio del coordinador se gasta en el 20–30% de solicitudes donde el enrutamiento es genuinamente ambiguo en vez de en el NDA que llega por cuadragésima vez.
Métricas de éxito
Sigue tres números por semana, los tres ya en el informe.
Cobertura de intake: la proporción del trabajo legal que entró por la puerta principal. Aproxímala con la razón de filas source = 'form' contra source = 'email', y apunta a que email quede por debajo del 30% al día 90. La cobertura es la métrica que decide si algo más aquí es real; un router que ve la mitad del trabajo produce un informe de demanda que se equivoca con confianza.
Durabilidad del autoservicio: de las solicitudes respondidas con una plantilla, el porcentaje donde la misma persona no volvió dentro de siete días. Apunta a 85% o mejor. Este es el contrapeso honesto a una tasa de deflexión, que solo mide que dijiste que no rápido.
Primer contacto en menos de diez minutos, en todos los carriles. Si se desvía, la causa es una llamada de API lenta o ejecuciones de n8n encoladas, no la lógica de enrutamiento. Revisa la lista de ejecuciones antes de tocar un umbral.
Frente a las alternativas
Frente al buzón compartido y la hoja de cálculo. El statu quo no cuesta nada operar y no produce datos. Funciona bien por debajo de unas 30 solicitudes al mes y se degrada de una forma específica por encima: la cola sigue siendo manejable mientras el reporte se vuelve ficción, porque nadie rellena una hoja de cálculo en una semana ocupada. Si solo quieres enrutamiento más rápido, una rotación y un buen autorespondedor te llevan casi todo el camino. Construye esto cuando alguien le pida al equipo legal justificar plantilla y la respuesta honesta sea que no sabes qué hiciste el trimestre pasado.
Frente a un producto de puerta de entrada legal.Checkbox y Streamline AI venden exactamente esto como producto, con constructor de formularios, diseñador de workflows y reportes ya incluidos. Ambos están detrás de cotización —ninguno publicaba un precio de entrada en su revisión de precios de agosto de 2026—, lo que los pone en un ciclo de compras empresarial y no en una tarde. Son la mejor opción si legal ops tiene presupuesto y no tiene soporte de ingeniería, y si una taxonomía con forma de proveedor encaja con tu mezcla de solicitudes. Este flow es la mejor opción cuando tus reglas de enrutamiento codifican algo específico de tu negocio —un umbral de facultades de firma, una unidad de negocio que siempre necesita abogado, un tipo de contraparte que te niegas a autoservir— porque esas reglas viven en un nodo Code que tú controlas y no en una pantalla de configuración que tienes que negociar.
Frente a una cola de tickets que ya tienes. Jira Service Management, ServiceNow o Zendesk pueden recibir un formulario de solicitud legal hoy mismo, sin costo de licencia nuevo si IT ya opera uno. Lo que enrutan son campos de formulario: el solicitante elige “revisión de contrato” y el ticket va a la cola de revisión de contratos. Eso funciona exactamente igual de bien que la autoclasificación de tus solicitantes, que es justo lo que los datos de intake muestran de forma consistente que no es confiable —el campo de tipo sugerido en este flow se le pasa al modelo explícitamente etiquetado como autodeclarado y posiblemente equivocado—. Usa el sistema de tickets si tu mezcla de solicitudes es estrecha y tu formulario puede enumerarla. Usa esto cuando la información que decide esté en un párrafo de texto libre.
Puntos de atención
El autoservicio se convierte en un muro de deflexión. Modo de falla: los solicitantes reciben un link a una plantilla, la plantilla no responde su pregunta real, y rodean al equipo legal por mensajes directos, donde el trabajo igual ocurre pero deja de contarse. Guarda: la consulta de recontacto del informe semanal cuenta las respuestas de autoservicio donde la misma persona volvió dentro de siete días, y marca por encima del 15%. La respuesta de autoservicio en Slack también termina con una salida explícita: responde en el hilo y pasa a la cola de revisión, sin formulario nuevo.
La deriva de taxonomía enruta trabajo nuevo a los abogados. Modo de falla: una regulación o línea de producto nueva produce solicitudes que no encajan en ninguno de los doce tipos, aterrizan como other y caen por defecto en el carril de abogado, así que la cola crece mientras el modelo parece estar funcionando. Guarda: el informe rankea request_type = 'other' y marca por encima del 10% del volumen, con la instrucción de que eso es un tipo faltante en la taxonomía y no una falla del modelo. Agrégalo al system prompt en Claude — Classify + Route.
Contenido privilegiado llega a la API. Modo de falla: alguien reenvía un hilo con contexto de litigio y un contrato rutinario adjunto, y el hilo completo entra en una petición de inferencia. Guarda: PRIVILEGE_PATTERNS desvía ocho categorías antes de cualquier llamada a la API, y Normalize Request corta el cuerpo a 4.000 caracteres para que un hilo reenviado largo quede truncado igual. Combina el flow con una política de IA para equipos legales escrita que autorice el flujo de datos de forma explícita, y vuelve a correr la prueba de verificación 4 del README después de cada edición de esos patrones.
El reloj del SLA cuenta las horas equivocadas. Modo de falla: el tiempo transcurrido se calcula en horas de calendario, las alertas de incumplimiento se disparan de noche y en fines de semana para solicitudes cómodamente dentro de su ventana, y el equipo silencia el canal en quince días. Guarda: Compute Breach Tier cuenta solo de lunes a viernes entre BUSINESS_START y BUSINESS_END, y el barrido mismo está limitado por cron a horas hábiles de días hábiles. La prueba de verificación 6 del README existe justamente para atrapar un desajuste de zona horaria entre el ajuste del workflow y esas constantes.
La norma de canal nunca se forma. Modo de falla: el formulario existe, y la gente le escribe igual al GC por email, porque la primera solicitud siempre se envía como siempre se envió. Guarda: el trigger del buzón legal-intake@ las atrapa, y el umbral de proporción de email del informe hace visible la brecha como un número y no como una sensación. Combínalo con autorespondedores en los buzones individuales de los abogados durante los primeros 30 días. Si la proporción de email sigue por encima del 30% al día 90, el problema es organizacional y ninguna edición de nodo lo va a arreglar.
Stack
n8n para la orquestación, Claude Sonnet 5 para la clasificación, Slack para cada cola y el informe semanal, Ironclad o tu propio CLM para el registro de materia del carril de playbook, Postgres para el directorio, el log y la política de SLA, y Gmail para el buzón de red de contención. Los conceptos detrás de las reglas de enrutamiento están en legal intake, y dónde encaja esto en la curva de capacidades está en el modelo de madurez de legal ops. Una vez enrutada la solicitud, el SOP de revisión de contratos gobierna lo que el carril de playbook hace realmente.
# 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);