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. 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 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).

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.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, file_watcher y email_watcher; el más habitual es webhook.

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.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 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_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.