Ir al contenido

Tutorial: resolver el RPA Challenge con colas

Este es el ejemplo que une todo lo demás: un robot real, completo y ejecutable que resuelve el clásico rpachallenge.com leyendo su trabajo desde una cola de NORA. Si vienes de la guía de 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 y, por último, patrones avanzados (revisión humana, DAG, anomalías).

Si es tu primer robot, haz antes el Tutorial 0: primer robot mínimo; esta página asume que ya publicaste y ejecutaste un robot sencillo.

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"]
PasoQué hacesGuía
1Crear cuenta, iniciar sesión y tener tu workspacePrimeros pasos
2Instalar el agente en una máquina Windows o macOSInstalar el agente
3Crear la máquina en la consola y pegar su nora_mk_... en el agenteMáquinas
4Esperar a que la máquina aparezca Online (heartbeat cada 30 s)Máquinas
5(Si tu robot usa credenciales) cargar assets cifradosAssets y credenciales

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

flowchart TD
    subgraph infra["Tu infraestructura (pasos 1-4)"]
        M["Máquina + Agente<br/>(Online)"]
    end
    subgraph trabajo["Qué ejecuta"]
        P["Proceso<br/>(release de tu código)"]
        Q[("Cola<br/>unidades de trabajo")]
        AS["Assets<br/>(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
PiezaQué esLo ves en
Máquina + AgenteDónde corre el robot (Win/macOS)Este tutorial (pasos 1–4)
ProcesoTu código publicado como releaseEste tutorial
ColaLista de unidades de trabajo a procesarEste tutorial
AssetsCredenciales/config cifradasTutorial 2
Programación / WebhookDisparar el robot por horario o eventoTutorial 2
Anomalías / revisión humana / DAGVigilancia y orquestación avanzadaTutorial 3

Este tutorial cubre Máquina → Proceso → Cola. Lo demás se construye encima en los tutoriales 2 y 3.

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.

flowchart LR
    D[data/people.json<br/>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]

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.

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)

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
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"]):

// 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"}
]

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.

# 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)»

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

rpa_challenge/workflows.py
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»

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.

# 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:

LlamadaQué le pasa al itemSDK por debajo
nora.claim_next(QUEUE)newin_progress (lo reclama solo este job)sdk.get_queue_item(queue)
nora.complete(QUEUE, item)in_progresscompleted (guarda result)sdk.complete_queue_item(queue, id, result)
nora.fail(QUEUE, item, err)in_progressfailed (reintenta hasta max_retries)sdk.fail_queue_item(queue, id, error)

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.

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

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.

Ventana de terminal
pip install nora-sdk # SDK (from nora_agent import sdk) + comando `nora`
pip install -r requirements.txt
python -m playwright install chromium
Ventana de terminal
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

main.py orquesta todo: carga la cola si está vacía y luego resuelve.

Ventana de terminal
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
{
"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)»

El mismo código, sin cambios. El flujo es el de primeros pasos, resumido para este ejemplo:

flowchart TD
    A[nora package] --> B[nora release push]
    B --> C[Crear Proceso<br/>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):

    Ventana de terminal
    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.

  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.

  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.

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

Ventana de terminal
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 para el detalle de los endpoints.

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

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

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.
  • 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.
  • Reintentos y SLA: ajusta max_retries de la cola y usa priority / deadline por item para que NORA despache primero lo urgente.

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), el patrón que viste arriba se aplica a cualquier proceso. Apégate a esto:

✅ 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.
  • 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.
  • 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)»
  • 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.
  • 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.

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.

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: 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: revisión humana de items, input atendido (ask_user), parada limpia (should_stop), flujos DAG y detección de anomalías.

Referencia relacionada: