Programaciones y triggers
NORA puede lanzar un proceso de dos maneras automáticas:
- Programaciones (schedules): ejecuciones recurrentes por horario, definidas con una expresión cron y una zona horaria. Pueden saltarse los días feriados.
- Triggers: ejecuciones disparadas por un evento externo:
webhook(una petición HTTP entrante),queue(cuando una cola acumula trabajo),file_watcher(un archivo nuevo en una carpeta) oemail_watcher(un correo entrante). El más habitual eswebhook; todos se detallan más abajo.
Ambos mecanismos terminan creando un job del proceso asociado. Consulta jobs para el ciclo de vida de la ejecución y arquitectura para el reparto a un agente.

Programaciones por horario (cron)
Sección titulada «Programaciones por horario (cron)»Una programación asocia un proceso con una expresión cron y una zona horaria. NORA calcula el próximo disparo (next_run_at) y, cuando llega el momento, crea un job y vuelve a recalcular el siguiente.
Campos de una programación
Sección titulada «Campos de una programación»| Campo | Tipo | Notas |
|---|---|---|
name | string | Nombre descriptivo. |
process_id | UUID | Proceso a ejecutar. Debe estar activo y pertenecer a tu organización. |
cron_expression | string | Expresión cron válida (se valida con croniter). |
timezone | string | Zona IANA; por defecto "UTC". El próximo disparo se calcula en esa zona y se guarda en UTC. |
machine_id | UUID | null | Máquina fija opcional. Si se omite, NORA elige una máquina en línea de la organización. |
description | string | null | Opcional. |
skip_holidays | boolean | Si es true, omite la ejecución en días feriados (por defecto false). |
is_enabled | boolean | Solo las habilitadas se evalúan. Al deshabilitar, next_run_at pasa a null. |
last_run_at / next_run_at | datetime | null | Última y próxima ejecución (UTC). |
Crear una programación
Sección titulada «Crear una programación»Lo más simple es desde el dashboard: Programaciones → Nueva, elige frecuencia/hora/días y zona horaria; el panel arma la expresión cron y muestra la próxima ejecución. Se gestiona con tu sesión del dashboard (admin/operator), no con X-API-Key.
Una expresión cron tiene 5 campos: minuto hora día-del-mes mes día-de-la-semana; por ejemplo 0 7 * * 1-5 = de lunes a viernes a las 07:00.
Endpoint: POST /api/v1/schedules (roles admin u operator). La respuesta va envuelta en { "data": ... } (ver autenticación).
POST /api/v1/schedules HTTP/1.1Host: nora-api.valisoftconsulting.comContent-Type: application/json
{ "name": "Cierre diario", "process_id": "3f1c…", "cron_expression": "0 7 * * 1-5", "timezone": "America/Lima", "skip_holidays": true}{ "success": true, "data": { "id": "…", "process_id": "3f1c…", "name": "Cierre diario", "cron_expression": "0 7 * * 1-5", "timezone": "America/Lima", "is_enabled": true, "skip_holidays": true, "next_run_at": "2026-06-15T12:00:00Z", "last_run_at": null }}El ejemplo programa el proceso de lunes a viernes a las 07:00 hora de Lima (almacenado como 12:00 UTC).
Operaciones disponibles
Sección titulada «Operaciones disponibles»| Método y ruta | Rol | Acción |
|---|---|---|
POST /api/v1/schedules | admin, operator | Crear. |
GET /api/v1/schedules | admin, operator, viewer | Listar (paginado; filtros process_id, is_enabled). |
GET /api/v1/schedules/{id} | admin, operator, viewer | Obtener una. |
PUT /api/v1/schedules/{id} | admin, operator | Actualizar (recalcula next_run_at si cambia cron o zona). |
PATCH /api/v1/schedules/{id}/toggle | admin, operator | Habilitar/deshabilitar. |
DELETE /api/v1/schedules/{id} | admin | Eliminar. |
Cómo se evalúan las programaciones
Sección titulada «Cómo se evalúan las programaciones»flowchart TD
A[Programaciones habilitadas con next_run_at <= ahora] --> B{skip_holidays y hoy es feriado?}
B -- Sí --> R[Recalcular next_run_at y omitir]
B -- No --> C{Máquina disponible en línea?}
C -- No --> S[Omitir este ciclo]
C -- Sí --> D{Job previo aún en ejecución?}
D -- Sí --> R
D -- No --> E[Crear job + actualizar last_run_at / next_run_at]
Detalles confirmados en el código:
- No solapamiento: si ya hay un job de ese proceso en la máquina con estado
pending,assignedorunning, se omite ese disparo y se recalcula el siguiente. - Sin máquina en línea: si la máquina fija (o cualquiera de la organización) está
offline, el ciclo se omite sin crear job. - Recuperación tras caída: al arrancar el servidor se reprocesan las programaciones cuyo
next_run_atquedó en el pasado dentro de las últimas 24 horas, creando un job de recuperación.
Feriados (holidays)
Sección titulada «Feriados (holidays)»Los feriados pertenecen a la organización y permiten que las programaciones con skip_holidays: true no se ejecuten esos días.
| Campo | Tipo | Notas |
|---|---|---|
name | string | Mínimo 2 caracteres. |
date | date | Fecha del feriado (YYYY-MM-DD). |
recurring | boolean | Si es true, coincide cada año por mes/día (ignora el año). |
Una fecha cuenta como feriado si coincide exactamente con date, o si el feriado es recurring y coinciden el mes y el día.
Operaciones (prefijo /api/v1/holidays): GET (lista paginada), POST (crear, rol admin), GET /{id}, PUT /{id} (rol admin), DELETE /{id} (rol admin).
POST /api/v1/holidays HTTP/1.1Content-Type: application/json
{ "name": "Año Nuevo", "date": "2027-01-01", "recurring": true }Triggers por evento (webhook)
Sección titulada «Triggers por evento (webhook)»Un trigger ejecuta un proceso cuando llega un evento. Los tipos válidos son webhook, queue, file_watcher y email_watcher; el más habitual es webhook.


Al crear un trigger de tipo webhook, NORA genera:
- un
webhook_tokenúnico que forma parte de la URL entrante, y - un
webhook_secret(HMAC-SHA256) para firmar las peticiones.
Administrar triggers
Sección titulada «Administrar triggers»Todas estas rutas requieren la función webhooks habilitada en tu plan y rol admin (el listado admite además operator):
| Método y ruta | Acción |
|---|---|
POST /api/v1/triggers | Crear (devuelve webhook_url y webhook_secret). |
GET /api/v1/triggers | Listar (con has_webhook_secret, sin el secreto). |
PATCH /api/v1/triggers/{id} | Actualizar nombre, estado, máquina o config. |
DELETE /api/v1/triggers/{id} | Eliminar. |
POST /api/v1/triggers/{id}/regenerate-secret | Regenerar el webhook_secret. |
{ "success": true, "data": { "id": "…", "name": "Alta de cliente CRM", "trigger_type": "webhook", "webhook_url": "/api/v1/triggers/inbound/AbC123…", "webhook_secret": "xY9…", "is_active": true }}Disparar el webhook entrante
Sección titulada «Disparar el webhook entrante»La URL de disparo es pública (no requiere sesión ni X-API-Key): se autentica por el token en la ruta y, opcionalmente, por la firma HMAC.
POST /api/v1/triggers/inbound/AbC123… HTTP/1.1Host: nora-api.valisoftconsulting.comContent-Type: application/jsonX-Webhook-Signature: sha256=<hmac-sha256(secret, cuerpo_crudo)>
{ "cliente_id": 4821, "origen": "crm" }{ "success": true, "data": { "trigger_id": "…", "job_id": "…", "status": "created" }}sequenceDiagram
participant Ext as Sistema externo
participant API as NORA API
participant Job as Job / Robot
Ext->>API: POST /api/v1/triggers/inbound/{token}
API->>API: Trigger activo? + firma HMAC válida?
API->>API: Anti-replay (firma ya usada?)
API->>API: Suscripción activa + cuota disponible
API->>Job: Crear job (payload como input_data)
API-->>Ext: { job_id, status: "created" }
Comportamiento del receptor, confirmado en el código:
- Firma obligatoria si hay secreto: si el trigger tiene
webhook_secret, la cabeceraX-Webhook-Signaturedebe ser una firmasha256=…válida; de lo contrario se rechaza (401). Si no hay secreto, no se exige firma. - Anti-replay: una misma firma no puede reutilizarse dentro de una ventana corta; un reenvío idéntico se rechaza como duplicado.
- Límite de tasa: el endpoint entrante por token (
POST /api/v1/triggers/inbound/{token}) está limitado a 120 peticiones/minuto (independiente del webhook por API key/api/v1/webhooks/trigger/{process_id}, que tiene su propio 60/min). - Cuota y suscripción: antes de crear el job se valida que la suscripción esté activa y que quede cuota mensual de ejecuciones.
- Datos de entrada: el cuerpo JSON se fusiona con la plantilla
input_templatedeconfig(si existe) y se pasa comoinput_datadel job. - Estados: trigger inactivo devuelve
403; token inexistente devuelve404.
La firma se calcula como HMAC-SHA256 del cuerpo crudo con el webhook_secret, en formato sha256=<hexadecimal>.
import hashlib, hmac
body = b'{"cliente_id": 4821, "origen": "crm"}'secret = "xY9…" # webhook_secret del triggerfirma = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()# Envía 'firma' en la cabecera X-Webhook-SignatureConsulta la referencia de la API para los endpoints relacionados.