# Estandar de Ejecucion: YAML + Exit Codes + Hooks Contrato comun para apps, pipelines y automatizaciones del fn-registry. Define como declarar flujos en YAML, que exit codes devolver, como reportar resultados y como integrarse con dag_engine y operations.db. --- ## Motivacion Tenemos dos patrones que ya funcionan: - **dag_engine** (`apps/dag_engine/`): orquesta DAGs con `command`/`function`, usa exit codes para decidir flujo, soporta `handlers` (init/success/failure/exit) y scheduling cron via `schedule:`. - **script_navegador**: ejecuta pasos tipados (CDP actions) desde YAML, registra todo en operations.db, sale con 0/1. Ambos comparten la misma idea: **YAML declara el flujo, exit code señala el resultado, logs estructurados permiten analisis posterior**. Este documento formaliza ese contrato para que cualquier app nueva lo siga desde el inicio. --- ## 1. Estructura YAML Toda app que ejecute un flujo debe leer un YAML con esta estructura minima: ```yaml # Cabecera obligatoria name: nombre_del_flujo description: "Que hace este flujo" # opcional pero recomendado # Variables de entorno disponibles para todos los pasos env: # opcional KEY: value # Pasos del flujo steps: - name: paso_1 # identificador unico dentro del flujo action: tipo_de_accion # dispatch key (navigate, command, query, etc.) # ... parametros especificos del action continue_on_error: false # default: false timeout_ms: 30000 # default: 30s, 0 = sin timeout depends: [] # IDs de pasos previos (vacio = secuencial) # Hooks opcionales hooks: on_success: "comando o script" # se ejecuta si exit code = 0 on_failure: "comando o script" # se ejecuta si exit code = 1 on_partial: "comando o script" # se ejecuta si exit code = 2 ``` ### Campos de la cabecera | Campo | Requerido | Descripcion | |-------|-----------|-------------| | `name` | si | Identificador del flujo (snake_case) | | `description` | no | Descripcion legible | | `env` | no | Variables de entorno (mapa key-value) | | `steps` | si | Lista de pasos a ejecutar | | `hooks` | no | Comandos a ejecutar segun resultado | ### Campos de cada step | Campo | Requerido | Default | Descripcion | |-------|-----------|---------|-------------| | `name` | si | — | ID unico del paso dentro del flujo | | `action` | si | — | Tipo de accion (cada app define sus propios actions) | | `continue_on_error` | no | `false` | Si `true`, un fallo no detiene el flujo | | `timeout_ms` | no | `30000` | Timeout del paso en ms (0 = sin timeout) | | `depends` | no | `[]` | Pasos que deben completarse antes | Los campos adicionales dependen del `action` y los define cada app/dominio. ### Actions por dominio Cada app registra sus actions validos. Ejemplos: | Dominio | Actions | App de referencia | |---------|---------|-------------------| | navegacion | `navigate`, `click`, `type`, `wait`, `screenshot`, `evaluate`, `get_html`, `wait_load`, `sleep` | script_navegador | | shell | `command`, `script` | dag_engine / apps genericas | | registry | `function`, `args` | dag_engine (invoca `fn run ` y captura `function_id` en `dag_step_results`) | | database | `query`, `migrate`, `backup` | futuras apps | | http | `request`, `assert_status`, `extract` | futuras apps | --- ## 2. Exit Codes Toda app/script que siga este estandar DEBE salir con uno de estos tres codigos: | Codigo | Constante | Significado | |--------|-----------|-------------| | `0` | `EXIT_SUCCESS` | Todos los pasos completaron sin error | | `1` | `EXIT_FAILURE` | Al menos un paso fallo y el flujo aborto | | `2` | `EXIT_PARTIAL` | Algunos pasos fallaron pero el flujo continuo (continue_on_error) | ### Reglas - Si todos los pasos son exitosos → `0` - Si algun paso falla y tiene `continue_on_error: false` → `1` (aborta) - Si algun paso falla pero todos los fallos tenian `continue_on_error: true` → `2` - Un timeout agotado cuenta como fallo del paso ### Compatibilidad con dag_engine dag_engine interpreta exit code != 0 como fallo. Para que `2` (partial) no dispare el handler de failure, el DAG que llama a la app puede usar `continue_on.exit_code`: ```yaml steps: - name: run_app command: ./mi_app --script flujo.yaml continue_on: exit_code: [2] # exit 2 no aborta el DAG (partial es tolerado) - name: check_result command: test $? -le 1 # falla solo si fue error fatal depends: [run_app] ``` O alternativamente, la app puede configurarse con `--strict` para mapear partial → failure (exit 1). --- ## 3. Output Estructurado Toda app DEBE escribir un **resumen JSON en stdout** al finalizar (despues de su output humano). El formato: ```json { "name": "nombre_del_flujo", "status": "success|failure|partial", "exit_code": 0, "started_at": "2026-04-01T10:00:00Z", "ended_at": "2026-04-01T10:00:05Z", "duration_ms": 5000, "steps_total": 5, "steps_ok": 4, "steps_failed": 1, "steps": [ { "name": "paso_1", "action": "navigate", "status": "ok", "elapsed_ms": 1200, "output": "optional output" }, { "name": "paso_2", "action": "click", "status": "error", "elapsed_ms": 500, "error": "elemento no encontrado: #btn-submit" } ] } ``` ### Separacion humano / maquina - **stderr**: logs legibles para humanos (progreso, warnings) - **stdout**: output JSON estructurado al final (para dag_engine, hooks, pipes) - Flag `--json` o `--quiet`: suprime output humano, solo JSON en stdout Esto permite que dag_engine capture el JSON: ```yaml steps: - name: navegacion command: ./script_navegador --script busqueda.yaml --json output: RESULT - name: analizar command: echo "$RESULT" | jq '.steps[] | select(.status=="error")' depends: [navegacion] ``` --- ## 4. Hooks Los hooks se ejecutan **despues** de todos los pasos, segun el exit code resultante. ### En el YAML del flujo ```yaml hooks: on_success: "notify-send 'Flujo completado'" on_failure: "echo 'FALLO: {{.Name}}' >> /tmp/failures.log" on_partial: "echo 'PARCIAL: {{.StepsFailed}} pasos fallaron' >> /tmp/warnings.log" on_step_error: "echo 'Step {{.StepName}} fallo: {{.Error}}'" # se ejecuta por cada paso fallido ``` ### Variables disponibles en hooks | Variable | Tipo | Descripcion | |----------|------|-------------| | `{{.Name}}` | string | Nombre del flujo | | `{{.Status}}` | string | success, failure, partial | | `{{.ExitCode}}` | int | 0, 1, 2 | | `{{.DurationMs}}` | int | Duracion total en ms | | `{{.StepsTotal}}` | int | Total de pasos | | `{{.StepsOk}}` | int | Pasos exitosos | | `{{.StepsFailed}}` | int | Pasos fallidos | | `{{.StepName}}` | string | Nombre del paso (solo en on_step_error) | | `{{.Error}}` | string | Mensaje de error (solo en on_step_error/on_failure) | ### En dag_engine (handlers globales) Los hooks del YAML son locales al flujo. Para orquestacion, dag_engine maneja sus propios handlers (`init/success/failure/exit`, alias `handler_on`): ```yaml # apps/dag_engine/dags_migrated/mi_automatizacion.yaml name: mi_automatizacion handlers: failure: - name: alerta command: echo "Fallo en mi_automatizacion" >> /var/log/fn_registry/failures.log success: - name: registrar command: echo "OK $(date)" >> /var/log/fn_registry/success.log steps: - name: ejecutar function: mi_pipeline_bash_pipelines args: ["flujo.yaml"] ``` --- ## 5. Integracion con operations.db Las apps que siguen el bucle reactivo DEBEN registrar la ejecucion en operations.db: ### Mapping del estandar a operations.db | Concepto estandar | Tabla operations.db | Campo | |-------------------|---------------------|-------| | Flujo completo | `executions` | `status`: success/failure/partial | | Cada paso | `logs` | `level`: info/error, `message`: resultado | | Exit code | `executions.metrics` | `{"exit_code": N}` | | Output JSON | `executions.metrics` | `{"steps": [...]}` | | Duracion | `executions.duration_ms` | milisegundos | | Pasos in/out | `executions.records_in/out` | total/exitosos | ### Patron de registro (referencia: script_navegador/ops.go) ``` 1. initOpsDB() → abrir/crear operations.db 2. EnsureEntities() → registrar recursos involucrados 3. EnsureRelations() → registrar como se conectan 4. [ejecutar flujo] 5. RecordRun() → insertar execution con metricas 6. LogStep() por paso → insertar log por cada paso 7. UpdateRelation() → status final de la relation ``` Este patron ya esta implementado en `script_navegador/ops.go` y sirve como referencia para nuevas apps. --- ## 6. Flujo completo: App → dag_engine → Hooks ``` dag_engine (scheduler) │ cron / manual / API │ ┌────▼────┐ │ DAG │ apps/dag_engine/dags_migrated/mi_dag.yaml │ step │ command: ./mi_app --script flujo.yaml --json └────┬────┘ │ ┌────▼────────────────┐ │ App (mi_app) │ │ │ │ 1. Load YAML │ │ 2. Validate steps │ │ 3. Init ops.db │ │ 4. Execute steps │ │ 5. Run hooks │ │ 6. Record to ops │ │ 7. JSON → stdout │ │ 8. Exit code │ └────┬────────────────┘ │ ┌──────────┼──────────┐ │ │ │ exit 0 exit 1 exit 2 success failure partial │ │ │ ▼ ▼ ▼ engine OK engine FAIL engine FAIL* handler handler (configurable) ``` *Con `continue_on: { exit_code: [2] }` en el step, exit 2 no aborta el DAG. --- ## 7. Implementacion: funciones del registry ### Existentes (ya en uso por script_navegador) Todas las funciones CDP del registry (`cdp_*_go_browser`, `chrome_launch_go_browser`) ya siguen el patron de retornar error. El runner de script_navegador las compone. ### Nuevas (por construir) | Funcion | Lang | Domain | Descripcion | |---------|------|--------|-------------| | `run_steps` | bash | shell | Ejecuta pasos de un YAML generico (action=command/script), reporta JSON y sale con 0/1/2 | | `report_execution_json` | bash | shell | Genera el JSON de salida estandar dado los resultados de pasos | | `exit_with_status` | bash | shell | Calcula exit code (0/1/2) a partir de contadores ok/fail/partial | Estas funciones Bash permiten que cualquier script nuevo siga el estandar sin reimplementar la logica de reporte y exit codes. --- ## 8. Ejemplo completo ### Script YAML (flujo.yaml) ```yaml name: backup_db description: "Backup de operations.db y push a remoto" steps: - name: check_db action: command command: "test -f operations.db" - name: backup action: command command: "cp operations.db backups/operations_$(date +%Y%m%d).db" depends: [check_db] - name: push action: command command: "git add backups/ && git commit -m 'backup' && git push" depends: [backup] continue_on_error: true hooks: on_failure: "echo 'Backup fallo' >> /tmp/backup_errors.log" ``` ### Invocacion desde dag_engine ```yaml name: backup_diario schedule: "0 2 * * *" working_dir: /home/lucas/fn_registry/apps/mi_app steps: - name: backup function: backup_db_bash_pipelines args: ["flujo.yaml", "--json"] handlers: failure: - name: alerta command: echo "Backup fallo $(date)" >> /var/log/fn_registry/failures.log ``` ### Output esperado (stdout) ```json { "name": "backup_db", "status": "partial", "exit_code": 2, "started_at": "2026-04-01T02:00:00Z", "ended_at": "2026-04-01T02:00:03Z", "duration_ms": 3000, "steps_total": 3, "steps_ok": 2, "steps_failed": 1, "steps": [ {"name": "check_db", "action": "command", "status": "ok", "elapsed_ms": 10}, {"name": "backup", "action": "command", "status": "ok", "elapsed_ms": 450}, {"name": "push", "action": "command", "status": "error", "elapsed_ms": 2540, "error": "remote rejected"} ] } ``` Exit code: `2` (partial — push fallo pero tenia continue_on_error). --- ## Resumen del contrato | Aspecto | Regla | |---------|-------| | Formato de flujo | YAML con `name` + `steps[]` | | Exit codes | 0=success, 1=failure, 2=partial | | Output maquina | JSON en stdout (con `--json`) | | Output humano | stderr (progreso, warnings) | | Error handling | `continue_on_error` por paso, hooks por resultado | | Trazabilidad | operations.db (executions + logs) | | Orquestacion | dag_engine como scheduler (`apps/dag_engine/`), apps/funciones del registry como ejecutores | | Funciones | Reutilizar del registry, actions por dominio |