Automatizaciones
Una automatización es una receta corta en YAML: un disparador y una lista de pasos que corren en orden. El disparador puede ser un horario (cron), un evento de GitHub o del tablero, una métrica que cruza un umbral, o el botón “Correr ahora”. Los pasos juntan datos baratos (una consulta al tablero, una métrica, un script, una llamada HTTP o a un MCP) y, si hace falta, abren una tarea, comentan en una existente o despiertan a un agente. Una revisión nocturna que no encuentra nada no gasta tokens.
Todo lo que necesita varios turnos o el criterio de una persona va al tablero como tarea. Las recetas no tienen ramas ni bucles más allá de condition, when y fanout, y una corrida entera se corta a los 10 minutos (configurable hasta una hora).
Las automatizaciones viven en “Automatizaciones”, en la barra lateral, y pertenecen al workspace: desde otro workspace no se ven. Los admins crean, editan, corren y borran recetas; los developers pueden leerlas y ver sus corridas.
Flujo recomendado: una tarea fija todos los lunes
Sección titulada «Flujo recomendado: una tarea fija todos los lunes»El ejemplo abre cada lunes a las 9 una tarea para revisar dependencias. Si la del lunes anterior sigue abierta, comenta en ella en vez de abrir otra. Necesitás rol admin en el workspace; con rol developer los botones de guardar y correr devuelven error.
-
Abrí “Automatizaciones” y hacé clic en “Nueva automatización”. Elegí una plantilla: “En blanco” alcanza para este ejemplo. Las plantillas con horario toman la zona horaria de tu navegador.

Cada plantilla es una receta que ya pasa la validación. -
La plantilla se abre en el editor con la insignia “sin guardar”. “Correr ahora”, “Desactivar” y el tacho quedan deshabilitados hasta guardar. Arriba del YAML, el formulario “Disparador” elige el tipo (Programada, Evento, Métrica, Manual) y sus campos, y “Insertar el ID de:” tiene selectores de tablero, etapa, agente, proyecto y entorno que escriben el ID en el cursor. Si el YAML no se puede leer, el formulario se bloquea con “Arreglá el YAML de abajo para volver a usar el formulario.” Para este ejemplo, reemplazá el YAML por la receta:
name: lunes de dependenciasdescription: una tarea por semana para revisar dependencias desactualizadastrigger:schedule: "0 9 * * 1"tz: America/Argentina/Buenos_Airespolicy:concurrency: forbidmax_agent_runs_per_day: 2timeout: 5msteps:- id: tickettask:title: "Revisar dependencias desactualizadas"body: |Listá las dependencias con versiones nuevas o avisos de seguridady abrí un PR con las que se puedan actualizar sin romper nada.tags: [dependencias]priority: mediumfingerprint: "dependencias-semanal"on_duplicate: commentEl nombre de arriba del editor y la línea
name:son el mismo dato: cambiar uno cambia el otro. -
Hacé clic en “Validar”. Si la receta está bien, abajo aparecen en verde el nombre, el tipo de disparador y las próximas cinco ejecuciones en tu hora local: así comprobás que el cron dice lo que querías.

Validar no guarda nada: muestra las próximas ejecuciones en tu hora local. Si algo está mal, el mismo lugar muestra en rojo el primer error. Una clave mal escrita, un evento que no existe, un
fanoutsinmax_itemso un tablero, etapa, agente, proyecto o entorno que no existe en el workspace se rechazan antes de guardar.
El cron acepta nombres en inglés (mon, tue) o números, no días en castellano. -
Hacé clic en “Guardar”. Aparece el aviso “Guardada”, la receta entra a la lista con “activa” y la cabecera muestra el cron, la zona y la próxima ejecución en tu hora local.
-
Probala con “Correr ahora”. Se abre la pestaña “Corridas” con la corrida desplegada: cada paso con su estado y un resumen (“Creó la tarea «Revisar dependencias desactualizadas»”, con link a la tarea). Corré otra vez y vas a ver que la segunda no abre otra tarea: encuentra el fingerprint y comenta en la tarea abierta (“La tarea … ya existía, así que comentó en ella”).

Abajo la primera corrida, que creó la tarea; arriba la segunda, que comentó en la misma.
La tarea creada aparece en el tablero por defecto del workspace, en su primera etapa (Backlog), con la etiqueta dependencias, y ahí se queda hasta que alguien la mueva. Para que un agente arranque solo, agregá stage: con el ID de una etapa que tenga agente (por ejemplo in_progress) y, si la etapa no trae uno por defecto, stage_assignments: { in_progress: <nombre del agente> }. La tarea entra a esa etapa como si la hubieras arrastrado.
Leer las corridas
Sección titulada «Leer las corridas»Cada fila de “Corridas” muestra cuándo arrancó, qué la disparó (“manual”, “programada”, “evento”, “métrica”, “datos de prueba”), el estado, cuántos agentes despertó y cuánto duró. Hacé clic en la fila para ver los pasos: cada uno con un resumen en castellano y, desplegable, “Salida completa (JSON)”. “Datos del disparador (trigger.payload)” muestra lo que recibió la receta. Mientras una corrida está en marcha, la fila tiene un botón “Cancelar”. La lista se actualiza sola cada 5 segundos.
| Estado | Qué pasó |
|---|---|
| “corriendo” | Está corriendo. |
| “correcta” | Terminó. Algún paso con continue_on_error pudo fallar. |
| “falló” | Un paso falló y la receta no decía continue_on_error. El error aparece en rojo arriba de los pasos. |
| “se pasó de tiempo” | Pasó el policy.timeout y se cortó. |
| “salteada” | Algo la frenó, con el motivo al lado: una condition falsa (no es una falla: la guardia dijo que no había nada que hacer), otra corrida en curso (concurrency: forbid), llegó tarde, se gastó el tope diario de agentes, un filter que no se pudo evaluar o una cadena de recetas demasiado larga. |
| “cancelada” | La cancelaste a mano o la reemplazó otra (concurrency: replace). |
“Corridas de agentes” cuenta sólo los pasos que despertaron a un agente: una tarea que entra a una etapa con agente o una mención que despierta a uno. Una tarea que cae en Backlog o un comentario por fingerprint cuentan 0.
Otras tareas
Sección titulada «Otras tareas»Reaccionar a un evento del tablero
Sección titulada «Reaccionar a un evento del tablero»Hay tres eventos del tablero:
| Evento | Sale cuando |
|---|---|
task.created |
Se crea una tarea: una persona, una subtarea o una automatización (no al clonar una tarea). |
task.stage_changed |
La tarea entra a una etapa nueva: la mueve una persona, avanza sola cuando el agente termina, la libera una aprobación o vuelve atrás. No sale cuando borrar una etapa mueve sus tareas. |
task.failed |
Falla la corrida de un agente sobre la tarea. |
El evento llega a la receta en trigger.payload con task_id, title, stage, board_id, status y tags; task.stage_changed suma from y to, una subtarea trae parent_id y una tarea de una receta trae source: automation. Con filter elegís qué eventos la disparan. Una receta nunca se dispara por una tarea que abrió ella misma, y una cadena de tres recetas que se disparan entre sí no dispara una cuarta.
Esta receta comenta en cada tarea nueva que llega con la etiqueta urgente:
name: avisar urgentesdescription: comenta en cada tarea nueva marcada como urgentetrigger: event: task.created filter: '"urgente" in trigger.payload.tags'steps: - id: aviso mention: task_id: "{{ trigger.payload.task_id }}" body: "Entró como urgente. Revisala en la daily de mañana."
El comentario aparece en la actividad de la tarea firmado por “Una automatización”:

Un paso mention sin agent: despierta al agente asignado a la etapa actual de la tarea. Si la etapa no tiene agente (Backlog, una etapa de fin), sólo comenta, y el resumen lo dice: “Comentó en «…»; su etapa no tiene agente para despertar”. Con agent: despierta a ese agente. La automatización pasa por el mismo camino que una mención de una persona, con los mismos topes por hilo. Ver Conversación e hilos.
Para probar una receta por evento o métrica sin provocar el evento, usá “Correr con datos de prueba”: abre un payload de ejemplo con la forma del real, editable, y la receta corre con él como trigger.payload. Cambiá task_id por el ID de una tarea que exista. “Correr ahora” corre con el payload vacío.
Si el filter no se puede evaluar (una función mal escrita, un campo del tipo equivocado), la corrida queda “salteada” con el error, así que una receta rota se ve en el historial. Validar también rechaza un filtro que lee un campo que el evento no trae.
Responder comentarios de review en GitHub
Sección titulada «Responder comentarios de review en GitHub»Los seis eventos de GitHub (github.pull_request_review_comment, github.pull_request_review, github.issue_comment, github.pull_request, github.push, github.check_suite) llegan por el webhook de la GitHub App. Primero el workspace tiene que tener la App instalada y reclamada: ver Git y GitHub. En una instalación self-hosted con un solo workspace, una instalación sin reclamar igual entrega sus eventos; con varios workspaces, o en la SaaS, se descartan hasta que alguien la reclame.
La plantilla “Responder comentarios de review” arranca lo más común: cuando una persona deja un comentario de review en un PR que pertenece a una tarea, menciona al agente de la etapa actual de esa tarea con el texto del comentario.
- En “Nueva automatización”, elegí “Responder comentarios de review”.
- Si querés que lo atienda un agente fijo y no el de la etapa, agregá
agent:en el pasoreplycon su nombre (el mismo que mencionarías con@en la tarea). - Hacé clic en “Validar” y después en “Guardar”.
- Probala con “Correr con datos de prueba”, con el ID de una tarea que tenga PR.
El filtro descarta comentarios de bots (sender_is_bot), que es lo que evita que un agente se conteste a sí mismo para siempre, y exige task_id: un PR que no salió de una tarea de Thulu no dispara nada. La plantilla “Barrido de reviews de PR” hace lo mismo cada hora con las tareas que tienen un PR abierto sin actividad en un día.
Con policy.debounce, una ráfaga se junta en una sola corrida: el primer evento de un PR abre la ventana, los que llegan dentro esperan, y al cerrarse la receta corre una vez. trigger.payload es el último evento y trigger.payload.events trae todos (hasta 50); la corrida muestra “Juntó 5 eventos”. La ventana no se estira mientras siguen llegando.
El payload de GitHub trae, entre otros, repo, pr_url, pr_number, pr_title, branch, comment, comment_by, sender_is_bot y review_state. Si el PR es de una tarea de Thulu, también task_id, task_title y task_stage.
Disparar por una métrica
Sección titulada «Disparar por una métrica»Un disparador metric revisa cada 5 minutos (configurable con every, mínimo 1m) una métrica de un entorno y se dispara cuando cruza el umbral. Después no vuelve a dispararse hasta que el valor regresa del otro lado del umbral, así que un incidente que dura una hora abre una sola corrida. Si en la ventana no hay datos, no hace nada.
name: latencia del checkouttrigger: metric: environment: e1f0c2a9b7d34c58 # ID del entorno, no su nombre metric: latency_p95 window: 15m agg: avg # avg, max, min, sum o last op: ">" # >, >=, < o <= threshold: 800policy: max_agent_runs_per_day: 4steps: - id: ticket task: title: "Checkout lento: p95 en {{ trigger.payload.value }} ms" priority: high fingerprint: "checkout-latencia" on_duplicate: commentenvironment lleva el ID del entorno, no su nombre: elegilo con el selector “Entorno…” de arriba del editor, que lo escribe por vos. Guardar rechaza un ID que no existe en el workspace (“no hay un entorno … en este workspace”). La plantilla “Guardia de errores” lo pide con un comentario en el YAML.
La métrica puede ser una señal fija del entorno (latency_p50, latency_p95, error_rate, throughput, cpu, memory, restarts, cost_daily, saturation) o una métrica propia registrada en ese entorno. Ver Métricas y dashboards.
Diagnosticar una corrida fallida
Sección titulada «Diagnosticar una corrida fallida»Abrí “Corridas”, desplegá la fila en rojo y leé el error del paso. Corregí la receta en “Receta”, guardá y probá con “Correr ahora” o “Correr con datos de prueba”. La lista de la izquierda muestra el estado de la última corrida de cada receta, así que una receta rota se ve sin entrar.

Lo que sólo se sabe al correr también aparece acá: un {{ }} que da una tarea que no existe, como en la captura, un script que no termina, un MCP que no responde. En un paso task, board y stage no admiten {{ }}, porque el texto llegaría tal cual: Guardar lo rechaza con “board no admite templates {{ }}”.
Pausar o borrar una receta
Sección titulada «Pausar o borrar una receta»“Desactivar” suspende la receta: no corre por horario, evento ni métrica, la cabecera dice “suspendida” y desaparece la próxima ejecución. “Correr ahora” sigue funcionando. “Activar” la vuelve a prender.
El tacho (“Eliminar automatización”) elimina la receta y todo su historial de corridas después de confirmar. No se puede deshacer; si dudás, desactivala.
Si tenés cambios sin guardar en el editor y elegís otra receta de la lista, Thulu te pide confirmar: “Se pierden los cambios a esta receta.”
La receta por dentro
Sección titulada «La receta por dentro»Disparadores
Sección titulada «Disparadores»Cada receta tiene exactamente uno.
| Disparador | Campos | Cuándo corre |
|---|---|---|
| Programada | schedule (cron de 5 campos o @hourly, @daily, @weekly, @monthly), tz |
En cada horario del cron. |
| Evento | event, filter (opcional) |
Cuando llega un evento del catálogo que cumple el filtro. |
| Métrica | metric con environment, metric, window, agg, op, threshold, every |
Cuando la métrica cruza el umbral. |
| Manual | manual: true |
Sólo con “Correr ahora”. Las otras tres también se pueden correr a mano. |
Cada paso tiene un id (minúsculas, dígitos y _, hasta 32 caracteres) y una sola acción. Lo que devuelve queda disponible para los pasos siguientes como steps.<id>.<campo>. Cualquier paso acepta when: (se saltea si es falso) y continue_on_error: true (si falla, la corrida sigue).
| Paso | Qué hace | Lo que devuelve |
|---|---|---|
query |
Lee tareas (filtros board, stage, tag, project_id, with_pr, open, stale_for, limit) o una métrica (environment, metric, window, agg). Con open: true deja afuera las tareas en etapas de fin. |
count y json.tasks, o value y has_data. |
condition |
Corta la corrida si la expresión es falsa. La corrida queda “salteada” con el motivo. | — |
task |
Abre una tarea (title, body, board, stage, stage_assignments, type, tags, priority, project_ids). priority acepta urgent, high, medium o low; type es el ID de un tipo de tarea del tablero. Con fingerprint, deduplica. |
task_id, created, duplicate. |
mention |
Comenta en una tarea existente y despierta a agent, o al agente de su etapa actual si no lo nombra. |
task_id. |
script |
Corre un comando en un sandbox vacío con node, python3, git, curl y jq. Hasta 10 minutos (2 por defecto). Un código de salida distinto de 0 no falla el paso. | stdout, stderr, exit_code, y json si la salida es JSON. |
http |
Llama a una URL, con un secreto del gestor de credenciales en credential. Hasta 30 segundos. |
status_code, text, json. |
mcp |
Llama a una tool de un MCP conectado. Por ahora sólo llega a servidores SSE sin credencial propia: uno registrado con Streamable HTTP o con credencial guardada se rechaza con un error que lo nombra. | Lo que devuelva la tool. |
fanout |
Repite sus pasos hijos por cada ítem de una lista. max_items es obligatorio, hasta 50. |
items, truncated. |
board y stage llevan identificadores, no rótulos: el tablero por defecto es default y sus etapas backlog, in_progress y done. Los selectores de arriba del editor escriben el ID por vos. Ver Tableros y etapas.
Fingerprint: una tarea por problema
Sección titulada «Fingerprint: una tarea por problema»Sin fingerprint, una receta que corre cada hora y encuentra el mismo problema abre 24 tareas por día. Con fingerprint, la primera corrida abre la tarea y las siguientes, mientras esa tarea siga abierta, hacen lo que diga on_duplicate:
on_duplicate |
Qué hace |
|---|---|
comment (por defecto) |
Comenta en la tarea abierta con el título y el cuerpo nuevos. |
skip |
No hace nada. |
create |
Abre otra tarea igual. |
Cuando la tarea llega a una etapa terminal, la próxima corrida abre una nueva. El fingerprint es un template: "deps-{{ trigger.fired_date }}" da una tarea por día; un texto fijo da una tarea por problema, dure lo que dure.
Política
Sección titulada «Política»| Campo | Por defecto | Qué controla |
|---|---|---|
concurrency |
forbid |
forbid no arranca otra corrida si hay una en curso; replace cancela la anterior; allow las deja superponerse. Vale también para “Correr ahora”. |
timeout |
10m |
Corte de la corrida entera. Máximo 1h. |
max_agent_runs_per_day |
sin tope | Cuántas veces puede despertar a un agente en 24 horas. Al llegar, la corrida queda “salteada” con el tope en el motivo. |
debounce |
sin debounce | Junta los eventos del mismo PR o de la misma tarea que llegan dentro de la ventana en una sola corrida al cerrarse. |
starting_deadline |
2m |
Si el daemon estuvo caído, una ejecución atrasada más que esto se saltea en vez de recuperarse. |
history_limit |
200 |
Corridas que se guardan por receta. |
Expresiones y templates
Sección titulada «Expresiones y templates»condition, when y filter son expresiones; los campos de texto aceptan templates {{ … }} con el mismo lenguaje. Se puede leer trigger.payload.*, trigger.fired_date (como 2026-10-19), steps.<id>.*, vars.* y, dentro de un fanout, el ítem actual.
Operadores: == != < <= > >=, && || !, + - * / %, in, paréntesis y listas (["created", "edited"]). Funciones: exists, size, len, contains, lower, upper, trim, default, int, string, matches, slice, join.
Validar revisa que las expresiones estén bien escritas y que los nombres existan: un steps.erros.value o una función desconocida son un error al validar, no un valor vacío a las 3 de la mañana.
Problemas comunes
Sección titulada «Problemas comunes»| Síntoma | Causa y salida |
|---|---|
| “Las automatizaciones no están habilitadas en esta instalación.” | El daemon no tiene el almacén de automatizaciones conectado. Es una cuestión de la instalación; ver Operación self-hosted. |
| Guardar falla con “línea 2: la clave “triger” no va acá” | Una clave mal escrita o mal sangrada. El mensaje nombra la línea y la clave. |
| Guardar falla con “no existe el tablero …” o “el tablero … no tiene la etapa …” | La receta nombra un rótulo en vez del ID. Usá los selectores de arriba del editor. |
| La receta programada corre a una hora rara | Falta tz, o el cron usa días en castellano. |
| La corrida queda “salteada” | Mirá el motivo: una condition falsa, otra corrida en curso, max_agent_runs_per_day agotado o un filter que falló. |
| Una receta por evento no deja ninguna corrida | El filter dio falso, o la tarea la abrió la misma receta. |
| La mención comenta pero el agente no hace nada | La etapa actual de la tarea no tiene agente, o el agente no atiende menciones: el resumen del paso lo dice. Nombrá un agente con agent:. |
Un paso mcp falla con “habla Streamable HTTP” o “se autentica con una credencial del workspace” |
Los pasos de una automatización todavía no llegan a esos servidores. |