Tasks, targets y executors
Definir y ejecutar tareas en Nx: un target es una operación con nombre, un executor es su implementación y una task es la instancia concreta que corre. La configuración vive en nx.json para los defaults y por proyecto para lo específico; dependsOn fija el orden, inputs y outputs gobiernan la caché, y run, run-many y affected disparan la ejecución.
Nx ejecuta tareas, y tres conceptos definen lo que una tarea es. El target es una operación con nombre que un proyecto sabe hacer —build, test, lint—. El executor es la implementación de ese target: la función que recibe unas opciones y hace el trabajo real. La task es la instancia concreta que corre en un momento dado: un proyecto más un target, un nodo del task graph. Separar estas tres capas es lo que permite que Nx describa las tareas de forma declarativa y decida por su cuenta el orden, la caché y la paralelización.
- Definir la terminología exacta: target, task y executor, y cómo se relacionan.
- Configurar targets por proyecto y fijar valores por defecto en
nx.json. - Declarar el orden con
dependsOny gobernar la caché coninputsyoutputs. - Ejecutar tareas con
nx run,nx run-manyynx affected.
Target, task y executor
Un target es un nombre: la etiqueta de una operación que un proyecto ofrece. Cuando dices nx run web:build, build es el target. No hace nada por sí mismo; es una entrada en la configuración que apunta a un executor y a unas opciones.
Un executor es la implementación. Se nombra con la forma paquete:nombre —por ejemplo @nx/vite:build— y es, literalmente, una función que recibe las opciones del target y el contexto de la ejecución, y realiza el trabajo llamando por debajo a la herramienta correspondiente. El executor más simple es nx:run-commands, que se limita a ejecutar un comando de shell; los executors de plugins envuelven a Vite, a Jest o a tsc con integración de caché y opciones tipadas.
Una task es lo que existe en tiempo de ejecución: la combinación de un proyecto, un target y opcionalmente una configuración. web:build, web:build:production y ui:build son tres tasks distintas. El task graph que viste en la lección anterior está hecho de estos nodos.
// project.json de la app web: un target que usa un executor
{
"name": "web",
"targets": {
"build": {
"executor": "@nx/vite:build",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/web"
}
}
}
}
Puesto en una tabla, el reparto de responsabilidades queda nítido: el target es el nombre que invocas, el executor es quien hace el trabajo, y la task es la ejecución concreta que resulta de combinarlos con una configuración.
| Concepto | Qué es | Ejemplo |
|---|---|---|
| Target | el nombre de una operación | build |
| Executor | la implementación que la ejecuta | @nx/vite:build |
| Task | la instancia que corre | web:build:production |
Dónde vive la configuración
La configuración de tareas se reparte en dos niveles, y entender ese reparto evita muchísima repetición. Lo específico de un proyecto vive en su project.json o en su package.json; lo común a todos vive en nx.json, bajo targetDefaults.
targetDefaults es herencia: defines una vez el comportamiento de build —su executor, su dependsOn, sus inputs y outputs— y todos los proyectos con un target build lo heredan. Cada proyecto solo declara lo que se desvía del defecto. Es el mismo principio de “convención sobre configuración” que hace manejable un monorepo grande: sin defaults, cada uno de cien proyectos repetiría el mismo bloque.
// nx.json: defaults heredados por todos los proyectos
{
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"outputs": ["{projectRoot}/dist"],
"cache": true
},
"test": {
"inputs": ["default", "^production"],
"cache": true
}
}
}
La regla práctica: si escribes la misma opción en más de dos proyectos, súbela a targetDefaults. Deja en cada project.json únicamente lo que es genuinamente propio de ese proyecto —una ruta de salida, una configuración de producción distinta—. Un monorepo saludable tiende a tener project.json cortos y un nx.json que concentra las políticas comunes.
dependsOn, inputs y outputs
Tres campos gobiernan cómo se comporta una tarea dentro del sistema: uno decide el orden, dos deciden la caché.
dependsOn declara qué debe correr antes. La notación con acento circunflejo, ^build, significa “el build de mis dependencias primero”; sin acento, build, significa “otro target de este mismo proyecto”. Esta sola línea, dependsOn: ["^build"], es la que hace que Nx construya las librerías antes que las apps que las consumen, recorriendo el project graph.
inputs y outputs son el corazón de la caché. Los inputs definen todo lo que, si cambia, invalida el resultado: los archivos fuente del proyecto, los de sus dependencias (^), variables de entorno relevantes, la versión de dependencias externas, las propias opciones del target. Nx calcula un hash de todos los inputs; ese hash es la identidad de la tarea. Los outputs declaran qué produce la tarea —una carpeta dist, un informe de cobertura— para que Nx sepa qué guardar en la caché y qué restaurar en un acierto.
flowchart LR
I1[archivos fuente] --> H[hash de inputs]
I2[deps upstream] --> H
I3[env y opciones] --> H
H --> D{existe en cache}
D -->|si| R[restaura outputs]
D -->|no| E[ejecuta y guarda]
style R fill:#a6e3a1,color:#11111b
style E fill:#f9e2af,color:#11111bLa lógica es exacta: mismo hash de inputs significa mismo resultado, siempre. Si Nx ya vio ese hash, restaura los outputs y reimprime la salida de terminal en milisegundos en vez de recomputar. Por eso la corrección de la caché depende por completo de que los inputs sean completos: si una tarea depende de un archivo que no está entre sus inputs, Nx cacheará resultados obsoletos y servirá builds equivocadas con toda confianza.
El error más peligroso no es declarar un input de más —eso solo provoca fallos de caché innecesarios, un problema de rendimiento— sino declarar uno de menos. Si un archivo influye en el resultado pero no figura en los inputs, dos ejecuciones con contenido distinto compartirán hash, y Nx entregará el output cacheado de la primera cuando debería recomputar. El síntoma es demoledor: builds correctas en verde que contienen código viejo. Ante la duda, incluye el input; la corrección vale más que un acierto de caché de más.
Entradas con nombre reutilizables
Repetir la lista de inputs en cada target es frágil. Nx permite definir conjuntos de entradas con nombre en nx.json, bajo namedInputs, y referenciarlos desde cualquier target. Es el mismo principio de los defaults aplicado a las entradas: defines una vez qué significa production —el código fuente menos los archivos de test— y lo reutilizas en todo el workspace.
// nx.json: conjuntos de entradas reutilizables
{
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": [
"default",
"!{projectRoot}/**/*.spec.ts",
"!{projectRoot}/**/*.test.ts"
]
}
}
La distinción entre default y production es la que evita que un cambio en un test invalide la caché del build: si los archivos de test no forman parte de las entradas de producción, tocarlos no altera el hash del artefacto construido, y su caché sigue válida. Modelar bien estos conjuntos es una de las palancas más efectivas para subir la tasa de aciertos.
Ejecutar: run, run-many y affected
Con la configuración en su sitio, disparar tareas tiene tres formas según el alcance.
nx run web:build ejecuta una tarea concreta; nx build web es su forma abreviada. nx run-many -t build ejecuta un target en todos los proyectos que lo tengan, en el orden que dicte el task graph y en paralelo donde se pueda. Y nx affected -t test es la joya: calcula qué proyectos se ven afectados por tus cambios —comparando tu rama con una base en git y recorriendo el project graph— y ejecuta el target solo en ellos.
# Una sola tarea
nx build web
# El mismo target en todos los proyectos
nx run-many -t build
# Solo en lo afectado por el diff frente a main
nx affected -t test --base=main
affected es lo que convierte un CI de veinte minutos en uno de dos: en un monorepo, la inmensa mayoría de los commits tocan una fracción pequeña del grafo, y no tiene sentido testear ni construir lo que demostrablemente no cambió ni depende de nada que cambió.
Configuraciones: variantes con nombre de un target
Un target puede tener varias configuraciones: variantes con nombre que sobrescriben opciones. La forma web:build:production selecciona la configuración production, que hereda las opciones base y cambia solo lo necesario. Es como Nx modela el mismo target en distintos entornos sin duplicar toda la definición.
// project.json: un target con una configuracion de produccion
{
"targets": {
"build": {
"executor": "@nx/vite:build",
"options": { "sourcemap": true },
"configurations": {
"production": { "sourcemap": false, "minify": true }
}
}
}
}
Hay una tentación al llegar a Nx: pensar en dependsOn, inputs y outputs como formularios burocráticos que hay que rellenar para que la herramienta funcione. Es justo lo contrario. Cada uno de esos campos es una pieza de conocimiento que le transfieres al sistema para que decida por ti algo que, sin él, tendrías que orquestar a mano. dependsOn: ["^build"] no es una obligación administrativa: es la frase con la que renuncias a ordenar las compilaciones tú mismo y dejas que el task graph lo haga con un ordenamiento topológico que nunca se equivoca. inputs y outputs no son metadatos: son la definición precisa de qué hace idéntica a una tarea, y por tanto la frontera exacta entre trabajo que hay que repetir y trabajo que se puede reutilizar. La diferencia entre un runner imperativo —donde tú escribes la secuencia de comandos y su orden— y Nx es que aquí describes las propiedades de cada tarea y el sistema deriva la ejecución óptima. Ese cambio de imperativo a declarativo es el que hace que la misma configuración sirva para correr en local, para paralelizar en una máquina de CI y para repartirse entre veinte agentes en la nube sin tocar una línea. No estás configurando comandos; estás describiendo un grafo de tareas con la suficiente fidelidad como para que otro —el planificador de Nx— tome todas las decisiones de ejecución mejor de lo que las tomarías a mano. El executor es la única capa imperativa que queda, y precisamente por eso conviene que sea delgada.
- Añade
dependsOn: ["^build"]albuildentargetDefaultsy comprueba connx graphque las librerías se ordenan antes que las apps. - Ejecuta un
build, vuelve a ejecutarlo sin cambios y observa el acierto de caché instantáneo con la salida restaurada. - Cambia una variable de entorno que la tarea use pero que no esté en
inputs: comprueba que Nx sirve la caché vieja, y luego arréglalo añadiéndola a los inputs. - Convierte un target que hoy corre un script con
nx:run-commandsy compáralo con el executor de plugin equivalente. - Mide el tiempo de
nx run-many -t testfrente anx affected -t testtras un commit pequeño y anota la diferencia.