Ir al contenido

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) o email_watcher (un correo entrante). El más habitual es webhook; 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 de NORA: lista de schedules con su expresión cron, zona horaria, próxima ejecución y estado activo.

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.

CampoTipoNotas
namestringNombre descriptivo.
process_idUUIDProceso a ejecutar. Debe estar activo y pertenecer a tu organización.
cron_expressionstringExpresión cron válida (se valida con croniter).
timezonestringZona IANA; por defecto "UTC". El próximo disparo se calcula en esa zona y se guarda en UTC.
machine_idUUID | nullMáquina fija opcional. Si se omite, NORA elige una máquina en línea de la organización.
descriptionstring | nullOpcional.
skip_holidaysbooleanSi es true, omite la ejecución en días feriados (por defecto false).
is_enabledbooleanSolo las habilitadas se evalúan. Al deshabilitar, next_run_at pasa a null.
last_run_at / next_run_atdatetime | nullÚltima y próxima ejecución (UTC).

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.1
Host: nora-api.valisoftconsulting.com
Content-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).

Método y rutaRolAcción
POST /api/v1/schedulesadmin, operatorCrear.
GET /api/v1/schedulesadmin, operator, viewerListar (paginado; filtros process_id, is_enabled).
GET /api/v1/schedules/{id}admin, operator, viewerObtener una.
PUT /api/v1/schedules/{id}admin, operatorActualizar (recalcula next_run_at si cambia cron o zona).
PATCH /api/v1/schedules/{id}/toggleadmin, operatorHabilitar/deshabilitar.
DELETE /api/v1/schedules/{id}adminEliminar.
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, assigned o running, 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_at quedó en el pasado dentro de las últimas 24 horas, creando un job de recuperación.

Los feriados pertenecen a la organización y permiten que las programaciones con skip_holidays: true no se ejecuten esos días.

CampoTipoNotas
namestringMínimo 2 caracteres.
datedateFecha del feriado (YYYY-MM-DD).
recurringbooleanSi 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.1
Content-Type: application/json
{ "name": "Año Nuevo", "date": "2027-01-01", "recurring": true }

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.

Modal "Crear trigger" de NORA con el tipo "Cola (item nuevo → arranca job)" seleccionado: proceso a ejecutar, cola "Facturas pendientes", items mínimos para arrancar (5) y máximo de jobs concurrentes (2).

Disparadores (triggers) en NORA: lista de triggers de tipo webhook con su proceso destino, estado y la indicación de si tienen secreto configurado.

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.

Todas estas rutas requieren la función webhooks habilitada en tu plan y rol admin (el listado admite además operator):

Método y rutaAcción
POST /api/v1/triggersCrear (devuelve webhook_url y webhook_secret).
GET /api/v1/triggersListar (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-secretRegenerar 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
}
}

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.1
Host: nora-api.valisoftconsulting.com
Content-Type: application/json
X-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 cabecera X-Webhook-Signature debe ser una firma sha256=… 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_template de config (si existe) y se pasa como input_data del job.
  • Estados: trigger inactivo devuelve 403; token inexistente devuelve 404.

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 trigger
firma = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
# Envía 'firma' en la cabecera X-Webhook-Signature

Consulta la referencia de la API para los endpoints relacionados.