La mayoría de los equipos de reclutamiento descubren los problemas del funnel en el quarterly business review. Para entonces, el rol lleva sesenta días abierto, dos de los tres mejores candidatos firmaron en otro lado, y el hiring manager perdió la confianza en el pipeline. Este workflow cierra esa brecha. Un flow de n8n corre cada noche contra tu ATS, calcula tasas de conversión y dwell times por rol-y-stage contra una baseline móvil de 90 días, marca desviaciones estadísticamente significativas, le pide a Claude que explique cada una en una o dos oraciones, y publica el resultado a un canal de Slack con ruteo según tipo antes del stand-up de la mañana siguiente.
El bundle en apps/web/public/artifacts/hiring-funnel-anomaly-n8n/hiring-funnel-anomaly-n8n.json trae quince nodos completamente configurados — dos triggers programados, un pull de Ashby, un nodo de Code de agregación con la lógica real de anomalías, un lookup de baseline en Postgres, el detector, un insert con dedupe, la llamada a Claude para la narrativa, el formateador de Slack, y un path paralelo de tendencia de time-to-hire. El _README.md que lo acompaña documenta las cuatro credenciales, las tres tablas de Postgres que tienes que crear, y una verificación de primer-run de cinco pasos que ejercita cada rama.
Cuándo usarlo
Deberías shippear esto cuando tu equipo de reclutamiento corre al menos ocho a diez roles activos en paralelo, el equipo lleva al menos noventa días en el mismo ATS (Ashby, Greenhouse, o Lever) para que exista una baseline, y al menos una persona del equipo tiene la salud del funnel como parte de su trabajo. Por debajo de esos umbrales la relación señal-ruido está mal: las baselines tienen demasiado ruido para fijar un umbral de z-score útil, y no hay nadie que vaya a actuar realmente sobre la alerta cuando dispare.
La otra precondición es disciplina taxonómica. Si tus stages tienen nombres distintos para cada rol, o los hiring managers crean nuevos stages libremente a mitad de búsqueda, la agregación por rol-y-stage producirá una larga cola de pares (rol, stage) con sample size de uno. La guarda MIN_SAMPLE = 20 del detector suprime esos, lo cual es correcto, pero terminarás sin ninguna señal en lugar de señal equivocada. Arregla la taxonomía de stages primero.
Cuándo NO usarlo
No shippees esto si corres menos de cinco roles activos. Las cuentas no dan — no hay suficientes eventos por par (rol, stage) para calcular una desviación estándar significativa de baseline, y vas a gastar más tiempo afinando umbrales que actuando sobre alertas. Una revisión semanal manual en un spreadsheet es honestamente mejor a esa escala.
No shippees esto si tu ATS es el único lugar donde existe data de candidatos y todavía no tienes un warehouse de analítica separado. El flow asume que puedes levantar una base Postgres a la que se le permita espejear data de candidatos. Si tu equipo de privacidad o de política de IA todavía no ha firmado para que la data de candidatos salga del ATS, corre este ejercicio solo sobre conteos agregados a nivel stage — quítale el cálculo de dwell a nivel candidato — y revísalo cuando la política se ponga al día.
No shippees esto encima de una taxonomía de stages que cambia cada semana. La detección de anomalías sobre una definición móvil de “stage” produce alertas que nadie puede interpretar. Estabiliza la taxonomía durante al menos un trimestre antes de prender el flow.
Por último, no shippees esto si la respuesta de tu equipo a una alerta de funnel sería “ya lo sabíamos”. El valor del workflow es la latencia de 24 horas sobre una métrica que de otra forma saldría a la luz en 60 días. Si ya tienes un standup diario donde esto se revisa en vivo, la alerta es redundante.
Setup
Construye la baseline primero. Corre un backfill de una sola vez sobre los últimos 90 días usando el mismo endpoint del application-feed de Ashby que usa el workflow, agrupa por (role_id, from_stage, to_stage), y escribe conversion_rate_mean, conversion_rate_stddev, dwell_seconds_p50, stage_sla_seconds, y sample_size a la tabla funnel_baselines. El DDL para esa tabla — y para role_tth_baselines y anomaly_alerts — está en el _README.md del bundle.
Importa hiring-funnel-anomaly-n8n.json del bundle a n8n. El workflow viene inactivo a propósito. Abre Settings y confirma que la timezone coincide con el horario laboral de tu equipo; los dos nodos Cron evalúan sus expresiones en la timezone del workflow, no en UTC. Crea las cuatro credenciales referenciadas por nombre en el JSON: PLACEHOLDER_ASHBY_CRED_ID, PLACEHOLDER_POSTGRES_CRED_ID, PLACEHOLDER_ANTHROPIC_CRED_ID, PLACEHOLDER_SLACK_CRED_ID. El README te lleva a través de cada una incluyendo los scopes de Slack (chat:write, chat:write.public) y la forma del Header Auth de Anthropic.
Pasa por la verificación de primer-run de cinco pasos antes de activar. El paso dos — insertar una fila sintética de baseline que tenga garantizado disparar — es el que más gente se salta y luego se pregunta por qué no salta ninguna alerta; hazlo. El paso tres confirma que la dedupe key está haciendo su trabajo, que es la guarda de costo alrededor de la llamada a Claude.
Refresca las baselines mensualmente. Las tasas de conversión drift con la estacionalidad, las condiciones de mercado y la composición del equipo, y una baseline desactualizada produce o spam de alertas o regresiones perdidas. El refresh es el mismo query que armó la baseline; cron-éalo como un workflow separado de n8n o como un job de SQL del lado de Postgres.
Qué hace el flow
El Cron de las 2am dispara un pull del application-feed de Ashby para las últimas 24 horas. El nodo Code agregador agrupa eventos por (role_id, from_stage, to_stage), calcula la tasa de conversión de hoy y el dwell time mediano para cada par, y emite un item por par. El nodo Postgres Lookup Baseline une cada par con su fila de baseline. El nodo Code Detect Anomalies aplica tres reglas: un z-score de conversión por stage por debajo de -2.0 contra la media de la baseline se marca como stage_conversion_drop, un dwell mediano que excede stage_sla_seconds * 1.5 se marca como candidate_stalled, y un par (rol, stage) con cero eventos hoy y menos de 20 eventos históricos se marca como new_role_no_movement con una nota de baja severidad de que se suprimieron los umbrales.
Cada flag se escribe a anomaly_alerts con una dedupe_key de role::from_stage::to_stage::anomaly_type::yyyy-mm-dd bajo una cláusula ON CONFLICT DO NOTHING. Solo los inserts que devolvieron una fila — es decir, alertas que aún no existían para hoy — proceden a la llamada de narrativa de Claude y al post de Slack. Esta es la guarda de costo: una re-corrida del mismo día del workflow no le cobra doble a Anthropic y no postea doble a Slack. El formateador de Slack rutea por tipo de anomalía: las caídas a nivel stage y los candidatos estancados van a #recruiting-alerts, las tendencias de time-to-hire y new-role-no-movement van a #recruiting-leadership, y las caídas de source-channel van a #sourcing.
El Cron de las 3am corre el path paralelo de tendencia de time-to-hire contra una tabla hires. Reformatea cualquier fila donde el promedio móvil de 7 días excede el umbral en el mismo envelope de alerta y las pasa por el mismo path de dedupe-y-post.
Realidad de costos
Para un equipo de 30 roles corriendo esto cada noche, espera alrededor de 600 agregaciones (rol, stage) por día. El pull de Ashby y los lookups de Postgres cuestan esencialmente nada. La llamada de narrativa a Claude usa claude-sonnet-4-6 con un cap de 256 tokens y dispara solo en alertas recién insertadas — para un equipo sano eso típicamente son 0 a 3 alertas por noche, o aproximadamente $0.05 a $0.15 por noche en gasto de tokens. Un spike de 20 regresiones simultáneas cuesta alrededor de $1.00. La dedupe key mantiene los re-runs gratis.
n8n self-hosted en un VPS de $20/mes maneja esta carga con margen de sobra; el plan Starter de n8n Cloud ($24/mes) también está bien. Postgres puede ser la misma instancia que respalda cualquier otra cosa que corras — las tres tablas son chicas, de bajo tráfico, e indexadas en una sola key compuesta. El costo marginal total está dominado por el people-time para mantener la baseline fresca y la taxonomía de stages estable, no por el runtime.
Si cambias a Opus para la llamada de narrativa, espera aproximadamente un multiplicador de 5x en el costo de tokens; eso rara vez vale la pena para una explicación de 1-2 oraciones.
Métrica de éxito
Trackea el tiempo mediano entre que una regresión (rol, stage) realmente empieza y que el equipo de reclutamiento toma acción sobre ella. Antes de este flow, esa latencia es típicamente de dos a seis semanas (la siguiente revisión de pipeline). Con el flow shippeado y vigilado, debería bajar a 24 a 48 horas. Si no — si las alertas disparan y nadie actúa — el problema no es el detector, es el ruteo o el umbral, y el fix es o apretar los canales hasta que la alerta llegue a una persona con autoridad para actuar, o aflojar el umbral de z-score hasta que el volumen de alertas coincida con lo que el equipo puede absorber.
Una métrica secundaria que vale la pena vigilar: alertas descartadas sin acción como porcentaje del total. Arriba de 30% estás alertando sobre ruido; debajo de 5% probablemente estás alertando de menos y perdiendo señal.
vs alternativas
La alternativa DIY es un job de Python o SQL que corre la misma agregación y postea a Slack vía webhook. Eso funciona y el costo por evento es más bajo, pero el grafo de n8n es la documentación — un ingeniero de recruiting-ops que se una el siguiente trimestre puede abrir el workflow, ver los ocho nodos en orden, y entender el sistema sin leer código. El path DIY también típicamente se salta el insert de dedupe y la guarda de costo alrededor de la llamada al LLM, que es donde llegan las facturas.
La alternativa off-the-shelf es comprar Gem, Ashby Analytics, o Datapeople para reportes de funnel. Esos son buenos productos y son la respuesta correcta para equipos que quieren dashboards manejados y no quieren ser dueños de una tabla de baseline en Postgres. Son la respuesta equivocada cuando quieres alertas de anomalía en Slack con narrativa adjunta, porque ninguno de ellos shippea eso hoy; shippean dashboards que alguien tiene que acordarse de revisar. El trade es: paga al vendor y pierde la latencia de alerta, o sé dueño del flow de n8n y gana la señal de 24 horas al costo de correr Postgres.
La alternativa del status quo — la revisión trimestral de pipeline — es lo que la mayoría de los equipos ya hace. No cuesta nada y saca a la luz las mismas regresiones, solo que sesenta días tarde. Si tus roles tardan seis meses en llenarse y una señal a sesenta días aún te deja tiempo para corregir, el status quo está honestamente bien.
Watch-outs
La fatiga de alertas es el modo de falla dominante. Un equipo que recibe diez alertas cada mañana va a empezar a ignorar las diez en una semana, incluyendo la que importaba. La guarda son las constantes MIN_SAMPLE = 20 y Z_THRESHOLD = 2.0 en el nodo Code Detect Anomalies, más el DWELL_MULTIPLIER = 1.5 para candidatos estancados. Empieza con esos valores, vigila el canal las primeras tres noches, y aprieta — no aflojes — hasta que la mañana mediana traiga 0 a 2 alertas. Afloja después si descubres que estás perdiendo regresiones reales.
El drift de baseline produce falsos negativos silenciosos. Las tasas de conversión cambian con el mercado laboral, el mix de sourcing del equipo, y el rol mismo. Una baseline calculada en noviembre contra un mercado caliente va a sub-marcar en un mercado suave y sobre-marcar en uno apretado. La guarda es el refresh mensual de funnel_baselines y role_tth_baselines desde el mismo query que las construyó — programa eso como un workflow recurrente de n8n o un job de Postgres y trata la falla de ese refresh como un incidente P1.
La auto-acción sobre una alerta de reclutamiento casi siempre está mal. La narrativa que devuelve Claude es correlación, no causación; tratarla como una directiva (“la conversión cayó 30%, rechaza automáticamente todos los no-shows de phone-screen”) va a agravar el problema. La guarda es estructural: el flow no tiene path de write-back al ATS. Si un contributor futuro propone agregar uno, empújalo de regreso. La IA saca a la luz la anomalía; los humanos diagnostican y actúan.
Datos privilegiados de candidatos saliendo del ATS. Las tablas de Postgres guardan role_id, nombres de stage, tasas de conversión, y dwell times, lo cual es solo agregado por diseño — pero una extensión descuidada que agregue nombres de candidato o info de contacto para habilitar narrativas más ricas creará una nueva superficie de privacidad. La guarda es el schema en _README.md: las tablas intencionalmente no tienen columnas que identifiquen al candidato. Si un contributor las quiere agregar, rutea el cambio por tu revisión de privacidad y política de IA.
La atribución de source-channel es tan buena como la data del ATS. La alerta opcional source_channel_drop (referenciada en el mapa de ruteo de Slack pero no habilitada por default en el bundle) depende de que cada applicant tenga una atribución de fuente limpia en Ashby. Si tu equipo es descuidado con el etiquetado de fuente, la alerta va a disparar sobre problemas de calidad de datos, no sobre problemas reales de canal. La guarda es un check de precondición: no habilites ese tipo de alerta hasta que la atribución de fuente esté al menos al 90% completa en tu export del ATS. Verifícalo con un query de SQL de una línea contra el application feed antes de prenderla.
Stack
Este flow asume n8n para orquestación, Ashby (o Greenhouse / Lever) como ATS, Postgres para el estado de baseline y de alertas, Claude para la explicación narrativa, y Slack para la entrega. Es el complemento operativo a las métricas definidas en recruiting funnel metrics y usa la distinción entre time-to-hire vs time-to-fill en el path de chequeo de tendencia.
# Hiring funnel anomaly detection — n8n bundle
## What this flow does
This bundle contains a complete n8n workflow that watches your applicant tracking system for funnel-shaped problems and surfaces them in Slack within 24 hours of the metric moving. Two scheduled triggers wake up nightly. The 2am job pulls the last 24 hours of stage-transition events from Ashby, aggregates them per role-by-stage, joins each row to a rolling baseline stored in Postgres, and emits an alert when today's conversion rate is at least two standard deviations below the baseline mean, when median dwell time in a stage exceeds the role's stage SLA by 50%, or when a role has had zero applicant movement in seven days. The 3am job recomputes a rolling 7-day time-to-hire per role and flags any role exceeding its threshold. Alerts are written to a deduplicated `anomaly_alerts` table so re-running the flow on the same day cannot re-fire. Newly inserted alerts are explained by Claude in one or two sentences and posted to a routing-aware Slack channel.
The two scheduled triggers are independent, which is deliberate. The per-stage detector reads a high-volume application feed; the time-to-hire trend check runs a heavier SQL aggregate. Splitting them by an hour avoids contention on the baselines table and lets you disable one path without breaking the other.
## Import
1. In n8n, open `Workflows`, click `Add workflow`, then `Import from file`.
2. Select `hiring-funnel-anomaly-n8n.json` from this bundle.
3. The workflow imports inactive. Do not toggle it active until credentials are wired and the first-run verification below passes.
4. Open `Settings` (top right) and confirm `Timezone` is the value you want — the JSON ships with `America/New_York` and both Cron expressions evaluate in that zone.
## Credentials
The flow references four placeholder credentials by name. Create each one under `Credentials` in n8n before running.
### Ashby — API
The HTTP Request node `Ashby — Application Feed (24h)` uses Basic Auth with your Ashby API key as the username and an empty password. Generate the key under `Settings → API` in Ashby. The default scope (`Read Applications`) is sufficient. If your ATS is Greenhouse, swap the URL to `https://harvest.greenhouse.io/v1/applications?updated_after={{ $now.minus({hours:24}).toISO() }}` and use the Greenhouse Harvest API key. For Lever, point at `https://api.lever.co/v1/opportunities?expand=stage&updated_at_start={{ ... }}` and use a Lever API key.
### Postgres — funnel-baselines
A standard Postgres connection. The flow expects three tables in the same database:
- `funnel_baselines (role_id text, from_stage text, to_stage text, conversion_rate_mean numeric, conversion_rate_stddev numeric, dwell_seconds_p50 numeric, stage_sla_seconds integer, sample_size integer, refreshed_at timestamptz, primary key (role_id, from_stage, to_stage))`
- `role_tth_baselines (role_id text primary key, role_name text, tth_baseline_days numeric, tth_threshold_days numeric)`
- `anomaly_alerts (id bigserial primary key, role_id text, from_stage text, to_stage text, anomaly_type text, severity text, current_value numeric, baseline_value numeric, window_end timestamptz, dedupe_key text unique, created_at timestamptz default now())`
The DDL is intentionally not bundled because every team's role and stage taxonomy is different. Build the baselines once from a 90-day backfill of the same Ashby feed before turning the flow active. Refresh `funnel_baselines` and `role_tth_baselines` monthly using the same query that built them.
### Anthropic — x-api-key
The narrative explanation step uses Anthropic's HTTP API directly via Header Auth. Create a Header Auth credential with `Name: x-api-key` and `Value: <your Anthropic API key>`. The model is pinned to `claude-sonnet-4-6` in the node body — change it there if you prefer Haiku for cost or Opus for higher-stakes alerts. Token spend scales with new alerts only because the explanation node sits behind the dedupe insert.
### Slack — bot token
Create a Slack app, install it to your workspace, grant `chat:write` and `chat:write.public`, and store the bot token in a Header Auth credential with `Name: Authorization` and `Value: Bearer xoxb-…`. The `Format Slack Message` code node maps each `anomaly_type` to a destination channel (`#recruiting-alerts`, `#sourcing`, `#recruiting-leadership`); make sure the bot has been invited to each channel or `chat.postMessage` will silently 200 with `ok: false`.
## First-run verification
Before flipping the workflow active, walk through these checks. Each one exercises a different branch of the graph; running them in order proves the whole flow without waiting for a real anomaly.
1. **Disable the 2am Cron, then click `Execute Workflow` from the Cron node manually.** Confirm the Ashby HTTP node returns at least one event. If it returns an empty array, your API token is scoped wrong or the time window has no activity.
2. **Insert one synthetic baseline row that is guaranteed to flag.** `INSERT INTO funnel_baselines (role_id, from_stage, to_stage, conversion_rate_mean, conversion_rate_stddev, sample_size) VALUES ('ROLE_TEST','phone_screen','onsite', 0.50, 0.05, 200);`. Then craft an aggregator output that reports `conversion_rate_today = 0.10` for the same key. Run the workflow and confirm a row appears in `anomaly_alerts` and a message lands in `#recruiting-alerts`.
3. **Run the same execution a second time.** Confirm the dedupe insert returns no rows, no Claude call is made, and no Slack message is sent. This validates the cost guard.
4. **Manually run the 3am Cron node.** With no roles exceeding their threshold, it should complete without writing a row. Insert a fake `hires` row that exceeds threshold, re-run, and confirm a `time_to_hire_trend` alert is persisted and posted to `#recruiting-leadership`.
5. **Delete the test rows from `anomaly_alerts` and `funnel_baselines`** before activating the flow. Forgetting this step pollutes your real baseline.
Once all five steps pass, toggle the workflow `Active`. Watch `#recruiting-alerts` for the first three nights and tighten the `Z_THRESHOLD` or `DWELL_MULTIPLIER` constants in the `Detect Anomalies` code node if the volume is wrong for your team.