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. El tipo principal es
webhook(una petición HTTP entrante crea un job).
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»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.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, 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.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 está limitado a 120 peticiones/minuto.
- 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.