This is the abridged developer documentation for NORA # NORA > Ejecuta, programa y controla todos tus robots desde un solo lugar. Para empezar Conoce los [conceptos clave](/conceptos/que-es-nora/) y sigue la guía de [primeros pasos](/guia/primeros-pasos/) para lanzar tu primer robot. Guía de la plataforma Instala el agente, gestiona máquinas, procesos, jobs, colas y assets desde el [Robots Center](/conceptos/arquitectura/). API para desarrolladores Integra tus sistemas: [autentícate](/api/autenticacion/), [dispara jobs](/api/disparar-jobs/) y consume la [referencia completa](/api/referencia/). ¿Vienes de otra herramienta? Guías de [migración desde UiPath](/migracion/desde-uipath/) y [Automation Anywhere](/migracion/desde-automation-anywhere/). Tutoriales Aprende con un ejemplo real de punta a punta: el [RPA Challenge con colas](/tutoriales/rpa-challenge-colas/), luego [assets y automatización](/tutoriales/assets-y-automatizacion/) y [patrones avanzados](/tutoriales/patrones-avanzados/). ¿Solo lanzas y vigilas procesos? Ve a [Jobs](/guia/jobs/) y [Colas](/guia/colas/). Operas el panel y no programas Lanza y supervisa sin escribir código: gestiona [Jobs](/guia/jobs/), trabaja con [Colas](/guia/colas/) y revisa [Anomalías](/guia/anomalias/). Referencia y para IA Toda la superficie en la [lámina de comandos](/referencia/lamina-comandos/). ¿Usas una IA? Dale [`/llms-full.txt`](/llms-full.txt) y mira [operar NORA con IA](/referencia/operar-con-ia/). # Assets vía API > Leer assets y credenciales desde la API pública de NORA con una API key (scope assets:read). Los **assets** son valores reutilizables que tus robots necesitan en tiempo de ejecución: rutas, direcciones de correo, claves de API de terceros, usuarios y contraseñas. NORA los almacena cifrados por tenant y permite que un robot los lea bajo demanda mediante una API key, sin tener que incrustar secretos en el código del proceso. Esta página documenta el único endpoint público de assets: la lectura por nombre. Para crear y administrar assets desde el panel, consulta la guía de [assets y credenciales](/guia/assets-y-credenciales/). ## Tipos de asset [Sección titulada «Tipos de asset»](#tipos-de-asset) Cada asset tiene un `type` que determina cómo se usa y si su valor puede revelarse desde el Robots Center (la UI): | Tipo | Descripción | Campos devueltos | | ------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------ | | `text` | Valor no sensible (rutas, URLs, correos, identificadores). Puede revelarse también desde la UI. | `value` | | `credential` | Par usuario/contraseña. Requiere `username` al crearse. | `value`, `username` | | `secret` | Valor sensible de un solo campo (token, clave de API de un tercero). | `value` | | `vault` | Referencia a un secreto en un proveedor externo (vault). Se resuelve en el momento de la lectura. | `value` (y `username` si el proveedor lo expone) | Además, cada asset pertenece a un **entorno** (`environment`): `dev`, `staging` o `production` (por defecto `production`). El nombre de un asset es único por tenant y entorno, de modo que puedes tener el mismo `db_password` con valores distintos en `dev` y en `production`. Los secretos son de solo escritura en la UI El valor descifrado de los assets de tipo `secret` y `credential` **nunca** se muestra en una sesión del panel: una vez guardados no pueden leerse de vuelta desde el Robots Center (la UI). Solo se consumen en tiempo de ejecución a través de esta API, usando una API key con el scope `assets:read`. Esto reduce la superficie de exposición de secretos. ## Leer un asset por nombre [Sección titulada «Leer un asset por nombre»](#leer-un-asset-por-nombre) ```http GET /api/v1/assets/by-name/{name}?environment=production ``` URL base de producción: `https://nora-api.valisoftconsulting.com`. La autenticación es por cabecera `X-API-Key` (consulta [autenticación](/api/autenticacion/)). ### Parámetros [Sección titulada «Parámetros»](#parámetros) | Parámetro | Ubicación | Requerido | Descripción | | ------------- | --------- | --------- | ------------------------------------------------------------------------------------- | | `name` | ruta | sí | Nombre exacto del asset. | | `environment` | query | no | Entorno del asset. Por defecto `production`. Valores: `dev`, `staging`, `production`. | ### Requisitos de la API key [Sección titulada «Requisitos de la API key»](#requisitos-de-la-api-key) * La API key debe tener el scope **`assets:read`**. La verificación es *fail-closed*: una key sin ese scope explícito recibe `403`. * Si la key declara una **lista de entornos permitidos**, el `environment` solicitado debe estar en ella. Por ejemplo, una key restringida a `["dev"]` no puede leer assets de `production` —así, una key filtrada de CI/CD no permite exfiltrar secretos de producción. * Límite de uso: **30 peticiones por minuto** por IP. * Cada lectura exitosa queda registrada en el log de auditoría del tenant (acción `access` sobre el recurso `asset`), incluyendo el nombre de la key y el entorno consultado. ### Respuesta [Sección titulada «Respuesta»](#respuesta) La respuesta va envuelta en el sobre estándar `SuccessResponse` (`{"success": true, "data": ...}`). El objeto `data` es un `AgentAssetResponse`: ```json { "success": true, "data": { "name": "sap_login", "type": "credential", "environment": "production", "value": "S3cr3t-P4ss", "username": "robot_finanzas" } } ``` El campo `username` solo aparece cuando el asset lo tiene (típicamente los de tipo `credential`). Para `text`, `secret` y la mayoría de `vault`, la respuesta incluye únicamente `value` además de `name`, `type` y `environment`. ### Errores [Sección titulada «Errores»](#errores) | Código | Causa | | ------ | ------------------------------------------------------------------------------------------------------------------ | | `403` | La API key no tiene el scope `assets:read`, o el `environment` solicitado no está en la lista permitida de la key. | | `404` | No existe un asset con ese `name` en el `environment` indicado. | | `422` | Parámetros inválidos. | | `429` | Se superó el límite de 30 peticiones por minuto. | Consulta el formato completo del sobre de error en [errores y límites](/api/errores-y-limites/). ## Ejemplos [Sección titulada «Ejemplos»](#ejemplos) ### curl [Sección titulada «curl»](#curl) ```bash curl -s "https://nora-api.valisoftconsulting.com/api/v1/assets/by-name/sap_login?environment=production" \ -H "X-API-Key: nora_ak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` ### Python [Sección titulada «Python»](#python) ```python import httpx API = "https://nora-api.valisoftconsulting.com/api/v1" HEADERS = {"X-API-Key": "nora_ak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} resp = httpx.get( f"{API}/assets/by-name/sap_login", params={"environment": "production"}, headers=HEADERS, ) resp.raise_for_status() asset = resp.json()["data"] usuario = asset.get("username") password = asset["value"] # usar las credenciales en el robot... ``` ## Flujo de consumo [Sección titulada «Flujo de consumo»](#flujo-de-consumo) ``` sequenceDiagram participant Bot as Robot (agente) participant API as NORA API participant DB as Almacén cifrado Bot->>API: GET /assets/by-name/{name}?environment=...
X-API-Key: nora_ak_... API->>API: Valida key + scope assets:read + entorno API->>DB: Busca asset (tenant, name, environment) DB-->>API: Asset cifrado API->>API: Descifra value/username + registra auditoría API-->>Bot: { "data": { value, username, ... } } ``` # Autenticación y scopes > Cómo crear API keys de NORA, la lista completa de scopes, el modelo recurso:acción, fail-closed, allowed_ips, environments, expiración, rotación y revocación. La API pública de NORA se autentica con **API keys** enviadas en la cabecera `X-API-Key`. Cada key pertenece a un workspace (tenant), lleva un conjunto de **scopes** que limitan a qué endpoints puede acceder y, opcionalmente, una lista de IPs permitidas, un conjunto de entornos y una fecha de expiración. * **URL base de producción:** `https://nora-api.valisoftconsulting.com` * **Prefijo de la API:** `/api/v1` * **Cabecera de autenticación:** `X-API-Key: nora_ak_...` ## Crear una API key [Sección titulada «Crear una API key»](#crear-una-api-key) Puedes crear una key desde la interfaz (**Settings → API Keys**) o directamente con la API de configuración. Crear, listar, rotar y revocar keys requiere rol **admin** del workspace. El endpoint de creación es: ```http POST /api/v1/settings/api-keys ``` Este endpoint usa autenticación de usuario (sesión / token de acceso), no `X-API-Key`. El cuerpo acepta los siguientes campos: | Campo | Tipo | Requerido | Descripción | | -------------- | ----------------- | --------- | ----------------------------------------------------------------------------------------------------------------- | | `name` | string | Sí | Nombre descriptivo (mínimo 2 caracteres). | | `scopes` | string\[] | Sí | Al menos un scope válido (ver tabla). | | `expires_at` | datetime \| null | No | Fecha de expiración. `null` = sin expiración. | | `allowed_ips` | string\[] \| null | No | Lista de IPs permitidas. `null` = sin restricción. | | `environments` | string\[] \| null | No | Restringe la lectura de assets a estos entornos. `null` = todos. Valores válidos: `dev`, `staging`, `production`. | Ejemplo de creación: ```bash curl -X POST https://nora-api.valisoftconsulting.com/api/v1/settings/api-keys \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Integración CI/CD", "scopes": ["jobs:read", "jobs:write"], "environments": ["staging"] }' ``` La respuesta va envuelta en `{"success": true, "data": {...}}` e incluye el campo `key` con el valor completo de la API key: ```json { "success": true, "data": { "id": "0d4c…", "name": "Integración CI/CD", "prefix": "nora_ak_AbC", "is_active": true, "last_used_at": null, "expires_at": null, "allowed_ips": null, "scopes": ["jobs:read", "jobs:write"], "environments": ["staging"], "created_at": "2026-06-19T12:00:00Z", "key": "nora_ak_..." } } ``` La key solo se muestra una vez El valor completo (`key`) se devuelve **únicamente** en la respuesta de creación (y de rotación). NORA solo guarda un hash HMAC-SHA256 de la key, nunca el valor en claro. Si la pierdes, no podrás recuperarla: deberás rotarla o crear una nueva. Guárdala en un gestor de secretos en el momento de crearla. Las keys tienen el prefijo fijo `nora_ak_` seguido de material aleatorio. El campo `prefix` (los primeros 12 caracteres) sirve para identificar la key en la interfaz sin exponer el valor completo. ## Usar la API key [Sección titulada «Usar la API key»](#usar-la-api-key) Envía la key en la cabecera `X-API-Key` en cada petición a la API pública: ```bash curl https://nora-api.valisoftconsulting.com/api/v1/machines \ -H "X-API-Key: nora_ak_..." ``` En cada uso, NORA actualiza el campo `last_used_at` de la key. Si la key está inactiva (revocada), expirada o el scope no coincide, la petición se rechaza. ## Modelo de scopes: `recurso:accion` [Sección titulada «Modelo de scopes: recurso:accion»](#modelo-de-scopes-recursoaccion) Los scopes de API key usan la notación `recurso:accion` (con dos puntos), un espacio de nombres distinto al de los permisos RBAC internos de los usuarios (que usan `recurso.accion` con punto). Cada scope habilita un conjunto concreto de endpoints de la API pública. ### Lista completa de scopes [Sección titulada «Lista completa de scopes»](#lista-completa-de-scopes) | Scope | Significado | Grupo | | ---------------- | ---------------------------------- | ------------ | | `assets:read` | Leer assets (credenciales/valores) | Credenciales | | `queues:read` | Leer items de colas | Colas | | `queues:write` | Agregar items a colas | Colas | | `jobs:read` | Ver estado de ejecuciones | Ejecuciones | | `jobs:write` | Disparar ejecuciones | Ejecuciones | | `jobs:stop` | Detener ejecuciones | Ejecuciones | | `processes:read` | Leer procesos | Procesos | | `machines:read` | Leer máquinas | Máquinas | Puedes consultar dinámicamente el catálogo de scopes aceptados (solo admin): ```http GET /api/v1/settings/api-keys/available-scopes ``` ## Comportamiento fail-closed [Sección titulada «Comportamiento fail-closed»](#comportamiento-fail-closed) La validación de scopes es **fail-closed**: una key solo puede usar un endpoint si su lista de scopes contiene **explícitamente** el scope requerido. Una key sin scopes (lista vacía o nula) **no tiene ningún permiso** y se rechaza con un error de autorización. ``` flowchart TD A[Petición con X-API-Key] --> B{Key activa y no expirada} B -- No --> X[401 No autorizado] B -- Sí --> C{IP en allowed_ips?} C -- No --> Y[403 IP no autorizada] C -- Sí --> D{Scope requerido presente} D -- No --> Z[403 Scope faltante] D -- Sí --> E[Petición permitida] ``` Al crearse vía API, toda key debe llevar al menos un scope (`scopes` exige `min_length=1`), por lo que no es posible crear keys sin restricción desde la superficie pública. ## Restricciones adicionales [Sección titulada «Restricciones adicionales»](#restricciones-adicionales) ### IPs permitidas (`allowed_ips`) [Sección titulada «IPs permitidas (allowed\_ips)»](#ips-permitidas-allowed_ips) Si la key define `allowed_ips`, NORA compara la IP de origen de la petición contra esa lista; si no coincide, devuelve `403 IP no autorizada`. Si es `null`, no hay restricción de IP. Precaución La comprobación usa la IP de conexión real, no la cabecera `X-Forwarded-For` (que cualquier cliente podría falsificar). Asegúrate de declarar la IP de salida real de tu integración. ### Entornos (`environments`) [Sección titulada «Entornos (environments)»](#entornos-environments) `environments` limita desde qué entornos puede **leer assets** la key. Por ejemplo, una key de CI/CD restringida a `["dev"]` o `["staging"]` no podrá leer secretos de `production`, conteniendo el impacto de una key filtrada. Si es `null`, la key puede leer assets de todos los entornos. Los valores válidos son `dev`, `staging` y `production`. ### Expiración (`expires_at`) [Sección titulada «Expiración (expires\_at)»](#expiración-expires_at) Si `expires_at` está definido y ya pasó, la key se rechaza con `401 API key expirada`. Define una expiración para keys temporales o de terceros. ## Rotación y revocación [Sección titulada «Rotación y revocación»](#rotación-y-revocación) **Rotar** una key genera un nuevo valor secreto manteniendo el mismo registro (nombre, scopes, IPs). El valor antiguo deja de funcionar y la respuesta devuelve el nuevo `key` (de nuevo, solo esta vez): ```http POST /api/v1/settings/api-keys/{key_id}/rotate ``` **Revocar** desactiva la key de forma permanente; cualquier petición posterior con ese valor se rechaza: ```http DELETE /api/v1/settings/api-keys/{key_id} ``` Tanto la creación, como la rotación y la revocación quedan registradas en el registro de auditoría del workspace. ## Buenas prácticas de seguridad [Sección titulada «Buenas prácticas de seguridad»](#buenas-prácticas-de-seguridad) * **Mínimo privilegio:** asigna solo los scopes que la integración necesita (p. ej. una integración de monitoreo solo necesita `jobs:read`). * **Una key por integración:** facilita revocar una sola sin afectar al resto. * **Restringe entornos:** scope las keys de CI/CD a `dev`/`staging` para que una filtración no exponga secretos de producción. * **Usa `allowed_ips`** cuando la integración tenga IPs de salida estables. * **Define expiración** en keys temporales y **rota** periódicamente las permanentes. * **Nunca** publiques la key en el frontend, repositorios o logs: trátala como una contraseña y guárdala en un gestor de secretos. * **Revoca de inmediato** cualquier key potencialmente comprometida. # Colas vía API > Encola y consulta items de una cola NORA desde tus sistemas con una API Key. Las **colas** son el mecanismo de NORA para alimentar a tus robots con unidades de trabajo (items). Esta página documenta los endpoints públicos que te permiten **encolar** y **consultar** items de una cola desde tus sistemas (ERP, backend, scripts) usando una **API Key**, sin necesidad del token de un agente. Si buscas la guía conceptual de colas, revisa [Colas](/guia/colas/). ## Autenticación y prefijo [Sección titulada «Autenticación y prefijo»](#autenticación-y-prefijo) * URL base de producción: `https://nora-api.valisoftconsulting.com` * Prefijo de la API: `/api/v1` * Autenticación: cabecera `X-API-Key: nora_ak_...` Cada endpoint exige un **scope** concreto en la API Key. Consulta cómo crear y limitar claves en [autenticación](/api/autenticacion/). | Acción | Scope requerido | | --------------- | ------------------------------------------------------ | | Encolar item(s) | `queues:write` (o una clave sin restricción de scopes) | | Listar items | `queues:read` (o una clave sin restricción de scopes) | ## Envoltura de respuesta [Sección titulada «Envoltura de respuesta»](#envoltura-de-respuesta) Todas las respuestas van envueltas. Las operaciones simples usan `SuccessResponse`: ```json { "success": true, "data": { } } ``` Los listados usan `PaginatedResponse`, que añade `meta`: ```json { "success": true, "data": [ ], "meta": { "page": 1, "limit": 20, "total": 0, "pages": 0 } } ``` ## Estados de un item de cola [Sección titulada «Estados de un item de cola»](#estados-de-un-item-de-cola) El campo `status` de cada item evoluciona durante su ciclo de vida. Los valores posibles son: | Estado | Significado | | ---------------- | ----------------------------------------------------------- | | `new` | Listo para tomar. Es el valor por defecto al crear. | | `in_progress` | Un robot lo está procesando. | | `pending_review` | A la espera de aprobación humana. | | `completed` | Procesado con éxito (con `result`). | | `failed` | Falló o fue rechazado (ver `error_message`, `retry_count`). | | `dead_letter` | Superó `max_retries`; no se reintenta solo. | ``` stateDiagram-v2 [*] --> new new --> in_progress in_progress --> completed in_progress --> pending_review in_progress --> failed pending_review --> new : aprobado pending_review --> failed : rechazado failed --> dead_letter : sin reintentos failed --> new : reintento ``` El número máximo de reintentos se define a nivel de cola (`max_retries`, entre 0 y 10; por defecto 3). ## Encolar un item [Sección titulada «Encolar un item»](#encolar-un-item) ```http POST /api/v1/queues/by-name/{name}/items ``` * Scope: `queues:write` * Límite de tasa: **60 solicitudes/minuto** (por API Key, o por IP). Cuerpo de la petición (`AddQueueItemRequest`): | Campo | Tipo | Requerido | Descripción | | ----------- | --------------------------- | --------- | --------------------------------------------------------------------- | | `data` | objeto | Sí | Carga de negocio del item (JSON libre). | | `priority` | entero | No (3) | 1 = baja, 3 = normal, 5 = urgente. | | `reference` | string \| null | No | Clave de negocio del productor; es buscable y se muestra en la lista. | | `deadline` | fecha-hora ISO 8601 \| null | No | SLA: los items con `deadline` más cercano se despachan primero. | | `postpone` | fecha-hora ISO 8601 \| null | No | El agente omite el item hasta que pase este momento. | Ejemplo con `curl`: ```bash curl -X POST \ "https://nora-api.valisoftconsulting.com/api/v1/queues/by-name/facturas/items" \ -H "X-API-Key: nora_ak_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "data": { "factura_id": "F-2026-0042", "monto": 1280.50 }, "priority": 5, "reference": "F-2026-0042", "deadline": "2026-06-30T23:59:00Z" }' ``` Respuesta `200`: ```json { "success": true, "data": { "id": "5b1c0a3e-2f4d-4a91-9c10-7e2f8c4d1a22", "queue_id": "0a9f7b2c-1d3e-4f56-8a7b-9c0d1e2f3a4b", "status": "new", "priority": 5, "reference": "F-2026-0042", "deadline": "2026-06-30T23:59:00Z", "postpone": null, "data": { "factura_id": "F-2026-0042", "monto": 1280.50 }, "result": null, "retry_count": 0, "error_message": null, "processed_by": null, "created_at": "2026-06-19T14:00:00Z", "processed_at": null, "reviewed_by": null, "reviewed_at": null } } ``` ## Encolar items en lote (bulk) [Sección titulada «Encolar items en lote (bulk)»](#encolar-items-en-lote-bulk) ```http POST /api/v1/queues/by-name/{name}/items/bulk ``` * Scope: `queues:write` * Límite de tasa: **20 solicitudes/minuto** (por API Key, o por IP). * Máximo **1000 items** por solicitud. Cuerpo de la petición (`BulkAddQueueItemsRequest`): | Campo | Tipo | Requerido | Descripción | | ------- | ---------------- | --------- | ----------------------------------------------------------- | | `items` | lista de objetos | Sí | Cada objeto es la **carga de negocio (`data`)** de un item. | El bulk solo lleva `data` En el alta por lote, cada elemento de `items` se guarda íntegro como el campo `data` del item. Los campos `priority`, `reference`, `deadline` y `postpone` **no** se aplican por item: todos quedan con prioridad por defecto (3) y sin referencia ni SLA. Si necesitas esos atributos, usa el alta de item individual. Ejemplo con `curl`: ```bash curl -X POST \ "https://nora-api.valisoftconsulting.com/api/v1/queues/by-name/facturas/items/bulk" \ -H "X-API-Key: nora_ak_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "factura_id": "F-2026-0043", "monto": 90.00 }, { "factura_id": "F-2026-0044", "monto": 410.75 } ] }' ``` Respuesta `200` (devuelve cuántos items se añadieron): ```json { "success": true, "data": { "added": 2 } } ``` ## Listar items de una cola [Sección titulada «Listar items de una cola»](#listar-items-de-una-cola) ```http GET /api/v1/queues/by-name/{name}/items ``` * Scope: `queues:read` * Límite de tasa: **60 solicitudes/minuto** (por API Key, o por IP). Parámetros de consulta: | Parámetro | Tipo | Por defecto | Restricciones | | --------- | ------ | ----------- | ------------------------------------------------------------------------------------ | | `page` | entero | 1 | entre 1 y 500 | | `limit` | entero | 20 | entre 1 y 100 | | `status` | string | (todos) | uno de: `new`, `in_progress`, `pending_review`, `completed`, `failed`, `dead_letter` | Los items se devuelven ordenados por fecha de creación **descendente** (más recientes primero). Ejemplo con `curl` (filtrando por estado): ```bash curl -G \ "https://nora-api.valisoftconsulting.com/api/v1/queues/by-name/facturas/items" \ -H "X-API-Key: nora_ak_xxxxxxxxxxxxxxxxxxxxxxxx" \ --data-urlencode "status=completed" \ --data-urlencode "page=1" \ --data-urlencode "limit=50" ``` Respuesta `200`: ```json { "success": true, "data": [ { "id": "5b1c0a3e-2f4d-4a91-9c10-7e2f8c4d1a22", "queue_id": "0a9f7b2c-1d3e-4f56-8a7b-9c0d1e2f3a4b", "status": "completed", "priority": 5, "reference": "F-2026-0042", "deadline": "2026-06-30T23:59:00Z", "postpone": null, "data": { "factura_id": "F-2026-0042", "monto": 1280.50 }, "result": { "estado": "registrada" }, "retry_count": 0, "error_message": null, "processed_by": "9d8c7b6a-5e4f-4321-8a90-0b1c2d3e4f5a", "created_at": "2026-06-19T14:00:00Z", "processed_at": "2026-06-19T14:05:00Z", "reviewed_by": null, "reviewed_at": null } ], "meta": { "page": 1, "limit": 50, "total": 1, "pages": 1 } } ``` ## Ejemplo en Python [Sección titulada «Ejemplo en Python»](#ejemplo-en-python) ```python import requests BASE = "https://nora-api.valisoftconsulting.com/api/v1" HEADERS = {"X-API-Key": "nora_ak_xxxxxxxxxxxxxxxxxxxxxxxx"} # Encolar un item con prioridad alta resp = requests.post( f"{BASE}/queues/by-name/facturas/items", headers=HEADERS, json={ "data": {"factura_id": "F-2026-0045", "monto": 75.0}, "priority": 5, "reference": "F-2026-0045", }, ) resp.raise_for_status() item = resp.json()["data"] print(item["id"], item["status"]) # ... new # Listar los items y revisar su estado real resp = requests.get( f"{BASE}/queues/by-name/facturas/items", headers=HEADERS, params={"limit": 100}, ) resp.raise_for_status() payload = resp.json() print(payload["meta"]["total"], "items en total") for it in payload["data"]: print(it["reference"], it["status"]) ``` ## Errores comunes [Sección titulada «Errores comunes»](#errores-comunes) | Código | Causa | | ------ | ---------------------------------------------------------------------------- | | `401` | Falta la cabecera `X-API-Key` o la clave es inválida. | | `403` | La clave no tiene el scope requerido (`queues:read` / `queues:write`). | | `404` | No existe una cola con ese `name` en tu organización. | | `422` | Cuerpo inválido (p. ej. más de 1000 items en bulk, o `status` no permitido). | | `429` | Se superó el límite de tasa del endpoint. | ## Siguientes pasos [Sección titulada «Siguientes pasos»](#siguientes-pasos) * Consulta cómo procesan los robots estos items en [Colas](/guia/colas/). * Revisa scopes y gestión de claves en [autenticación](/api/autenticacion/). # Consultar jobs > Consulta el estado y el resultado de una ejecución, detén un job en curso y aprende el patrón de polling. Cuando disparas un proceso con la API pública obtienes un **job** (ejecución). Un job es asíncrono: el endpoint de disparo responde de inmediato con un job en estado `pending`, y el robot lo va avanzando hasta un estado final. Esta página explica cómo consultar su estado y resultado, qué estados existen y cómo detener una ejecución en curso. Todos los endpoints aquí descritos pertenecen a la API pública y se autentican con la cabecera `X-API-Key: nora_ak_...`. Consulta [autenticación](/api/autenticacion/) para crear y gestionar tus claves. La URL base de producción es `https://nora-api.valisoftconsulting.com` y el prefijo de la API es `/api/v1`. ## Estados de un job [Sección titulada «Estados de un job»](#estados-de-un-job) Un job pasa por uno de estos seis estados. Solo `completed`, `failed` y `cancelled` son **finales** (el job ya no cambiará). | Estado | Significado | ¿Final? | | ----------- | ---------------------------------------------------------- | ------- | | `pending` | Creado y encolado, aún sin asignar a una máquina. | No | | `assigned` | Asignado a una máquina; el agente está por arrancarlo. | No | | `running` | El robot se está ejecutando. | No | | `completed` | Terminó correctamente. El resultado está en `output_data`. | Sí | | `failed` | Terminó con error. El detalle está en `error_message`. | Sí | | `cancelled` | Cancelado o detenido antes de completar. | Sí | ``` flowchart TD A[pending] --> B[assigned] B --> C[running] C --> D[completed] C --> E[failed] A --> F[cancelled] B --> F C --> F ``` ## Consultar estado y resultado [Sección titulada «Consultar estado y resultado»](#consultar-estado-y-resultado) ```http GET /api/v1/jobs/{job_id} ``` Devuelve el job completo. Requiere el scope `jobs:read` (o una clave sin restricción de scopes). Límite de uso: **60 solicitudes por minuto** por clave o IP. ### Ejemplo (curl) [Sección titulada «Ejemplo (curl)»](#ejemplo-curl) ```bash curl https://nora-api.valisoftconsulting.com/api/v1/jobs/3f1c0b4a-7c2e-4a1d-9b8e-1a2b3c4d5e6f \ -H "X-API-Key: nora_ak_xxxxxxxxxxxxxxxx" ``` ### Respuesta [Sección titulada «Respuesta»](#respuesta) Como toda la API pública, la respuesta va envuelta en `{"success": true, "data": ...}`: ```json { "success": true, "data": { "id": "3f1c0b4a-7c2e-4a1d-9b8e-1a2b3c4d5e6f", "tenant_id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d", "process_id": "b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e", "machine_id": "c3d4e5f6-a7b8-9c0d-1e2f-3a4b5c6d7e8f", "status": "completed", "priority": 3, "input_data": { "factura": "F001-123" }, "output_data": { "total": 1180.0, "estado": "registrada" }, "logs": null, "error_message": null, "started_at": "2026-06-19T14:03:11Z", "finished_at": "2026-06-19T14:04:02Z", "retry_count": 0, "stop_requested": false, "progress_percent": 100, "progress_message": "Listo", "created_at": "2026-06-19T14:03:00Z", "updated_at": "2026-06-19T14:04:02Z", "process_name": "Registro de facturas", "machine_name": "BOT-CONTA-01" } } ``` Campos más relevantes: | Campo | Tipo | Descripción | | ------------------------------- | ------------------ | ----------------------------------------------- | | `status` | string | Estado actual (ver tabla de estados). | | `output_data` | objeto \| null | Resultado del robot. Disponible al `completed`. | | `error_message` | string \| null | Mensaje de error cuando el estado es `failed`. | | `progress_percent` | entero \| null | Avance reportado por el robot (0–100). | | `progress_message` | string \| null | Mensaje de avance del robot. | | `stop_requested` | boolean | `true` si se solicitó detener el job. | | `started_at` / `finished_at` | fecha-hora \| null | Inicio y fin reales de la ejecución. | | `process_name` / `machine_name` | string \| null | Nombres del proceso y la máquina asociados. | ## Esperar el resultado: patrón de polling [Sección titulada «Esperar el resultado: patrón de polling»](#esperar-el-resultado-patrón-de-polling) Como la ejecución es asíncrona, para obtener el resultado se consulta el job de forma periódica hasta que alcance un estado final (`completed`, `failed` o `cancelled`). ``` sequenceDiagram participant C as Cliente participant API as NORA API C->>API: POST /api/v1/jobs/trigger API-->>C: { data: { id, status: "pending" } } loop cada N segundos C->>API: GET /api/v1/jobs/{job_id} API-->>C: { data: { status } } end Note over C: status final → leer output_data / error_message ``` ### Ejemplo (Python) [Sección titulada «Ejemplo (Python)»](#ejemplo-python) ```python import time import requests BASE = "https://nora-api.valisoftconsulting.com/api/v1" HEADERS = {"X-API-Key": "nora_ak_xxxxxxxxxxxxxxxx"} FINAL = {"completed", "failed", "cancelled"} def esperar_job(job_id: str, timeout: int = 600, intervalo: int = 5) -> dict: """Consulta el job hasta que termine o se agote el tiempo.""" limite = time.time() + timeout while time.time() < limite: r = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS) r.raise_for_status() job = r.json()["data"] if job["status"] in FINAL: return job time.sleep(intervalo) raise TimeoutError(f"El job {job_id} no terminó en {timeout}s") job = esperar_job("3f1c0b4a-7c2e-4a1d-9b8e-1a2b3c4d5e6f") if job["status"] == "completed": print("Resultado:", job["output_data"]) else: print("No completó:", job["status"], job.get("error_message")) ``` ## Detener un job en curso [Sección titulada «Detener un job en curso»](#detener-un-job-en-curso) ```http POST /api/v1/jobs/{job_id}/stop ``` Solicita la detención de una ejecución. Requiere el scope `jobs:stop` (o una clave sin restricción de scopes). Límite de uso: **30 solicitudes por minuto** por clave o IP. Esta operación marca el job con `stop_requested = true`; el agente detecta la señal y detiene el robot. **Solo se puede detener un job en estado `running`**: con cualquier otro estado la API responde `422` con `code: "VALIDATION_ERROR"`. ### Ejemplo (curl) [Sección titulada «Ejemplo (curl)»](#ejemplo-curl-1) ```bash curl -X POST \ https://nora-api.valisoftconsulting.com/api/v1/jobs/3f1c0b4a-7c2e-4a1d-9b8e-1a2b3c4d5e6f/stop \ -H "X-API-Key: nora_ak_xxxxxxxxxxxxxxxx" ``` ### Respuesta [Sección titulada «Respuesta»](#respuesta-1) Devuelve el job actualizado (envuelto en `data`), con `stop_requested` en `true`: ```json { "success": true, "data": { "id": "3f1c0b4a-7c2e-4a1d-9b8e-1a2b3c4d5e6f", "status": "running", "stop_requested": true } } ``` La detención no es instantánea `POST /jobs/{job_id}/stop` solo **solicita** la parada. El estado seguirá siendo `running` hasta que el agente atienda la señal; el job terminará en `cancelled` (o `failed`/`completed` si alcanza a finalizar antes). Confirma el resultado con el patrón de polling sobre `GET /jobs/{job_id}`. ## Errores [Sección titulada «Errores»](#errores) Los errores siguen el formato estándar de la API: `{"success": false, "error": {"code": "...", "message": "..."}}`. | HTTP | `code` | Cuándo ocurre | | ---- | --------------------- | ----------------------------------------------------------------- | | 401 | `UNAUTHORIZED` | Falta `X-API-Key` o la clave es inválida. | | 403 | `FORBIDDEN` | La clave no tiene el scope requerido (`jobs:read` o `jobs:stop`). | | 404 | `NOT_FOUND` | El job no existe o no pertenece a tu organización. | | 422 | `VALIDATION_ERROR` | Intentas detener un job que no está `running`. | | 429 | `RATE_LIMIT_EXCEEDED` | Se superó el límite de solicitudes por minuto. | ## Ver también [Sección titulada «Ver también»](#ver-también) * [Disparar ejecuciones](/api/disparar-jobs/) * [Autenticación](/api/autenticacion/) # Disparar jobs > Lanza la ejecución de un proceso en NORA con POST /api/v1/jobs/trigger usando una API key. El endpoint `POST /api/v1/jobs/trigger` lanza la ejecución de un proceso desde tus propios sistemas usando una API key. Es la forma recomendada de integrar NORA con aplicaciones externas, *backends* o automatizaciones que necesiten arrancar un robot bajo demanda. Una vez creado el job, consulta su avance y resultado con el endpoint descrito en [consultar jobs](/api/consultar-jobs/). ## Resumen [Sección titulada «Resumen»](#resumen) | Atributo | Valor | | ---------------------- | ----------------------------------------------------------------------------------------------------------------- | | Método y ruta | `POST /api/v1/jobs/trigger` | | URL base de producción | `https://nora-api.valisoftconsulting.com` | | Autenticación | Cabecera `X-API-Key: nora_ak_...` | | Scope requerido | `jobs:write` (o una key sin restricción de scopes) | | Límite de tasa | 30 peticiones por minuto, por API key (el cupo se comparte por key aunque las peticiones vengan de distintas IPs) | | Disponibilidad | Planes **Pro** y **Enterprise** (la feature de API keys) | ## Cuerpo de la petición [Sección titulada «Cuerpo de la petición»](#cuerpo-de-la-petición) El cuerpo es un objeto JSON con los siguientes campos: | Campo | Tipo | Requerido | Descripción | | ------------ | ------ | --------- | ------------------------------------------------------------------------------------------------------------------------------ | | `process_id` | UUID | Sí | Identificador del proceso a ejecutar. El proceso debe existir, estar activo y pertenecer a tu organización. | | `machine_id` | UUID | No | Máquina donde ejecutar el job. Si se omite, NORA selecciona automáticamente una máquina activa y conectada de tu organización. | | `input_data` | objeto | No | Argumentos de entrada para el robot, como pares clave-valor. Máximo 1 MB serializado en JSON. | | `priority` | entero | No | Prioridad del job. Por defecto `3`. Ver la nota sobre prioridades. | Prioridades según el plan El valor de `priority` debe estar entre 1 y 5. En la práctica, los planes **Pro** y **Enterprise** (los únicos con API keys) admiten las prioridades `1` (baja), `3` (normal, valor por defecto) y `5` (urgente). Enviar una prioridad no permitida por tu plan devuelve un error. ## Respuesta [Sección titulada «Respuesta»](#respuesta) Respuesta `200 OK`. Como todas las respuestas de la API, el resultado viene envuelto en un objeto con `success` y `data`. El campo `data` contiene el job recién creado (esquema `JobResponse`). Un job nuevo arranca en estado `pending`. Campos relevantes de `data`: | Campo | Tipo | Descripción | | -------------- | -------------- | ---------------------------------------------------------------------------------------------- | | `id` | UUID | Identificador del job. Úsalo para consultar su estado. | | `status` | cadena | Estado del job. Valores: `pending`, `assigned`, `running`, `completed`, `failed`, `cancelled`. | | `process_id` | UUID | Proceso ejecutado. | | `machine_id` | UUID | Máquina asignada (la indicada o la autoseleccionada). | | `priority` | entero | Prioridad efectiva del job. | | `input_data` | objeto \| null | Argumentos de entrada enviados. | | `output_data` | objeto \| null | Resultado del robot (se rellena al terminar). | | `created_at` | fecha-hora | Momento de creación. | | `process_name` | cadena \| null | Nombre del proceso. | | `machine_name` | cadena \| null | Nombre de la máquina. | ```json { "success": true, "data": { "id": "8b1f0a4c-7d2e-4a9b-9c3f-2e1d5a6b7c8d", "tenant_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "process_id": "3f9d2b10-4c6e-4f1a-bd2c-9a8b7c6d5e4f", "machine_id": "7e6d5c4b-3a2f-1b0c-9d8e-7f6a5b4c3d2e", "status": "pending", "priority": 3, "input_data": { "cliente": "ACME", "monto": 1250.5 }, "output_data": null, "logs": null, "error_message": null, "started_at": null, "finished_at": null, "retry_count": 0, "stop_requested": false, "created_at": "2026-06-19T14:03:11.482Z", "updated_at": "2026-06-19T14:03:11.482Z", "process_name": "Conciliación bancaria", "machine_name": "BOT-PROD-01" } } ``` ## Ejemplo con curl [Sección titulada «Ejemplo con curl»](#ejemplo-con-curl) ```bash curl -X POST "https://nora-api.valisoftconsulting.com/api/v1/jobs/trigger" \ -H "X-API-Key: nora_ak_tu_clave_aqui" \ -H "Content-Type: application/json" \ -d '{ "process_id": "3f9d2b10-4c6e-4f1a-bd2c-9a8b7c6d5e4f", "input_data": { "cliente": "ACME", "monto": 1250.5 }, "priority": 3 }' ``` ## Ejemplo con Python (requests) [Sección titulada «Ejemplo con Python (requests)»](#ejemplo-con-python-requests) ```python import requests BASE_URL = "https://nora-api.valisoftconsulting.com" API_KEY = "nora_ak_tu_clave_aqui" resp = requests.post( f"{BASE_URL}/api/v1/jobs/trigger", headers={ "X-API-Key": API_KEY, "Content-Type": "application/json", }, json={ "process_id": "3f9d2b10-4c6e-4f1a-bd2c-9a8b7c6d5e4f", # "machine_id": "...", # opcional: si se omite, NORA elige una máquina conectada "input_data": {"cliente": "ACME", "monto": 1250.5}, "priority": 3, }, timeout=30, ) resp.raise_for_status() job = resp.json()["data"] print("Job creado:", job["id"], "->", job["status"]) ``` ## Flujo de ejecución [Sección titulada «Flujo de ejecución»](#flujo-de-ejecución) ``` sequenceDiagram participant Cli as Tu sistema participant API as NORA API participant Bot as Máquina (agente) Cli->>API: POST /api/v1/jobs/trigger (X-API-Key) API-->>Cli: 200 { data: { id, status: "pending" } } API->>Bot: Asigna el job a la máquina Bot-->>API: running -> completed / failed Cli->>API: GET /api/v1/jobs/{id} (sondeo de estado) API-->>Cli: 200 { data: { status, output_data } } ``` ## Errores comunes [Sección titulada «Errores comunes»](#errores-comunes) | Código | Causa | | ------ | ------------------------------------------------------------------------------------------------- | | `401` | Falta la cabecera `X-API-Key` o la key es inválida. | | `403` | La key no tiene el scope `jobs:write`, la IP no está permitida, o la suscripción no está activa. | | `404` | El `process_id` (o el `machine_id` indicado) no existe en tu organización. | | `422` | Cuerpo inválido: falta `process_id`, `input_data` supera 1 MB, o `priority` fuera de rango (1–5). | | `429` | Se superó el límite de 30 peticiones por minuto. | # Errores y límites > Forma del cuerpo de error, códigos HTTP, catálogo de excepciones y límites de tasa (rate limits) reales de la API pública de NORA. Esta página describe cómo informa errores la API pública de NORA, qué códigos HTTP devuelve y cuáles son los límites de tasa (rate limits) reales por endpoint. La URL base de producción es `https://nora-api.valisoftconsulting.com` y todos los endpoints públicos cuelgan del prefijo `/api/v1`. ## Forma del cuerpo de error [Sección titulada «Forma del cuerpo de error»](#forma-del-cuerpo-de-error) Todas las respuestas de la API van envueltas. Las correctas usan `{"success": true, "data": ...}` (ver [autenticación](/api/autenticacion/)) y las de error siguen una forma uniforme con un objeto `error`: ```json { "success": false, "error": { "code": "NOT_FOUND", "message": "Recurso no encontrado", "details": { }, "request_id": "b3c1e2a4-..." } } ``` Campos del objeto `error`: | Campo | Tipo | Presencia | Descripción | | ------------ | -------------- | ---------------- | -------------------------------------------------------------------- | | `code` | string | siempre | Código estable de error legible por máquina (ver tablas abajo). | | `message` | string | siempre | Mensaje descriptivo en español, pensado para humanos. | | `details` | objeto o lista | opcional | Información adicional. Solo aparece cuando hay detalles que aportar. | | `request_id` | string | cuando hay traza | Identificador de la petición; inclúyelo al reportar incidencias. | ### Errores de validación (422) [Sección titulada «Errores de validación (422)»](#errores-de-validación-422) Cuando el cuerpo enviado no pasa la validación, `details` es una **lista** de errores por campo. Cada elemento tiene `field` (ruta del campo, separada por `→`) y `message`: ```json { "success": false, "error": { "code": "VALIDATION_ERROR", "message": "Error de validación en los datos enviados", "details": [ { "field": "name", "message": "Field required" } ] } } ``` ## Códigos HTTP [Sección titulada «Códigos HTTP»](#códigos-http) NORA usa el código HTTP de forma consistente con el `code` del cuerpo. Estos son los códigos que puedes encontrar: | HTTP | `code` típico | Significado en NORA | | ----- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `401` | `UNAUTHORIZED` | Falta la credencial o es inválida (API key ausente/incorrecta, token expirado). | | `403` | `FORBIDDEN` | Autenticado, pero sin permisos para el recurso o la operación (incluye aislamiento entre organizaciones y funciones no incluidas en tu plan). | | `404` | `NOT_FOUND` | El recurso no existe o no pertenece a tu organización. | | `409` | `CONFLICT` | Conflicto de estado: el recurso ya existe o choca con otro. | | `413` | `PAYLOAD_TOO_LARGE` | El cuerpo de la petición supera el límite (2 MiB para endpoints JSON ordinarios). | | `422` | `VALIDATION_ERROR` | Los datos enviados no superan la validación de esquema. | | `429` | `RATE_LIMIT_EXCEEDED` | Se superó el límite de tasa del endpoint (ver tabla más abajo). | | `500` | `INTERNAL_ERROR` | Error interno no controlado del servidor. | Respuesta 413 El error `413 PAYLOAD_TOO_LARGE` se genera en un middleware antes de procesar el cuerpo y, por compatibilidad, su JSON usa la forma plana `{"code", "message"}` (sin envoltura `error`). El resto de errores sí usan el objeto `error` descrito arriba. ## Catálogo de excepciones de negocio [Sección titulada «Catálogo de excepciones de negocio»](#catálogo-de-excepciones-de-negocio) La lógica de negocio de NORA lanza un conjunto acotado de excepciones, cada una con su par fijo de HTTP + `code`: | Excepción | HTTP | `code` | Mensaje por defecto | | ----------------------- | ----- | --------------------- | -------------------------------- | | `NotFoundException` | `404` | `NOT_FOUND` | ”Recurso no encontrado” | | `ConflictException` | `409` | `CONFLICT` | ”El recurso ya existe” | | `UnauthorizedException` | `401` | `UNAUTHORIZED` | ”Credenciales inválidas” | | `ForbiddenException` | `403` | `FORBIDDEN` | ”Permisos insuficientes” | | `ValidationException` | `422` | `VALIDATION_ERROR` | ”Error de validación” | | `RateLimitException` | `429` | `RATE_LIMIT_EXCEEDED` | ”Límite de solicitudes excedido” | Cada excepción puede llevar un `message` y `details` propios según el caso; los de la tabla son los valores por defecto cuando no se especifica nada. Además, hay dos códigos que no provienen de la lógica de negocio sino de manejadores genéricos: * `HTTP_ERROR`: errores HTTP de bajo nivel (por ejemplo, método no permitido) que no encajan en una excepción de negocio. * `INTERNAL_ERROR`: cualquier excepción no controlada (HTTP `500`). El detalle real se registra del lado del servidor; el cliente solo recibe un mensaje genérico. ## Límites de tasa (rate limits) [Sección titulada «Límites de tasa (rate limits)»](#límites-de-tasa-rate-limits) NORA aplica límites de tasa por endpoint con una ventana de un minuto. Al superarlos, recibes `429` con `code` `RATE_LIMIT_EXCEEDED`. ### Clave de límite (bucket) [Sección titulada «Clave de límite (bucket)»](#clave-de-límite-bucket) En los endpoints de la API pública (los que se autentican con `X-API-Key`), el límite se contabiliza **por API key**, no por IP: la clave se identifica por un hash SHA-256 del valor de la cabecera `X-API-Key`. Si no hay API key, se cae a la **IP de origen**. Esto evita que una misma key esquive su cuota rotando IPs, o que su cuota se diluya tras un NAT compartido. ### Límites por endpoint público [Sección titulada «Límites por endpoint público»](#límites-por-endpoint-público) Estos son los límites reales de los endpoints de la superficie pública (`X-API-Key`): | Método y ruta | Límite | | ----------------------------------------------- | ------ | | `GET /api/v1/jobs/{job_id}` | 60/min | | `POST /api/v1/jobs/trigger` | 30/min | | `POST /api/v1/jobs/{job_id}/stop` | 30/min | | `GET /api/v1/processes/list` | 60/min | | `GET /api/v1/machines/list` | 60/min | | `GET /api/v1/assets/by-name/{name}` | 30/min | | `POST /api/v1/queues/by-name/{name}/items` | 60/min | | `GET /api/v1/queues/by-name/{name}/items` | 60/min | | `POST /api/v1/queues/by-name/{name}/items/bulk` | 20/min | | `POST /api/v1/webhooks/trigger/{process_id}` | 60/min | Más allá de estos límites por endpoint, se aplica un límite global por defecto de **120/min** a las rutas que no declaran uno propio. ### Flujo de decisión [Sección titulada «Flujo de decisión»](#flujo-de-decisión) ``` flowchart TD A[Petición a /api/v1] --> B{X-API-Key válida?} B -- No --> E1[401 UNAUTHORIZED] B -- Sí --> C{Dentro del límite del endpoint?} C -- No --> E2[429 RATE_LIMIT_EXCEEDED] C -- Sí --> D{Permisos y plan para el recurso?} D -- No --> E3[403 FORBIDDEN] D -- Sí --> F{Recurso existe en tu organización?} F -- No --> E4[404 NOT_FOUND] F -- Sí --> G{Datos válidos?} G -- No --> E5[422 VALIDATION_ERROR] G -- Sí --> H[200/201 success: true] ``` ## Ejemplo de manejo [Sección titulada «Ejemplo de manejo»](#ejemplo-de-manejo) ```python import httpx BASE = "https://nora-api.valisoftconsulting.com/api/v1" headers = {"X-API-Key": "nora_ak_..."} resp = httpx.post(f"{BASE}/jobs/trigger", headers=headers, json={...}) if resp.status_code == 429: # Límite excedido: espera y reintenta con retroceso ... elif not resp.is_success: err = resp.json()["error"] print(err["code"], err["message"], err.get("request_id")) else: data = resp.json()["data"] ``` Consulta también [autenticación](/api/autenticacion/) para obtener y usar tu API key, y la guía de [jobs](/guia/jobs/) para los flujos de ejecución. # Introducción a la API > Qué es la API pública de NORA, URL base, prefijo /api/v1, autenticación con X-API-Key y formato de respuestas. La **API pública de NORA** te permite integrar la plataforma RPA con tus propios sistemas: disparar y consultar *jobs*, leer *assets*, encolar y consumir *queue items*, y listar *máquinas* y *procesos*. Toda la superficie pública se autentica con una **API key** (cabecera `X-API-Key`) y devuelve respuestas JSON con un formato consistente. Esta API es ideal para escenarios como: lanzar una automatización desde un ERP o un formulario web, alimentar una cola de trabajo desde un sistema externo, o consultar el resultado de un *job* desde un pipeline de datos. ## URL base y prefijo [Sección titulada «URL base y prefijo»](#url-base-y-prefijo) | Entorno | URL base | | ---------- | ----------------------------------------- | | Producción | `https://nora-api.valisoftconsulting.com` | Todos los endpoints de la API pública viven bajo el prefijo **`/api/v1`**. Por lo tanto, la URL completa de un recurso se construye así: ```plaintext https://nora-api.valisoftconsulting.com/api/v1/ ``` Por ejemplo, para disparar un *job* harías una petición a `https://nora-api.valisoftconsulting.com/api/v1/jobs/trigger`. ## Autenticación [Sección titulada «Autenticación»](#autenticación) La API pública usa **API keys** enviadas en la cabecera HTTP `X-API-Key`. Cada clave tiene el prefijo `nora_ak_` y se crea desde **Settings → API Keys** en la aplicación. ```http X-API-Key: nora_ak_xxxxxxxxxxxxxxxxxxxxxxxx ``` Cada API key puede limitarse mediante *scopes* con la forma `recurso:acción` (por ejemplo `jobs:read`, `jobs:write`, `assets:read`, `queues:write`) y, opcionalmente, una lista de IPs permitidas. Para los detalles completos sobre *scopes*, creación y rotación de claves, consulta [autenticación](/api/autenticacion/). Trata tu API key como un secreto Una API key concede acceso a los recursos de tu organización según sus *scopes*. No la incluyas en repositorios, frontends ni URLs. Guárdala en variables de entorno o en un gestor de secretos. ## Requisito de plan [Sección titulada «Requisito de plan»](#requisito-de-plan) La capacidad de usar API keys es la *feature* **`api_keys`**, disponible solo en los planes **Pro** y **Enterprise**. En los planes **Free** y **Starter** no se pueden crear claves ni consumir los endpoints públicos. | Plan | `api_keys` | Límite de claves | | ---------- | ---------- | ---------------- | | Free | No | 0 | | Starter | No | 0 | | Pro | Sí | 5 | | Enterprise | Sí | Sin límite | Las rutas públicas están protegidas por el plan activo de la organización: si una clave sobrevive a una bajada de plan que ya no incluye `api_keys`, dejará de funcionar. ## Formato de respuesta [Sección titulada «Formato de respuesta»](#formato-de-respuesta) Las respuestas correctas se envuelven en un objeto con `success` y `data`. Para un único recurso, `data` contiene el objeto: ```json { "success": true, "data": { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "status": "pending" } } ``` Las listas paginadas añaden además un objeto `meta` con la información de paginación: ```json { "success": true, "data": [ { "id": "..." } ], "meta": { "page": 1, "limit": 20, "total": 57, "pages": 3 } } ``` Los errores siguen una forma distinta, con `success: false` y un objeto `error`: ```json { "success": false, "error": { "code": "forbidden", "message": "Esta función requiere el plan pro. Tu plan actual: starter.", "details": { "error": "feature_not_available", "feature": "api_keys", "current_plan": "starter", "required_plan": "pro" } } } ``` El campo `details` es opcional y aporta contexto adicional según el error. ## Panorama de recursos [Sección titulada «Panorama de recursos»](#panorama-de-recursos) La superficie pública de la API cubre estos recursos: | Recurso | Para qué sirve | Documentación | | -------- | ----------------------------------------- | -------------------------------------- | | Jobs | Disparar, consultar y detener ejecuciones | [jobs](/api/disparar-jobs/) | | Colas | Encolar y listar *queue items* por nombre | [colas](/api/colas/) | | Assets | Leer un *asset* descifrado por nombre | [assets](/api/assets/) | | Máquinas | Listar las máquinas de la organización | [máquinas](/guia/maquinas/) | | Procesos | Listar los procesos activos | [procesos](/guia/procesos-y-paquetes/) | | Webhooks | Disparar un proceso vía webhook | [webhooks](/api/webhooks/) | ``` flowchart TD C["Tu sistema (cliente)"] -->|"X-API-Key: nora_ak_..."| A["API NORA /api/v1"] A --> J["Jobs"] A --> Q["Colas"] A --> S["Assets"] A --> M["Máquinas"] A --> P["Procesos"] A --> W["Webhooks"] ``` ## Primer ejemplo con curl [Sección titulada «Primer ejemplo con curl»](#primer-ejemplo-con-curl) Este ejemplo dispara un proceso creando un nuevo *job*. El único campo obligatorio es `process_id`; si omites `machine_id`, NORA selecciona automáticamente una máquina en línea y activa de tu organización. ```bash curl -X POST "https://nora-api.valisoftconsulting.com/api/v1/jobs/trigger" \ -H "X-API-Key: nora_ak_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "process_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "input_data": { "cliente": "ACME", "monto": 1250 }, "priority": 3 }' ``` Respuesta (recortada): ```json { "success": true, "data": { "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "process_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "machine_id": "0f8fad5b-d9cb-469f-a165-70867728950e", "status": "pending", "priority": 3, "created_at": "2026-06-19T14:05:00Z" } } ``` # SDK de robots y logging > Cómo loguear con el SDK de NORA, leer la configuración de ejecución y trabajar con assets y colas desde tus robots. El SDK `nora_agent` es la forma oficial de que un robot hable con NORA (Robots Center) desde su propio código: escribir logs, reportar progreso, leer credenciales (assets), consumir colas y reaccionar a señales del operador. Esta página está pensada para **autores de robots** y se centra en logging y en la configuración de ejecución. El SDK funciona en dos modos, sin que cambies tu código: * **Job gestionado** — cuando NORA lanza el robot, el agente inyecta `NORA_JOB_ID`, el token de ejecución y la URL de la API. El SDK envía todo a la plataforma. * **Desarrollo local** — sin `NORA_JOB_ID` (por ejemplo bajo `nora dev run`), las funciones de log y progreso imprimen por `stdout` para que sigas viendo la salida. ## Loguear correctamente [Sección titulada «Loguear correctamente»](#loguear-correctamente) La función real es: ```python from nora_agent import sdk sdk.log(level: str, message: str, data: dict | None = None) -> None ``` Uso básico: ```python from nora_agent import sdk sdk.log("info", "Inicio del proceso de facturación") sdk.log("warning", "Reintentando descarga (intento 2)") sdk.log("error", "No se pudo abrir el portal del proveedor") ``` No uses print() ni timestamps propios * **No** uses `print()` para los logs del negocio: en un job gestionado no llegan a la plataforma. Usa siempre `sdk.log(...)`. * **No** antepongas tu propia hora ni el nivel al mensaje. La vista **Jobs → Logs** ya muestra **Hora** y **Nivel** en columnas; un prefijo manual aparecería duplicado. ### Niveles válidos [Sección titulada «Niveles válidos»](#niveles-válidos) | Nivel | Cuándo usarlo | | --------- | ---------------------------------------------------------- | | `info` | Hitos normales del proceso (inicio, paso completado, fin). | | `warning` | Algo recuperable: reintento, dato faltante, fallback. | | `error` | Un fallo que impide completar el item o el job. | El SDK normaliza el nivel a mayúsculas internamente, así que `"info"` e `"INFO"` producen el mismo resultado. Mantén estos tres niveles; la plataforma los espera para colorear y filtrar. ### Datos estructurados con `data` [Sección titulada «Datos estructurados con data»](#datos-estructurados-con-data) Para adjuntar contexto, pásalo por el parámetro `data` (un `dict`), **no** lo concatenes al texto del mensaje: ```python sdk.log( "info", "Factura procesada", {"invoice_id": "F-2026-0042", "amount": 1290.50, "currency": "EUR"}, ) ``` El SDK serializa `data` como JSON al final de la línea. Mantén el mensaje legible para humanos y deja los valores estructurados en `data`. ### Formato que emite el SDK (contrato) [Sección titulada «Formato que emite el SDK (contrato)»](#formato-que-emite-el-sdk-contrato) Internamente, `sdk.log()` construye una línea con este formato y la envía al stream de logs del job: ```text [2026-06-19T08:30:00Z] [INFO] Factura procesada | {"invoice_id": "F-2026-0042", "amount": 1290.5} ``` El timestamp es **ISO-8601 en UTC** (`YYYY-MM-DDTHH:MM:SSZ`). El front de NORA parsea exactamente este contrato; por eso el robot no debe inventar otro formato ni anteponer su propia marca de tiempo. ## Reportar progreso [Sección titulada «Reportar progreso»](#reportar-progreso) Para mover la barra de progreso del job (0–100): ```python sdk.update_progress(50, "Mitad de los registros procesados") ``` El valor se acota automáticamente al rango 0–100. En dev local imprime `[progress] 50% - ...` por `stdout`. ## Configuración de ejecución y resolución de pantalla [Sección titulada «Configuración de ejecución y resolución de pantalla»](#configuración-de-ejecución-y-resolución-de-pantalla) El agente inyecta la configuración del job mediante variables de entorno. Las más útiles para un robot: | Variable | Significado | | --------------------- | ------------------------------------------------------------ | | `NORA_JOB_ID` | ID del job que lanzó el robot (ausente en dev local). | | `NORA_DISPLAY_WIDTH` | Ancho de pantalla configurado en la máquina (Robots Center). | | `NORA_DISPLAY_HEIGHT` | Alto de pantalla configurado en la máquina. | Puedes recuperar el ID del job de forma segura con `sdk.get_job_id()` (devuelve `None` fuera de un job gestionado). Para la resolución, **prioriza `NORA_DISPLAY_WIDTH` / `NORA_DISPLAY_HEIGHT`** sobre la resolución “viva” del sistema operativo. En una VM sin sesión RDP abierta, la resolución real del SO puede quedar pegada en un valor pequeño o incorrecto (p. ej. 1024×768) y hacer fallar la automatización: ```python import os from nora_agent import sdk def screen_resolution() -> tuple[int, int]: w = os.environ.get("NORA_DISPLAY_WIDTH", "") h = os.environ.get("NORA_DISPLAY_HEIGHT", "") if w.isdigit() and h.isdigit(): return int(w), int(h) return 1920, 1080 # fallback solo para dev local ancho, alto = screen_resolution() sdk.log("info", f"Resolución: {ancho}x{alto}", {"width": ancho, "height": alto}) ``` ## Argumentos de entrada y salida [Sección titulada «Argumentos de entrada y salida»](#argumentos-de-entrada-y-salida) Los parámetros que declaras en `nora.json` (ver [argumentos](/guia/argumentos/)) llegan al robot sin llamada de red y se devuelven resultados con `set_output`: ```python from nora_agent import sdk mes = sdk.get_input("mes") # valor del argumento reintentos = sdk.get_input("reintentos", 3) # o un default si no vino todos = sdk.get_inputs() # dict completo de entrada sdk.set_output("facturas_ok", 142) # reporta un resultado sdk.set_output({"total": 18540.50}) # o varios (se mergean en output_data) ``` `get_input()`/`get_inputs()` funcionan también en `nora dev run` (pásalos con `--input '{...}'`). `set_output` escribe en `output_data`, visible en **Jobs → Salida** y disponible como entrada del siguiente nodo en un [flujo DAG](/guia/flujos-dag/). ## Assets (credenciales y configuración cifrada) [Sección titulada «Assets (credenciales y configuración cifrada)»](#assets-credenciales-y-configuración-cifrada) Lee un asset descifrado por nombre. Devuelve un `dict` con `{name, type, environment, value, username?}` (el `value` viene **tipado** según el tipo del asset: `integer`→`int`, `number`→`float`, `bool`→`bool`, el resto `str`): ```python cred = sdk.get_asset("portal-proveedor") # environment="production" por defecto usuario = cred.get("username") secreto = cred["value"] ``` ## Colas (queues) [Sección titulada «Colas (queues)»](#colas-queues) El SDK cubre el ciclo de vida de una cola. Las funciones más usadas: | Función | Para qué sirve | | ------------------------------------------------ | ----------------------------------------------------- | | `queue_pending(queue)` | Cuántos items quedan claimables (status `new`). | | `queue_stats(queue)` | Conteo por estado, sin consumir nada. | | `get_queue_item(queue)` | Reclama el siguiente item (o `None` si está vacía). | | `complete_queue_item(queue, item_id, result)` | Marca el item como completado con datos de resultado. | | `fail_queue_item(queue, item_id, error_message)` | Marca el item como fallido (puede reintentar). | | `add_queue_item(queue, data, priority=3, ...)` | Encola un único item. | | `add_queue_items(queue, items, priority=3)` | Encola varios; devuelve cuántos se añadieron. | | `send_queue_item_for_review(queue, item_id)` | Manda el item a revisión humana. | | `wait_for_queue_review(queue, item_id)` | Bloquea hasta `"approved"` o `"rejected"`. | Patrón típico de consumo (dispatcher + performer): ```python from nora_agent import sdk QUEUE = "RPA-Challenge" # Cargar la cola solo si está vacía (idempotente). if sdk.queue_pending(QUEUE) == 0: sdk.add_queue_items(QUEUE, registros) # registros: list[dict] # Procesar item a item. while True: item = sdk.get_queue_item(QUEUE) if item is None: break try: # ... trabajo sobre item["data"] ... sdk.complete_queue_item(QUEUE, item["id"], {"ok": True}) except Exception as e: sdk.fail_queue_item(QUEUE, item["id"], str(e)) raise ``` La prioridad usa la escala `1=baja`, `3=normal`, `5=urgente`. `add_queue_item()` acepta además `reference`, `deadline` y `postpone` (datetime o cadena ISO-8601). ## Control del job y human-in-the-loop [Sección titulada «Control del job y human-in-the-loop»](#control-del-job-y-human-in-the-loop) * `sdk.should_stop()` → `True` si el operador pidió **Stop**/**Kill** desde el dashboard. Conviene consultarlo dentro de bucles largos para salir limpiamente. * `sdk.ask_user(prompt, options=None, timeout=3600.0)` → pide un dato al operador y **bloquea** hasta la respuesta. Requiere un job gestionado. ```python if sdk.should_stop(): sdk.log("warning", "Detención solicitada por el operador; cierro ordenadamente") return decision = sdk.ask_user("¿Aprobar el pago?", options=["Sí", "No"]) ``` Solo en job gestionado `should_stop()`, `ask_user()`, `request_user_input()`, `wait_for_user_input()` y las señales requieren `NORA_JOB_ID`. Fuera de un job gestionado (dev local) lanzan `RuntimeError`, porque no hay job al que asociarlos. ## Buenas prácticas [Sección titulada «Buenas prácticas»](#buenas-prácticas) * Centraliza el trato con NORA en un módulo (como `nora.py` del ejemplo `examples/rpa-challenge`), de modo que tus workflows solo llamen a `log`, `claim_next`, etc., sin saber de HTTP. * Loguea hitos, no ruido: un `info` al inicio/fin de cada paso y `warning`/`error` cuando algo se desvía. * Adjunta el contexto por `data`, no en el texto. * Reporta progreso en pasos significativos para que el operador vea avance real. ## Referencias [Sección titulada «Referencias»](#referencias) * [Autenticación de la API](/api/autenticacion/) * [Jobs](/guia/jobs/) * [Arquitectura](/conceptos/arquitectura/) # Webhooks > Dispara un proceso de NORA desde un sistema externo con POST /api/v1/webhooks/trigger/{process_id}, con validación de payload y rate limit. El endpoint de webhook permite que un sistema externo (un ERP, un formulario, un CRM, una herramienta de CI/CD, etc.) dispare la ejecución de un proceso de NORA enviando una sola petición HTTP. Es la vía más directa para integrar NORA con eventos de negocio: cuando ocurre algo en tu sistema, lanzas un *job*. ## Endpoint [Sección titulada «Endpoint»](#endpoint) ```http POST /api/v1/webhooks/trigger/{process_id} ``` * URL base de producción: `https://nora-api.valisoftconsulting.com` * `process_id` (en la ruta): UUID del proceso a ejecutar. * Devuelve `201 Created` con el *job* creado. ## Autenticación [Sección titulada «Autenticación»](#autenticación) La autenticación es por **API key**, igual que el resto de la superficie pública. Envía la cabecera: ```http X-API-Key: nora_ak_... ``` La key se crea en *Settings → API Keys* y debe estar activa y no expirada. A diferencia de [`/jobs/trigger`](/api/disparar-jobs/), este endpoint **no exige un scope concreto** (`jobs:write`): basta con una API key válida del tenant cuyo plan incluya la feature `webhooks`. Si la key define una lista de IPs permitidas, la IP de origen debe estar en ella. Además de la autenticación, la petición pasa por estas comprobaciones, en orden: 1. La feature `webhooks` está disponible en el plan del tenant. 2. La cuota mensual de ejecuciones no está agotada (se incrementa de forma atómica en cada disparo). 3. La suscripción del tenant está activa. 4. El proceso existe, pertenece al tenant de la key y está activo (`is_active = true`). Si no, `404`. ## Cuerpo de la petición [Sección titulada «Cuerpo de la petición»](#cuerpo-de-la-petición) El cuerpo es JSON con el esquema `WebhookTriggerRequest`: | Campo | Tipo | Requerido | Descripción | | ------------ | ----------- | --------- | -------------------------------------------- | | `machine_id` | UUID | Sí | Máquina (robot) donde se ejecutará el *job*. | | `input_data` | objeto JSON | No | Datos de entrada que se pasan al proceso. | ```json { "machine_id": "8f2b1c9e-3a44-4b2e-9d1a-6c0f5e7a2b11", "input_data": { "numero_factura": "F001-000123", "monto": 1500.50 } } ``` `machine_id` es obligatorio En el webhook, `machine_id` es obligatorio (a diferencia de `/jobs/trigger`, donde es opcional). Debes indicar explícitamente en qué máquina correrá el *job*. ## Validación del payload (JSON Schema) [Sección titulada «Validación del payload (JSON Schema)»](#validación-del-payload-json-schema) Si el proceso define un `input_schema` (un JSON Schema configurado en el proceso), NORA valida `input_data` contra él **antes** de crear el *job*: * Si el proceso tiene `input_schema` y no envías `input_data`, recibes `422` con el mensaje: *“El proceso requiere input\_data según su schema”*. * Si `input_data` no cumple el esquema, recibes `422` con el detalle del error de validación de JSON Schema, por ejemplo: *“input\_data no cumple el schema del proceso: ‘monto’ is a required property”*. Si el proceso **no** define `input_schema`, `input_data` se acepta tal cual (sin validación de forma) y se entrega al proceso. ``` flowchart TD A[POST /webhooks/trigger/process_id] --> B{API key válida?} B -- No --> E1[401 / 403] B -- Sí --> C{Plan incluye webhooks + cuota + suscripción?} C -- No --> E2[403 / límite] C -- Sí --> D{Proceso existe, del tenant y activo?} D -- No --> E3[404] D -- Sí --> F{Proceso tiene input_schema?} F -- Sí --> G{input_data cumple el schema?} G -- No --> E4[422] G -- Sí --> H[Crear job] F -- No --> H H --> I[201 Created con el job] ``` ## Rate limit [Sección titulada «Rate limit»](#rate-limit) El webhook por API key (`/api/v1/webhooks/trigger/{process_id}`) está limitado a **60 peticiones por minuto**. El cupo se aplica por API key (no por IP): si la cabecera `X-API-Key` está presente, todas las peticiones con esa key comparten el mismo cupo aunque provengan de distintas IPs. Al superarlo, la API responde con `429 Too Many Requests`. ## Respuesta [Sección titulada «Respuesta»](#respuesta) La respuesta sigue el envoltorio estándar de la API: `{"success": true, "data": ...}`. En `data` viaja el *job* creado (`JobResponse`), inicialmente en estado `pending`. ```json { "success": true, "data": { "id": "b3d8a1f0-7c2e-4a59-9f12-0a4d6e8c1b22", "tenant_id": "1a2b3c4d-0000-0000-0000-000000000001", "process_id": "5e6f7a8b-1111-2222-3333-444455556666", "machine_id": "8f2b1c9e-3a44-4b2e-9d1a-6c0f5e7a2b11", "status": "pending", "priority": 3, "input_data": { "numero_factura": "F001-000123", "monto": 1500.50 }, "output_data": null, "logs": null, "error_message": null, "started_at": null, "finished_at": null, "retry_count": 0, "stop_requested": false, "created_at": "2026-06-19T12:00:00Z", "updated_at": "2026-06-19T12:00:00Z" } } ``` Cada disparo queda registrado en el *audit log* del tenant con `action = "trigger_job"` y `source = "webhook"`. ## Ejemplo con curl [Sección titulada «Ejemplo con curl»](#ejemplo-con-curl) ```bash curl -X POST \ "https://nora-api.valisoftconsulting.com/api/v1/webhooks/trigger/5e6f7a8b-1111-2222-3333-444455556666" \ -H "X-API-Key: nora_ak_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "machine_id": "8f2b1c9e-3a44-4b2e-9d1a-6c0f5e7a2b11", "input_data": { "numero_factura": "F001-000123", "monto": 1500.50 } }' ``` ## Webhook vs. /jobs/trigger [Sección titulada «Webhook vs. /jobs/trigger»](#webhook-vs-jobstrigger) Ambos endpoints crean un *job* con una API key, pero están pensados para usos distintos: | Aspecto | `POST /webhooks/trigger/{process_id}` | `POST /jobs/trigger` | | -------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------- | | `process_id` | En la ruta (URL) | En el cuerpo | | Scope requerido | Ninguno (solo key válida) | `jobs:write` | | Feature de plan | `webhooks` (Pro/Enterprise) | `api_keys` (Pro/Enterprise) | | `machine_id` | Obligatorio | Opcional (NORA auto-selecciona una máquina online y activa) | | `priority` | No configurable (queda en el valor por defecto) | Configurable (1–5) | | Validación contra `input_schema` | Sí, si el proceso lo define | No | | Rate limit | 60/min por key | 30/min por key | | Pensado para | URL fija por proceso para integraciones tipo “webhook” externas | Disparo programático genérico desde tu backend | ## Errores comunes [Sección titulada «Errores comunes»](#errores-comunes) | Código | Causa | | ------ | ------------------------------------------------------------------------------------------------------------------------ | | `401` | Falta la cabecera `X-API-Key`, o la key es inválida/revocada/expirada. | | `403` | El plan no incluye `webhooks`, la IP no está autorizada para la key, o la suscripción no está activa. | | `404` | El proceso no existe, no pertenece al tenant o está inactivo. | | `422` | El cuerpo no cumple el esquema (`machine_id` faltante/ inválido) o `input_data` no cumple el `input_schema` del proceso. | | `429` | Se superó el límite de 60 peticiones por minuto. | Consulta también [autenticación](/api/autenticacion/) y la guía de [jobs](/guia/jobs/). # Arquitectura y Robots Center > Cómo se conectan el Robots Center, las máquinas, los agentes y los robots de NORA, y el modelo de seguridad de tokens. NORA es una plataforma RPA SaaS. Su arquitectura separa claramente **dónde se decide qué ejecutar** (la nube) de **dónde se ejecuta de verdad** (tu máquina). Esta página describe cada componente, cómo se conectan y el modelo de seguridad de tokens que protege tus credenciales. ## Componentes [Sección titulada «Componentes»](#componentes) | Componente | Dónde vive | Responsabilidad | | ----------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | **Plataforma NORA / Robots Center** | Nube (backend) | Orquesta: cola de jobs, procesos, máquinas, assets, colas (queues), releases firmados, auditoría. Es el plano de control. | | **Agente** | En cada máquina (Windows o macOS) | Proceso de larga vida que se autentica contra el Robots Center, reclama jobs, descarga el robot, lo ejecuta como subproceso y reporta estado. | | **Máquina** | Windows o macOS | El equipo donde corre el agente. Registrada en el Robots Center con una clave de máquina (`nora_mk_...`). | | **Robot / Paquete** | Código Python subido a NORA | La automatización. Se empaqueta en un *release* firmado criptográficamente y se asocia a un **proceso**. El agente lo ejecuta. | ## Plano de control vs. plano de ejecución (datos) [Sección titulada «Plano de control vs. plano de ejecución (datos)»](#plano-de-control-vs-plano-de-ejecución-datos) La separación es deliberada y es la base del modelo de seguridad: * **Plano de control** (agente ↔ Robots Center): el agente usa su **token de máquina** (`nora-agent`, 24 h) para tareas operativas: reclamar el siguiente job, enviar heartbeat, reportar estado/logs/progreso, descargar releases. Este token **nunca se entrega al robot**. * **Plano de ejecución / datos** (robot ↔ Robots Center): el robot lee assets (secretos/credenciales) y opera colas. Para esto recibe un **token de ejecución por job** (`nora-exec`), corto y acotado, distinto del token de máquina. El agente arranca el robot como un **subproceso con un entorno mínimo** (allowlist de variables del SO, no `os.environ` completo). Inyecta `NORA_API_URL`, `NORA_JOB_ID` y `NORA_EXEC_TOKEN`; **excluye siempre la clave de máquina** (`NORA_MACHINE_KEY`). ## Flujo de orquestación [Sección titulada «Flujo de orquestación»](#flujo-de-orquestación) ``` flowchart TD U["Usuario / API pública
(X-API-Key: nora_ak_...)"] -->|dispara proceso| RC["Robots Center
(nube / backend)"] RC -->|encola job| Q[("Cola de jobs")] A["Agente
(en la máquina)"] -->|GET /agent/jobs/next
token nora-agent| RC RC -->|job + exec_token + manifiesto| A A -->|descarga release firmado| RC A -->|ejecuta como subproceso
inyecta NORA_EXEC_TOKEN| R["Robot
(Python)"] R -->|lee assets / opera colas
token nora-exec| RC A -->|status / logs / progreso
token nora-agent| RC RC -->|estado en tiempo real| U ``` El usuario dispara un proceso desde la consola o con la [API pública](/api/autenticacion/) (`POST /api/v1/jobs/trigger`, cabecera `X-API-Key: nora_ak_...`). El Robots Center crea un **job** en cola. El agente, que sondea continuamente, lo reclama, descarga el release, verifica su firma y ejecuta el robot. ## Modelo de seguridad de tokens [Sección titulada «Modelo de seguridad de tokens»](#modelo-de-seguridad-de-tokens) NORA usa tres audiencias de token JWT, separadas y validadas por endpoint: | Token | Audiencia (`aud`) | Vida | Para qué | Se entrega al robot | | ---------- | ----------------- | --------------------------------- | -------------------------------------------------- | ------------------- | | Máquina | `nora-agent` | 24 h | Plano de control: poll de jobs, heartbeat, estado | **No** | | Ejecución | `nora-exec` | Timeout del job + 5 min de gracia | Plano de datos: leer assets, operar colas | **Sí** | | Desarrollo | `nora-dev` | Corta (por defecto 30 min) | CLI `nora dev` local; solo entornos no productivos | Sí (local) | ### Token de máquina (`nora-agent`) [Sección titulada «Token de máquina (nora-agent)»](#token-de-máquina-nora-agent) El agente se autentica con su clave de máquina (`nora_mk_...`) en `POST /agent/auth` y recibe un JWT de máquina de 24 h. Este token solo sirve para el plano de control. Si la máquina se desactiva, el acceso se revoca de inmediato aunque el token siga dentro de su TTL. ### Token de ejecución por job (`nora-exec`) [Sección titulada «Token de ejecución por job (nora-exec)»](#token-de-ejecución-por-job-nora-exec) Cuando el agente reclama un job (`GET /agent/jobs/next`), el backend **acuña un token de ejecución ligado a esa terna (máquina, job, proceso)**. Sus claims incluyen `tenant_id`, `machine_id`, `job_id`, `process_id`, `envs` y, si el proceso declara un **manifiesto de assets** (`Process.required_assets`), la lista `assets` permitida. Propiedades de contención: * **Caduca con el job.** Al reportarse el job como `completed`/`failed`/`cancelled`, el token deja de ser usable: leer assets o colas después devuelve `401`. * **Acotado al manifiesto.** Si el proceso declara assets, el token solo desbloquea esos; pedir otro asset devuelve `403`. Igual para colas. * **Aislamiento de entorno.** Un token no puede leer assets de un entorno distinto al de sus claims. Los jobs gestionados leen `production`. Aunque el `nora-exec` se filtrara, solo abre los recursos declarados y muere con el job: el robot no puede vaciar la bóveda de secretos del tenant. ### Token de desarrollo (`nora-dev`) [Sección titulada «Token de desarrollo (nora-dev)»](#token-de-desarrollo-nora-dev) Para desarrollo local con el CLI `nora dev`, el backend acuña un token corto restringido a entornos **no productivos**. Si sus claims incluyeran `production`, el backend lo rechaza. Así un desarrollador nunca lee secretos de producción desde su equipo. ## Ciclo de vida de un job (poll / ejecución) [Sección titulada «Ciclo de vida de un job (poll / ejecución)»](#ciclo-de-vida-de-un-job-poll--ejecución) ``` sequenceDiagram participant A as Agente participant RC as Robots Center participant R as Robot loop Plano de control (token nora-agent) A->>RC: POST /agent/heartbeat A->>RC: GET /agent/jobs/next end RC-->>A: job + exec_token (+ manifiesto) A->>RC: GET /agent/releases/{id}/download RC-->>A: ZIP firmado Note over A: Verifica la firma del release A->>R: ejecuta subproceso (inyecta NORA_EXEC_TOKEN) R->>RC: GET /agent/assets/{name} (token nora-exec) RC-->>R: valor descifrado (audita el acceso) R-->>A: termina A->>RC: PATCH /agent/jobs/{id}/status = completed Note over RC: el exec_token queda inservible ``` El agente verifica la **firma criptográfica** de cada release antes de ejecutarlo y rechaza releases sin firma válida (evita ejecución de código manipulado). Cada lectura de asset en runtime queda registrada en la **auditoría** con su contexto (job, proceso, máquina, tipo de token). ## Glosario [Sección titulada «Glosario»](#glosario) * **Robots Center** — el backend en la nube que orquesta todo. Es el plano de control. * **Agente** — proceso instalado en cada máquina que reclama y ejecuta jobs. * **Máquina** — equipo Windows/macOS registrado donde corre el agente. * **Robot / Paquete** — automatización en Python empaquetada como release firmado. * **Proceso** — configuración que asocia un release a una máquina y define su manifiesto de assets y timeout. * **Job** — una ejecución concreta de un proceso. Ver [jobs](/guia/jobs/). * **Asset** — credencial/secreto cifrado del tenant; los robots los leen con su token de ejecución. * **Cola (queue)** — estructura de items de trabajo que los robots consumen y producen. * **Token de máquina / ejecución / desarrollo** — ver [autenticación](/api/autenticacion/). # Glosario > Definiciones breves de los términos clave de NORA: robot, agente, máquina, paquete, proceso, release, job, cola, asset, programación, trigger, flujo DAG, workspace, API key y scope. Esta página reúne los términos que aparecen en el producto, la API y el resto de la documentación de NORA. Cada definición describe el concepto tal como existe en la plataforma; cuando hay una guía dedicada, se enlaza. ## Términos del producto [Sección titulada «Términos del producto»](#términos-del-producto) | Término | Definición | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **NORA** | Plataforma RPA SaaS de Valisoft para orquestar la automatización de procesos en estaciones Windows y macOS. Centraliza la gestión de robots, su despliegue, programación, ejecución y monitoreo. | | **Robots Center** | Nombre de la consola de orquestación de NORA: el panel web donde se administran máquinas, procesos, ejecuciones, colas y credenciales. | | **Robot / bot** | La automatización en Python que NORA ejecuta. Se despliega como [paquete](/guia/procesos-y-paquetes/) y se configura como [proceso](/guia/procesos-y-paquetes/); cada ejecución concreta sobre una máquina es un [job](/guia/jobs/). | | **Agente** | El programa de NORA que se instala en cada máquina (Windows o macOS). Establece la conexión con la plataforma, recibe trabajo, ejecuta los robots y reporta estado y resultados. Ver [instalación del agente](/guia/instalacion-agente/). | ## Infraestructura de ejecución [Sección titulada «Infraestructura de ejecución»](#infraestructura-de-ejecución) | Término | Definición | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Máquina** (`machines`) | Una estación de trabajo (Windows o macOS) con el agente instalado. Tiene un `name`, una `machine_key` única para emparejarla, un `status` (por defecto `offline`) y un límite de ejecuciones simultáneas (`max_concurrent_jobs`). Configura resolución, profundidad de color, escalado DPI y modo de sesión (`rdp` o `console`). Ver [máquinas](/guia/maquinas/). | | **Grupo de máquinas** (`machine_groups`) | Una agrupación lógica de máquinas (relación de pertenencia vía `machine_group_members`). Sirve para organizar la flota y repartir trabajo entre estaciones equivalentes. | ## Automatización y despliegue [Sección titulada «Automatización y despliegue»](#automatización-y-despliegue) | Término | Definición | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Paquete (package)** (`packages`) | El contenedor lógico de una automatización. Tiene `name` y `description` y agrupa todas sus versiones publicadas ([releases](/guia/procesos-y-paquetes/)). | | **Release / Versión** (`releases`) | Una versión concreta de un [paquete](/guia/procesos-y-paquetes/): el artefacto subido (`file_key`, `file_hash`, `file_size`), su punto de entrada (`entry_point`) y su `version`. La combinación paquete + versión es única. | | **Proceso** (`processes`) | La unidad ejecutable: vincula una `release` con su configuración de ejecución (esquema de entrada `input_schema`, `timeout_seconds`, reintentos `max_retries`/`auto_retry`, SLA, assets requeridos `required_assets`, etiquetas). Es lo que se dispara para producir un job. Ver [procesos y paquetes](/guia/procesos-y-paquetes/). | | **Job** (`jobs`) | Una ejecución concreta de un proceso sobre una máquina. Lleva `status` (por defecto `pending`), datos de entrada/salida (`input_data`/`output_data`), `logs`, tiempos (`started_at`/`finished_at`), `priority`, reintentos y progreso. Puede originarse manualmente, por una [programación](/guia/programaciones-y-triggers/) o por un trigger. Ver [jobs](/guia/jobs/). | ## Colas de trabajo [Sección titulada «Colas de trabajo»](#colas-de-trabajo) | Término | Definición | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Cola (queue)** (`queues`) | Una lista de unidades de trabajo a procesar por los robots. Tiene `name` (único por workspace), `description` y `max_retries`. Ver [colas](/guia/colas/). | | **Item de cola** (`queue_items`) | Un elemento individual dentro de una cola. Lleva `data` (la carga útil), `status` (por defecto `new`), `priority`, una `reference` de negocio, `deadline` (SLA) y `postpone` (aplazamiento). Un robot lo toma, lo procesa y guarda su `result`. | ## Credenciales y datos [Sección titulada «Credenciales y datos»](#credenciales-y-datos) | Término | Definición | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Asset** (`assets`) | Un valor reutilizable y cifrado que los procesos consumen en ejecución. Su `type` puede ser `text`, `credential` o `secret`, y vive en un `environment` (`dev`, `staging` o `production`). El valor se guarda cifrado (`encrypted_value`). Ver [assets y credenciales](/guia/assets-y-credenciales/). | | **Credencial** | Un asset de tipo `credential`: par usuario/secreto cifrado (`encrypted_username` + `encrypted_value`) pensado para inicios de sesión que el robot necesita. | ## Disparadores [Sección titulada «Disparadores»](#disparadores) | Término | Definición | | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Programación (schedule)** (`schedules`) | Una ejecución recurrente de un proceso definida por una `cron_expression` y una `timezone`. Puede fijar la máquina destino, habilitarse/deshabilitarse (`is_enabled`) y omitir feriados (`skip_holidays`). Ver [programaciones y triggers](/guia/programaciones-y-triggers/). | | **Trigger** (`triggers`) | Un disparador basado en evento. El `trigger_type` puede ser `webhook` (una URL entrante con `webhook_token` y `webhook_secret` opcional para firma HMAC, que lanza un proceso al recibir una petición; ver [webhooks](/api/webhooks/) y [triggers por evento webhook](/guia/programaciones-y-triggers/#triggers-por-evento-webhook)), `file_watcher` (vigila una ruta en una máquina y dispara al aparecer un archivo), `email_watcher` (sondea una cuenta IMAP y dispara al llegar un correo) o `queue` (vigila una cola y lanza un job cuando hay suficientes items nuevos, respetando un máximo de jobs concurrentes). El más habitual es `webhook`. Ver [programaciones y triggers](/guia/programaciones-y-triggers/). | | **Flujo DAG** (`process_dags`) | Un grafo dirigido acíclico de procesos: define `nodes` (cada uno asociado a un proceso) y `edges` (dependencias entre ellos) para orquestar varios procesos encadenados. Cada corrida es una `dag_execution` con sus `node_states`. Ver [flujos DAG](/guia/flujos-dag/). | ## Observabilidad [Sección titulada «Observabilidad»](#observabilidad) | Término | Definición | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Anomalía** (`anomalies`) | Una desviación detectada automáticamente en el comportamiento de un proceso. Tiene `type` (`duration_spike`, `error_rate_spike`, `pattern`), `severity` (`info`, `warning`, `critical`), valores esperado/real y un estado de resolución. Ver [anomalías](/guia/anomalias/). | ## Organización y acceso [Sección titulada «Organización y acceso»](#organización-y-acceso) | Término | Definición | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Workspace / Tenant** (`tenants`) | El espacio de trabajo aislado de una organización. Todo recurso (máquinas, procesos, colas, assets…) pertenece a un tenant y no es visible desde otros. Lleva `name`, `slug`, plan, estado de suscripción y límites (`bot_limit`, `user_limit`). | | **API key** (`api_keys`) | Una credencial de acceso programático a la [API pública](/api/introduccion/). Se presenta en la cabecera `X-API-Key` y su valor empieza por `nora_ak_`. Puede restringirse por IP (`allowed_ips`), por entorno (`environments`) y por permisos (`scopes`). Ver [autenticación](/api/autenticacion/). | | **Scope** | El permiso granular que limita lo que una API key puede hacer. Una clave sin scopes asignados (valor vacío) no tiene restricciones y puede usar todos los permisos disponibles. | ### Scopes disponibles [Sección titulada «Scopes disponibles»](#scopes-disponibles) Los scopes válidos para una API key son: | Scope | Permite | | ---------------- | ------------------------- | | `assets:read` | Leer assets | | `queues:read` | Leer items de colas | | `queues:write` | Agregar items a colas | | `jobs:read` | Ver estado de ejecuciones | | `jobs:write` | Disparar ejecuciones | | `jobs:stop` | Detener ejecuciones | | `processes:read` | Leer procesos | | `machines:read` | Leer máquinas | Acceso denegado por defecto La comprobación de scopes es restrictiva: por ejemplo, leer un asset por nombre exige explícitamente el scope `assets:read`. Una clave sin ese scope es rechazada. Asigna a cada clave únicamente los scopes que necesite. ## Cómo se relacionan los conceptos [Sección titulada «Cómo se relacionan los conceptos»](#cómo-se-relacionan-los-conceptos) ``` flowchart TD PKG["Paquete"] --> REL["Release / Versión"] REL --> PROC["Proceso"] PROC --> JOB["Job"] MAQ["Máquina (agente)"] --> JOB SCH["Programación"] -.dispara.-> JOB TRG["Trigger / Webhook"] -.dispara.-> JOB DAG["Flujo DAG"] -.orquesta.-> PROC PROC -.lee.-> AST["Asset / Credencial"] QUE["Cola"] --> ITM["Item de cola"] JOB -.consume.-> ITM ``` Todos estos recursos viven dentro de un mismo [workspace/tenant](/conceptos/arquitectura/) y se consultan o disparan mediante la [API pública](/api/introduccion/) con una [API key](/api/autenticacion/). Todos estos términos corresponden a recursos de la [API pública](/api/introduccion/) de NORA. # ¿Qué es NORA? > NORA es la plataforma SaaS de Valisoft para ejecutar, programar y controlar robots de automatización escritos en Python sobre tus máquinas Windows y macOS. **NORA** es la plataforma SaaS (software en la nube, sin instalación local) de Valisoft para **ejecutar, programar y controlar robots de automatización** (RPA: automatización de tareas repetitivas que normalmente haría una persona) escritos en **Python**. Desde un único panel —el **Robots Center**— defines tus automatizaciones, decides en qué máquinas corren y cuándo, y sigues cada ejecución en tiempo real. NORA es multi-tenant: cada organización (tenant) trabaja en un espacio aislado, con sus propios robots, máquinas, usuarios y datos. ## Qué problema resuelve [Sección titulada «Qué problema resuelve»](#qué-problema-resuelve) Ejecutar automatizaciones “a mano” no escala: scripts dispersos en distintos equipos, sin visibilidad de qué corrió, cuándo falló ni por qué; credenciales pegadas en el código; sin reintentos, sin colas de trabajo y sin forma de orquestar varios procesos encadenados. NORA centraliza ese ciclo de vida: * **Despliegue y versionado** de los robots como paquetes. * **Ejecución controlada** sobre máquinas registradas, con seguimiento de estado y logs. * **Programación** por calendario, disparadores por evento y orquestación de flujos. * **Gestión segura de credenciales y configuración** (assets) fuera del código. * **Observabilidad**: estado de jobs, logs, métricas y detección de anomalías. ## Para quién es [Sección titulada «Para quién es»](#para-quién-es) Está pensada para **desarrolladores y usuarios técnicos** que construyen automatizaciones en Python y necesitan operarlas de forma fiable: equipos de RPA, automatización de procesos y operaciones de TI que quieren orquestar robots sin montar su propia infraestructura de scheduling y monitoreo. ## Capacidades principales [Sección titulada «Capacidades principales»](#capacidades-principales) | Concepto | Qué es | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Robots Center** | El área central de NORA que orquesta y controla todos tus robots. | | **Máquinas / agentes** | Equipos Windows o macOS donde corren los robots. En cada máquina se instala el **agente** de NORA, que se autentica, recibe trabajo y reporta el estado. | | **Procesos** | La definición de una automatización: qué paquete de robot se ejecuta y con qué parámetros. | | **Paquetes (packages)** | El código del robot empaquetado y versionado, que el agente descarga y ejecuta. | | **Jobs** | Cada ejecución concreta de un proceso, con su estado, parámetros y logs. | | **Colas (queues)** | Listas de elementos de trabajo que los robots consumen y procesan, ideales para repartir carga por lotes. | | **Assets** | Valores de configuración y credenciales reutilizables (cifrados), que los robots leen por nombre en vez de escribirlos fijos dentro del código (hardcodearlos). | | **Programaciones (schedules)** | Lanzan jobs automáticamente según un calendario. | | **Triggers** | Disparan jobs ante un evento externo, p. ej. por **webhook** entrante. | | **Flujos DAG (workflows)** | Encadenan varios procesos como un grafo de nodos y dependencias, con seguimiento del estado de cada nodo. | | **Detección de anomalías** | Detecta comportamientos atípicos en las ejecuciones (p. ej. picos de duración o de tasa de error) y los clasifica por severidad. | | **API pública** | Permite integrar NORA desde sistemas externos: disparar jobs, consultar estado, leer assets y alimentar colas. | ### Cómo encajan las piezas [Sección titulada «Cómo encajan las piezas»](#cómo-encajan-las-piezas) ``` flowchart TD User[Usuario / sistema externo] -->|panel o API| RC[Robots Center] RC -->|programa, dispara, orquesta| Jobs[Jobs] Sched[Programaciones] --> Jobs Trig[Triggers / webhooks] --> Jobs DAG[Flujos DAG] --> Jobs Jobs -->|asignados a| Machines[Máquinas Windows / macOS] Machines -->|el agente ejecuta el robot| Robot[Robot Python] Robot -->|lee config y credenciales| Assets[Assets] Robot -->|consume elementos| Queues[Colas] Robot -->|estado y logs| RC RC -->|analiza ejecuciones| Anom[Detección de anomalías] ``` ## En qué se diferencia [Sección titulada «En qué se diferencia»](#en-qué-se-diferencia) * **Robots en Python nativo.** Tus automatizaciones son código Python; no dependes de un lenguaje propietario ni de un diseñador visual cerrado. Aprovechas todo el ecosistema de Python. * **Despliegue en tus máquinas.** Los robots corren sobre **tus propios equipos Windows o macOS** mediante el agente de NORA. El plano de control (la capa SaaS que coordina y decide qué corre) es SaaS; la ejecución ocurre donde tú decides. * **Orquestación completa de extremo a extremo.** Programaciones, triggers por webhook, colas, assets cifrados y flujos DAG en una sola plataforma, con observabilidad y detección de anomalías incorporadas. * **Integrable por API.** Una API pública con autenticación por clave permite incorporar NORA en tus sistemas e integraciones. # Detección de anomalías > Cómo NORA detecta ejecuciones fuera de lo normal (duración y tasa de error) mediante z-score y comparación semanal. NORA observa el histórico de tus [jobs](/guia/jobs/) y marca de forma automática las ejecuciones que se salen de lo normal. El objetivo es avisarte de degradaciones (un robot que de pronto tarda el doble, o un proceso que empieza a fallar más de lo habitual) sin que tengas que vigilar los paneles manualmente. ![Detección de anomalías en NORA: tarjetas con la severidad, el tipo de anomalía, los valores esperado y real, y el proceso afectado.](/_astro/anomalias.CBmH4xDF_17yCy.webp) La detección de anomalías es una función de plan: está disponible en **Pro** y **Enterprise**, y no en Starter. Si el plan del tenant no la incluye, las rutas de la API responden con un error de funcionalidad no habilitada. ## Qué detecta NORA [Sección titulada «Qué detecta NORA»](#qué-detecta-nora) Actualmente NORA ejecuta dos detectores. Ambos trabajan por proceso y por tenant, y solo consideran procesos con suficiente historial. | Tipo (`type`) | Qué busca | Cómo lo decide | | ------------------ | ----------------------------------------------------------------- | --------------------------------------------------------------- | | `duration_spike` | Un job que tardó mucho más de lo habitual para su proceso | z-score de la duración frente a la media de los últimos 30 días | | `error_rate_spike` | Un proceso cuya tasa de error subió respecto a la semana anterior | comparación de tasa de error semana actual vs. semana previa | ### Anomalías de duración (z-score) [Sección titulada «Anomalías de duración (z-score)»](#anomalías-de-duración-z-score) Para cada proceso, NORA calcula la **media** y la **desviación estándar** de la duración (`finished_at − started_at`) de los jobs completados en los últimos 30 días. Para construir esa línea base se exigen al menos **5 jobs completados** (`MIN_SAMPLES`); los procesos con menos historial se ignoran. Luego revisa los jobs completados en la última hora y calcula su z-score: ```text z = (duración_del_job − media) / desviación_estándar ``` Si `z > 2.0` (`DEVIATION_THRESHOLD`), el job se marca como anomalía. La severidad depende de cuán lejos quede de lo normal: * `warning` cuando `2.0 < z < 3.0` * `critical` cuando `z >= 3.0` Los jobs cuyo proceso tiene desviación estándar 0 (duraciones siempre idénticas) no se evalúan, para evitar divisiones inválidas. Tampoco se vuelve a marcar un job que ya tenga una anomalía `duration_spike`. ### Anomalías de tasa de error (semana vs. semana) [Sección titulada «Anomalías de tasa de error (semana vs. semana)»](#anomalías-de-tasa-de-error-semana-vs-semana) Este detector compara, por proceso, la tasa de error de los **últimos 7 días** contra la de los **7 días anteriores**, contando solo jobs en estado `completed` o `failed` y exigiendo al menos 5 jobs (`MIN_SAMPLES`) en cada ventana. Se genera una anomalía `error_rate_spike` cuando: * la tasa actual supera a la histórica en más de **20 puntos** (`ERROR_RATE_THRESHOLD = 0.20`), **y** * la tasa actual es mayor al **10 %**. La severidad es `warning` si la tasa actual es menor al 50 %, y `critical` si la iguala o supera. Como con la duración, no se duplican anomalías ya registradas para ese proceso en la misma semana. ## Cómo funciona a alto nivel [Sección titulada «Cómo funciona a alto nivel»](#cómo-funciona-a-alto-nivel) La detección corre en segundo plano dentro del backend, en un bucle que se ejecuta **cada hora** (`ANOMALY_INTERVAL = 3600`). Para que solo un proceso la ejecute en despliegues con varias réplicas, toma un lock distribuido (`anomaly_detection`) antes de cada pasada. ``` flowchart TD A[Bucle horario] --> B{Lock distribuido
disponible?} B -- no --> A B -- si --> C[Calcular linea base
por proceso 30 dias] C --> D[Revisar jobs recientes] D --> E{z-score mayor a 2
o tasa de error +20pp?} E -- no --> A E -- si --> F[Crear registro Anomaly] F --> G{severity = critical?} G -- si --> H[Enviar notificacion] G -- no --> A H --> A ``` Cuando se detectan anomalías de duración **críticas**, NORA dispara una notificación por los canales configurados del tenant, reutilizando el evento `job_failed` con el `job_id` y el mensaje de la anomalía. Las anomalías de tipo `warning` se registran pero no notifican. ## Consultar y resolver anomalías [Sección titulada «Consultar y resolver anomalías»](#consultar-y-resolver-anomalías) Las anomalías se exponen en la API bajo el prefijo `/api/v1/anomalies`. Estas rutas forman parte del panel y usan la sesión autenticada del usuario; no se invocan con `X-API-Key`. El acceso requiere rol `admin`, `operator` o `viewer` para consultarlas, y `admin` u `operator` para resolverlas. ### Listar anomalías [Sección titulada «Listar anomalías»](#listar-anomalías) ```http GET /api/v1/anomalies?page=1&limit=20&severity=critical&is_resolved=false ``` Parámetros de consulta: | Parámetro | Tipo | Por defecto | Descripción | | ------------- | ------------ | ----------- | ----------------------------------------- | | `page` | entero ≥ 1 | `1` | Página de resultados | | `limit` | entero 1–100 | `20` | Elementos por página | | `severity` | string | — | Filtra por `info`, `warning` o `critical` | | `is_resolved` | booleano | — | Filtra por estado de resolución | La respuesta es paginada (`data` + `meta`): ```json { "success": true, "data": [ { "id": "f1e2d3c4-0000-0000-0000-000000000000", "type": "duration_spike", "severity": "critical", "process_id": "a1b2c3d4-0000-0000-0000-000000000000", "job_id": "9f8e7d6c-0000-0000-0000-000000000000", "message": "Job tardo 412s (promedio: 95s, desvio: 4.2σ)", "expected_value": 95.0, "actual_value": 412.0, "is_resolved": false, "created_at": "2026-06-19T14:03:11+00:00" } ], "meta": { "page": 1, "limit": 20, "total": 1, "pages": 1 } } ``` Los campos `expected_value` y `actual_value` son numéricos: para `duration_spike` representan segundos (promedio y duración real); para `error_rate_spike` representan porcentajes (tasa histórica y tasa actual). ### Marcar una anomalía como resuelta [Sección titulada «Marcar una anomalía como resuelta»](#marcar-una-anomalía-como-resuelta) ```http PATCH /api/v1/anomalies/{anomaly_id}/resolve ``` Marca la anomalía como resuelta (`is_resolved = true`). Cada tenant solo ve y resuelve sus propias anomalías. ```json { "success": true, "data": { "id": "f1e2d3c4-0000-0000-0000-000000000000", "is_resolved": true } } ``` ## Buenas prácticas [Sección titulada «Buenas prácticas»](#buenas-prácticas) * Mantén un historial sano: la línea base necesita al menos 5 ejecuciones completadas por proceso en 30 días; los procesos nuevos no generan anomalías de duración hasta acumular muestras. * Configura los canales de notificación del tenant para enterarte de las anomalías críticas en cuanto ocurren. * Revisa periódicamente las anomalías `warning`: no notifican, pero suelen anticipar una degradación antes de que se vuelva crítica. # Argumentos de entrada y salida > Declara los parámetros de tu robot en nora.json, recíbelos precargados en un formulario al lanzar el job, léelos en runtime con get_input() y devuelve resultados con set_output(). Un robot rara vez hace siempre exactamente lo mismo: procesa **el mes** que le indiques, **la sucursal** que toque, o **el lote** que llegó. NORA te deja declarar esos parámetros una sola vez y entonces: el panel te muestra un **formulario precargado** al lanzar el job, el robot los **lee en runtime**, y puede **devolver resultados** de vuelta a NORA. Es el equivalente a los *Input/Output Arguments* de UiPath, pero declarados en un único sitio (`nora.json`) y leídos con el SDK de Python. ``` flowchart LR A["nora.json
inputs / outputs"] -->|nora release push| B["Proceso
(input_schema)"] B -->|formulario precargado| C["Lanzar job
(panel / API / cron)"] C -->|input_data validado| D["Robot"] D -->|sdk.get_input| D D -->|sdk.set_output| E["output_data
(visible en el job)"] ``` ## 1. Declarar los argumentos en `nora.json` [Sección titulada «1. Declarar los argumentos en nora.json»](#1-declarar-los-argumentos-en-norajson) En el manifiesto del robot añade un bloque `inputs` (y opcionalmente `outputs`). Cada argumento lleva **nombre, tipo, si es requerido, un valor por defecto y una descripción**: ```json { "name": "facturacion-mensual", "version": "1.2.0", "entry_point": "main.py", "inputs": [ { "name": "mes", "type": "text", "required": true, "description": "Mes a facturar (YYYY-MM)" }, { "name": "reintentos", "type": "integer", "default": 3, "description": "Reintentos por factura" }, { "name": "enviar_correo", "type": "bool", "default": true } ], "outputs": [ { "name": "facturas_ok", "type": "integer" }, { "name": "total", "type": "number" } ] } ``` Tipos soportados: `text`, `integer`, `number`, `bool`. Al publicar el release (`nora release push`), NORA guarda esos argumentos y deriva un **JSON Schema** que el proceso hereda (`input_schema` / `output_schema`). Ese schema es lo que alimenta el formulario del panel y la validación. ## 2. El formulario precargado al lanzar [Sección titulada «2. El formulario precargado al lanzar»](#2-el-formulario-precargado-al-lanzar) Cuando lanzas el proceso desde **Robots Center → Ejecutar**, en lugar de un cuadro de JSON crudo, NORA construye un **formulario** a partir del schema: un campo por argumento, con su tipo (texto, número, casilla), su valor por defecto y marca de obligatorio. Lo mismo aparece al crear una **Programación**. Siempre puedes alternar a **“Editar como JSON”** si prefieres pegar el objeto a mano. ![Modal "Ejecutar Proceso" de NORA con el formulario de argumentos precargado desde el schema del proceso: campo de texto "mes" (requerido), número "reintentos" con valor 3, y casilla "enviar\_correo" marcada, cada uno con su descripción.](/_astro/ejecutar-argumentos.B7gYmeUi_ZSNNLE.webp) ``` sequenceDiagram participant U as Operador participant N as Robots Center participant J as Job U->>N: Ejecutar proceso "facturacion-mensual" N-->>U: Formulario: mes*, reintentos (3), enviar_correo (✓) U->>N: mes = "2026-06" N->>N: Valida input_data contra input_schema N->>J: Crea el job con input_data ``` Si el `input_data` no cumple el schema (falta un requerido, tipo equivocado), NORA responde `422` **antes** de crear el job, con el detalle del campo que falla. ## 3. Leer los argumentos en el robot [Sección titulada «3. Leer los argumentos en el robot»](#3-leer-los-argumentos-en-el-robot) Dentro del robot, el agente inyecta los argumentos y el SDK los expone. No hay llamada de red: se leen igual que en producción y en `nora dev run`. ```python from nora_agent import sdk mes = sdk.get_input("mes") # "2026-06" reintentos = sdk.get_input("reintentos", 3) # valor o default si falta todos = sdk.get_inputs() # dict completo: {"mes": ..., ...} ``` | Función | Devuelve | | -------------------------------------- | ------------------------------------------------------- | | `sdk.get_input(name, default=None)` | El valor del argumento `name` (o `default` si no vino). | | `sdk.get_input()` / `sdk.get_inputs()` | El dict completo de argumentos. | ## 4. Devolver resultados con `set_output` [Sección titulada «4. Devolver resultados con set\_output»](#4-devolver-resultados-con-set_output) Cuando el robot termina (o durante la ejecución), puede escribir **resultados** que quedan guardados en el job y visibles en su detalle (**Jobs → Salida**). Útil para reportar conteos, importes o referencias. ```python sdk.set_output("facturas_ok", 142) # una clave sdk.set_output({"facturas_ok": 142, "total": 18540.50}) # varias a la vez ``` Las llamadas se **mergean** sobre `output_data`, así que puedes reportar de forma incremental. El resultado se ve en el panel y, si encadenas procesos con [flujos DAG](/guia/flujos-dag/), se pasa como entrada del siguiente nodo. ## 5. Probar en local con argumentos [Sección titulada «5. Probar en local con argumentos»](#5-probar-en-local-con-argumentos) Al depurar desde tu IDE, pásale los argumentos con `--input` (un JSON) y el robot los leerá con `get_input()` exactamente como en un job real: ```bash nora dev run main.py --input '{"mes": "2026-06", "reintentos": 5}' ``` ## 6. Programaciones y triggers con argumentos [Sección titulada «6. Programaciones y triggers con argumentos»](#6-programaciones-y-triggers-con-argumentos) * **Programaciones (cron):** al crear la programación puedes fijar el `input_data` que se pasará a **cada** ejecución (ver [programaciones](/guia/programaciones-y-triggers/)). * **Webhook / trigger:** el payload entrante se fusiona con la plantilla del trigger y se pasa como `input_data`, validado contra el schema del proceso (ver [webhooks](/api/webhooks/)). * **API:** `POST /api/v1/jobs/trigger` acepta `input_data` (ver [disparar jobs](/api/disparar-jobs/)). ## Resumen [Sección titulada «Resumen»](#resumen) | Paso | Dónde | Qué | | -------- | -------------------------------- | --------------------------------------------- | | Declarar | `nora.json` (`inputs`/`outputs`) | nombre, tipo, requerido, default, descripción | | Publicar | `nora release push` | deriva `input_schema`/`output_schema` | | Lanzar | Panel / cron / webhook / API | formulario precargado + validación | | Leer | Robot | `sdk.get_input()` / `get_inputs()` | | Devolver | Robot | `sdk.set_output()` → visible en el job | Referencia rápida de todas las funciones en la [lámina de comandos](/referencia/lamina-comandos/). # Assets y credenciales > Guarda configuración, credenciales y secretos de forma segura y consúmelos desde tus robots, con soporte para vaults externos. Un **asset** es un valor reutilizable que vive en NORA y que tus robots consumen en tiempo de ejecución: una ruta de carpeta, una dirección de correo, un usuario y contraseña, una API key, etc. En lugar de incrustar estos datos en el código del proceso, los defines una vez en NORA y el robot los pide por nombre cuando los necesita. ![Bóveda de activos de NORA: tabla con nombre, tipo (secreto, credencial, texto), entorno, valor enmascarado y descripción.](/_astro/activos.ljRLpfxf_1ex3gd.webp) Los assets están cifrados en reposo (cifrado autenticado con una clave maestra y datos asociados —AAD— que vinculan cada ciphertext a su campo) y están aislados por organización (*tenant*): un robot solo puede leer los assets de su propia organización. ## Tipos de asset [Sección titulada «Tipos de asset»](#tipos-de-asset) Un asset tiene un campo `type` que determina cómo se almacena y si su valor puede revelarse desde la interfaz. | Tipo | Descripción | ¿Legible desde el Robots Center? | | ----------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------- | | `text` | Valor de configuración no sensible (ruta, correo, URL, parámetro). | Sí, un administrador puede revelarlo. | | `credential` | Par usuario + contraseña/secreto. El campo `username` es obligatorio. | No (solo escritura). | | `secret` | Valor sensible único (API key, token, contraseña). | No (solo escritura). | | `vault` | Referencia a un secreto guardado en un vault externo; el valor se resuelve en cada lectura. | No (se resuelve en tiempo de ejecución). | | `integer` · `number` · `bool` | Valor de configuración **tipado**: el robot recibe un `int`, `float` o `bool` ya convertido (no un string). | Sí (no son secretos). | Secretos de solo escritura Una vez guardado, el valor de un asset `secret` o `credential` **no puede volver a leerse desde la interfaz del Robots Center**. El endpoint `GET /api/v1/assets/{asset_id}/value` devuelve `403 Forbidden` para estos tipos y registra el intento en el log de auditoría. Solo los assets `text` pueden revelarse en la UI. Si pierdes el valor, actualízalo; no podrás recuperarlo. ### Identidad de un asset [Sección titulada «Identidad de un asset»](#identidad-de-un-asset) Cada asset se identifica por la combinación **nombre + entorno** dentro de la organización (restricción única). El campo `environment` puede ser `dev`, `staging` o `production` (por defecto `production`). Esto te permite tener, por ejemplo, un asset `api-erp` distinto en `staging` y en `production` sin colisión. Campos disponibles al crear un asset: | Campo | Requerido | Notas | | ------------- | ----------------- | ----------------------------------------------------------------- | | `name` | Sí | Nombre lógico, único por entorno. | | `type` | Sí | `text`, `credential`, `secret` o `vault`. | | `value` | Sí | Valor a cifrar (o, para `vault`, la configuración del proveedor). | | `username` | Solo `credential` | Usuario asociado a la credencial. | | `description` | No | Texto libre. | | `expires_at` | No | Marca de caducidad (informativa). | | `environment` | No | `dev` \| `staging` \| `production` (por defecto `production`). | ## Assets de tipo credencial [Sección titulada «Assets de tipo credencial»](#assets-de-tipo-credencial) Un asset `credential` guarda dos partes cifradas por separado: el `username` y el `value` (la contraseña o secreto). Ambos campos son obligatorios al crear el asset; si omites `username`, la API responde con error de validación. ```bash curl -X POST https://nora-api.valisoftconsulting.com/api/v1/assets \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "erp-login", "type": "credential", "username": "robot_facturacion", "value": "S3cret0-ERP", "environment": "production" }' ``` La respuesta va envuelta en `{"data": ...}` y **nunca** incluye el valor ni el usuario descifrados, solo los metadatos del asset: ```json { "data": { "id": "a1b2c3d4-0000-0000-0000-000000000000", "name": "erp-login", "type": "credential", "environment": "production", "description": null, "expires_at": null, "created_at": "2026-06-19T10:00:00Z", "updated_at": "2026-06-19T10:00:00Z" } } ``` Crear, actualizar y eliminar assets requiere rol `admin`; listarlos y ver sus metadatos también lo permite el rol `operator`. ## Integración con vaults externos [Sección titulada «Integración con vaults externos»](#integración-con-vaults-externos) Un asset de tipo `vault` no guarda el secreto en NORA: guarda la **configuración de conexión** a un vault externo y resuelve el valor en cada lectura. Proveedores soportados: * `azure_keyvault` — Azure Key Vault (REST API + credenciales de cliente OAuth2). * `aws_secrets_manager` — AWS Secrets Manager (vía `boto3`). * `hashicorp_vault` — HashiCorp Vault (API HTTP, KV v1 y v2). La configuración del proveedor se guarda como JSON en el campo `value` e incluye, como mínimo, las claves `provider` y `secret_name`, más los parámetros propios del proveedor: | Proveedor | Campos requeridos | Opcionales | | --------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------- | | `azure_keyvault` | `vault_url`, `azure_tenant_id`, `azure_client_id`, `azure_client_secret` | — | | `aws_secrets_manager` | `aws_region` | `aws_access_key_id`, `aws_secret_access_key` | | `hashicorp_vault` | `vault_url`, `vault_token` | `mount_path` (def. `secret`), `kv_version` (def. `2`) | Cuando el secreto del vault es un JSON con `username`/`password` (AWS y HashiCorp), NORA devuelve `value` (la contraseña) y `username`. ## Cómo los usa un robot (dentro de un job gestionado) [Sección titulada «Cómo los usa un robot (dentro de un job gestionado)»](#cómo-los-usa-un-robot-dentro-de-un-job-gestionado) Los robots **no usan tu sesión ni una cabecera `X-API-Key`**. Cuando lanzas un job, el agente inyecta un **token de ejecución por job** (`nora-exec`) acotado a esa ejecución, y el SDK lo usa de forma transparente. En el código del proceso pides el asset por nombre: ```python from nora_agent import sdk cred = sdk.get_asset("erp-login") cred["username"], cred["value"] ``` Por debajo, `get_asset` hace `GET /api/v1/agent/assets/{name}?environment=...` con la cabecera `Authorization: Bearer `. Si el proceso declara un **manifiesto de assets**, el agente los precarga en la variable `NORA_ASSETS` antes de arrancar, de modo que `get_asset` los resuelve sin tráfico de red. No metas claves `nora_ak_` en el robot Nunca incrustes una `X-API-Key` (`nora_ak_...`) en el código del proceso. Al empaquetar con `nora package` la operación **aborta si detecta secretos** en el código. Los valores se leen siempre con `sdk.get_asset`, que usa el token de ejecución del job. ## Lectura desde un sistema externo (API pública, X-API-Key) [Sección titulada «Lectura desde un sistema externo (API pública, X-API-Key)»](#lectura-desde-un-sistema-externo-api-pública-x-api-key) Un sistema fuera de NORA (otra aplicación, un script de integración) puede leer assets a través de la **API pública** con una cabecera `X-API-Key` (clave `nora_ak_...`) que debe tener el scope `assets:read`. El endpoint busca por nombre y entorno y devuelve el valor descifrado (o resuelto desde el vault): ```bash curl "https://nora-api.valisoftconsulting.com/api/v1/assets/by-name/erp-login?environment=production" \ -H "X-API-Key: nora_ak_xxxxxxxxxxxxxxxxxxxx" ``` ```json { "data": { "name": "erp-login", "type": "credential", "environment": "production", "value": "S3cret0-ERP", "username": "robot_facturacion" } } ``` Detalles de comportamiento: * Requiere scope `assets:read` (o una key sin restricción de scopes). * Si la API key declara una lista blanca de entornos, el `environment` pedido debe estar en ella. * El endpoint está limitado a **30 peticiones por minuto** por IP. * Cada lectura exitosa queda registrada en el log de auditoría de la organización. ``` sequenceDiagram participant Bot as Robot (agente) participant Ext as Sistema externo participant API as API NORA participant Vault as Vault externo Bot->>API: GET /api/v1/agent/assets/{name}
Authorization: Bearer (token nora-exec) Ext->>API: GET /api/v1/assets/by-name/{name}
X-API-Key (assets:read) alt Asset tipo vault API->>Vault: Resuelve secreto (Azure/AWS/HashiCorp) Vault-->>API: value (+ username) else text / credential / secret API->>API: Descifra value (+ username) end API-->>Bot: { "data": { value, username, ... } } API-->>Ext: { "data": { value, username, ... } } ``` ## Referencias de API [Sección titulada «Referencias de API»](#referencias-de-api) * [Assets (API pública)](/api/assets/) — endpoint `GET /assets/by-name/{name}` y detalles de scopes. * [Autenticación](/api/autenticacion/) — cómo crear y usar API keys `nora_ak_...`. * [Jobs](/guia/jobs/) — cómo un robot ejecuta procesos que consumen assets. Consulta [Assets (API pública)](/api/assets/) y la [referencia de la API](/api/introduccion/) para los endpoints relacionados. # Colas (queues) > Reparte trabajo entre robots con colas: items, estados, productor/consumidor y aprobación humana. Una **cola** (queue) es una lista de unidades de trabajo que NORA reparte entre uno o varios robots. En lugar de codificar “qué procesar” dentro del bot, los datos se cargan como **items** y los robots los consumen uno a uno. Esto permite paralelizar el trabajo, reintentar lo que falla y mantener un registro auditable de cada unidad procesada. ![Colas de trabajo en NORA, cada una con su recuento de items nuevos, en proceso, completados y fallidos.](/_astro/colas.D116rIz2_2ieotf.webp) El patrón es **productor / consumidor**: * El **productor** llena la cola con items (desde el panel, desde otro bot, o vía API con `X-API-Key`). * El **consumidor** (un robot ejecutando un job) toma items, los procesa y reporta el resultado. ``` flowchart TD P[Productor: panel / API / otro bot] -->|añade items| Q[(Cola)] Q -->|toma el siguiente item| R[Robot / Job] R -->|completa o falla| Q R -.->|envía a revisión| H[Revisor humano] H -.->|aprueba / rechaza| Q ``` ## Anatomía de una cola [Sección titulada «Anatomía de una cola»](#anatomía-de-una-cola) Una cola pertenece a un workspace (tenant) y su `name` es único dentro de él. Campos principales (modelo `Queue`): | Campo | Descripción | | ------------- | ---------------------------------------------------------------------------------- | | `name` | Nombre único en el workspace. El robot lo usa para identificar la cola. | | `description` | Texto libre opcional. | | `max_retries` | Reintentos antes de marcar un item como `dead_letter`. Rango 0–10 (por defecto 3). | ## Items y sus estados [Sección titulada «Items y sus estados»](#items-y-sus-estados) Cada **item** (`QueueItem`) lleva los datos del trabajo en `data` (un objeto JSON libre) y avanza por una máquina de estados. Campos relevantes: | Campo | Descripción | | ----------------------------- | ---------------------------------------------------------------------- | | `data` | Datos del item en formato JSON, definidos por el productor. | | `priority` | `1` = baja, `3` = normal (por defecto), `5` = urgente. | | `reference` | Clave de negocio opcional fijada por el productor (buscable). | | `deadline` | SLA opcional; los items con deadline más próximo se despachan primero. | | `postpone` | El robot omite el item hasta que llegue este instante. | | `result` | Resultado JSON que escribe el robot al completar. | | `retry_count` | Número de reintentos consumidos. | | `error_message` | Motivo del último fallo o rechazo. | | `processed_by` | `id` del job que procesó el item. | | `reviewed_by` / `reviewed_at` | Usuario y momento de la revisión humana. | Estados que escribe la plataforma a lo largo del ciclo de vida: | Estado | Significado | | ---------------- | ---------------------------------------------- | | `new` | Listo para ser tomado por un robot. | | `in_progress` | Un robot lo está procesando. | | `pending_review` | Pausado, esperando aprobación humana. | | `completed` | Procesado con éxito (lleva `result`). | | `failed` | Falló o fue rechazado por un revisor. | | `dead_letter` | Superó `max_retries`; ya no se reintenta solo. | ``` stateDiagram-v2 [*] --> new new --> in_progress in_progress --> completed in_progress --> pending_review in_progress --> failed pending_review --> new : aprobado pending_review --> failed : rechazado failed --> dead_letter : sin reintentos failed --> new : reintento dead_letter --> new : reintento manual ``` ## Cargar items (productor) [Sección titulada «Cargar items (productor)»](#cargar-items-productor) Desde la API pública, el productor añade items a una cola identificándola por nombre, con una API key de scope `queues:write`. La respuesta va envuelta en `{ "data": ... }` (ver [autenticación](/api/autenticacion/)). ```bash curl -X POST \ "https://nora-api.valisoftconsulting.com/api/v1/queues/by-name/facturas/items" \ -H "X-API-Key: nora_ak_xxx" \ -H "Content-Type: application/json" \ -d '{ "data": {"numero": "F-001", "proveedor": "Acme", "monto": 15420}, "priority": 5, "reference": "F-001" }' ``` ```json { "success": true, "data": { "id": "…", "queue_id": "…", "status": "new", "priority": 5, "reference": "F-001", "data": {"numero": "F-001", "proveedor": "Acme", "monto": 15420}, "result": null, "retry_count": 0 } } ``` Para carga masiva existe el endpoint `.../items/bulk` (hasta 1000 items por petición, scope `queues:write`). Los detalles completos de todos los endpoints están en la referencia de [colas en la API](/api/colas/). ## Consumir items (robot) [Sección titulada «Consumir items (robot)»](#consumir-items-robot) El robot, dentro de su job, toma el siguiente item disponible y reporta el desenlace mediante el NORA Agent SDK. La selección respeta `priority`, `deadline` y `postpone`. ```python from nora_agent import sdk while True: item = sdk.get_queue_item("facturas") # toma el siguiente; None si está vacía if item is None: break try: resultado = procesar(item["data"]) sdk.complete_queue_item("facturas", item["id"], resultado) except DatoInvalido as e: # Excepción de NEGOCIO: el dato está mal, reintentar no ayuda → terminal. sdk.fail_queue_item("facturas", item["id"], str(e), exception_type="business") except Exception as e: # Excepción de SISTEMA (transitoria): reintenta hasta max_retries → dead_letter. sdk.fail_queue_item("facturas", item["id"], str(e)) # system es el default ``` ## Aprobación humana (human-in-the-loop) [Sección titulada «Aprobación humana (human-in-the-loop)»](#aprobación-humana-human-in-the-loop) Cuando un paso requiere validación, el robot puede **pausar el item** y esperar la decisión de una persona antes de continuar. El flujo lo expone el SDK y la decisión se toma desde el panel. ``` sequenceDiagram participant R as Robot participant N as NORA participant H as Revisor R->>N: send_queue_item_for_review(item) Note over N: status → pending_review N-->>H: notificación en el panel H->>N: Aprobar / Rechazar R->>N: wait_for_queue_review(item) N-->>R: "approved" o "rejected" alt aprobado R->>N: complete_queue_item(...) else rechazado Note over N: status → failed end ``` 1. El robot llama a `send_queue_item_for_review(...)`; el item pasa a `pending_review` y se notifica al panel. 2. El robot bloquea con `wait_for_queue_review(...)` (sondea hasta un timeout configurable). 3. Un revisor abre la cola, revisa los `data` del item y **aprueba** (vuelve a `new` para que el robot lo procese) o **rechaza** (queda `failed`). 4. El robot recibe `"approved"` o `"rejected"` y actúa en consecuencia. ### Quién puede revisar [Sección titulada «Quién puede revisar»](#quién-puede-revisar) Una cola puede tener **revisores asignados** (`QueueReviewer`). Si los tiene, solo esos usuarios reciben la notificación y pueden decidir. Si no tiene ninguno, cualquier `admin` u `operator` del workspace puede revisar. Asignar revisores es exclusivo de `admin`. | Rol | Crear/editar colas y cargar items | Aprobar/rechazar | Asignar revisores | | ---------- | :-------------------------------: | :------------------------------------------------: | :---------------: | | `admin` | Sí | Sí | Sí | | `operator` | Sí | Sí (si está asignado o la cola no tiene revisores) | No | | `viewer` | No (solo lectura) | No | No | Desde el panel también puedes aplicar **acciones masivas** sobre varios items a la vez: `retry`, `approve`, `reject` o `delete`. La superficie pública completa de la API está documentada en la [referencia de la API](/api/introduccion/). # Flujos DAG > Orquesta varios procesos con dependencias mediante flujos DAG en NORA: define nodos, conecta aristas y ejecuta en orden topológico. Un **flujo DAG** (grafo acíclico dirigido, *Directed Acyclic Graph*) permite encadenar varios [procesos](/guia/procesos-y-paquetes/) de NORA en un único flujo orquestado, donde unos nodos dependen de que otros terminen antes de arrancar. Es la herramienta indicada cuando un resultado de negocio no se logra con un solo robot, sino con varios que deben ejecutarse en cierto orden. ![Listado de flujos DAG en NORA, cada uno con su descripción de la cadena de procesos y acciones para editar, ejecutar o visualizar.](/_astro/flujos.CRyJDzDG_Z2gNyKi.webp) ## Qué es un DAG en NORA [Sección titulada «Qué es un DAG en NORA»](#qué-es-un-dag-en-nora) En pocas palabras, un flujo DAG encadena varios robots para que se ejecuten en cierto orden: unos esperan a que otros terminen antes de arrancar. Un DAG se compone de dos listas: * **Nodos** (`nodes`): cada nodo apunta a un proceso (`process_id`) y representa un paso del flujo. * **Aristas** (`edges`): cada arista declara una dependencia dirigida `source → target`, es decir, “el nodo `target` no arranca hasta que `source` haya terminado”. Al ejecutar el DAG, NORA: 1. Detecta los **nodos raíz** (los que no aparecen como `target` de ninguna arista) y crea un [job](/guia/jobs/) para cada uno en la máquina indicada. 2. A medida que cada job termina, avanza el DAG: cuando **todos** los predecesores de un nodo están `completed`, dispara ese nodo. 3. Marca la ejecución como `completed` cuando todos los nodos terminan correctamente, o como `failed` si alguno falla. ``` flowchart TD A["Extraer facturas (proceso A)"] --> B["Validar datos (proceso B)"] A --> C["Generar reporte (proceso C)"] B --> D["Cargar al ERP (proceso D)"] C --> D D --> E["Enviar notificacion (proceso E)"] ``` En este ejemplo, `D` (cargar al ERP) espera a que terminen **B** y **C**; y `E` solo arranca cuando `D` finaliza. Los nodos `B` y `C` pueden ejecutarse en paralelo porque ambos solo dependen de `A`. ![Editor visual de flujos DAG en NORA: nodos conectados por aristas dirigidas, cada nodo con su proceso y su estado de ejecución codificado por color.](/_astro/flujo-dag.C6i6xZDh_Z1NRQOL.webp) ## Estados de ejecución [Sección titulada «Estados de ejecución»](#estados-de-ejecución) Cada ejecución (`DAGExecution`) tiene un `status` global y un mapa `node_states` por nodo: | Campo | Valores | | ---------------------- | ------------------------------------------- | | `status` (ejecución) | `running`, `completed`, `failed` | | `node_states[node_id]` | `pending`, `running`, `completed`, `failed` | Si cualquier nodo termina con `failed`, la ejecución completa pasa a `failed` y no se disparan más nodos. Se requiere al menos un nodo raíz Si todas las aristas forman un ciclo (no hay nodo sin predecesores), la ejecución se rechaza con un error de validación (“DAG no tiene nodos raíz (ciclo detectado)”). Un DAG, por definición, no puede contener ciclos. ## Crear un flujo desde el editor visual [Sección titulada «Crear un flujo desde el editor visual»](#crear-un-flujo-desde-el-editor-visual) La forma habitual de armar un flujo, y la recomendada para operadores, es el editor visual de la consola. No necesitas escribir nada de API. 1. Entra en **Flujos** y pulsa **Crear flujo** (disponible para los roles `admin` y `operator`). 2. **Añade nodos** al lienzo y, desde el panel lateral, elige el proceso que ejecutará cada nodo. 3. **Conecta los nodos** arrastrando desde la salida de uno hasta la entrada de otro. La flecha significa que “el destino espera a que el origen termine”. 4. No te preocupes por las coordenadas: las **posiciones x/y de cada nodo las guarda el editor solo** según dónde los colocas en el lienzo. 5. Ponle **nombre** al flujo y **guárdalo**. 6. Para lanzarlo, pulsa **Ejecutar** y elige la **máquina** donde correrán los robots. Puedes seguir el progreso en el lienzo: el **color de cada nodo** indica su estado (pendiente, en ejecución, completado o fallido). ## Autenticación y URL base (equivalente por API, para integraciones) [Sección titulada «Autenticación y URL base (equivalente por API, para integraciones)»](#autenticación-y-url-base-equivalente-por-api-para-integraciones) Lo que sigue describe cómo hacer lo mismo desde la API, pensado para integraciones y automatizaciones; los operadores no lo necesitan. Ten en cuenta que el `id` de cada nodo y sus coordenadas `x`/`y` son detalles que en la consola gestiona el editor visual por ti. Los endpoints de DAG forman parte de la API interna del panel y se autentican con la **sesión de usuario (JWT)**, no con la cabecera `X-API-Key` de la API pública. Las operaciones de creación, edición y ejecución requieren rol `admin` u `operator`; la lectura admite además `viewer`; el borrado exige `admin`. * URL base: `https://nora-api.valisoftconsulting.com` * Prefijo: `/api/v1` * Recurso: `/api/v1/dags` Todas las respuestas van envueltas en `{"success": true, "data": ...}`. ## Definir un DAG (equivalente por API, para integraciones) [Sección titulada «Definir un DAG (equivalente por API, para integraciones)»](#definir-un-dag-equivalente-por-api-para-integraciones) `POST /api/v1/dags` ```json { "name": "Cierre contable diario", "description": "Extrae, valida y carga facturas al ERP", "nodes": [ { "id": "a", "process_id": "1f1c…-uuid-del-proceso-A", "label": "Extraer facturas", "x": 0, "y": 0 }, { "id": "b", "process_id": "2a2d…-uuid-del-proceso-B", "label": "Validar datos", "x": 200, "y": -80 }, { "id": "c", "process_id": "3b3e…-uuid-del-proceso-C", "label": "Generar reporte", "x": 200, "y": 80 }, { "id": "d", "process_id": "4c4f…-uuid-del-proceso-D", "label": "Cargar al ERP", "x": 400, "y": 0 } ], "edges": [ { "source": "a", "target": "b" }, { "source": "a", "target": "c" }, { "source": "b", "target": "d" }, { "source": "c", "target": "d" } ] } ``` Campos de cada nodo: `id` (string único dentro del DAG), `process_id` (UUID del proceso), `label` (texto visible) y coordenadas `x`/`y` para el lienzo del editor visual. Cada arista define `source` y `target` con los `id` de los nodos. ## Operaciones disponibles (equivalente por API, para integraciones) [Sección titulada «Operaciones disponibles (equivalente por API, para integraciones)»](#operaciones-disponibles-equivalente-por-api-para-integraciones) | Método y ruta | Descripción | Rol mínimo | | -------------------------------------------- | ---------------------------------------------- | ---------- | | `POST /api/v1/dags` | Crea un DAG | `operator` | | `GET /api/v1/dags` | Lista los DAG de la organización | `viewer` | | `GET /api/v1/dags/{dag_id}` | Obtiene un DAG | `viewer` | | `PUT /api/v1/dags/{dag_id}` | Actualiza nombre, descripción, nodos o aristas | `operator` | | `DELETE /api/v1/dags/{dag_id}` | Elimina un DAG | `admin` | | `POST /api/v1/dags/{dag_id}/execute` | Inicia una ejecución | `operator` | | `GET /api/v1/dags/executions/{execution_id}` | Consulta el estado de una ejecución | `viewer` | ## Ejecutar un DAG (equivalente por API, para integraciones) [Sección titulada «Ejecutar un DAG (equivalente por API, para integraciones)»](#ejecutar-un-dag-equivalente-por-api-para-integraciones) `POST /api/v1/dags/{dag_id}/execute` La ejecución necesita la máquina donde correrán los jobs. La máquina también se valida contra tu organización. ```bash curl -X POST \ https://nora-api.valisoftconsulting.com/api/v1/dags/{dag_id}/execute \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "machine_id": "9f9a…-uuid-de-la-maquina" }' ``` Respuesta (envuelta): ```json { "success": true, "data": { "id": "…", "dag_id": "…", "status": "running", "node_states": { "a": "running", "b": "pending", "c": "pending", "d": "pending" }, "trigger_machine_id": "9f9a…", "created_at": "2026-06-19T12:00:00Z", "updated_at": "2026-06-19T12:00:00Z" } } ``` NORA arranca los nodos raíz (aquí `a`) y, conforme sus jobs finalizan, avanza automáticamente los nodos cuyos predecesores ya están `completed`. Consulta el progreso con `GET /api/v1/dags/executions/{execution_id}` revisando `status` y `node_states`. ## Cuándo usar un flujo DAG [Sección titulada «Cuándo usar un flujo DAG»](#cuándo-usar-un-flujo-dag) Usa un DAG cuando: * Un resultado depende de **varios procesos** que deben ejecutarse en un orden concreto (p. ej. extraer → validar → cargar). * Hay pasos que pueden correr **en paralelo** y otros que deben **converger** (varios nodos apuntando al mismo `target`). * Quieres una sola unidad de orquestación con estado y trazabilidad por nodo, en lugar de lanzar jobs sueltos manualmente. Para una tarea de un solo proceso, basta con lanzar un [job](/guia/jobs/) directamente; para ejecuciones recurrentes por calendario, combina el DAG con [programaciones](/guia/programaciones-y-triggers/) si tu plan las incluye. Los flujos DAG están disponibles en los planes **Pro** y **Enterprise**. Consulta la [referencia de la API](/api/introduccion/) para los endpoints relacionados. # Instalar el agente > Instala el agente de NORA en Windows y macOS: clave de máquina, arranque automático, modo desatendido, sesión RDP y resolución. El **agente de NORA** es el programa que se instala en cada máquina donde se ejecutan los robots. Se conecta al servidor de NORA con la **clave de máquina**, envía latidos (heartbeat) para mantenerse en línea, recibe trabajos y ejecuta los procesos automatizados. NORA solo es compatible con **Windows** y **macOS**. ## Requisitos [Sección titulada «Requisitos»](#requisitos) * **Máquina registrada en NORA.** Antes de instalar el agente, crea la máquina en el panel. Al crearla se genera una **clave de máquina** con prefijo `nora_mk_...` que el agente necesita para autenticarse. * **Sistema operativo:** Windows 10 o superior, o macOS 12 o superior. * **Conectividad HTTPS saliente** hacia el servidor de NORA. En producción la URL de la API es `https://nora-api.valisoftconsulting.com/api/v1`. HTTPS obligatorio El agente exige `https://` en `NORA_API_URL`. Solo se admite `http://` cuando el host es `localhost`, `127.0.0.1` o `::1` (entornos de desarrollo). Cualquier otra URL en texto plano hace que el agente termine con error. ## Cómo se conecta el agente a la máquina [Sección titulada «Cómo se conecta el agente a la máquina»](#cómo-se-conecta-el-agente-a-la-máquina) El agente lee su configuración de un archivo `.env` ubicado junto al ejecutable (o de variables de entorno): | Variable | Descripción | Valor por defecto | | ------------------ | ----------------------------------------------------- | ---------------------------------- | | `NORA_API_URL` | URL base de la API de NORA, con el prefijo `/api/v1`. | `http://localhost:8000/api/v1` | | `NORA_MACHINE_KEY` | Clave de máquina (`nora_mk_...`). **Obligatoria.** | — (sin ella, el agente no arranca) | Con esos datos, el agente se autentica contra `POST /api/v1/agent/auth` enviando la clave de máquina. El servidor devuelve un token de acceso (JWT) que el agente usa en el resto de llamadas. A partir de ahí: 1. Envía un **heartbeat** cada 30 segundos a `POST /api/v1/agent/heartbeat` con información del sistema, para figurar **en línea** en el panel. 2. Sondea trabajos pendientes cada 5 segundos. 3. Refresca la **configuración de máquina** (resolución, modo de sesión, auto-login) desde `GET /api/v1/agent/machine-config` cada 60 segundos, de modo que los cambios hechos en el panel se aplican sin reinstalar el agente. ``` sequenceDiagram participant A as Agente participant S as NORA (API) A->>S: POST /agent/auth { machine_key } S-->>A: { access_token, machine_id, tenant_id } loop cada 30 s A->>S: POST /agent/heartbeat end loop cada 5 s A->>S: GET /agent/jobs/next S-->>A: trabajo o vacío end ``` El archivo `.env` se guarda con permisos restringidos al usuario actual (`icacls` en Windows, `chmod 600` en macOS), porque contiene la clave de máquina. ## Instalación en Windows [Sección titulada «Instalación en Windows»](#instalación-en-windows) 1. Descarga el agente desde el panel de NORA: entra a **Machines**, selecciona la máquina y descarga el paquete `.zip` para Windows. Incluye `nora-agent.exe` y un `.env` preconfigurado con `NORA_API_URL` y `NORA_MACHINE_KEY`. 2. Extrae el `.zip` en cualquier carpeta (por ejemplo, Descargas), manteniendo el `.exe` y el `.env` juntos. 3. Haz doble clic en `nora-agent.exe`. Como el binario no está firmado, Windows SmartScreen puede avisar: haz clic en **Más información** y luego en **Ejecutar de todas formas**. 4. El instalador integrado se ejecuta automáticamente y: * Copia el agente y el `.env` a `C:\Users\\.nora-agent\`. * Registra una **Tarea programada** llamada **NORA Agent** (se inicia al iniciar sesión, con 30 s de retraso, y se reinicia si el proceso falla). * Registra una segunda tarea **NORA Session Manager** que se ejecuta como `SYSTEM` y gestiona la sesión interactiva (ver más abajo). * Crea un acceso directo **NORA Agent** en el menú Inicio. * Inicia el agente en segundo plano y muestra un mensaje de confirmación. ## Instalación en macOS [Sección titulada «Instalación en macOS»](#instalación-en-macos) 1. Descarga el paquete `.zip` para macOS desde el panel (**Machines** > máquina > descargar). Incluye el binario `nora-agent-macos` y el `.env` preconfigurado. 2. Extrae el `.zip`. 3. Ejecuta el binario desde la Terminal: ```bash chmod +x nora-agent-macos ./nora-agent-macos ``` 4. Si Gatekeeper bloquea la ejecución, ve a **Ajustes del Sistema > Privacidad y Seguridad** y pulsa **Abrir de todas formas**. Como alternativa, quita la cuarentena desde la Terminal: ```bash xattr -cr ~/.nora-agent/nora-agent-macos ``` 5. El instalador integrado: * Copia el agente y el `.env` a `~/.nora-agent/`. * Crea un **LaunchAgent** en `~/Library/LaunchAgents/com.nora.agent.plist` con `RunAtLoad` y `KeepAlive`, y lo carga con `launchctl`. * Escribe los logs en `~/Library/Logs/nora-agent.log`. * Crea `~/Applications/NORA Agent.app` para encontrarlo en Spotlight. ## Arranque automático al reiniciar [Sección titulada «Arranque automático al reiniciar»](#arranque-automático-al-reiniciar) El agente se ejecuta como servicio de **usuario** (no de sistema) porque necesita una **sesión de escritorio** para automatizar aplicaciones. Por eso arranca al **iniciar sesión** un usuario. | Sistema | Mecanismo | Arranca tras reinicio | Se reinicia si falla | | ------- | --------------------------------------------------------------------- | --------------------- | ---------------------------------------------------- | | Windows | Tarea programada **NORA Agent** (disparador al iniciar sesión, +30 s) | Al iniciar sesión | Sí (`RestartOnFailure`: cada 1 min, hasta 999 veces) | | macOS | LaunchAgent `com.nora.agent` (`RunAtLoad` + `KeepAlive`) | Al iniciar sesión | Sí (launchd lo relanza) | Si tras reiniciar la red tarda en subir, el agente reintenta conectarse sin rendirse. ## Modo desatendido con auto-login [Sección titulada «Modo desatendido con auto-login»](#modo-desatendido-con-auto-login) En equipos que se reinician **sin que nadie inicie sesión**, hay que configurar el inicio de sesión automático para que el sistema entre solo a la sesión y dispare el agente. ### Windows (automático desde el panel) [Sección titulada «Windows (automático desde el panel)»](#windows-automático-desde-el-panel) 1. En el panel de NORA, edita la máquina y define el **usuario y la contraseña de Windows** del equipo. 2. El agente lee esa configuración desde `GET /api/v1/agent/machine-config` y aplica el auto-login automáticamente. La contraseña se guarda **cifrada mediante LSA Secrets** (no en texto plano en el registro). 3. Reinicia el equipo para validar: Windows inicia sesión solo, la tarea dispara y el agente conecta. Para desactivarlo, quita el usuario de Windows de la máquina en el panel; el agente eliminará el auto-login en el siguiente refresco de configuración. ### macOS (manual, una sola vez) [Sección titulada «macOS (manual, una sola vez)»](#macos-manual-una-sola-vez) macOS no permite automatizar el auto-login de forma segura, así que se habilita a mano: 1. Ve a **Ajustes del Sistema > Usuarios y grupos > Inicio de sesión automático** y elige la cuenta del equipo. 2. Puede requerir desactivar **FileVault**, que bloquea el auto-login. 3. Reinicia para validar: el Mac entra solo a la sesión y el LaunchAgent arranca el agente. ## Sesión RDP y resolución de pantalla [Sección titulada «Sesión RDP y resolución de pantalla»](#sesión-rdp-y-resolución-de-pantalla) En equipos Windows desatendidos, la sesión de consola física suele quedar atascada en una resolución baja (800x600 con el adaptador de pantalla básico). Para evitarlo, NORA usa un modelo de **sesión RDP de loopback** (similar al de UiPath): * La tarea **NORA Session Manager** se ejecuta como `SYSTEM` y, cuando hay un trabajo, establece una sesión interactiva a la resolución configurada mediante una conexión RDP local (FreeRDP). El robot se ejecuta dentro de esa sesión. * La resolución, la profundidad de color, la escala (DPI) y el modo de sesión se definen en el panel y se aplican vía `machine-config`. El agente inyecta estos valores en el entorno del robot como variables separadas: `NORA_DISPLAY_WIDTH` y `NORA_DISPLAY_HEIGHT` (la resolución configurada, por defecto 1920 x 1080), `NORA_DISPLAY_DEPTH` (32) y `NORA_DISPLAY_SCALE` (100). En el SDK lee siempre `NORA_DISPLAY_WIDTH`/`NORA_DISPLAY_HEIGHT` (ver [SDK de robots](/api/sdk-robots/)). * El **modo de sesión** puede ser `rdp` (sesión RDP de loopback, resolución flexible — predeterminado) o `console` (consola física). Cuando ya hay una sesión RDP activa (humana o de loopback), el agente **no** fuerza la resolución de la pantalla, porque esa sesión ya lleva la resolución correcta. El ajuste directo de resolución solo se intenta en una consola headless sin RDP y en modo desatendido. ## Verificar la versión y el estado [Sección titulada «Verificar la versión y el estado»](#verificar-la-versión-y-el-estado) El agente muestra su versión al arrancar, con el formato `NORA Agent vX.Y.Z` (por ejemplo, `NORA Agent v0.7.7`). Para confirmar que el agente está activo: * **Panel de NORA:** la máquina debe aparecer **en línea** unos 30 segundos después de instalar el agente. * **Windows:** ```bat schtasks /Query /TN "NORA Agent" /V ``` El estado debe ser `Ready` o `Running`. * **macOS:** ```bash launchctl list | grep nora ``` Debe aparecer `com.nora.agent`. Para ver el log en vivo: ```bash tail -f ~/Library/Logs/nora-agent.log ``` ## Solución de problemas [Sección titulada «Solución de problemas»](#solución-de-problemas) * **La máquina no aparece en línea.** Verifica que `NORA_MACHINE_KEY` y `NORA_API_URL` del `.env` (junto al ejecutable) sean correctos y que el equipo tenga conectividad HTTPS al servidor. * **`NORA_MACHINE_KEY environment variable is required`.** Falta la clave de máquina en el `.env` o en el entorno. Añádela y vuelve a iniciar el agente. * **`NORA_API_URL debe usar https://`.** La URL de la API no usa HTTPS (y no es localhost). Corrígela en el `.env`. * **SmartScreen (Windows) o Gatekeeper (macOS) bloquean el ejecutable.** Es esperable porque el binario no está firmado; usa **Ejecutar de todas formas** / **Abrir de todas formas**, o `xattr -cr` en macOS. ## Páginas relacionadas [Sección titulada «Páginas relacionadas»](#páginas-relacionadas) * [Autenticación de la API](/api/autenticacion/) * [Jobs](/guia/jobs/) * [Arquitectura](/conceptos/arquitectura/) # Jobs (ejecuciones) > Qué es un job en NORA, cómo lanzarlo desde la interfaz o por API, sus estados, los logs y cómo detenerlo. Un **job** es una ejecución concreta de un [proceso](/guia/procesos-y-paquetes/) sobre una [máquina](/guia/maquinas/). Cuando lanzas un proceso, NORA crea un job, lo encola y una máquina con el agente conectado lo recoge, ejecuta el robot y reporta el resultado. Cada job guarda su estado, sus logs, los datos de entrada/salida y, si falla, el mensaje de error. ![Tabla de jobs en NORA mostrando proceso, estado, máquina, duración, fecha de inicio y origen de cada ejecución.](/_astro/trabajos.CU5bAIfV_Z1kR299.webp) ## Anatomía de un job [Sección titulada «Anatomía de un job»](#anatomía-de-un-job) Un job referencia siempre un proceso y una máquina, y lleva estos campos principales (ver respuesta de la API más abajo): | Campo | Descripción | | --------------------------------------- | --------------------------------------------------------------------- | | `id` | Identificador único del job (UUID). | | `process_id` / `machine_id` | Proceso ejecutado y máquina asignada. | | `status` | Estado actual (ver [Estados](#estados)). | | `priority` | Prioridad de encolado: `1` baja, `3` normal, `5` urgente (rango 1–5). | | `input_data` | Datos de entrada para el robot (objeto JSON, máx. 1 MB). | | `output_data` | Datos devueltos por el robot al finalizar. | | `logs` | Flujo de logs acumulado. | | `error_message` | Mensaje de error si el job falló. | | `started_at` / `finished_at` | Marcas de inicio y fin de ejecución. | | `progress_percent` / `progress_message` | Progreso reportado por el robot. | | `retry_count` | Número de reintento (0 = ejecución original). | ## Cómo se lanza un job [Sección titulada «Cómo se lanza un job»](#cómo-se-lanza-un-job) ### Desde la interfaz [Sección titulada «Desde la interfaz»](#desde-la-interfaz) En el panel de NORA, abre un proceso y pulsa **Ejecutar**. Selecciona la máquina destino (debe estar activa y en línea), ajusta la prioridad y, si el proceso necesita datos, el panel te mostrará un formulario; complétalo con los valores que te indique el responsable del robot (por ejemplo, el mes a facturar). Esos datos viajan internamente como `input_data`. NORA crea el job en estado `pending` y lo asigna a la máquina cuando esta tenga capacidad libre. ![Modal "Ejecutar Proceso" de NORA mostrando el formulario de parámetros de entrada precargado desde el proceso: campo de texto "mes" (obligatorio), número "reintentos" y casilla "enviar\_correo", cada uno con su descripción.](/_astro/ejecutar-argumentos.B7gYmeUi_ZSNNLE.webp) Ese formulario se construye automáticamente a partir de los argumentos que el proceso **declara** en su `nora.json`, y el robot los **lee en runtime** con el SDK. En corto: ```python # Dentro del robot (main.py) — sin llamadas de red, lo inyecta el agente from nora_agent import sdk mes = sdk.get_input("mes") # el valor que puso el operador en el formulario reintentos = sdk.get_input("reintentos", 3) # con default si no vino sdk.set_output({"facturas_ok": 142}) # devuelve resultados → visibles en Jobs → Salida ``` Y se declaran una sola vez en `nora.json` (lo que genera el formulario de arriba): ```json "inputs": [ { "name": "mes", "type": "text", "required": true, "description": "Mes a facturar (YYYY-MM)" }, { "name": "reintentos", "type": "integer", "default": 3 } ] ``` Para crear un job desde la UI necesitas rol **admin** u **operator**. El rol **viewer** solo puede consultar jobs. ### Por API [Sección titulada «Por API»](#por-api) Los clientes externos lanzan jobs con una clave de API (`X-API-Key: nora_ak_...`). El endpoint y el payload exactos se documentan en [Disparar jobs](/api/disparar-jobs/). En resumen: ```bash curl -X POST https://nora-api.valisoftconsulting.com/api/v1/jobs/trigger \ -H "X-API-Key: nora_ak_xxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "process_id": "f1e2d3c4-0000-0000-0000-000000000000", "input_data": {"cliente": "ACME"}, "priority": 3 }' ``` `machine_id` es opcional en la API por clave: si lo omites, NORA elige automáticamente una máquina activa y en línea del tenant. La respuesta va envuelta en `{"success": true, "data": {...}}`: ```json { "success": true, "data": { "id": "9a8b7c6d-0000-0000-0000-000000000000", "process_id": "f1e2d3c4-0000-0000-0000-000000000000", "machine_id": "1234abcd-0000-0000-0000-000000000000", "status": "pending", "priority": 3, "input_data": {"cliente": "ACME"}, "output_data": null, "logs": null, "error_message": null, "started_at": null, "finished_at": null, "retry_count": 0, "stop_requested": false, "process_name": "Facturación diaria", "machine_name": "VM-PROD-01" } } ``` Crear un job por API requiere una clave con scope `jobs:write`. El endpoint está limitado a 30 solicitudes por minuto. Consulta [autenticación](/api/autenticacion/) para los scopes. ## Estados [Sección titulada «Estados»](#estados) El `status` de un job evoluciona según estas transiciones válidas (definidas en el backend): ``` flowchart TD pending["pending
(en cola)"] assigned["assigned
(asignado a máquina)"] running["running
(en ejecución)"] completed["completed"] failed["failed"] cancelled["cancelled"] pending -->|máquina lo recoge| assigned assigned -->|robot inicia| running running -->|éxito| completed running -->|error| failed pending -->|cancelar| cancelled assigned -->|cancelar| cancelled running -->|cancelar| cancelled ``` | Estado | Significado | | ----------- | ----------------------------------------------------------------------------- | | `pending` | Job creado y en cola, a la espera de que una máquina con capacidad lo recoja. | | `assigned` | Una máquina lo tomó de la cola; el robot aún no ha comenzado. | | `running` | El robot está ejecutándose. Se fija `started_at`. | | `completed` | Terminó correctamente. Se fija `finished_at` y se guarda `output_data`. | | `failed` | Terminó con error. Se fija `finished_at` y se guarda `error_message`. | | `cancelled` | Cancelado antes de completar. | ## Logs [Sección titulada «Logs»](#logs) Mientras el job se ejecuta, el robot envía sus logs y NORA los **anexa** al campo `logs` del job, visible en tiempo casi real en la interfaz. El SDK produce cada línea con este formato: ```plaintext [2026-06-19T14:03:11Z] [INFO] Procesando factura 0042 [2026-06-19T14:03:12Z] [ERROR] Timeout al abrir el portal | {"intento": 3} ``` Es decir: marca de tiempo UTC ISO‑8601 entre corchetes, nivel en mayúsculas (`INFO`, `WARNING`, `ERROR`, `DEBUG`) y el mensaje. Si pasas datos estructurados, se añaden tras una barra `|` como JSON. La función `log()` del SDK genera estas líneas; ver [SDK de robots](/api/sdk-robots/) para los detalles. Los logs antiguos pueden **archivarse** según la política de retención. Si un job tiene logs archivados, se recuperan por separado a través de su endpoint de logs archivados. ## Cómo detener un job [Sección titulada «Cómo detener un job»](#cómo-detener-un-job) En el panel, abre la tabla de **Jobs**, localiza el job `running` y pulsa **Detener** (Stop); para abortar de inmediato un job `pending`/`assigned`/`running` usa **Cancelar** (Cancel) (también en lote). Internamente, Stop marca `stop_requested = true` y el agente termina el robot de forma controlada; Cancel lo mueve a `cancelled` y, si seguía vivo, el agente recibe la señal `kill`. Stop solo aplica a jobs `running`. Por API, con una clave de scope `jobs:stop`: ```bash curl -X POST https://nora-api.valisoftconsulting.com/api/v1/jobs/{job_id}/stop \ -H "X-API-Key: nora_ak_xxxxxxxxxxxxxxxxxxxx" ``` La respuesta de stop devuelve el job actualizado, envuelto en `{"success": true, "data": {...}}`. # Máquinas > Registra y administra las máquinas Windows o macOS donde NORA ejecuta tus robots: estados, grupos y resolución de pantalla. Una **máquina** en NORA representa un equipo físico o virtual —solo Windows o macOS— donde se instala el *agente* de NORA y donde realmente corren los robots. Cada máquina pertenece a una organización (*tenant*), tiene una credencial propia (la **machine key**) y puede ejecutar uno o varios *jobs* a la vez según su configuración. ![Listado de máquinas en NORA, agrupadas por entorno, con su estado de conexión, clave, último latido y trabajos concurrentes.](/_astro/maquinas.vxT1Qgp4_ZomAx2.webp) ## Modelo de una máquina [Sección titulada «Modelo de una máquina»](#modelo-de-una-máquina) Los campos principales de una máquina (modelo `Machine`) son: | Campo | Tipo | Descripción | | ------------------------------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------ | | `name` | texto | Nombre legible de la máquina (mínimo 2 caracteres). | | `machine_key` | secreto | Credencial con la que el agente se autentica. Se guarda *hasheada* y, además, cifrada para poder revelarla una sola vez. | | `status` | enum | Estado operativo: `online`, `offline`, `busy` o `maintenance`. | | `is_active` | bool | Si está activa y puede recibir trabajo. Por defecto `true`. | | `max_concurrent_jobs` | entero | Número máximo de *jobs* simultáneos. Por defecto `1`. | | `last_seen_at` | fecha | Último *heartbeat* recibido del agente. | | `missed_heartbeats` | entero | Latidos perdidos consecutivos (sirve para marcarla offline). | | `system_info` | objeto | Información de sistema reportada por el agente. | | `display_resolution`, `color_depth`, `dpi_scaling`, `session_mode` | varios | Configuración de pantalla para la ejecución (ver más abajo). | La machine key se muestra una sola vez Al crear la máquina, la API devuelve `machine_key` en texto plano **solo en esa respuesta**. Después solo se ve la pista de los últimos 4 caracteres (`machine_key_last4`, p. ej. `****a1b2`). Un administrador puede volver a revelarla con `GET /machines/{id}/key` (operación auditada), o regenerar el paquete del agente, que ya la incluye en su archivo `.env`. ## Registrar una máquina y ponerla Online [Sección titulada «Registrar una máquina y ponerla Online»](#registrar-una-máquina-y-ponerla-online) El alta la hace un admin. Vía recomendada (panel): 1. **Máquinas → Crear máquina** (nombre mínimo 2 caracteres). 2. NORA muestra la `machine_key` una sola vez (cópiala) y ofrece **Descargar agente** (Windows/macOS). 3. Instala el agente; al conectarse, la máquina pasa de `offline` a `online`. El mismo alta también por API (sesión, no API key): ```bash curl -X POST https://nora-api.valisoftconsulting.com/api/v1/machines \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"name": "BOT-CONTABILIDAD-01"}' ``` Respuesta (envuelta en `data`, ver [introducción a la API](/api/introduccion/)): ```json { "success": true, "data": { "id": "0f4c…", "name": "BOT-CONTABILIDAD-01", "machine_key": "nora_mk_…", "status": "offline", "created_at": "2026-06-19T10:00:00Z" } } ``` A continuación se descarga el paquete del agente para la plataforma deseada (`platform=windows` o `platform=macos`). NORA genera un ZIP con el ejecutable del agente, un `README.txt` con los pasos de instalación, el script de instalación y un archivo `.env` que ya contiene la `NORA_MACHINE_KEY` y la URL de la API. Al instalarse, el agente se registra para arrancar con el sistema y empieza a enviar *heartbeats*; entonces la máquina pasa de `offline` a `online`. No existe un paso manual de “aprobación”: la máquina queda lista en cuanto el agente, autenticado con su *machine key*, se conecta por primera vez. Para retirar una máquina, desactívala (`is_active=false`) o elimínala (solo si no tiene *jobs* en su historial). ``` flowchart TD A[Admin crea la máquina] --> B[Descarga el paquete del agente] B --> C[Instala el agente en Windows o macOS] C --> D[Agente se autentica con la machine key] D --> E[Heartbeats: status pasa a online] E --> F[Lista para ejecutar jobs] ``` ## Estados [Sección titulada «Estados»](#estados) | Estado | Significado | | ------------- | ---------------------------------------------- | | `offline` | Sin conexión. Es el estado inicial al crearla. | | `online` | Conectada y disponible para recibir trabajo. | | `busy` | Ocupada ejecutando uno o más *jobs*. | | `maintenance` | En mantenimiento; no se le asigna trabajo. | El filtro `status` de los listados de la API privada acepta `online`, `offline`, `busy` y `maintenance`. El endpoint público acepta `online`, `offline` y `busy`. ## Grupos de máquinas [Sección titulada «Grupos de máquinas»](#grupos-de-máquinas) Un **grupo de máquinas** (`MachineGroup`) reúne varias máquinas bajo un nombre para repartir trabajo entre ellas en lugar de fijar un *job* a un equipo concreto. La pertenencia es una relación muchos-a-muchos (`MachineGroupMember`). Operaciones disponibles bajo el prefijo `/api/v1/machine-groups` (rol **admin** para crear, modificar y borrar; **operator**/**viewer** pueden listar): | Método y ruta | Acción | | ----------------------------------- | --------------------------------------------- | | `GET /machine-groups` | Listar grupos del *tenant*. | | `POST /machine-groups` | Crear grupo (`name`, opcional `machine_ids`). | | `PATCH /machine-groups/{group_id}` | Renombrar o cambiar las máquinas del grupo. | | `DELETE /machine-groups/{group_id}` | Eliminar el grupo. | ```bash curl -X POST https://nora-api.valisoftconsulting.com/api/v1/machine-groups \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"name": "Contabilidad", "machine_ids": ["0f4c…", "1a2b…"]}' ``` El objeto de respuesta de un grupo incluye `id`, `name`, `machine_ids` (lista) y `created_at`. ## Resolución de pantalla [Sección titulada «Resolución de pantalla»](#resolución-de-pantalla) Los robots de interfaz dependen de la pantalla en la que corren. Cada máquina lleva una configuración de *display* que el agente aplica a la sesión donde ejecuta el robot. Es el equivalente de NORA a los ajustes de resolución de UiPath: | Campo | Valores permitidos | Por defecto | Notas | | -------------------- | --------------------------------- | ----------- | -------------------------------------- | | `display_resolution` | `ANCHOxALTO`, p. ej. `1920x1080` | `1920x1080` | Ancho 640–7680, alto 480–4320. | | `color_depth` | `16`, `24`, `32` | `32` | Profundidad de color (bits por píxel). | | `dpi_scaling` | `100`, `125`, `150`, `175`, `200` | `100` | Escalado DPI, en porcentaje. | | `session_mode` | `rdp`, `console` | `rdp` | Tipo de sesión donde corre el robot. | Sobre `session_mode`: * **`rdp`**: el robot corre en una sesión RDP de bucle local con resolución, profundidad y DPI configurables. Es el modo recomendado para ejecución desatendida. * **`console`**: el robot corre en la sesión de consola física del equipo y usa la resolución del adaptador. Es el equivalente al “Login to Console” de UiPath. En Windows se pueden definir además `windows_username` y `windows_password` (esta última se cifra en el backend y nunca se devuelve en las respuestas; la API solo expone `windows_autologon_configured`) para iniciar sesión automáticamente en la máquina antes de ejecutar. Estos valores se modifican con `PUT /api/v1/machines/{machine_id}` (rol **admin** u **operator**): ```bash curl -X PUT https://nora-api.valisoftconsulting.com/api/v1/machines/0f4c… \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"display_resolution": "1920x1080", "color_depth": 32, "dpi_scaling": 125, "session_mode": "rdp"}' ``` ## Listar máquinas desde la API pública [Sección titulada «Listar máquinas desde la API pública»](#listar-máquinas-desde-la-api-pública) Para integraciones externas, NORA expone un endpoint de solo lectura autenticado con [API key](/api/introduccion/): ```http GET /api/v1/machines/list X-API-Key: nora_ak_... ``` Requiere el *scope* `machines:read` (o una key sin restricciones) y está limitado a 60 peticiones por minuto. Admite el filtro opcional `?status=online|offline|busy`. ```bash curl https://nora-api.valisoftconsulting.com/api/v1/machines/list \ -H "X-API-Key: nora_ak_..." ``` ```json { "data": [ { "id": "0f4c…", "name": "BOT-CONTABILIDAD-01", "status": "online", "is_active": true, "max_concurrent_jobs": 1, "last_seen_at": "2026-06-19T10:05:00Z" } ], "total": 1 } ``` # Planes y facturación > Planes Starter, Pro y Enterprise de NORA: límites, features y facturación con Culqi y comprobantes SUNAT. NORA se ofrece en tres planes comerciales (**Starter**, **Pro** y **Enterprise**), cada uno con sus propios límites de robots, usuarios y ejecuciones, y con un conjunto creciente de funcionalidades. Esta página describe qué incluye cada plan y cómo funciona la facturación. ![Página de facturación de NORA con el plan actual, el comparativo de planes Starter, Pro y Enterprise, y el historial de comprobantes.](/_astro/facturacion.B_UJOKf7_1pEpA.webp) Pagos en línea — Próximamente La activación de planes con tarjeta **aún no está disponible** en la plataforma. Mientras habilitamos la pasarela de pago, para activar o cambiar de plan escribe a ****. El equipo configura tu cuenta y, cuando los pagos en línea estén activos, podrás gestionar la suscripción desde la aplicación. ## Comparativa de planes [Sección titulada «Comparativa de planes»](#comparativa-de-planes) Precios mensuales y anuales en la moneda configurada por el plan. El plan **Free** existe para evaluación interna; los planes comerciales son **Starter**, **Pro** y **Enterprise**. | Característica | Starter | Pro | Enterprise | | ------------------------ | --------------- | ------------------------------- | ------------------------------- | | Precio mensual | 99 | 249 | 749 | | Precio anual | 990 | 2490 | 7490 | | Límite de robots (bots) | 5 | 20 | 9999 (efectivamente sin límite) | | Límite de usuarios | 3 | 10 | Sin límite | | Ejecuciones / mes | Sin límite | Sin límite | Sin límite | | Retención de logs (días) | 90 | 180 | 365 | | Prioridades de job | normal (3) | low (1), normal (3), urgent (5) | low (1), normal (3), urgent (5) | | Límite de API keys | 0 (no incluido) | 5 | Sin límite | | Canales de notificación | email | email, slack, teams, webhook | email, slack, teams, webhook | ## Features por plan [Sección titulada «Features por plan»](#features-por-plan) NORA aplica *feature gating* por plan: cada funcionalidad declara el plan mínimo que la habilita. A continuación, las features incluidas en cada plan (acumulativas: cada plan incluye lo anterior y agrega más). ### Starter [Sección titulada «Starter»](#starter) Operación RPA completa para equipos pequeños: * `dashboard`, `jobs`, `processes`, `releases`, `packages` * `machines`, `machine_groups` * `schedules` (programaciones), `triggers` no incluido * `queues` (colas), `assets` (activos) - `holidays` (calendarios de feriados) - `notifications` por **email** - `team_invitations` (invitar usuarios) ### Pro [Sección titulada «Pro»](#pro) Todo lo de Starter, más automatización avanzada, observabilidad y acceso programático: * `api_keys` — claves de API para la [API pública](/api/autenticacion/) (hasta **5**) * `triggers` — disparadores de procesos * `webhooks` — disparo de procesos vía webhook * `dags` — orquestación de [flujos/DAGs](/guia/flujos-dag/) * `folders` — organización por carpetas * `mfa` — autenticación multifactor * `oauth_sso` — inicio de sesión con Google/Microsoft * `audit_logs` — registros de auditoría * `ai_assistant`, `anomalies`, `consumption_reports` — asistente IA, detección de anomalías e informes de consumo * `notifications` por **email, Slack, Teams y webhook** ### Enterprise [Sección titulada «Enterprise»](#enterprise) Todo lo de Pro, más gobernanza, identidad corporativa e integraciones empresariales: * `saml_sso` — inicio de sesión federado SAML * `scim` — aprovisionamiento de usuarios SCIM * `graphql` — API GraphQL * `rbac_custom`, `custom_roles` — control de acceso y roles personalizados * `bulk_operations` — operaciones masivas * `attended_robot` — robots atendidos * `vault_integration` — integración con bóveda de secretos * `custom_retention` — retención de datos a medida * **API keys sin límite** ## Facturación [Sección titulada «Facturación»](#facturación) La facturación de NORA está pensada para el mercado peruano: cobros mediante **Culqi** y emisión de **comprobantes electrónicos SUNAT** mediante **Nubefact**. ``` sequenceDiagram participant U as Cliente (admin) participant F as Frontend (Culqi.js) participant N as NORA backend participant C as Culqi participant Nb as Nubefact (SUNAT) U->>F: Ingresa datos de tarjeta F->>C: Tokeniza tarjeta (token_id) F->>N: POST /billing/subscribe (token_id, plan, ciclo, moneda, datos fiscales) N->>C: Crea customer + card + suscripción C-->>N: Webhook de cobro confirmado N->>Nb: Emite comprobante (factura/boleta/exportación) Nb-->>N: Respuesta SUNAT (serie, correlativo, PDF/XML) ``` ### Pasarela de pago: Culqi [Sección titulada «Pasarela de pago: Culqi»](#pasarela-de-pago-culqi) El cobro se realiza con Culqi mediante suscripciones recurrentes: 1. El frontend carga **Culqi.js** y tokeniza la tarjeta en el navegador (los datos de la tarjeta **nunca** pasan por los servidores de NORA). 2. Se envía el `token_id` junto con el plan, el ciclo de facturación (`monthly` o `annual`), la moneda (`PEN` o `USD`) y los datos fiscales. 3. NORA crea/reutiliza el *customer* en Culqi, asocia la tarjeta y crea la **suscripción recurrente**. Culqi cobra según el ciclo y notifica los eventos por webhook. Los importes en Culqi se manejan en **céntimos** (enteros). ### Comprobantes SUNAT: Nubefact [Sección titulada «Comprobantes SUNAT: Nubefact»](#comprobantes-sunat-nubefact) Tras cada cobro confirmado, NORA emite un comprobante electrónico a través de Nubefact (OSE autorizado por SUNAT). El tipo de comprobante se determina por el perfil fiscal del cliente: | Perfil fiscal del cliente | Tipo de comprobante | | -------------------------------------------- | ---------------------------------- | | Empresa peruana con **RUC** | Factura | | Persona natural peruana (**DNI**/CE) | Boleta | | Cliente no domiciliado (**EXT** o país ≠ PE) | Exportación de servicios (IGV = 0) | El IGV vigente en Perú es del **18 %** y se incluye en el precio para clientes peruanos; las exportaciones de servicios no llevan IGV. Los datos fiscales que se envían al suscribir son: tipo de documento (`RUC`, `DNI`, `CE` o `EXT`), número de documento, razón social/nombre y dirección fiscal. ### Consultar comprobantes emitidos [Sección titulada «Consultar comprobantes emitidos»](#consultar-comprobantes-emitidos) Los administradores de la organización pueden listar los comprobantes electrónicos emitidos desde el panel. La respuesta incluye, por comprobante: tipo (`factura`/`boleta`/ `exportacion`), serie y correlativo, moneda, subtotal, IGV y total (en céntimos), estado SUNAT y enlaces a PDF/XML cuando están disponibles. ### Cancelar la suscripción [Sección titulada «Cancelar la suscripción»](#cancelar-la-suscripción) La cancelación de la suscripción la solicita un administrador desde el panel. NORA enruta la cancelación a Culqi y la suscripción queda marcada para no renovarse. ## Cómo activar un plan hoy [Sección titulada «Cómo activar un plan hoy»](#cómo-activar-un-plan-hoy) Mientras los pagos en línea están en modo **Próximamente**: 1. Escribe a **** indicando el plan deseado (Starter, Pro o Enterprise), el ciclo (mensual o anual) y tus datos fiscales. 2. El equipo de Valisoft activa el plan en tu organización. 3. Recibirás el comprobante electrónico SUNAT correspondiente. Para Enterprise, además, el equipo coordina las integraciones de identidad (SAML/SCIM) y la retención de datos a medida. El IGV aplicado a los comprobantes es del 18 % (tasa vigente en Perú). Los límites y *features* exactos de cada plan se reflejan en tu panel de **Facturación**. # Primeros pasos > Quickstart de extremo a extremo: del registro a tu primer robot RPA en ejecución, paso a paso. Esta guía te lleva desde una cuenta vacía hasta ver tu primer robot ejecutándose en NORA. Cada paso enlaza a la guía detallada correspondiente por si necesitas profundizar. ![Panel principal de NORA con tarjetas de procesos activos, máquinas en línea, trabajos del día, tasa de éxito y la curva de ejecución.](/_astro/panel.Be7XX4MT_Zfv2BR.webp) NORA es un orquestador de RPA: el servidor (la consola web) decide *qué* ejecutar y *cuándo*, y un **agente** instalado en una máquina Windows o macOS es quien ejecuta realmente el robot. Conviene tener clara esa separación antes de empezar; si quieres el panorama completo, consulta [arquitectura](/conceptos/arquitectura/). ``` flowchart TD A[1. Cuenta y workspace] --> B[2. Instalar el agente] B --> C[3. Máquina Online] C --> D[4. Empaquetar robot y crear proceso] D --> E[5. Lanzar job y ver logs] E --> F[6. Automatizar: programaciones o API] ``` ## 1. Crear cuenta e iniciar sesión [Sección titulada «1. Crear cuenta e iniciar sesión»](#1-crear-cuenta-e-iniciar-sesión) 1. Abre la consola en **** y regístrate. 2. Al registrarte se crea tu **workspace** (organización). Todos tus recursos —máquinas, paquetes, procesos, jobs— pertenecen a ese workspace y están aislados de los demás. 3. Inicia sesión. Si tu organización tiene activado un segundo factor (MFA) o SSO, complétalo aquí. ## 2. Instalar el agente en una máquina (perfil técnico) [Sección titulada «2. Instalar el agente en una máquina (perfil técnico)»](#2-instalar-el-agente-en-una-máquina-perfil-técnico) El agente es el software que corre en la máquina donde se ejecuta el robot. **Antes de instalarlo, primero hay que crear la máquina en la consola** (el siguiente paso te da la clave que el agente necesita). * **Sistemas soportados:** Windows 10+ o macOS 12+. * **Conectividad:** la máquina debe poder alcanzar el servidor por HTTPS (puerto 443). El procedimiento completo (ejecutable, instalación con `pip`, servicio de arranque automático, logs y solución de problemas) está en [instalación del agente](/guia/instalacion-agente/). ## 3. Registrar la máquina y verla Online [Sección titulada «3. Registrar la máquina y verla Online»](#3-registrar-la-máquina-y-verla-online) 1. En la consola ve a **Machines** y crea una nueva máquina. 2. Al crearla, NORA genera una **Machine Key** con el prefijo `nora_mk_...`. Es el secreto que autentica al agente; cópiala en ese momento. 3. Instala el agente usando esa clave. La forma recomendada es el **instalador descargable** (un binario, sin Python): trae un `.env` preconfigurado y se cubre paso a paso en [Instalar el agente](/guia/instalacion-agente/). Pega ahí tu `nora_mk_...` cuando se te pida. 4. El agente envía un *heartbeat* cada 30 segundos. En menos de 30 s la máquina debería aparecer como **Online** en la consola. La Machine Key es sensible Si se pierde, se regenera desde **Machines → (tu máquina) → Regenerar Key**. Detalles de estados, conexión y mantenimiento en [máquinas](/guia/maquinas/). ## 4. Empaquetar un robot y crear un proceso (perfil técnico) [Sección titulada «4. Empaquetar un robot y crear un proceso (perfil técnico)»](#4-empaquetar-un-robot-y-crear-un-proceso-perfil-técnico) Un **robot** es tu código (por ejemplo `main.py` más un `requirements.txt`). Para llevarlo a NORA se empaqueta como un **release** dentro de un **paquete**, y luego se crea un **proceso** que asocia ese paquete a las máquinas donde correrá. Usa el CLI `nora` (incluido en el `nora-sdk`) desde la carpeta del robot: ```bash pip install nora-sdk # 1) Inicia sesión en el CLI (abre el navegador; apunta a producción por defecto) nora login # 2) Empaqueta el robot (excluye venv, cachés y secretos automáticamente) nora package --entry main.py # 3) Publica el release en el Robots Center # (crea el paquete si no existe) nora release push ``` * `nora package` genera un `.zip` y escribe/actualiza el manifiesto `nora.json` (campos `name`, `version`, `entry_point`). La versión se auto-incrementa (parche por defecto; usa `--bump minor|major` o `--version X.Y.Z`). * Un escaneo de secretos aborta el empaquetado si detecta claves o `.env` reales; corrígelo antes de continuar (o, con cuidado, `--allow-secrets`). * Tras publicar el release, **crea el proceso** en la consola: ve a **Processes**, elige el paquete/versión y las máquinas elegibles. Más detalle sobre versiones, manifiesto y procesos en [procesos y paquetes](/guia/procesos-y-paquetes/). ## 5. Lanzar un job y ver los logs [Sección titulada «5. Lanzar un job y ver los logs»](#5-lanzar-un-job-y-ver-los-logs) Un **job** es una ejecución concreta de un proceso en una máquina. Este es el punto de partida del operador: todo se hace desde el panel, sin línea de comandos. 1. En **Processes**, abre tu proceso y pulsa **Run** (Lanzar). Si tienes varias máquinas elegibles, elige una. 2. NORA crea el job y lo asigna a una máquina **Online**. El agente descarga el release, instala dependencias y ejecuta el `entry_point`. 3. Sigue la ejecución en **Jobs**: verás el estado (en cola, ejecutando, completado o fallido), el progreso y los **logs en vivo** que tu robot emite con `sdk.log(...)` y `sdk.update_progress(...)`. Estados, reintentos y diagnóstico en [jobs](/guia/jobs/). ## 6. Opcional: automatizar [Sección titulada «6. Opcional: automatizar»](#6-opcional-automatizar) Con tu primer robot funcionando, puedes dejar de lanzarlo a mano: * **Programaciones y triggers:** ejecuta el proceso por horario (cron) o ante eventos. Ver [programaciones y triggers](/guia/programaciones-y-triggers/). * **API pública:** lanza jobs desde tus propios sistemas con una API Key (`X-API-Key: nora_ak_...`). Las respuestas vienen envueltas en `{"success": true, "data": ...}`. ```bash curl -X POST https://nora-api.valisoftconsulting.com/api/v1/jobs/trigger \ -H "X-API-Key: nora_ak_xxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"process_id": "00000000-0000-0000-0000-000000000000"}' ``` El único campo obligatorio es `process_id`. Si omites `machine_id`, NORA elige automáticamente una máquina activa y en línea del workspace. Crea la clave en **Settings → API Keys**. Ver [autenticación](/api/autenticacion/) y [disparar jobs](/api/disparar-jobs/). ## Resumen del flujo [Sección titulada «Resumen del flujo»](#resumen-del-flujo) | Paso | Dónde | Resultado | | ---- | -------------------------- | ------------------------------------ | | 1 | Consola web | Workspace creado, sesión iniciada | | 2 | Máquina (Win/macOS) | Agente instalado | | 3 | **Machines** | Máquina **Online** con `nora_mk_...` | | 4 | CLI `nora` + **Processes** | Release publicado y proceso creado | | 5 | **Processes / Jobs** | Primer job ejecutado, logs visibles | | 6 | Schedules / API | Ejecución automatizada | # Procesos y paquetes > Diferencia entre Paquete, Release y Proceso; cómo empaquetar y subir un robot, y cómo publicar versiones en NORA. En NORA, lo que ejecuta un robot se modela en tres niveles. Entenderlos evita confusiones al subir código y al lanzar [jobs](/guia/jobs/). ![Listado de procesos en NORA, con carpeta, etiquetas, paquete y versión, timeout, reintentos y SLA de cada proceso.](/_astro/procesos.Cb3qG-BK_Z743Gr.webp) | Concepto | Qué es | Cambia con | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | **Paquete** (`Package`) | El robot como producto: un nombre y una descripción. Agrupa todas sus versiones. | Casi nunca (es la identidad del robot). | | **Release** (versión) | El **código** del robot empaquetado en un ZIP, con su versión, su *entry point* y el hash del archivo. | Cada vez que cambias el código (subes una versión nueva). | | **Proceso** (`Process`) | Lo que **se ejecuta**: apunta a un Release y añade la configuración de ejecución (parámetros, *timeout*, reintentos, *assets* requeridos, carpeta…). | Cuando ajustas configuración o promueves/haces *rollback* a otra versión. | ``` flowchart TD P[Paquete: facturacion] --> R1[Release 1.0.0] P --> R2[Release 1.0.1] P --> R3[Release 1.1.0] PR[Proceso: Facturacion mensual] -->|release_id activo| R3 PR -.->|rollback| R2 ``` Un Paquete tiene muchos Releases (la versión es única por paquete). Un Proceso fija un Release como activo mediante `release_id`; cambiarlo permite promover una versión nueva o volver a una anterior sin tocar el robot. ## Estructura de un robot [Sección titulada «Estructura de un robot»](#estructura-de-un-robot) Un robot es una carpeta con, como mínimo, un *entry point* (por defecto `main.py`) y, normalmente, un `requirements.txt`. Ejemplo real del directorio `examples/rpa-challenge/`: ```text rpa-challenge/ ├── main.py # entry point (orquesta el flujo) ├── requirements.txt # dependencias (playwright>=1.40) ├── rpa_challenge/ # módulos del robot ├── data/ └── nora.json # manifiesto (lo crea/actualiza `nora package`) ``` El *entry point* usa el SDK `nora_agent` para registrar logs, leer *assets*, consumir colas y reportar progreso. Si falta `requirements.txt`, el agente no instalará dependencias (la CLI te avisa). Nunca subas secretos El empaquetador excluye automáticamente entornos virtuales (`.venv/`, `venv/`), cachés (`__pycache__/`, `.pytest_cache/`…), metadatos de VCS/editor y, sobre todo, archivos `.env` y `credentials.json`. Guarda tus credenciales como [Assets](/guia/assets-y-credenciales/), no en el código. ## Empaquetar con la CLI [Sección titulada «Empaquetar con la CLI»](#empaquetar-con-la-cli) La CLI `nora` (paquete `nora_agent`) construye el ZIP de forma segura. Desde la carpeta del robot: ```bash # Vista previa: lista qué se incluiría, sin escribir nada nora package --list # Construye el ZIP en dist/-.zip nora package ``` `nora package` hace tres cosas: 1. **Resuelve la versión** desde el manifiesto `nora.json`. El primer paquete usa `1.0.0`; los siguientes auto-incrementan (`--bump patch|minor|major|none`, por defecto `patch`). Puedes fijarla con `--version 2.0.0`. 2. **Escribe `nora.json`** con `name`, `version` y `entry_point` (se incluye en el ZIP y sirve de fuente de la versión para la próxima vez). 3. **Escanea en busca de secretos** (claves privadas, `nora_ak_…`, tokens, claves AWS). Si encuentra algo, **aborta** antes de escribir el ZIP; usa `--allow-secrets` solo si es un falso positivo. Opciones útiles: `--entry workflow.py` (otro *entry point*), `--exclude '*.csv'` (excluir más archivos, repetible), `--gitignore` (honrar también `.gitignore`). ## Publicar una versión (Release) [Sección titulada «Publicar una versión (Release)»](#publicar-una-versión-release) Con la sesión iniciada (`nora login`), sube el ZIP como Release. `release push` es **solo para administradores** y crea el paquete si no existe: ```bash nora login nora package nora release push # sube dist/-.zip ``` Otros subcomandos: `nora release list`, `nora release delete ` y `nora release download `. ![Listado de paquetes en NORA, con la última versión publicada, el número de releases y los procesos asociados a cada paquete.](/_astro/paquetes.B5Ta9A8j_ZauUdm.webp) ### Bajo el capó: la API de Releases [Sección titulada «Bajo el capó: la API de Releases»](#bajo-el-capó-la-api-de-releases) La CLI llama a estos endpoints (base de producción `https://nora-api.valisoftconsulting.com`, prefijo `/api/v1`, autenticación con sesión *Bearer*). La subida es `multipart/form-data`: ```http POST /api/v1/releases Content-Type: multipart/form-data package_id=&version=1.0.1&entry_point=main.py file=@rpa-challenge-1.0.1.zip ``` El backend valida que el ZIP sea válido, sin rutas con `..` ni absolutas, con tope de 50 MB subidos y 500 MB descomprimidos, y calcula `file_hash` (SHA-256). La respuesta va envuelta en `{"success": true, "data": …}`: ```json { "success": true, "data": { "id": "…", "package_id": "…", "version": "1.0.1", "file_hash": "…", "file_size": 20480, "entry_point": "main.py", "is_active": true, "uploaded_by": "…" } } ``` La descarga (`GET /api/v1/releases/{release_id}/download`) devuelve el ZIP y expone la cabecera `X-Release-Hash` para verificar integridad. ## Crear el Proceso [Sección titulada «Crear el Proceso»](#crear-el-proceso) Un Release es solo código; para ejecutarlo, crea un Proceso que lo apunte. `POST /api/v1/processes` (rol `admin`) recibe, entre otros campos: * `name`, `description`, `release_id` (obligatorio), `folder_id`. * `input_schema`: esquema de los parámetros de entrada del job. Normalmente lo declaras una vez en `nora.json` (`inputs`) y NORA lo deriva solo; con él, el panel de **Ejecutar** muestra un formulario precargado en lugar de un JSON crudo. Detalle completo en [Argumentos de entrada/salida](/guia/argumentos/). * `timeout_seconds` (0 = sin límite), `max_retries`, `auto_retry`, `retry_delay_seconds`. * `sla_deadline_minutes`, `on_success_trigger_process_id` (encadenar procesos), `tags`. * `required_assets`: lista de *assets* que el robot puede leer en tiempo de ejecución. Si no está vacía, el token por job se limita exactamente a esos nombres (un robot comprometido no puede leer el resto de la bóveda del tenant). ![Formulario de parámetros de entrada que NORA genera al ejecutar un proceso, derivado de su input\_schema: campos "mes", "reintentos" y "enviar\_correo" con sus tipos y descripciones.](/_astro/ejecutar-argumentos.B7gYmeUi_ZSNNLE.webp) Para cambiar la versión activa (promover o hacer *rollback*) sin recrear el proceso, usa `PATCH /api/v1/processes/{process_id}/active-release` con `{"release_id": "…"}`. Activar/desactivar un proceso: `PATCH /api/v1/processes/{process_id}/toggle`. ## Listar procesos desde la API pública [Sección titulada «Listar procesos desde la API pública»](#listar-procesos-desde-la-api-pública) Para integraciones externas, NORA expone un endpoint con **API key** (cabecera `X-API-Key: nora_ak_…`), que requiere el *scope* `processes:read` y está limitado a 60 peticiones/minuto: ```bash curl https://nora-api.valisoftconsulting.com/api/v1/processes/list \ -H "X-API-Key: nora_ak_tu_clave_aqui" ``` Devuelve solo procesos activos, paginado (`page`, `limit`, `folder_id` opcionales) y con la envoltura habitual: ```json { "success": true, "data": [ { "id": "…", "name": "Facturacion mensual", "release_id": "…", "is_active": true } ], "meta": { "page": 1, "limit": 20, "total": 1, "pages": 1 } } ``` Con el `id` del proceso ya puedes dispararlo desde la API. Consulta [jobs](/guia/jobs/) y [autenticación](/api/autenticacion/). # Programaciones y triggers > Ejecuta robots por horario (cron, con feriados) o por eventos entrantes mediante triggers de webhook. 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](/guia/jobs/) para el ciclo de vida de la ejecución y [arquitectura](/conceptos/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.](/_astro/programaciones.sOzyx0Oi_6dxX5.webp) ## Programaciones por horario (cron) [Sección titulada «Programaciones por horario (cron)»](#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»](#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»](#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](/api/autenticacion/)). ```http 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 } ``` ```json { "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»](#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»](#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`, `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. Zonas horarias Usa identificadores IANA válidos (por ejemplo `America/Lima`, `Europe/Madrid`, `UTC`). El próximo disparo se calcula en la zona indicada y se persiste convertido a UTC. ## Feriados (holidays) [Sección titulada «Feriados (holidays)»](#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`). ```http POST /api/v1/holidays HTTP/1.1 Content-Type: application/json { "name": "Año Nuevo", "date": "2027-01-01", "recurring": true } ``` ## Triggers por evento (webhook) [Sección titulada «Triggers por evento (webhook)»](#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`**. ![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).](/_astro/trigger-cola.r15My7Ge_Z1vLK3y.webp) ![Disparadores (triggers) en NORA: lista de triggers de tipo webhook con su proceso destino, estado y la indicación de si tienen secreto configurado.](/_astro/disparadores.CvHWIYLj_1aATyA.webp) 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. El secreto solo se muestra una vez El `webhook_secret` se devuelve **al crear el trigger** (y al regenerarlo). En el listado nunca se expone: solo verás `has_webhook_secret`. Guárdalo en un lugar seguro. ### Administrar triggers [Sección titulada «Administrar triggers»](#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`. | ```json { "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»](#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. ```http POST /api/v1/triggers/inbound/AbC123… HTTP/1.1 Host: nora-api.valisoftconsulting.com Content-Type: application/json X-Webhook-Signature: sha256= { "cliente_id": 4821, "origen": "crm" } ``` ```json { "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=`. ```python 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](/api/introduccion/) para los endpoints relacionados. # Ruta de aprendizaje > El camino recomendado para dominar NORA, de cero a producción: instala el agente, publica tu primer robot, y avanza hasta colas, assets, programaciones y flujos avanzados. ¿Primera vez con NORA? Sigue esta ruta en orden. Cada paso construye sobre el anterior: empiezas entendiendo qué es, conectas una máquina, publicas un robot y avanzas hacia las capacidades de orquestación (colas, assets, triggers, flujos). No hace falta leerlo todo de golpe — pero este es el orden que evita saltos. ``` flowchart TD A["1. ¿Qué es NORA?"] --> B["2. Primeros pasos
+ Instalar el agente"] B --> C["3. Procesos y paquetes
(publicar un robot)"] C --> D["4. Jobs + Argumentos
(ejecutar con parámetros)"] D --> E["5. Colas"] E --> F["6. Assets y credenciales"] F --> G["7. Programaciones y triggers
(desatendido)"] G --> H["8. Flujos DAG + Anomalías
(avanzado)"] H --> T["Tutoriales paso a paso"] ``` ## El camino, paso a paso [Sección titulada «El camino, paso a paso»](#el-camino-paso-a-paso) 1. **[¿Qué es NORA?](/conceptos/que-es-nora/)** — la idea y para qué sirve. Si vienes de UiPath o Automation Anywhere, mira también la [arquitectura](/conceptos/arquitectura/). 2. **[Primeros pasos](/guia/primeros-pasos/)** e **[Instalar el agente](/guia/instalacion-agente/)** — crea tu cuenta y conecta tu primera **[máquina](/guia/maquinas/)** (Windows o macOS). 3. **[Procesos y paquetes](/guia/procesos-y-paquetes/)** — empaqueta tu robot con la CLI, publícalo como *Release* y crea el *Proceso* que lo ejecuta. 4. **[Jobs](/guia/jobs/)** y **[Argumentos de entrada/salida](/guia/argumentos/)** — lanza ejecuciones, pásales parámetros con un formulario precargado y recibe resultados. 5. **[Colas](/guia/colas/)** — procesa lotes de trabajo de forma resiliente (el patrón *dispatcher/performer*). 6. **[Assets y credenciales](/guia/assets-y-credenciales/)** — guarda secretos y configuraciones y léelos desde el robot sin hardcodear nada. 7. **[Programaciones y triggers](/guia/programaciones-y-triggers/)** — ejecuta sin tocar nada: por horario (cron), por webhook o cuando llega trabajo a una cola. 8. **[Flujos DAG](/guia/flujos-dag/)** y **[Detección de anomalías](/guia/anomalias/)** — encadena varios procesos y vigila comportamientos raros. Nivel avanzado. ## Cuando quieras construir, no solo leer [Sección titulada «Cuando quieras construir, no solo leer»](#cuando-quieras-construir-no-solo-leer) Empieza por el **[Tutorial 0: tu primer robot mínimo](/tutoriales/primer-robot-minimo/)** (de cero a un robot corriendo en 10 min) y sigue con el resto de **[Tutoriales paso a paso](/tutoriales/rpa-challenge-colas/)**: proyectos reales y guiados (el RPA Challenge con colas, automatización con assets, y patrones avanzados). Y ten siempre a mano la **[lámina de comandos](/referencia/lamina-comandos/)** como chuleta del SDK y la CLI. # Migración desde Automation Anywhere > Mapa de conceptos de Automation Anywhere a NORA y pasos para reescribir, empaquetar y orquestar tus bots en Python. Si llegas desde Automation Anywhere (A360 / Control Room), encontrarás equivalencias claras en NORA. La diferencia central es que **los robots de NORA son Python nativo**: no hay TaskBots ni MetaBots construidos en un editor visual. Reescribes la automatización en Python, la empaquetas y NORA la ejecuta, programa y controla en tus máquinas Windows y macOS. Esta guía traduce la terminología y propone un plan de migración. Si la nomenclatura de NORA te resulta nueva, empieza por el [glosario](/conceptos/glosario/). ## Mapa de conceptos [Sección titulada «Mapa de conceptos»](#mapa-de-conceptos) | Automation Anywhere | NORA | Notas | | --------------------------- | -------------------------- | -------------------------------------------------------------------------------- | | Control Room | **Robots Center** | El centro que orquesta y controla todos tus robots desde la plataforma. | | Bot / TaskBot | **Paquete** | Tu código Python empaquetado. Cada subida genera una versión (Release). | | Process / Process Discovery | **Proceso** | Unidad ejecutable que apunta a un Release de un Paquete. | | Queue (WLM) | **Cola** | Trabajo pendiente con ítems (items) que los robots consumen. | | Credential / Locker | **Asset / Credencial** | Valores y secretos cifrados, separados por entorno (`environment`). | | Bot Runner / Bot Agent | **Agente + Máquina** | El Agente es el proceso instalado; la Máquina es el host registrado donde corre. | | Schedule / Trigger | **Programación / Trigger** | Ejecución por calendario (cron) o por evento/webhook. | | Activity / Deployment | **Job** | Una ejecución concreta de un Proceso en una Máquina. | | Folder | **Carpeta (folder)** | Agrupa procesos para organización y permisos. | ## Diferencias clave [Sección titulada «Diferencias clave»](#diferencias-clave) * **Python nativo, sin editor de bots.** En Automation Anywhere construyes TaskBots con acciones arrastrables. En NORA escribes Python normal con las librerías que prefieras (`playwright`, `selenium`, `pyautogui`, `requests`, `pandas`, etc.) y registras eventos con el SDK de NORA en lugar de la acción `Log To File`. * **Ejecución en tus máquinas.** El Agente de NORA se instala en tus equipos **Windows y macOS** y ejecuta los robots ahí. NORA orquesta; el cómputo corre en tu infraestructura. * **Integración por API pública.** Donde A360 usa la API del Control Room, NORA ofrece una [API pública](/api/introduccion/) con cabecera `X-API-Key` (claves con prefijo `nora_ak_`) y *scopes* por recurso (`jobs:write`, `queues:write`, etc.). La feature de API keys está disponible en los planes **Pro** y **Enterprise**. * **Asignación de Bot Runner simplificada.** Al disparar un Job por API puedes omitir `machine_id`: NORA elige automáticamente una máquina activa y en línea del tenant, en lugar de gestionar device pools. ## Pasos sugeridos de migración [Sección titulada «Pasos sugeridos de migración»](#pasos-sugeridos-de-migración) ``` flowchart TD A[Reimplementar la logica en Python] --> B[Empaquetar el bot] B --> C[Subir el Paquete a NORA] C --> D[Crear un Proceso sobre el Release] D --> E[Configurar Colas y Credenciales] E --> F[Programar con Programacion o Trigger] F --> G[Ejecutar y monitorear Jobs] ``` 1. **Reimplementa la lógica en Python.** Traduce cada acción del TaskBot a código. Las credenciales del Locker pasan a ser **Assets/Credenciales** de NORA (leídos por API o SDK); las acciones de log se sustituyen por el logging del SDK (niveles `info` / `warning` / `error`). Revisa [procesos y paquetes](/guia/procesos-y-paquetes/) para la estructura esperada. 2. **Empaqueta el bot.** Reúne código y dependencias en un Paquete. Al subirlo, NORA crea un **Release** versionado. 3. **Sube el Paquete** a NORA desde la plataforma. 4. **Crea un Proceso** que apunte al Release. Aquí defines `timeout_seconds`, reintentos (`max_retries`, `auto_retry`), `input_schema` y los `required_assets`. 5. **Configura Colas y Credenciales** equivalentes a tus Queues (WLM) y Lockers, separando por entorno. 6. **Programa la ejecución** con una **Programación** (cron) o un **Trigger** (evento/webhook), o dispara bajo demanda por API. ### Alimentar una Cola por API (equivalente a cargar work items) [Sección titulada «Alimentar una Cola por API (equivalente a cargar work items)»](#alimentar-una-cola-por-api-equivalente-a-cargar-work-items) ```bash curl -X POST "https://nora-api.valisoftconsulting.com/api/v1/queues/by-name/facturas/items" \ -H "X-API-Key: nora_ak_..." \ -H "Content-Type: application/json" \ -d '{ "data": {"numero": "F-001", "monto": 1250.50}, "priority": 3, "reference": "F-001" }' ``` Respuesta (envuelta en `data`): ```json { "success": true, "data": { "id": "7c6d5e4f-...", "queue_id": "2b3c4d5e-...", "status": "new", "priority": 3, "reference": "F-001", "data": {"numero": "F-001", "monto": 1250.50}, "created_at": "2026-06-19T12:00:00Z" } } ``` Sin conversión automática NORA no importa ni convierte archivos `.atmx` / `.bot` de Automation Anywhere. La lógica se **reescribe** en Python. Aprovecha para consolidar TaskBots y MetaBots dispersos en módulos Python reutilizables. ## Siguientes pasos [Sección titulada «Siguientes pasos»](#siguientes-pasos) * [Glosario de conceptos](/conceptos/glosario/) * [Procesos y paquetes](/guia/procesos-y-paquetes/) * [Introducción a la API](/api/introduccion/) # Migración desde UiPath > Mapa de conceptos de UiPath a NORA y pasos para reescribir, empaquetar y orquestar tus automatizaciones en Python. Si vienes de UiPath, los conceptos que ya conoces (Orchestrator, Processes, Queues, Assets, Robots) tienen un equivalente directo en NORA. La diferencia de fondo es que **los robots de NORA son Python nativo**: no hay diseñador visual ni actividades arrastrables. Reescribes la lógica en Python, la empaquetas y NORA se encarga de ejecutarla, programarla y controlarla en tus máquinas Windows y macOS. Esta guía te ayuda a traducir tu modelo mental y a planificar la migración. Si aún no conoces la terminología de NORA, revisa primero el [glosario](/conceptos/glosario/). ## Mapa de conceptos [Sección titulada «Mapa de conceptos»](#mapa-de-conceptos) | UiPath | NORA | Notas | | ------------------ | -------------------------- | ---------------------------------------------------------------------------------- | | Orchestrator | **Robots Center** | El centro que orquesta y controla todos tus robots desde la plataforma. | | Process | **Proceso** | Unidad ejecutable. En NORA apunta a una versión publicada (Release) de un Paquete. | | Queue | **Cola** | Trabajo pendiente con ítems (items) que los robots consumen. | | Asset / Credential | **Asset / Credencial** | Valores y secretos cifrados, separados por entorno (`environment`). | | Robot / Bot Runner | **Agente + Máquina** | El Agente es el proceso instalado; la Máquina es el host registrado donde corre. | | Package (`.nupkg`) | **Paquete** | Tu código Python. Cada subida genera una versión (Release). | | Trigger / Schedule | **Trigger / Programación** | Disparo por evento/webhook o ejecución por calendario (cron). | | Job | **Job** | Una ejecución concreta de un Proceso en una Máquina. | | Folder | **Carpeta (folder)** | Agrupa procesos para organización y permisos. | ## Diferencias clave [Sección titulada «Diferencias clave»](#diferencias-clave) * **Python nativo, sin diseñador visual.** En UiPath modelas el flujo con actividades en Studio. En NORA escribes Python normal: usas las librerías que quieras (`playwright`, `selenium`, `pyautogui`, `requests`, `pandas`, etc.) y registras eventos con el SDK de NORA en lugar de `Log Message`. * **Ejecución en tus máquinas.** El Agente de NORA se instala en tus equipos **Windows y macOS** y ejecuta los robots ahí. NORA orquesta; el cómputo es tuyo. * **Integración por API pública.** Donde UiPath usa la API de Orchestrator, NORA expone una [API pública](/api/introduccion/) con cabecera `X-API-Key` (claves con prefijo `nora_ak_`) y *scopes* por recurso (`jobs:write`, `queues:read`, etc.). La feature de API keys está disponible en los planes **Pro** y **Enterprise**. * **Asignación de máquina opcional.** Al disparar un Job por API puedes omitir `machine_id`: NORA selecciona automáticamente una máquina activa y en línea del tenant. ## Pasos sugeridos de migración [Sección titulada «Pasos sugeridos de migración»](#pasos-sugeridos-de-migración) ``` flowchart TD A[Reimplementar la lógica en Python] --> B[Empaquetar el robot] B --> C[Subir el Paquete a NORA] C --> D[Crear un Proceso sobre el Release] D --> E[Configurar Colas y Assets] E --> F[Programar con Trigger o Programación] F --> G[Ejecutar y monitorear Jobs] ``` 1. **Reimplementa la lógica en Python.** Traduce cada actividad de UiPath a código. Las lecturas de `Asset`/`Credential` pasan a leerse vía API o SDK; los `Log Message` se sustituyen por el logging del SDK de NORA (niveles `info` / `warning` / `error`). Revisa [procesos y paquetes](/guia/procesos-y-paquetes/) para la estructura esperada. 2. **Empaqueta el robot.** Reúne tu código y dependencias en un Paquete. Al subirlo, NORA crea un **Release** versionado. 3. **Sube el Paquete** a NORA desde la plataforma. 4. **Crea un Proceso** que apunte al Release. Aquí defines `timeout_seconds`, reintentos (`max_retries`, `auto_retry`), `input_schema` y los `required_assets`. 5. **Configura Colas y Assets** equivalentes a tus Queues y Assets de UiPath, separando por entorno. 6. **Programa la ejecución** con un **Trigger** (evento/webhook) o una **Programación** (cron), o dispara bajo demanda por API. ### Disparar un Proceso por API (equivalente a “Start Job”) [Sección titulada «Disparar un Proceso por API (equivalente a “Start Job”)»](#disparar-un-proceso-por-api-equivalente-a-start-job) ```bash curl -X POST "https://nora-api.valisoftconsulting.com/api/v1/jobs/trigger" \ -H "X-API-Key: nora_ak_..." \ -H "Content-Type: application/json" \ -d '{ "process_id": "b3f1c2d4-5678-4abc-9012-3456789abcde", "input_data": {"cliente": "ACME"}, "priority": 3 }' ``` Respuesta (envuelta en `data`): ```json { "success": true, "data": { "id": "9a8b7c6d-...", "process_id": "b3f1c2d4-...", "machine_id": null, "status": "pending", "priority": 3, "created_at": "2026-06-19T12:00:00Z" } } ``` Sin conversión automática NORA no importa ni convierte proyectos `.xaml` de UiPath. La lógica se **reescribe** en Python. Es una oportunidad para simplificar flujos y eliminar actividades que en Python son una línea de código. ## Siguientes pasos [Sección titulada «Siguientes pasos»](#siguientes-pasos) * [Glosario de conceptos](/conceptos/glosario/) * [Procesos y paquetes](/guia/procesos-y-paquetes/) * [Introducción a la API](/api/introduccion/) # Lámina de comandos > Referencia rápida de toda la superficie de NORA: funciones del SDK (nora_agent), comandos del CLI (nora), variables de entorno, enums y endpoints de la API pública con scopes. Toda la capacidad de NORA de un vistazo: el SDK del robot, el CLI, las variables que el agente inyecta, los enums y los endpoints públicos. Para la explicación a fondo de cada cosa, sigue el enlace de cada bloque. ## SDK del robot — `from nora_agent import sdk` [Sección titulada «SDK del robot — from nora\_agent import sdk»](#sdk-del-robot--from-nora_agent-import-sdk) La forma oficial de que un robot hable con NORA. Las funciones marcadas con 🔒 requieren un **job gestionado** (`NORA_JOB_ID` presente); fuera de él lanzan `RuntimeError`. El resto (colas, assets, logging, progreso) funciona también bajo `nora dev run` con `NORA_EXEC_TOKEN`. ### Logging y progreso [Sección titulada «Logging y progreso»](#logging-y-progreso) | Función | Firma | Qué hace | | ----------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | `log` | `log(level: str, message: str, data: dict \| None = None) -> None` | Envía una línea de log estructurada al dashboard (en dev local imprime por stdout). | | `update_progress` | `update_progress(percent: int, message: str \| None = None) -> None` | Mueve la barra de progreso del job (0–100). | ### Info del job y señales de control [Sección titulada «Info del job y señales de control»](#info-del-job-y-señales-de-control) | Función | Firma | Qué hace | | ---------------- | ----------------------------- | ------------------------------------------------------------------------- | | `get_job_id` | `get_job_id() -> str \| None` | ID del job actual; `None` en dev local. | | `get_job_signal` | `get_job_signal() -> str` 🔒 | Señal del job: `"none"`, `"stop"` o `"kill"`. | | `should_stop` | `should_stop() -> bool` 🔒 | `True` si el operador pidió Stop/Kill. Chéquealo dentro de bucles largos. | ### Argumentos de entrada/salida [Sección titulada «Argumentos de entrada/salida»](#argumentos-de-entradasalida) | Función | Firma | Qué hace | | ------------ | --------------------------------------------------- | ---------------------------------------------------------------- | | `get_input` | `get_input(name: str \| None = None, default=None)` | Valor de un argumento (o el dict completo si `name` es `None`). | | `get_inputs` | `get_inputs() -> dict` | Todos los argumentos de entrada del job. | | `set_output` | `set_output(key_or_dict, value=None) -> None` | Reporta/mergea resultados en `output_data` (visibles en el job). | > Se declaran en `nora.json` (`inputs`/`outputs`); ver [argumentos](/guia/argumentos/). ### Assets (credenciales/config cifradas) [Sección titulada «Assets (credenciales/config cifradas)»](#assets-credencialesconfig-cifradas) | Función | Firma | Qué hace | | ----------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `get_asset` | `get_asset(name: str, environment: str = "production") -> dict` | Asset descifrado por nombre → `{name, type, environment, value, username?}` (`value` tipado según el tipo). | ### Colas (queues) [Sección titulada «Colas (queues)»](#colas-queues) | Función | Firma | Qué hace | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `get_queue_item` | `get_queue_item(queue_name: str) -> dict \| None` | Reclama el siguiente item (o `None` si está vacía). | | `queue_pending` | `queue_pending(queue_name: str) -> int` | Cuántos items quedan claimables (status `new`). | | `queue_stats` | `queue_stats(queue_name: str) -> dict[str, int]` | Conteo por estado, sin consumir nada. | | `complete_queue_item` | `complete_queue_item(queue_name: str, item_id: str, result: dict) -> None` | Marca el item completado con su resultado. | | `fail_queue_item` | `fail_queue_item(queue_name: str, item_id: str, error_message: str, exception_type="system") -> None` | Marca el item fallido. `system` = reintenta hasta `max_retries`; `business` = terminal (no reintenta). | | `add_queue_item` | `add_queue_item(queue_name: str, data: dict, priority: int = 3, reference=None, deadline=None, postpone=None) -> dict` | Encola un único item (con `reference`/`deadline`/`postpone` opcionales). | | `add_queue_items` | `add_queue_items(queue_name: str, items: list[dict], priority: int = 3) -> int` | Encola varios con la misma `priority`; devuelve cuántos. | | `send_queue_item_for_review` | `send_queue_item_for_review(queue_name: str, item_id: str) -> dict` | Manda el item a revisión humana (`pending_review`). | | `wait_for_queue_review` | `wait_for_queue_review(queue_name: str, item_id: str, poll_interval=5.0, timeout=3600.0) -> str` | Bloquea hasta `"approved"` o `"rejected"`. | > Las funciones de cola y de assets no necesitan job gestionado: corren bajo `nora dev run` (solo requieren `NORA_EXEC_TOKEN`). ### Input atendido (human-in-the-loop) 🔒 [Sección titulada «Input atendido (human-in-the-loop) 🔒»](#input-atendido-human-in-the-loop-) | Función | Firma | Qué hace | | --------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | `ask_user` | `ask_user(prompt: str, options: list[str] \| None = None, poll_interval=5.0, timeout=3600.0) -> Any` | Atajo: pide un dato al operador y **bloquea** hasta la respuesta. | | `request_user_input` | `request_user_input(prompt: str, options: list[str] \| None = None) -> dict` | Lanza la solicitud (no bloquea). | | `wait_for_user_input` | `wait_for_user_input(poll_interval=5.0, timeout=3600.0) -> Any` | Bloquea hasta que el operador responda. | > `ask_user(...)` = `request_user_input(...)` + `wait_for_user_input(...)`. ### Mantenimiento del agente [Sección titulada «Mantenimiento del agente»](#mantenimiento-del-agente) | Función | Firma | Qué hace | | ------------------ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `check_for_update` | `check_for_update(current_version: str) -> dict` | ¿Hay versión más nueva del agente? Devuelve `{update_available, latest_version, is_mandatory, changelog, download_url_macos, download_url_windows}`. | Detalle de uso y buenas prácticas en [SDK de robots y logging](/api/sdk-robots/). ## CLI `nora` — `pip install nora-sdk` [Sección titulada «CLI nora — pip install nora-sdk»](#cli-nora--pip-install-nora-sdk) | Comando | Flags clave (default) | Qué hace | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `nora login` | `--api-url` (default `https://nora-api.valisoftconsulting.com/api/v1`, override con env `NORA_API_URL`), `--password`, `--email` | Inicia sesión (por navegador; `--password` para headless). | | `nora logout` | — | Olvida la sesión guardada. | | `nora dev run ` | `--environment dev`, `--assets`, `--ttl 1800`, `--input '{...}'` | Ejecuta el robot local con datos en vivo (token de dev corto). `--input` pasa argumentos (`NORA_INPUT`). | | `nora dev env` | `--format dotenv`, `--write `, `--ttl 28800`, `--environment dev` | Escribe `NORA_API_URL` + `NORA_EXEC_TOKEN` para depurar en el IDE. | | `nora package [path]` | `--entry main.py`, `--bump patch`, `--version`, `--name`, `--exclude`, `--gitignore`, `--allow-secrets`, `--list` | Empaqueta el robot en `.zip` (excluye venv/cachés/secretos; auto-incrementa versión). | | `nora release push [path]` | `--package`, `--version`, `--entry`, `--file`, `--no-create` | Sube el `.zip` como release (crea el paquete si no existe). | | `nora release list [path]` | `--package` | Lista las versiones subidas. | | `nora release delete ` | `--package` | Elimina una versión (admin). | | `nora release download ` | `--package`, `-o` | Descarga el `.zip` de una versión. | Flujo de despliegue completo en [primeros pasos](/guia/primeros-pasos/) y [procesos y paquetes](/guia/procesos-y-paquetes/). ## Variables de entorno (las inyecta el agente) [Sección titulada «Variables de entorno (las inyecta el agente)»](#variables-de-entorno-las-inyecta-el-agente) El robot las lee con `os.environ.get(...)`. **Nunca** se le pasan los secretos del agente (p. ej. `NORA_MACHINE_KEY`). | Variable | Contiene | | -------------------------------------------- | ------------------------------------------------------------------------------- | | `NORA_JOB_ID` | ID del job que lanzó el robot (ausente en dev local). | | `NORA_API_URL` | URL base de la API (`https://nora-api.valisoftconsulting.com/api/v1`). | | `NORA_EXEC_TOKEN` | Token de ejecución de corta vida, acotado a este job y sus assets/colas. | | `NORA_ASSETS` | JSON con los assets precargados del proceso (`get_asset` los sirve sin red). | | `NORA_INPUT` | JSON con los argumentos de entrada del job (`get_input`/`get_inputs` los leen). | | `NORA_DISPLAY_WIDTH` / `NORA_DISPLAY_HEIGHT` | Resolución configurada en la máquina (úsala antes que la del SO). | | `NORA_DISPLAY_DEPTH` / `NORA_DISPLAY_SCALE` | Profundidad de color (bits) y escala DPI (%). | | `NORA_SESSION_MODE` | `"rdp"` (sesión RDP loopback) o `"console"` (consola física). | | `NORA_UNATTENDED` | `"1"` si la máquina corre desatendida (auto-login), `"0"` si atendida. | ## Enums y constantes [Sección titulada «Enums y constantes»](#enums-y-constantes) **Estados de un item de cola** | Estado | Significado | | ---------------- | ------------------------------------------------------- | | `new` | Listo para ser reclamado. | | `in_progress` | Un robot lo está procesando. | | `pending_review` | Pausado, esperando aprobación humana. | | `completed` | Procesado con éxito (lleva `result`). | | `failed` | Falló o fue rechazado (reintenta si quedan reintentos). | | `dead_letter` | Superó `max_retries`; ya no se reintenta solo. | ``` stateDiagram-v2 [*] --> new new --> in_progress in_progress --> completed in_progress --> pending_review in_progress --> failed pending_review --> new : aprobado pending_review --> failed : rechazado failed --> dead_letter : sin reintentos failed --> new : reintento ``` **Prioridad de items:** `1` = baja · `3` = normal (por defecto) · `5` = urgente. **Niveles de log:** `info` · `warning` · `error` (recomendados; el SDK los normaliza a mayúsculas). **Tipos de asset:** `text` · `credential` (usuario+valor) · `secret` · `vault` (bóveda externa) · `integer` · `number` · `bool` (el SDK devuelve el `value` ya tipado). **Tipos de trigger:** `webhook` · `queue` (item nuevo → arranca job) · `file_watcher` · `email_watcher`. **Excepción de cola:** `system` (transitoria, reintenta) · `business` (dato inválido, terminal). **Estados de job:** `pending` · `assigned` · `running` · `completed` · `failed` · `cancelled`. ## API pública (X-API-Key) — operaciones más usadas [Sección titulada «API pública (X-API-Key) — operaciones más usadas»](#api-pública-x-api-key--operaciones-más-usadas) Base: `https://nora-api.valisoftconsulting.com/api/v1`. Respuestas envueltas en `{"success": true, "data": ...}`. Cada key declara **scopes** (`recurso:acción`). | Acción | Endpoint | Scope | `curl` | | ---------------------------------------- | --------------------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------- | | Disparar un job | `POST /jobs/trigger` | `jobs:write` | `curl -X POST .../jobs/trigger -H "X-API-Key: nora_ak_…" -d '{"process_id":"…"}'` | | Consultar un job | `GET /jobs/{id}` | `jobs:read` | `curl .../jobs/ -H "X-API-Key: nora_ak_…"` | | Detener un job | `POST /jobs/{id}/stop` | `jobs:stop` | `curl -X POST .../jobs//stop -H "X-API-Key: nora_ak_…"` | | Listar procesos (descubrir `process_id`) | `GET /processes/list` | `processes:read` | `curl .../processes/list -H "X-API-Key: nora_ak_…"` | | Listar máquinas | `GET /machines/list` | `machines:read` | `curl .../machines/list -H "X-API-Key: nora_ak_…"` | | Cargar items en lote | `POST /queues/by-name/{name}/items/bulk` | `queues:write` | `curl -X POST .../queues/by-name//items/bulk -H "X-API-Key: nora_ak_…" -d @items.json` | | Encolar un item | `POST /queues/by-name/{name}/items` | `queues:write` | `curl -X POST .../queues/by-name//items -H "X-API-Key: nora_ak_…" -d '{"data":{…}}'` | | Listar items de una cola | `GET /queues/by-name/{name}/items` | `queues:read` | `curl .../queues/by-name//items -H "X-API-Key: nora_ak_…"` | | Leer un asset | `GET /assets/by-name/{name}?environment=production` | `assets:read` | `curl ".../assets/by-name/?environment=production" -H "X-API-Key: nora_ak_…"` | | Webhook por proceso | `POST /webhooks/trigger/{process_id}` | — (cualquier key válida; feature `webhooks`) | `curl -X POST .../webhooks/trigger/ -H "X-API-Key: nora_ak_…" -d '{…}'` | > Programaciones (cron) y administración de triggers se gestionan con tu **sesión del dashboard** (no `X-API-Key`); solo el **webhook entrante** por token es público. Ver [autenticación y scopes](/api/autenticacion/), [disparar jobs](/api/disparar-jobs/), [colas vía API](/api/colas/) y [webhooks](/api/webhooks/). ## Enlaces [Sección titulada «Enlaces»](#enlaces) * [SDK de robots y logging](/api/sdk-robots/) · [Colas](/guia/colas/) · [Assets](/guia/assets-y-credenciales/) * [Programaciones y triggers](/guia/programaciones-y-triggers/) · [Flujos DAG](/guia/flujos-dag/) · [Anomalías](/guia/anomalias/) * [Tutoriales](/tutoriales/rpa-challenge-colas/) · [Operar NORA con IA](/referencia/operar-con-ia/) # Operar NORA con IA > Cómo darle a un asistente de IA todo el contexto de NORA y un recetario tarea→llamada para que pueda operar la plataforma de extremo a extremo: robots, colas, assets, disparo y vigilancia. Esta página tiene dos usos: que **una IA** (un agente, un copiloto) sepa operar NORA, y que **tú** le pegues el contexto correcto para que te ayude a construir robots e integraciones sin adivinar. ## Dale el contexto completo a tu IA [Sección titulada «Dale el contexto completo a tu IA»](#dale-el-contexto-completo-a-tu-ia) NORA publica su documentación en formato [llms.txt](https://llmstxt.org/), pensado para modelos de lenguaje: | Archivo | Qué es | Cuándo usarlo | | ------------------------------------ | -------------------------------------------------------- | ---------------------------------------------------------- | | [`/llms.txt`](/llms.txt) | Índice: títulos, descripciones y enlaces de toda la doc. | Para que la IA sepa **qué** existe y navegue. | | [`/llms-full.txt`](/llms-full.txt) | **Volcado completo** de toda la doc en Markdown. | Para pegar **todo** el conocimiento de NORA como contexto. | | [`/llms-small.txt`](/llms-small.txt) | Versión compacta. | Cuando la ventana de contexto es limitada. | ## Modelo mental (en 4 frases) [Sección titulada «Modelo mental (en 4 frases)»](#modelo-mental-en-4-frases) 1. **Una máquina** Windows/macOS con el **agente** instalado es donde corre el robot; debe estar **Online**. 2. **Un proceso** es tu código Python (un release) que el agente ejecuta como un **job**. 3. **Una cola** reparte unidades de trabajo entre robots; **los assets** guardan credenciales cifradas que el robot lee en runtime. 4. El job se **dispara** a mano, por **cron** o por **webhook/API**, y se **vigila** con logs, progreso y detección de anomalías. El robot habla con la plataforma con `from nora_agent import sdk`. Los sistemas externos hablan con la **API pública** (`https://nora-api.valisoftconsulting.com/api/v1`, cabecera `X-API-Key`, respuestas `{"success": true, "data": ...}`). ## Recetario tarea → llamada [Sección titulada «Recetario tarea → llamada»](#recetario-tarea--llamada) Cada receta dice el objetivo y la llamada exacta. Las firmas completas están en la [Lámina de comandos](/referencia/lamina-comandos/). ### Dentro de un robot (SDK `nora_agent`) [Sección titulada «Dentro de un robot (SDK nora\_agent)»](#dentro-de-un-robot-sdk-nora_agent) | Quiero… | Llamada | Ver | | ----------------------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | Loguear un hito | `sdk.log("info", "mensaje", {"clave": valor})` | [SDK](/api/sdk-robots/) | | Reportar avance | `sdk.update_progress(50, "mitad")` | [SDK](/api/sdk-robots/) | | Leer un argumento de entrada | `mes = sdk.get_input("mes")` (declarado en `nora.json`) | [Argumentos](/guia/argumentos/) | | Devolver un resultado | `sdk.set_output({"total": 142})` | [Argumentos](/guia/argumentos/) | | Leer una credencial | `cred = sdk.get_asset("portal"); cred["username"], cred["value"]` | [Assets](/guia/assets-y-credenciales/) | | Tomar el siguiente item de una cola | `item = sdk.get_queue_item("MiCola")` (`None` si vacía) | [Colas](/guia/colas/) | | Cerrar un item OK / con error | `sdk.complete_queue_item("MiCola", item["id"], {...})` · `sdk.fail_queue_item(..., "motivo")` | [Colas](/guia/colas/) | | Encolar trabajo desde el robot | `sdk.add_queue_items("MiCola", lista_de_dicts)` | [Colas](/guia/colas/) | | Pedir aprobación humana de un item | `sdk.send_queue_item_for_review("MiCola", item["id"])` + `sdk.wait_for_queue_review(...)` | [Colas](/guia/colas/#aprobaci%C3%B3n-humana-human-in-the-loop) | | Preguntarle algo al operador | `sdk.ask_user("¿Continúo?", ["Sí", "No"])` | [SDK](/api/sdk-robots/) | | Salir limpio si piden Stop | `if sdk.should_stop(): return` | [Tutorial 3](/tutoriales/patrones-avanzados/) | | Usar la resolución correcta | leer `NORA_DISPLAY_WIDTH/HEIGHT` antes que la del SO | [SDK](/api/sdk-robots/) | ### Desde fuera (API pública, `X-API-Key`) [Sección titulada «Desde fuera (API pública, X-API-Key)»](#desde-fuera-api-pública-x-api-key) | Quiero… | Llamada | Scope | Ver | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------- | -------------------------------------- | | Listar procesos | `GET /processes/list` → `data[].id` | `processes:read` | [Autenticación](/api/autenticacion/) | | Disparar un job | `POST /jobs/trigger` con `{"process_id":"…"}` (`machine_id` opcional: si se omite, NORA elige una máquina online) | `jobs:write` | [Disparar jobs](/api/disparar-jobs/) | | Ver el estado/resultado de un job | `GET /jobs/{id}` (patrón de polling) | `jobs:read` | [Consultar jobs](/api/consultar-jobs/) | | Cargar muchos items en una cola | `POST /queues/by-name/{name}/items/bulk` | `queues:write` | [Colas vía API](/api/colas/) | | Leer un asset | `GET /assets/by-name/{name}?environment=production` | `assets:read` | [Assets vía API](/api/assets/) | | Disparar por webhook fijo | `POST /webhooks/trigger/{process_id}` | *ninguno* (requiere feature webhooks) | [Webhooks](/api/webhooks/) | ### Desde la consola / sesión (no `X-API-Key`) [Sección titulada «Desde la consola / sesión (no X-API-Key)»](#desde-la-consola--sesión-no-x-api-key) | Quiero… | Cómo | Ver | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | Programar por horario | `POST /api/v1/schedules` (cron + zona horaria) | [Programaciones](/guia/programaciones-y-triggers/) | | Crear un trigger de webhook con HMAC | `POST /api/v1/triggers` (devuelve token+secret) | [Triggers](/guia/programaciones-y-triggers/#triggers-por-evento-webhook) | | Disparar el trigger entrante (emisor) | `POST /api/v1/triggers/inbound/{webhook_token}` con cabecera `X-Webhook-Signature: sha256=` (clave = `webhook_secret`); endpoint público (sin `X-API-Key`); el prefijo `sha256=` es **obligatorio** | [Triggers](/guia/programaciones-y-triggers/#triggers-por-evento-webhook) | | Orquestar varios procesos | `POST /api/v1/dags` + `/dags/{id}/execute` | [Flujos DAG](/guia/flujos-dag/) | | Revisar anomalías | `GET /api/v1/anomalies?severity=critical&is_resolved=false` | [Anomalías](/guia/anomalias/) | ## Construir y desplegar un robot (lo que haría una IA) [Sección titulada «Construir y desplegar un robot (lo que haría una IA)»](#construir-y-desplegar-un-robot-lo-que-haría-una-ia) ```bash pip install nora-sdk # SDK + CLI nora login # sesión nora dev run main.py # probar con datos en vivo (sin agente) nora package # empaquetar (auto-versiona, excluye secretos) nora release push # subir como release # luego: crear el Proceso en la consola y lanzar/programar el job ``` Tutorial guiado de punta a punta: [Primer robot (RPA Challenge)](/tutoriales/rpa-challenge-colas/). ## Reglas que una IA debe respetar [Sección titulada «Reglas que una IA debe respetar»](#reglas-que-una-ia-debe-respetar) * **Nunca** poner secretos en el código ni en los datos: usar **assets** (`get_asset`). `nora package` aborta si detecta un secreto. * Loguear con `sdk.log(...)`, **no** con `print()` (en un job gestionado, `print` no llega a la plataforma). * Las funciones que **bloquean** esperando al operador (`ask_user`) y las señales de control (`should_stop`/`get_job_signal`) solo funcionan en un **job gestionado** lanzado desde el Robots Center, no en `nora dev run`. * Cerrar **siempre** cada item de cola (`complete`/`fail`); no dejarlo `in_progress`. * Preferir `NORA_DISPLAY_WIDTH/HEIGHT` a la resolución del SO en máquinas desatendidas. * En `nora dev run`/`dev env` el token está acotado a **dev/staging** (nunca `production`). Como `sdk.get_asset(name)` usa `environment="production"` por defecto, en pruebas locales pasa el entorno explícito: `sdk.get_asset("portal", environment="dev")`; el default `production` solo lee assets dentro de un **job gestionado**. ## Enlaces [Sección titulada «Enlaces»](#enlaces) * [Lámina de comandos](/referencia/lamina-comandos/) — SDK + CLI + endpoints + enums. * [Tutoriales](/tutoriales/rpa-challenge-colas/) — ejemplo real de punta a punta. * [`/llms-full.txt`](/llms-full.txt) — toda la doc para pegar a tu IA. # Tutorial 2: Assets y automatización > Sobre el robot del RPA Challenge: saca las credenciales del código a la bóveda cifrada (assets) y deja de lanzarlo a mano — prográmalo por cron o dispáralo por webhook/API. En el [Tutorial 1](/tutoriales/rpa-challenge-colas/) montaste un robot real que resuelve el RPA Challenge leyendo su trabajo de una cola, y lo lanzaste **a mano**. Aquí lo llevamos al mundo real con dos cosas que todo proceso de producción necesita: 1. **Credenciales fuera del código** — leerlas cifradas desde **assets**. 2. **Disparo automático** — que el robot corra solo, por **horario (cron)** o ante un **evento (webhook/API)**. ## Parte 1 — Credenciales con Assets [Sección titulada «Parte 1 — Credenciales con Assets»](#parte-1--credenciales-con-assets) ### El problema [Sección titulada «El problema»](#el-problema) El RPA Challenge es público, pero **tu proceso real no lo será**: tendrá un portal con usuario y contraseña, una URL de ERP, un token de API. La regla de oro: Nunca pongas secretos en el código Ni en el `.py`, ni en `data/*.json`, ni en el `.env` que empaquetas. `nora package` **aborta** si detecta un secreto. Los secretos van en **assets**: una bóveda cifrada, por organización y por entorno (`dev`/`staging`/`production`). ### Cómo lo lee el robot [Sección titulada «Cómo lo lee el robot»](#cómo-lo-lee-el-robot) Dentro de un job gestionado, el robot pide el asset por nombre y NORA se lo entrega **descifrado**, sin que el robot sepa de claves de API ni de HTTP. El ejemplo ya lo hace para la URL del reto: si existe un asset `rpa-challenge-url`, lo usa; si no, cae a la URL por defecto. ```python # rpa_challenge/workflows.py — _resolve_url() def _resolve_url() -> str: asset = nora.asset(URL_ASSET) or {} # nora.asset envuelve sdk.get_asset url = asset.get("value") or RPA_URL # fallback si el asset no existe if url != RPA_URL: nora.log("info", f"URL tomada del asset '{URL_ASSET}'.") return url ``` `nora.asset(name)` envuelve la función real del SDK: ```python from nora_agent import sdk cred = sdk.get_asset("erp-login") # environment="production" por defecto usuario = cred.get("username") secreto = cred["value"] # valor descifrado, solo durante el job ``` `get_asset` devuelve un `dict` con `{name, type, environment, value, username?}`. No necesitas `X-API-Key` dentro del job: el agente inyecta un token de ejecución con alcance restringido. Detalle en [SDK de robots](/api/sdk-robots/#assets-credenciales-y-configuraci%C3%B3n-cifrada). ### Crear el asset [Sección titulada «Crear el asset»](#crear-el-asset) Los assets se crean solo desde la consola: **Settings → Assets → Nuevo**, con rol `admin` (la API de escritura `POST /api/v1/assets` requiere sesión de dashboard admin, no `X-API-Key`). Por API key los assets solo se **leen** (scope `assets:read`, `GET /api/v1/assets/by-name/{name}`). ![Vista de Assets en NORA: la bóveda cifrada de credenciales y configuración por entorno](/_astro/activos.ljRLpfxf_1ex3gd.webp) Para un portal real usarías `type: "credential"` con `username` + `value`. Un asset `credential` o `secret` **no se puede volver a leer desde la UI** (solo actualizar). ### Declarar los assets que el proceso necesita [Sección titulada «Declarar los assets que el proceso necesita»](#declarar-los-assets-que-el-proceso-necesita) En el **Proceso**, lista los *Assets requeridos* (p. ej. `rpa-challenge-url`). Así el agente los **precarga** y los inyecta en el job; `get_asset` los sirve sin llamada de red y el token de ejecución solo puede leer **esos**, no toda la bóveda. ``` sequenceDiagram participant Bot as Robot (job) participant Ag as Agente participant API as NORA Note over Ag: el proceso declara required_assets Ag->>API: precarga los assets del job API-->>Ag: valores descifrados (scoped) Bot->>Ag: sdk.get_asset("rpa-challenge-url") Ag-->>Bot: { value: "https://..." } (sin red) ``` ## Parte 2 — Automatizar el disparo [Sección titulada «Parte 2 — Automatizar el disparo»](#parte-2--automatizar-el-disparo) Hasta ahora lanzabas el job con **Run**. NORA puede dispararlo solo de dos maneras. Ambas terminan creando un **job** del proceso (ver [jobs](/guia/jobs/)). ``` flowchart LR CRON["Programación (cron)
cada día 7:00"] --> J[Job del proceso] HOOK["Webhook / API
evento externo"] --> J J --> M["Máquina Online
ejecuta el robot"] ``` ### Opción A — Por horario (cron) [Sección titulada «Opción A — Por horario (cron)»](#opción-a--por-horario-cron) Lo más simple es desde el panel: **Programaciones → Nueva**; eliges proceso, días y hora, y el panel arma el `cron` por ti. ![Vista de Programaciones en NORA: lista de disparos por horario (cron) con su proceso, expresión y próxima ejecución](/_astro/programaciones.sOzyx0Oi_6dxX5.webp) El `cron` es una expresión de 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. Si prefieres la API, el equivalente es: ```http POST /api/v1/schedules HTTP/1.1 Host: nora-api.valisoftconsulting.com Content-Type: application/json { "name": "RPA Challenge diario", "process_id": "3f1c…", "cron_expression": "0 7 * * 1-5", "timezone": "America/Lima", "skip_holidays": true } ``` Esto lo corre de lunes a viernes a las 07:00 de Lima. Puntos clave (todos verificados en la guía): * **Zona horaria IANA** (`America/Lima`, `Europe/Madrid`, `UTC`). El disparo se calcula en esa zona y se guarda en UTC. * **No solapa:** si el job anterior sigue corriendo, ese disparo se omite. * **Sin máquina Online:** se omite el ciclo (no hay dónde ejecutar). * **Feriados:** con `skip_holidays: true` salta los días que definas en *Feriados*. Detalle completo y operaciones (`toggle`, `PUT`, `DELETE`) en [programaciones y triggers](/guia/programaciones-y-triggers/). ### Opción B — Por evento (webhook / API) [Sección titulada «Opción B — Por evento (webhook / API)»](#opción-b--por-evento-webhook--api) Cuando el disparador es un sistema externo (“llegó una factura”, “se creó un cliente”), tienes dos caminos: **B.1 — Trigger de webhook (token por trigger, firma HMAC).** Creas un trigger y NORA te da una URL pública con un `webhook_token` y un `webhook_secret`. El sistema externo hace POST y NORA crea el job con el payload como `input_data`: ![Vista de Disparadores (triggers/webhooks) en NORA: URLs públicas por proceso con su token y firma HMAC](/_astro/disparadores.CvHWIYLj_1aATyA.webp) ```http POST /api/v1/triggers/inbound/AbC123… HTTP/1.1 Host: nora-api.valisoftconsulting.com Content-Type: application/json X-Webhook-Signature: sha256= { "lote": "2026-06-26", "origen": "erp" } ``` La firma es obligatoria si el trigger tiene secreto; hay anti-replay y límite de 120 req/min. Ver [programaciones y triggers → triggers](/guia/programaciones-y-triggers/#triggers-por-evento-webhook). **B.2 — Webhook con tu API Key (URL fija por proceso).** Más simple si el llamador es tu propio backend: una `X-API-Key` y el `process_id` en la ruta. ```bash curl -X POST \ "https://nora-api.valisoftconsulting.com/api/v1/webhooks/trigger/5e6f7a8b-…" \ -H "X-API-Key: nora_ak_xxxxxxxxxxxxxxxxxxxx" -H "Content-Type: application/json" \ -d '{ "lote": "2026-06-26" }' ``` Valida el payload contra el JSON Schema del proceso y está limitado a 60 req/min por key. Comparación webhook vs. `/jobs/trigger` en [webhooks](/api/webhooks/). ### ¿Y la cola? [Sección titulada «¿Y la cola?»](#y-la-cola) Cuando automatizas, recuerda **quién llena la cola**. Tienes dos patrones: * **El robot la llena** (como en el Tutorial 1): `load_queue()` carga si está vacía. Ideal cuando los datos los produce el propio robot o un archivo fijo. * **Un productor externo la llena** por API antes (o en vez) de disparar el job, con una key de scope `queues:write`: ```bash curl -X POST \ "https://nora-api.valisoftconsulting.com/api/v1/queues/by-name/RPA-Challenge/items/bulk" \ -H "X-API-Key: nora_ak_..." -H "Content-Type: application/json" \ -d @data/people.json ``` Así el webhook que recibe “llegaron 200 facturas” puede **cargar la cola** y luego **disparar el robot**, que las consume una a una. Ver [colas vía API](/api/colas/). ## Resumen [Sección titulada «Resumen»](#resumen) | Necesidad | Solución | Dónde | | --------------------------------- | ------------------------ | -------------------------------------------------- | | Credenciales fuera del código | **Assets** (`get_asset`) | [assets](/guia/assets-y-credenciales/) | | Correr cada día/hora | **Programación (cron)** | [programaciones](/guia/programaciones-y-triggers/) | | Correr ante un evento externo | **Webhook/Trigger** | [webhooks](/api/webhooks/) | | Llenar la cola desde otro sistema | **API `queues:write`** | [colas vía API](/api/colas/) | ## Siguientes pasos [Sección titulada «Siguientes pasos»](#siguientes-pasos) * **➡️ [Tutorial 3 — Patrones avanzados](/tutoriales/patrones-avanzados/):** revisión humana de items, input atendido, parada limpia, flujos DAG y detección de anomalías. * [Assets y credenciales](/guia/assets-y-credenciales/) — tipos, entornos, vaults externos. * [Programaciones y triggers](/guia/programaciones-y-triggers/) — cron, feriados, HMAC. # Tutorial 3: Patrones avanzados > Sobre el robot del RPA Challenge: revisión humana de items, input atendido, parada limpia con should_stop, orquestación con flujos DAG y detección de anomalías. Con el [Tutorial 1](/tutoriales/rpa-challenge-colas/) (primer robot) y el [Tutorial 2](/tutoriales/assets-y-automatizacion/) (assets + automatización) ya tienes un proceso de producción completo. Esta última parte cubre los patrones que separan un robot que “corre” de uno **gobernable**: que una persona pueda intervenir, que pare limpio, que se orqueste con otros y que te avise cuando algo se desvía. Los tres primeros patrones **ya están en el código** del ejemplo `rpa-challenge` y se activan con flags, sin alterar el flujo por defecto. Requieren un job gestionado La revisión humana y el input atendido **bloquean** esperando una decisión, así que solo funcionan en un **job real** lanzado desde el Robots Center (no en `nora dev run`). Fuera de un job gestionado, el SDK lanzaría `RuntimeError`. ## 1. Parada limpia con `should_stop()` [Sección titulada «1. Parada limpia con should\_stop()»](#1-parada-limpia-con-should_stop) Un operador puede pulsar **Stop** o **Kill** sobre un job en marcha. Un robot bien hecho **lo consulta dentro de su bucle** y sale ordenadamente en vez de morir a medias. El ejemplo lo chequea antes de cada ronda: ```python # rpa_challenge/workflows.py — dentro de solve_challenge() for ronda in range(1, ROUNDS + 1): if nora.should_stop(): # ¿el operador pidió detener? nora.log("warning", f"Detención solicitada; corto en la ronda {ronda}.") break item = nora.claim_next(QUEUE) ... ``` `nora.should_stop()` envuelve `sdk.should_stop()`, que devuelve `True` si la señal del job es `stop` o `kill`. Es **no bloqueante** y seguro: en dev local devuelve `False`, así que no cambia nada. Consúltalo en cualquier bucle largo. ## 2. Input atendido (`ask_user`) [Sección titulada «2. Input atendido (ask\_user)»](#2-input-atendido-ask_user) A veces el robot necesita una decisión humana en mitad del flujo: “¿confirmo este pago?”, “¿qué sucursal proceso?”. `ask_user` **pausa el job**, muestra la pregunta en el dashboard y bloquea hasta la respuesta. En el ejemplo se activa con `RPA_ASK=1`: ```python # rpa_challenge/workflows.py — al inicio de solve_challenge() if ASK_BEFORE_START: if nora.ask_user("¿Empiezo a resolver el RPA Challenge?", ["Sí", "No"]) == "No": nora.log("info", "El operador canceló antes de empezar.") return ``` `ask_user(prompt, options=None, timeout=3600.0)` combina `request_user_input` + `wait_for_user_input` y devuelve **la opción elegida o el texto escrito** por el operador (un `str`), así que puedes compararla directamente. Con `options` muestra botones; sin ellos, un campo de texto. ```bash # Probarlo en producción: RPA_ASK=1 # como variable de entorno del proceso → el job pausa pidiendo confirmación ``` ## 3. Revisión humana de items de cola [Sección titulada «3. Revisión humana de items de cola»](#3-revisión-humana-de-items-de-cola) Distinto de `ask_user`: aquí lo que se aprueba es **un item concreto de la cola**. El robot lo manda a revisión, una persona ve sus `data` en el panel y **aprueba** (continúa) o **rechaza** (se salta). Patrón clásico *human-in-the-loop* para pagos, altas o cualquier paso que exija “cuatro ojos”. Se activa con `RPA_REVIEW=1`: ```python # rpa_challenge/workflows.py — dentro del bucle, antes de rellenar if REVIEW_EACH_ITEM: nora.send_for_review(QUEUE, item) # status -> pending_review if nora.wait_review(QUEUE, item) == "rejected": nora.log("warning", f"Ronda {ronda}: item rechazado; lo salto.") continue ``` ``` sequenceDiagram participant R as Robot participant N as NORA participant H as Revisor R->>N: send_for_review(item) Note over N: status → pending_review N-->>H: notificación en el panel H->>N: Aprobar / Rechazar R->>N: wait_review(item) (bloquea) N-->>R: "approved" / "rejected" ``` Las funciones reales: `sdk.send_queue_item_for_review(queue, item_id)` y `sdk.wait_for_queue_review(queue, item_id, timeout=3600.0)` (devuelve `"approved"` o `"rejected"`, o lanza `TimeoutError`). Quién puede revisar (revisores asignados vs. cualquier `admin`/`operator`) está en [colas → aprobación humana](/guia/colas/#aprobaci%C3%B3n-humana-human-in-the-loop). ## 4. Orquestar varios robots con flujos DAG [Sección titulada «4. Orquestar varios robots con flujos DAG»](#4-orquestar-varios-robots-con-flujos-dag) Un solo robot resuelve un paso. Cuando un resultado de negocio necesita **varios procesos en cierto orden** —extraer → validar → cargar, con un reporte en paralelo— se modela como un **flujo DAG**: nodos (procesos) y aristas (dependencias `source → target`). ![Editor de flujos DAG en NORA: nodos (procesos) conectados por aristas que definen el orden de ejecución](/_astro/flujo-dag.C6i6xZDh_Z1NRQOL.webp) ``` flowchart LR A[Extraer] --> B[Validar] A --> C[Generar reporte] B --> D[Cargar al ERP] C --> D D --> E[Notificar] ``` `B` y `C` corren en paralelo (solo dependen de `A`); `D` espera a ambos. Se crea con `POST /api/v1/dags` y se lanza con `POST /api/v1/dags/{dag_id}/execute`; cada ejecución lleva un `status` global y un `node_states` por nodo (`pending`/`running`/`completed`/ `failed`). Crear y ejecutar flujos DAG se hace desde la consola, o por API **con una sesión de dashboard de rol `admin`/`operator`** (no con `X-API-Key`). Pasa el token de sesión en la cabecera `Authorization: Bearer`: ```http POST /api/v1/dags/3f1c…/execute HTTP/1.1 Host: nora-api.valisoftconsulting.com Authorization: Bearer ``` Cómo aplica al RPA Challenge: podrías separar **cargar la cola** (un proceso productor) de **resolver** (el consumidor) y encadenarlos con una arista. El detalle de nodos, aristas y orden topológico está en [flujos DAG](/guia/flujos-dag/). ## 5. Detección de anomalías [Sección titulada «5. Detección de anomalías»](#5-detección-de-anomalías) No tienes que vigilar los jobs a mano: NORA los analiza **cada hora** y marca lo que se sale de lo normal. Dos detectores activos: ![Vista de Anomalías en NORA: jobs marcados por picos de duración o de tasa de error, con su severidad](/_astro/anomalias.CBmH4xDF_17yCy.webp) | Tipo | Qué detecta | Cómo | | ------------------ | ----------------------------------------- | ---------------------------------------- | | `duration_spike` | Un job que tardó mucho más de lo habitual | z-score vs. media de 30 días | | `error_rate_spike` | Un proceso que de pronto falla más | tasa de error semana actual vs. anterior | Severidad `warning` (`2.0 < z < 3.0`) o `critical` (`z ≥ 3.0`); las **críticas de duración** disparan notificación reutilizando el evento `job_failed`. Si tu robot del RPA Challenge normalmente tarda 40 s y un día tarda 5 min, te enteras sin mirar. Se consultan con tu sesión del dashboard (no `X-API-Key`): ```http GET /api/v1/anomalies?severity=critical&is_resolved=false HTTP/1.1 Host: nora-api.valisoftconsulting.com ``` Detalle de los cálculos y cómo resolverlas en [detección de anomalías](/guia/anomalias/). ## Resumen de la serie [Sección titulada «Resumen de la serie»](#resumen-de-la-serie) Ya recorriste NORA de punta a punta sobre un mismo ejemplo: | Tutorial | Qué añadiste | | ------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | [1 · Primer robot](/tutoriales/rpa-challenge-colas/) | Máquina Online → proceso → **cola**, logs y progreso | | [2 · Assets y automatización](/tutoriales/assets-y-automatizacion/) | **Credenciales** cifradas, **cron** y **webhook/API** | | **3 · Patrones avanzados** | **Stop limpio**, **input atendido**, **revisión humana**, **DAG**, **anomalías** | El esqueleto no cambió en ningún momento: el robot lee de la cola, hace su parte y reporta. Todo lo demás —credenciales, disparo, gobierno, orquestación, vigilancia— lo pone NORA alrededor. Eso es lo que hace que el mismo patrón te sirva para el RPA Challenge hoy y para tu proceso de negocio mañana. ## Referencias [Sección titulada «Referencias»](#referencias) * [SDK de robots y logging](/api/sdk-robots/) — todas las funciones del SDK. * [Colas (queues)](/guia/colas/) · [Flujos DAG](/guia/flujos-dag/) · [Anomalías](/guia/anomalias/) * [Arquitectura y Robots Center](/conceptos/arquitectura/) # Tutorial 0: tu primer robot mínimo > De cero a un robot corriendo en NORA en 10 minutos: crea un robot mínimo, pruébalo en local, publícalo y lánzalo desde el panel. El punto de partida antes de los tutoriales avanzados. Antes de meternos con colas, assets o automatización de navegador, hagamos lo más importante: **ver un robot tuyo corriendo en NORA de principio a fin**. Este robot no hace nada espectacular —saluda y termina— pero recorre **todo el ciclo real**: escribir el código, probarlo en tu máquina, publicarlo y ejecutarlo desde el panel. Cuando esto te funcione, lo demás es solo añadir lógica. ## Antes de empezar [Sección titulada «Antes de empezar»](#antes-de-empezar) Necesitas tres cosas (si ya las tienes, salta al paso 1): 1. Una cuenta de NORA — ver [Primeros pasos](/guia/primeros-pasos/). 2. **Python 3.11+** y el SDK instalado: `pip install nora-sdk`. 3. Una **máquina conectada** (el agente instalado y en línea) — ver [Instalar el agente](/guia/instalacion-agente/). La necesitarás solo en el paso 4, para la ejecución gestionada. ![Lista de Máquinas en el panel de NORA con un agente conectado y en línea.](/_astro/maquinas.vxT1Qgp4_ZomAx2.webp) ## Paso 1 — Crea el proyecto [Sección titulada «Paso 1 — Crea el proyecto»](#paso-1--crea-el-proyecto) Crea una carpeta con **dos archivos**. Eso es un robot de NORA: nada más. ```text mi-primer-robot/ ├── nora.json # el manifiesto: nombre, versión, punto de entrada y argumentos └── main.py # tu código ``` **`nora.json`** — declara el robot y un argumento de entrada opcional (`nombre`): ```json { "name": "mi-primer-robot", "version": "1.0.0", "entry_point": "main.py", "inputs": [ { "name": "nombre", "type": "text", "default": "mundo", "description": "A quién saludar" } ] } ``` **`main.py`** — lee el argumento, deja un par de logs y reporta progreso y un resultado: ```python from nora_agent import sdk def main() -> None: # 1) Leer un argumento de entrada (lo escribes al lanzar; usa el default si no viene). nombre = sdk.get_input("nombre", "mundo") # 2) Registrar mensajes — se ven en vivo en el detalle del job. sdk.log("info", f"¡Hola, {nombre}! Mi primer robot en NORA está corriendo.") # 3) (Aquí iría tu automatización real: abrir una app, leer un Excel, etc.) sdk.update_progress(50, "Trabajando") # 4) Devolver un resultado — queda guardado en el job (pestaña Salida). sdk.set_output("saludo", f"Hola {nombre}") sdk.update_progress(100, "Listo") sdk.log("info", "Terminado ✅") if __name__ == "__main__": main() ``` Eso es todo el robot. `sdk.get_input`, `sdk.log`, `sdk.update_progress` y `sdk.set_output` son las cuatro funciones que usarás en casi todos tus robots. ## Paso 2 — Pruébalo en tu máquina [Sección titulada «Paso 2 — Pruébalo en tu máquina»](#paso-2--pruébalo-en-tu-máquina) No hace falta publicar nada para probarlo. Inicia sesión una vez y ejecútalo en local con datos en vivo: ```bash # Inicia sesión (abre el navegador para autorizar) nora login # Ejecuta el robot pasándole el argumento "nombre" nora dev run main.py --input '{"nombre": "Kathy"}' ``` Verás en tu terminal los logs del robot. `nora dev run` usa un **token de desarrollo corto** y corre **el mismo código** que correrá en producción: lo que funciona aquí, funciona allá. ## Paso 3 — Publícalo [Sección titulada «Paso 3 — Publícalo»](#paso-3--publícalo) Cuando estés conforme, empaqueta y sube el robot. Dos comandos: ```bash # 1) Empaqueta en un .zip (excluye venv, cachés y secretos automáticamente) nora package --entry main.py # 2) Sube el .zip como una versión (Release). Crea el paquete si no existe. nora release push ``` `nora release push` te devuelve el **paquete** y la **versión** publicada. Más detalle en [Procesos y paquetes](/guia/procesos-y-paquetes/). ## Paso 4 — Crea el Proceso y lánzalo [Sección titulada «Paso 4 — Crea el Proceso y lánzalo»](#paso-4--crea-el-proceso-y-lánzalo) Un *Release* es solo código subido; para ejecutarlo necesitas un **Proceso** que lo apunte. En el panel ve a **Procesos → Nuevo proceso**, elige tu paquete `mi-primer-robot` y su versión, ponle un nombre y guárdalo. ![Listado de Procesos en el panel de NORA.](/_astro/procesos.Cb3qG-BK_Z743Gr.webp) Ahora ábrelo y pulsa **Ejecutar**. Como declaraste el argumento `nombre`, NORA te muestra un **formulario precargado**: escribe un valor (o deja el default), elige la máquina y lanza. ![Modal "Ejecutar Proceso" con el formulario de argumentos: el campo "nombre" precargado desde el robot.](/_astro/ejecutar-argumentos.B7gYmeUi_ZSNNLE.webp) ## Paso 5 — Mira el resultado [Sección titulada «Paso 5 — Mira el resultado»](#paso-5--mira-el-resultado) NORA crea el job, lo asigna a tu máquina y el agente ejecuta el robot. Abre el job desde **Trabajos**: verás su estado, el **progreso** que reportaste, los **logs** en vivo y, en la pestaña **Salida**, el `saludo` que devolviste con `set_output`. ![Detalle de un job en NORA con su estado, progreso, logs y la salida del robot.](/_astro/job-detalle.BZuhzA9B_ZjGslx.webp) 🎉 ¡Eso es! Acabas de recorrer el ciclo completo: **escribir → probar → publicar → ejecutar → ver resultado**. Todo lo demás en NORA es añadir capacidades a este mismo esqueleto. ## Siguientes pasos [Sección titulada «Siguientes pasos»](#siguientes-pasos) * **[Argumentos de entrada/salida](/guia/argumentos/)** — profundiza en `get_input` / `set_output` y los tipos de argumentos. * **[Tutorial 1: RPA Challenge con colas](/tutoriales/rpa-challenge-colas/)** — un robot real que lee su trabajo desde una **cola**, con automatización de navegador. * **[Lámina de comandos](/referencia/lamina-comandos/)** — la chuleta del SDK y la CLI. # Tutorial: resolver el RPA Challenge con colas > Ejemplo completo de extremo a extremo: un robot que resuelve rpachallenge.com leyendo su trabajo desde una cola de NORA, paso a paso, del IDE a producción. Este es el ejemplo que une todo lo demás: un robot real, completo y ejecutable que resuelve el clásico [rpachallenge.com](https://rpachallenge.com/) **leyendo su trabajo desde una cola de NORA**. Si vienes de la [guía de colas](/guia/colas/) y te preguntabas “vale, ¿pero cómo se ve esto de principio a fin?”, esta es la página. El código vive en el repositorio de NORA, en `examples/rpa-challenge/`. Aquí lo recorremos pieza por pieza y luego lo ejecutamos de dos formas: **desde tu IDE con datos en vivo** y **en producción gestionado por el agente** — el mismo código, sin cambios. Esta es la **primera parte** de una serie de tres. Aquí montas tu primer robot de punta a punta; luego le sumas [credenciales y automatización](/tutoriales/assets-y-automatizacion/) y, por último, [patrones avanzados](/tutoriales/patrones-avanzados/) (revisión humana, DAG, anomalías). Sobre los `nora.algo(...)` de este tutorial Para que el código se lea limpio, el ejemplo define **sus propios envoltorios** en un módulo `nora.py` (p.ej. `nora.claim_next`, `nora.complete`, `nora.fail`, `nora.enqueue`, `nora.pending`). **No son funciones del SDK**: cada una llama por dentro a la función real (`sdk.get_queue_item`, `sdk.complete_queue_item`, `sdk.fail_queue_item`, `sdk.add_queue_items`, `sdk.queue_pending` — ver la [lámina de comandos](/referencia/lamina-comandos/)). El proyecto completo y ejecutable (incluido ese `nora.py`) está en el repositorio de NORA, en **`examples/rpa-challenge/`**; cópialo de ahí para correrlo tal cual. Si es tu primer robot, haz antes el [Tutorial 0: primer robot mínimo](/tutoriales/primer-robot-minimo/); esta página asume que ya publicaste y ejecutaste un robot sencillo. ## Antes de empezar: el orden correcto [Sección titulada «Antes de empezar: el orden correcto»](#antes-de-empezar-el-orden-correcto) No se empieza por el código. Primero **conectas tu infraestructura** y solo entonces programas y empaquetas. Este es el orden, y **no puedes saltarte ningún paso**: ``` flowchart LR A["1· Cuenta y workspace"] --> B["2· Instalar el agente"] B --> C["3· Registrar la máquina"] C --> D["4· Máquina Online (heartbeat)"] D --> E["5· (opcional) Assets/credenciales"] E --> F["6· Ya sí: programar y empaquetar el robot"] ``` | Paso | Qué haces | Guía | | ---- | ------------------------------------------------------------------------ | ----------------------------------------------------- | | 1 | Crear cuenta, iniciar sesión y tener tu **workspace** | [Primeros pasos](/guia/primeros-pasos/) | | 2 | Instalar el **agente** en una máquina Windows o macOS | [Instalar el agente](/guia/instalacion-agente/) | | 3 | Crear la **máquina** en la consola y pegar su `nora_mk_...` en el agente | [Máquinas](/guia/maquinas/) | | 4 | Esperar a que la máquina aparezca **Online** (heartbeat cada 30 s) | [Máquinas](/guia/maquinas/) | | 5 | *(Si tu robot usa credenciales)* cargar **assets** cifrados | [Assets y credenciales](/guia/assets-y-credenciales/) | Este tutorial empieza en el paso 6 De aquí en adelante **damos por hecho que ya tienes una máquina Online**. Si todavía no la tienes, completa primero los pasos 1–4 en [Primeros pasos](/guia/primeros-pasos/); si intentas correr un robot sin una máquina Online, no habrá dónde ejecutarlo. ## El mapa de NORA [Sección titulada «El mapa de NORA»](#el-mapa-de-nora) Antes de meternos al ejemplo, ten claro **cómo encajan las piezas**. Un robot es solo una de ellas; NORA orquesta todo lo demás alrededor: ![Panel principal de NORA (Robots Center) con el resumen de máquinas, procesos y jobs](/_astro/panel.Be7XX4MT_Zfv2BR.webp) ``` flowchart TD subgraph infra["Tu infraestructura (pasos 1-4)"] M["Máquina + Agente
(Online)"] end subgraph trabajo["Qué ejecuta"] P["Proceso
(release de tu código)"] Q[("Cola
unidades de trabajo")] AS["Assets
(credenciales cifradas)"] end subgraph disparo["Cómo se dispara"] MAN["Manual (Run)"] CRON["Programación (cron)"] HOOK["Webhook / API"] end subgraph obs["Cómo lo vigilas"] J["Jobs + Logs"] AN["Anomalías"] end P --> M Q -.alimenta.-> P AS -.credenciales.-> P MAN --> J CRON --> J HOOK --> J M --> J J --> AN ``` | Pieza | Qué es | Lo ves en | | ------------------------------------- | --------------------------------------- | -------------------------------------------------- | | **Máquina + Agente** | Dónde corre el robot (Win/macOS) | Este tutorial (pasos 1–4) | | **Proceso** | Tu código publicado como release | Este tutorial | | **Cola** | Lista de unidades de trabajo a procesar | **Este tutorial** | | **Assets** | Credenciales/config cifradas | [Tutorial 2](/tutoriales/assets-y-automatizacion/) | | **Programación / Webhook** | Disparar el robot por horario o evento | [Tutorial 2](/tutoriales/assets-y-automatizacion/) | | **Anomalías / revisión humana / DAG** | Vigilancia y orquestación avanzada | [Tutorial 3](/tutoriales/patrones-avanzados/) | Este tutorial cubre **Máquina → Proceso → Cola**. Lo demás se construye encima en los tutoriales 2 y 3. ## El escenario [Sección titulada «El escenario»](#el-escenario) El RPA Challenge muestra un formulario con 7 campos y te pide enviarlo **10 veces**, una por cada registro de un conjunto de datos. La trampa: **en cada ronda reordena los campos**, así que no puedes confiar en su posición — hay que ubicarlos por el texto de su etiqueta. Completar las 10 rondas correctamente da `your success rate: 100%`. Es un banco de pruebas perfecto para colas porque el trabajo es, literalmente, **una lista de unidades homogéneas**: cada registro de persona es un item, cada item es una ronda. Modelarlo con una cola nos da gratis el paralelismo, los reintentos y el registro auditable que describe la [guía de colas](/guia/colas/). ``` flowchart LR D[data/people.json
10 registros] -->|load_queue| Q[(Cola RPA-Challenge)] Q -->|claim_next| R[Robot Playwright] R -->|rellena 1 ronda| W[rpachallenge.com] R -->|complete / fail| Q R -->|progress 10%..100%| N[Robots Center] ``` ## Estructura del proyecto [Sección titulada «Estructura del proyecto»](#estructura-del-proyecto) La idea central: **`main.py` es el workflow principal** que se lee de arriba a abajo e invoca sub-workflows en orden. Cada sub-workflow hace **una** cosa. Todo el trato con NORA se concentra en un módulo (`nora.py`), y la automatización del navegador en otro (`browser.py`), de modo que los workflows quedan limpios. ```plaintext examples/rpa-challenge/ ├── data/people.json # fuente de datos (10 registros) ├── main.py # WORKFLOW PRINCIPAL — orquesta los sub-workflows ├── dispatch.py # atajo opcional: corre solo el sub-workflow de carga ├── requirements.txt # playwright └── rpa_challenge/ ├── config.py # constantes: QUEUE, URL, campos, nº de rondas ├── workflows.py # SUB-WORKFLOWS: load_queue() y solve_challenge() ├── nora.py # acceso a NORA: log/progress + cola └── browser.py # automatización del navegador (Playwright) ``` ## La configuración [Sección titulada «La configuración»](#la-configuración) Todas las constantes en un solo sitio. Fíjate en `FIELD_LABELS`: como el reto reordena los campos, se localizan por el **texto de la etiqueta**, no por posición. rpa\_challenge/config.py ```python from pathlib import Path QUEUE = "RPA-Challenge" # nombre de la cola en el Robots Center RPA_URL = "https://rpachallenge.com/" DATA_FILE = Path(__file__).resolve().parent.parent / "data" / "people.json" ROUNDS = 10 # 10 rondas = 100% # (texto de la etiqueta en la web, clave del registro) FIELD_LABELS = [ ("First Name", "first_name"), ("Last Name", "last_name"), ("Company Name", "company_name"), ("Role in Company", "role"), ("Address", "address"), ("Email", "email"), ("Phone Number", "phone"), ] ``` Los datos son un simple array JSON; cada objeto será un item de la cola (`item["data"]`): ```json // data/people.json (extracto) [ {"first_name": "John", "last_name": "Smith", "company_name": "Acme Corp", "role": "Engineer", "address": "123 Main St", "email": "john.smith@acme.test", "phone": "555-0101"}, {"first_name": "Maria", "last_name": "Garcia", "company_name": "Globex", "role": "Analyst", "address": "456 Oak Ave", "email": "maria.garcia@globex.test", "phone": "555-0102"} ] ``` ## El workflow principal [Sección titulada «El workflow principal»](#el-workflow-principal) `main.py` es el entry point que se sube como release y ejecuta el agente. Se lee de corrido: primero deja constancia de la resolución de pantalla que usará, y luego invoca los dos sub-workflows. ```python # main.py (esencia) from rpa_challenge import nora from rpa_challenge.workflows import load_queue, solve_challenge def main() -> None: nora.log("info", ">> RPA Challenge — workflow principal") load_queue() # Paso 1: asegurar datos en la cola (los carga si faltan) solve_challenge() # Paso 2: procesar la cola y resolver el reto nora.log("info", "<< RPA Challenge — terminado") if __name__ == "__main__": main() ``` ## Sub-workflow 1 — cargar la cola (idempotente) [Sección titulada «Sub-workflow 1 — cargar la cola (idempotente)»](#sub-workflow-1--cargar-la-cola-idempotente) Antes de procesar nada, nos aseguramos de que la cola tenga trabajo. La clave es que sea **idempotente**: si ya hay items pendientes, no recarga. Así puedes re-correr el robot sin duplicar el trabajo. ![Detalle de una cola en NORA con sus estadísticas (Nuevos, En proceso, Pendiente revisión, Completados, Fallidos) y la tabla de items en distintos estados](/_astro/cola-detalle.DxHv7n5p_Zxz8xj.webp) rpa\_challenge/workflows.py ```python import json from . import browser, nora from .config import DATA_FILE, QUEUE, ROUNDS def load_queue() -> None: """Si la cola está vacía, la carga con los registros de data/people.json.""" pendientes = nora.pending(QUEUE) if pendientes > 0: nora.log("info", f"La cola '{QUEUE}' ya tiene {pendientes} pendientes; no recargo.") return registros = json.loads(DATA_FILE.read_text(encoding="utf-8")) nora.log("info", f"Cola vacía -> cargando {len(registros)} registros en '{QUEUE}'.") nora.enqueue(QUEUE, registros) ``` `nora.pending()` envuelve `sdk.queue_pending(queue)` (cuenta los items en estado `new` sin consumir ninguno) y `nora.enqueue()` envuelve `sdk.add_queue_items(queue, records)` (carga en lote). ## Sub-workflow 2 — consumir la cola y resolver [Sección titulada «Sub-workflow 2 — consumir la cola y resolver»](#sub-workflow-2--consumir-la-cola-y-resolver) Aquí está el corazón del patrón productor/consumidor: una ronda del formulario = un item de la cola. Reclamamos, rellenamos, y según el desenlace **completamos** o **fallamos** el item, reportando progreso en cada vuelta. ```python # rpa_challenge/workflows.py (continuación) def solve_challenge() -> None: """1 item de la cola = 1 ronda del formulario. 10 rondas = 100%.""" with browser.session() as page: browser.open_challenge(page) for ronda in range(1, ROUNDS + 1): item = nora.claim_next(QUEUE) if item is None: nora.log("warning", f"Cola vacía en la ronda {ronda} (se necesitan {ROUNDS}).") break try: browser.fill_round(page, item["data"]) nora.complete(QUEUE, item) nora.progress(min(ronda * 10, 100), f"Ronda {ronda}/{ROUNDS}") nora.log("info", f"Ronda {ronda}/{ROUNDS} enviada.") except Exception as e: nora.fail(QUEUE, item, str(e)) raise nora.log("info", f"Resultado: {browser.read_result(page)}") ``` Lo importante del ciclo de vida del item: | Llamada | Qué le pasa al item | SDK por debajo | | ----------------------------- | -------------------------------------------------------- | -------------------------------------------- | | `nora.claim_next(QUEUE)` | `new` → `in_progress` (lo reclama solo este job) | `sdk.get_queue_item(queue)` | | `nora.complete(QUEUE, item)` | `in_progress` → `completed` (guarda `result`) | `sdk.complete_queue_item(queue, id, result)` | | `nora.fail(QUEUE, item, err)` | `in_progress` → `failed` (reintenta hasta `max_retries`) | `sdk.fail_queue_item(queue, id, error)` | Reclama, no leas `claim_next()` **reclama** el item (lo marca `in_progress` para este job), no solo lo lee. Por eso dos robots consumiendo la misma cola nunca toman el mismo item: la plataforma garantiza la exclusión. Si tu robot muere sin cerrar el item, el reintento lo devuelve a `new` para que otro lo retome. ## La automatización del navegador [Sección titulada «La automatización del navegador»](#la-automatización-del-navegador) `browser.py` está aislado de NORA: solo sabe abrir el reto, rellenar una ronda y leer el resultado. Nota cómo `fill_round` ubica cada campo por el texto de su etiqueta —la defensa contra el reordenamiento de cada ronda. ```python # rpa_challenge/browser.py (esencia) def fill_round(page, record: dict) -> None: """Ubica cada campo por el TEXTO de su etiqueta (el reto los reordena).""" for label, key in FIELD_LABELS: page.locator( f"xpath=//label[contains(normalize-space(.), '{label}')]/following::input[1]" ).fill(str(record.get(key, ""))) page.locator("input[value='Submit']").click() ``` El navegador arranca `headless=True` (apto para desatendido). Cámbialo a `False` en `session()` si quieres verlo en pantalla en una máquina con sesión interactiva. ## Ejecutar y depurar desde tu IDE [Sección titulada «Ejecutar y depurar desde tu IDE»](#ejecutar-y-depurar-desde-tu-ide) Esta es la parte que más se agradece: **desarrollas con breakpoints y el robot jala datos en vivo del Robots Center, sin instalar el agente.** El agente es para producción desatendida; para programar, basta un token de desarrollo. Necesitas tu cuenta lista `nora login` y `nora dev run` hablan con tu workspace en el Robots Center. Para que la cola tenga dónde vivir y el robot pueda crear jobs, ya debes haber hecho los pasos 1–4 de [Antes de empezar](#antes-de-empezar-el-orden-correcto) (cuenta + máquina **Online**). Aún **no** necesitas el agente instalado para *depurar* desde el IDE, pero sí una cuenta con sesión iniciada. ### 1. Instalar (una sola vez) [Sección titulada «1. Instalar (una sola vez)»](#1-instalar-una-sola-vez) ```bash pip install nora-sdk # SDK (from nora_agent import sdk) + comando `nora` pip install -r requirements.txt python -m playwright install chromium ``` ### 2. Token y URL para el IDE [Sección titulada «2. Token y URL para el IDE»](#2-token-y-url-para-el-ide) ```bash nora login # abre el navegador: apruebas con tu sesión web nora dev env --write .env # escribe NORA_API_URL + NORA_EXEC_TOKEN (token 8h) # añade .env a tu .gitignore ``` ### 3. Correr [Sección titulada «3. Correr»](#3-correr) `main.py` orquesta todo: carga la cola si está vacía y luego resuelve. ```bash nora dev run main.py # carga (si falta) + consume → "your success rate: 100%" ``` Para depurar con breakpoints en VS Code, apunta una configuración `debugpy` a `main.py` cargando el `.env`, y pon breakpoints en `main.py` o dentro de `load_queue()` / `solve_challenge()`. Cada `nora.claim_next()` traerá un item **real** de la cola. .vscode/launch.json ```json { "version": "0.2.0", "configurations": [ { "name": "RPA Challenge (main)", "type": "debugpy", "request": "launch", "program": "${workspaceFolder}/examples/rpa-challenge/main.py", "envFile": "${workspaceFolder}/examples/rpa-challenge/.env", "cwd": "${workspaceFolder}/examples/rpa-challenge", "console": "integratedTerminal" } ] } ``` ## Ejecutar en producción (gestionado por el agente) [Sección titulada «Ejecutar en producción (gestionado por el agente)»](#ejecutar-en-producción-gestionado-por-el-agente) El mismo código, sin cambios. El flujo es el de [primeros pasos](/guia/primeros-pasos/), resumido para este ejemplo: ``` flowchart TD A[nora package] --> B[nora release push] B --> C[Crear Proceso
entry: main.py] C --> D[Crear cola RPA-Challenge] D --> E[Lanzar job en una máquina con agente] E --> F[Logs en vivo + cola al 100%] ``` 1. **Empaqueta y publica el release** (entry point `main.py`): ```bash nora package # crea dist/rpa-challenge-1.0.0.zip con SOLO el código nora release push # crea el paquete si no existe y sube el zip ``` `nora package` auto-incrementa la versión, excluye `.venv`/cachés/secretos y aborta si detecta un `.env` o claves reales. Usa siempre **la versión que te imprime** (el Robots Center exige versión única por release). Detalle en [procesos y paquetes](/guia/procesos-y-paquetes/). 2. **Crea un Proceso** apuntando a esa release (*Assets requeridos*: vacío). Se hace en la consola (Processes → elegir paquete/versión) o por API con rol admin (`release_id` obligatorio); ver [Crear el Proceso](/guia/procesos-y-paquetes/#crear-el-proceso). 3. **Crea la cola `RPA-Challenge`** en el Robots Center. 4. **Lanza el job** en una máquina con el agente Online. Como `load_queue()` carga la cola solo si está vacía, **no necesitas un paso de carga aparte**: el propio robot la llena en su primera corrida. ### Opcional: cargar la cola por API [Sección titulada «Opcional: cargar la cola por API»](#opcional-cargar-la-cola-por-api) Si prefieres llenar la cola desde otro sistema en vez de dejar que el robot lo haga, usa una API key con scope `queues:write` (planes **Pro** y **Enterprise**): ```bash curl -X POST \ "https://nora-api.valisoftconsulting.com/api/v1/queues/by-name/RPA-Challenge/items/bulk" \ -H "X-API-Key: nora_ak_..." -H "Content-Type: application/json" \ -d @data/people.json ``` Ver [colas vía API](/api/colas/) para el detalle de los endpoints. ## Ver el resultado [Sección titulada «Ver el resultado»](#ver-el-resultado) Mientras corre y al terminar, tienes tres lugares donde mirar: ![Detalle de un job completado en NORA con sus logs en vivo, la barra de progreso y la salida del robot](/_astro/job-detalle.BZuhzA9B_ZjGslx.webp) * **Jobs → Logs**: los `nora.log(...)` aparecen con Hora y Nivel; verás `Ronda 1/10`, `Ronda 2/10`… y al final `Resultado: your success rate: 100%`. * **Jobs → Progreso**: la barra avanza de 10% en 10% gracias a `nora.progress(...)`. * **Colas → RPA-Challenge**: los 10 items pasan de `new` a `completed`. Si alguno falla, lo verás en `failed` con su `error_message`; selecciónalo y pulsa **Reintentar** para devolverlo a la cola. ## Lo que cambiarías para tu caso real [Sección titulada «Lo que cambiarías para tu caso real»](#lo-que-cambiarías-para-tu-caso-real) Este demo es la plantilla. Para adaptarlo a un proceso de negocio de verdad: * **Tus datos**: reemplaza `data/people.json` por tu fuente (o carga la cola por API / desde otro bot productor). El robot consumidor no cambia. * **Tu trabajo**: cambia `browser.fill_round(...)` por tu automatización (otro portal, Excel, SAP, una API…). El esqueleto de cola se queda igual. * **Credenciales**: si tu proceso necesita usuario/clave, no los pongas en el código; léelos con `nora.asset("mi-portal")` (envuelve `sdk.get_asset`). Ver [assets y credenciales](/guia/assets-y-credenciales/). * **Aprobación humana**: si un item necesita validación de una persona antes de cerrarse, usa `send_queue_item_for_review` + `wait_for_queue_review`. Ver [colas → aprobación humana](/guia/colas/#aprobaci%C3%B3n-humana-human-in-the-loop). * **Reintentos y SLA**: ajusta `max_retries` de la cola y usa `priority` / `deadline` por item para que NORA despache primero lo urgente. ## Buenas prácticas y qué NO hacer [Sección titulada «Buenas prácticas y qué NO hacer»](#buenas-prácticas-y-qué-no-hacer) Esta es la parte que separa un robot que “funciona en mi máquina” de uno que aguanta en producción. Una vez que tu máquina está **Online** (ver [primeros pasos](/guia/primeros-pasos/)), el patrón que viste arriba se aplica a cualquier proceso. Apégate a esto: ### Datos y trabajo [Sección titulada «Datos y trabajo»](#datos-y-trabajo) | ✅ Haz esto | ❌ Evita esto | | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Pon el trabajo en la **cola** como items (`data` JSON) y deja que el robot los consuma. | Incrustar la lista de “qué procesar” dentro del bot (un `for` sobre un array fijo en el código). | | Haz la carga **idempotente** (`if pending == 0: enqueue`), para poder re-correr sin duplicar. | Recargar la cola en cada arranque a ciegas: duplicas items y trabajo. | | Una unidad de negocio = **un item**. Si falla uno, los demás siguen. | Procesar las 10 rondas como un solo bloque: un fallo tira todo el lote. | | Usa `reference` (clave de negocio) y `priority`/`deadline` para ordenar y rastrear. | Meter el identificador de negocio solo dentro del texto de un log. | ### Ciclo de vida del item [Sección titulada «Ciclo de vida del item»](#ciclo-de-vida-del-item) * **Siempre cierra el item**: `complete` si salió bien, `fail` si no. Un item que quedó `in_progress` porque el robot murió sin cerrarlo solo se recupera por reintento/timeout — no lo dejes al azar. * **Captura la excepción por item, no por job**: envuelve el trabajo de cada item en `try/except` y llama a `fail(...)` antes de decidir si abortas. Así el item que falló queda marcado con su `error_message` en vez de desaparecer. * **No proceses dos veces el mismo item**: `claim_next()` ya garantiza exclusión entre jobs; no inventes tu propio “marcar como leído” en paralelo. ### Credenciales y secretos [Sección titulada «Credenciales y secretos»](#credenciales-y-secretos) Nunca pongas secretos en el código * **No** escribas usuarios, contraseñas, API keys ni tokens en el `.py` ni en `data/*.json`. `nora package` **aborta** si detecta un secreto, justamente para frenarte. * **No** subas tu `.env` de desarrollo: va en `.gitignore` y lo excluye el empaquetado. * Lee las credenciales en runtime con `nora.asset("mi-portal")` (envuelve `sdk.get_asset`), que te las entrega **descifradas** solo durante el job. Ver [assets y credenciales](/guia/assets-y-credenciales/). ### Logs y progreso [Sección titulada «Logs y progreso»](#logs-y-progreso) * Usa `sdk.log(...)` (o `nora.log(...)`), **no `print()`**: en un job gestionado los `print` no llegan a la plataforma. * **No** antepongas tu propia hora ni el nivel al mensaje: la vista **Jobs → Logs** ya muestra Hora y Nivel en columnas; un prefijo manual sale duplicado. * Loguea **hitos, no ruido**: un `info` al inicio/fin de cada paso, `warning`/`error` cuando algo se desvía. Adjunta el contexto estructurado por el parámetro `data`, no concatenado al texto. * Reporta progreso en pasos significativos (`nora.progress(...)`) para que el operador vea avance real desde el dashboard. ### Ejecución desatendida (lo que rompe en una VM y no en tu laptop) [Sección titulada «Ejecución desatendida (lo que rompe en una VM y no en tu laptop)»](#ejecución-desatendida-lo-que-rompe-en-una-vm-y-no-en-tu-laptop) * **Resolución de pantalla**: prioriza la **configurada en NORA** (`NORA_DISPLAY_WIDTH`/`HEIGHT`) sobre la del SO. Sin sesión RDP, la resolución “viva” puede quedar pegada en 1024×768 y hacer fallar clics y `fill`. El `main.py` del demo ya lo hace. * **Localiza por contenido, no por coordenadas**: el demo ubica cada campo por el texto de su etiqueta, no por su posición en pantalla. Nunca uses clics por píxel ni `sleep` fijos largos; usa esperas por elemento (Playwright las trae). * **Respeta el Stop del operador**: en bucles largos consulta `sdk.should_stop()` y sal ordenadamente. No dejes un robot que ignore la señal de detener. * **Headless en producción**: el demo corre `headless=True`. Solo pásalo a `False` en una máquina con sesión interactiva donde quieras verlo. ### Versionado y despliegue [Sección titulada «Versionado y despliegue»](#versionado-y-despliegue) * Versiona `nora.json` en tu repo (es la fuente de la versión) y agrega `dist/` al `.gitignore`. * Usa **la versión que imprime `nora package`** al crear el release: el Robots Center exige versión única, así evitas colisiones al re-subir. * Prueba con `nora dev run` **antes** de empaquetar. El mismo código que depuras es el que despliegas; no hay un “modo producción” distinto que te dé sorpresas. ### Mentalidad [Sección titulada «Mentalidad»](#mentalidad) > **El robot no debe saber de dónde viene el trabajo ni a dónde va el resultado.** Lee de la cola, hace su parte, reporta. Eso es lo que hace que el mismo esqueleto te sirva para el RPA Challenge hoy y para tu proceso de facturación mañana: solo cambias los **datos** (la cola) y el **trabajo** (`fill_round`), no la plomería. ## Siguientes pasos [Sección titulada «Siguientes pasos»](#siguientes-pasos) Ya tienes un robot real corriendo de punta a punta. Sigue la serie para usar **el resto de NORA** sobre esta misma base: * **➡️ [Tutorial 2 — Assets y automatización](/tutoriales/assets-y-automatizacion/):** saca las credenciales del código a la bóveda cifrada (`get_asset`) y deja de lanzar el robot a mano — prográmalo por **cron** o dispáralo por **webhook/API**. * **➡️ [Tutorial 3 — Patrones avanzados](/tutoriales/patrones-avanzados/):** revisión humana de items, input atendido (`ask_user`), parada limpia (`should_stop`), **flujos DAG** y **detección de anomalías**. Referencia relacionada: * [Colas (queues)](/guia/colas/) — el modelo completo: estados, revisión, roles. * [SDK de robots y logging](/api/sdk-robots/) — todas las funciones del SDK. * [Procesos y paquetes](/guia/procesos-y-paquetes/) — versiones, manifiesto, releases. * [Colas vía API](/api/colas/) — cargar y consultar items desde tus sistemas.