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.
Antes de empezar: el orden correcto
Sección titulada «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 |
| 2 | Instalar el agente en una máquina Windows o macOS | Instalar el agente |
| 3 | Crear la máquina en la consola y pegar su nora_mk_... en el agente | Máquinas |
| 4 | Esperar a que la máquina aparezca Online (heartbeat cada 30 s) | Máquinas |
| 5 | (Si tu robot usa credenciales) cargar assets cifrados | Assets y credenciales |
El mapa de NORA
Sección titulada «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:

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
| 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 |
| Programación / Webhook | Disparar el robot por horario o evento | Tutorial 2 |
| Anomalías / revisión humana / DAG | Vigilancia y orquestación avanzada | Tutorial 3 |
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 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]
Estructura del proyecto
Sección titulada «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.
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»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.
from pathlib import Path
QUEUE = "RPA-Challenge" # nombre de la cola en el Robots CenterRPA_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"}]El workflow principal
Sección titulada «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.
# main.py (esencia)from rpa_challenge import norafrom 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.

import jsonfrom . import browser, norafrom .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:
| 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) |
La automatización del navegador
Sección titulada «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.
# 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»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.
1. Instalar (una sola vez)
Sección titulada «1. Instalar (una sola vez)»pip install nora-sdk # SDK (from nora_agent import sdk) + comando `nora`pip install -r requirements.txtpython -m playwright install chromium2. Token y URL para el IDE
Sección titulada «2. Token y URL para el IDE»nora login # abre el navegador: apruebas con tu sesión webnora dev env --write .env # escribe NORA_API_URL + NORA_EXEC_TOKEN (token 8h)# añade .env a tu .gitignore3. Correr
Sección titulada «3. Correr»main.py orquesta todo: carga la cola si está vacía y luego resuelve.
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.
{ "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%]
-
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ódigonora release push # crea el paquete si no existe y sube el zipnora packageauto-incrementa la versión, excluye.venv/cachés/secretos y aborta si detecta un.envo claves reales. Usa siempre la versión que te imprime (el Robots Center exige versión única por release). Detalle en procesos y paquetes. -
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_idobligatorio); ver Crear el Proceso. -
Crea la cola
RPA-Challengeen el Robots Center. -
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»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):
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.jsonVer colas vía API para el detalle de los endpoints.
Ver el resultado
Sección titulada «Ver el resultado»Mientras corre y al terminar, tienes tres lugares donde mirar:

- Jobs → Logs: los
nora.log(...)aparecen con Hora y Nivel; verásRonda 1/10,Ronda 2/10… y al finalResultado: 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
newacompleted. Si alguno falla, lo verás enfailedcon suerror_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»Este demo es la plantilla. Para adaptarlo a un proceso de negocio de verdad:
- Tus datos: reemplaza
data/people.jsonpor 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")(envuelvesdk.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_retriesde la cola y usapriority/deadlinepor 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»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:
Datos y trabajo
Sección titulada «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»- Siempre cierra el item:
completesi salió bien,failsi no. Un item que quedóin_progressporque 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/excepty llama afail(...)antes de decidir si abortas. Así el item que falló queda marcado con suerror_messageen 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»Logs y progreso
Sección titulada «Logs y progreso»- Usa
sdk.log(...)(onora.log(...)), noprint(): en un job gestionado losprintno 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
infoal inicio/fin de cada paso,warning/errorcuando algo se desvía. Adjunta el contexto estructurado por el parámetrodata, 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 yfill. Elmain.pydel 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
sleepfijos 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 aFalseen una máquina con sesión interactiva donde quieras verlo.
Versionado y despliegue
Sección titulada «Versionado y despliegue»- Versiona
nora.jsonen tu repo (es la fuente de la versión) y agregadist/al.gitignore. - Usa la versión que imprime
nora packageal crear el release: el Robots Center exige versión única, así evitas colisiones al re-subir. - Prueba con
nora dev runantes 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»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»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:
- Colas (queues) — el modelo completo: estados, revisión, roles.
- SDK de robots y logging — todas las funciones del SDK.
- Procesos y paquetes — versiones, manifiesto, releases.
- Colas vía API — cargar y consultar items desde tus sistemas.