Databricks Tips #8: Jobs & Workflows — streaming y triggers que arrancan solos

Databricks Tips
Data Engineering
Streaming
File arrival triggers, table update triggers, Trigger.AvailableNow, Job Clusters y las limitaciones que no están en el tutorial.
Autor
Publicado

4 de junio de 2026

Laboratorio práctico — AutoLoader + AvailableNow + microbatch bronze→silver con CDF. Ejecutable en Databricks Free Edition.


Databricks tiene cuatro formas de disparar un Job automáticamente. Dos de ellas — file arrival y table update — son event-driven y te permiten armar pipelines de streaming sin dejar un cluster corriendo 24/7. Pero tienen limitaciones que no están en el tutorial de 5 minutos.

En este post vamos a ver cómo funcionan, cuándo usarlos, y los errores que te van a hacer perder horas.


Jobs en 2 minutos

Un Job en Databricks es una unidad de ejecución programada. Pensalo como un cron job con esteroides: puede tener múltiples Tasks (notebooks, scripts Python, SQL, JARs, pipelines DLT), cada una con sus dependencias, y todo orquestado como un DAG.

La diferencia clave con un notebook interactivo:

Notebook interactivo Job
Compute All-Purpose Cluster (siempre prendido) Job Cluster (se prende y se apaga)
Ejecución Manual, ad-hoc Automática, programada o event-driven
Costo Pagás mientras esté prendido Pagás solo lo que usás
Uso Exploración, desarrollo Producción

La analogía: un notebook interactivo es como dejar la cocina prendida todo el día por si querés cocinar. Un Job es como prender la hornalla solo cuando tenés los ingredientes listos.

Los 4 triggers

Databricks ofrece cuatro tipos de trigger para disparar Jobs automáticamente:

Cuatro tipos de triggers en Databricks Jobs: Scheduled, File Arrival, Table Update y Continuous.

Cuatro tipos de triggers en Databricks Jobs: Scheduled, File Arrival, Table Update y Continuous.
Trigger Dispara cuando… Latencia típica Costo compute
Scheduled Pasa el tiempo configurado (cron) Fija (según schedule) Bajo si es esporádico
File Arrival Llegan archivos nuevos a un Volume o external location ~1 min (con file events) Solo cuando hay datos
Table Update Una tabla Delta/Iceberg se actualiza ~1 min (con file events) Solo cuando hay datos
Continuous Siempre (restart automático al terminar) Sub-60 seg Alto (cluster siempre vivo)

Los dos que nos interesan hoy son File Arrival y Table Update: event-driven, eficientes, y con trampas que no son obvias.

Job Clusters: por qué siempre en producción

Antes de meternos con triggers, hay que hablar de dónde corre tu Job. Porque si usás un trigger event-driven con un All-Purpose Cluster… estás tirando plata.

All-Purpose Cluster vs Job Cluster: el primero paga tiempo idle, el segundo solo paga lo que ejecuta.

All-Purpose Cluster vs Job Cluster: el primero paga tiempo idle, el segundo solo paga lo que ejecuta.

Job Cluster = se crea cuando el Job arranca, se destruye cuando termina. Pagás solo por el tiempo de ejecución.

All-Purpose Cluster = está siempre prendido (o con auto-termination). Pagás todo el tiempo que esté vivo, ejecutes o no.

Para pipelines event-driven, la cuenta es simple:

  • Si tus datos llegan cada 3 horas → el Job Cluster corre ~15 min por run → pagás ~1 hora/día
  • Con All-Purpose → pagás 24 horas/día (o lo que dure antes del auto-terminate, y después tarda en re-encender)

Regla: en producción, siempre Job Cluster. All-Purpose es para desarrollo interactivo.

TipTip: Instance Pools

Si el startup del Job Cluster te parece lento (~5 min), usá Instance Pools. Mantienen VMs pre-calentadas y reducen el startup a ~1-2 min.

File Arrival Trigger

El file arrival trigger monitorea un Volume de Unity Catalog o una external location y dispara tu Job cuando detecta archivos nuevos. Chequea cada ~1 minuto (best effort).

Configuración

  1. En Jobs & Pipelines, seleccioná tu Job
  2. En Schedules & TriggersAdd triggerFile arrival
  3. En Storage location, ingresá la ruta del Volume:
Listado 1: Ruta del Volume para configurar el file arrival trigger
/Volumes/mi_catalogo/mi_schema/mi_volume/bronze/
  1. Configurá las opciones avanzadas:
    • Minimum time between triggers (segundos): tiempo mínimo entre runs. Si llegan archivos durante este período, se acumulan y disparan un solo run al finalizar.
    • Wait after last change (segundos): espera X segundos después del último archivo nuevo. Si llega otro archivo, resetea el timer. Útil cuando los archivos llegan en batches.
  2. Click en Test connection para validar → Save

Monitoreo recursivo

El trigger monitorea todos los subdirectorios de la ruta configurada. Si configurás /Volumes/catalog/schema/volume/bronze/, también detecta archivos en:

Listado 2: Subdirectorios monitoreados recursivamente por el trigger
/Volumes/catalog/schema/volume/bronze/2026/06/
/Volumes/catalog/schema/volume/bronze/2026/06/09/
/Volumes/catalog/schema/volume/bronze/clientes/

File events

Para mejor performance, habilitá file events en la external location. Sin file events, Databricks hace listing de archivos (polling). Con file events, usa notificaciones del cloud provider (Azure Event Grid, AWS S3 Events) — la detección baja de minutos a segundos.

ImportanteImportante: habilitá file events

Sin file events habilitados, tenés un límite de 50 triggers por workspace y 10.000 archivos por path monitoreado. Con file events, estos límites desaparecen. Habilitarlos es un one-time setup en la external location.

Limitaciones del File Arrival Trigger

Acá es donde la documentación se pone interesante. Estas son las trampas:

Limitación Con file events Sin file events
Archivos por path Sin límite Máx 10.000
Triggers por workspace Sin límite documentado Máx 50
Overwrites No disparan No disparan
Detección Segundos ~1 min (best effort)

Las que duelen

1. Overwrites no disparan runs. Si sobreescribís un archivo con el mismo nombre, el trigger no se entera. Esto es by design, no un bug. Si tu proceso upstream hace PUT sobre el mismo archivo, necesitás otra estrategia (nombres con timestamp, o table update trigger sobre la tabla destino).

2. Timeout por cambios ajenos al subpath. Con file events habilitados, si configurás el trigger en un subpath de una external location (ej: /bronze/clientes/), cambios en otros subpaths de la misma external location (ej: /bronze/ventas/, /bronze/productos/) pueden generar metadatos que el trigger necesita procesar. En entornos con muchos cambios, esto puede causar timeout y estado de error.

Solución: creá un Volume dedicado de Unity Catalog que apunte específicamente al directorio que querés monitorear. Así aislás el trigger del ruido.

3. Paths fantasma en S3/GCS. Si el directorio configurado no existe o fue borrado en S3 o GCS, el trigger sigue evaluando sin error. No falla, no te notifica — simplemente no encuentra archivos y no dispara runs. En Azure (ADLS), sí da error.

4. ADLS y FlushWithClose. En Azure, file events escucha el evento FlushWithClose para detectar archivos nuevos. Algunas APIs de Azure no emiten este evento, lo que puede retrasar la detección. Si tus archivos llegan por una API que no emite FlushWithClose, vas a tener que investigar el modo de notificación clásico.

Table Update Trigger

El table update trigger monitorea tablas de Unity Catalog y dispara tu Job cuando detecta cambios (inserts, updates, merges, deletes).

Tablas soportadas

  • Delta managed tables (Unity Catalog)
  • Iceberg managed tables (Unity Catalog)
  • External tables backed by Delta Lake
  • Materialized views
  • Streaming tables
  • Views y metric views de UC (con restricciones — ver limitaciones)
  • Delta Sharing tables y system tables (Beta)

Configuración

  1. En Jobs & Pipelines, seleccioná tu Job
  2. Add triggerTable update
  3. Agregá las tablas a monitorear (hasta 10)
  4. Si seleccionás más de una, elegí el modo:
    • Any table is updated: dispara cuando cualquiera cambia
    • All tables are updated: dispara cuando todas cambiaron
  5. Opciones avanzadas (mismo patrón que file arrival):
    • Minimum time between triggers
    • Wait after last change
  6. Test triggerSave

Parámetros dinámicos

Cuando usás table update triggers, Databricks inyecta parámetros que podés usar en tu notebook:

Listado 3: Parámetros dinámicos inyectados por el table update trigger
# Qué tablas se actualizaron (JSON list)
updated = dbutils.widgets.get("job.trigger.table_update.updated_tables")

# Timestamp del último commit que disparó el trigger
ts = dbutils.widgets.get(
    "job.trigger.table_update.mi_catalogo.mi_schema.mi_tabla.commit_timestamp.iso_datetime"
)

# Versión del último commit
version = dbutils.widgets.get(
    "job.trigger.table_update.mi_catalogo.mi_schema.mi_tabla.version"
)

Estos parámetros te permiten hacer procesamiento incremental inteligente: solo procesás lo que cambió desde la última ejecución.

Limitaciones del Table Update Trigger

Limitación Detalle
Tablas por trigger Máx 10
Views Las dependencias cuentan como tablas. View con 11 tablas dependientes = no se puede usar
Views dependientes Máx 10 views dependientes por view monitoreada
False positives en views Cambios filtrados por la view igual disparan el job
Sin file events Máx 1.000 jobs con table update trigger por workspace
Delta Sharing Solo Databricks-to-Databricks (Beta). Open sharing no soportado

La trampa de las views

Si monitoreás una view que filtra por WHERE region = 'LATAM', y alguien actualiza filas con region = 'EU'el trigger se dispara igual. Databricks monitorea las tablas subyacentes, no el resultado de la view. Tu Job va a correr innecesariamente.

Y peor: si tu view depende de 6 tablas, y agregás otra view que depende de 5 tablas al mismo trigger, ya estás en 11 — y el trigger falla.

Tip: usá tablas directamente en el trigger, no views. Es más predecible.

Trigger.AvailableNow — el trigger que te falta

Cuando combinás un trigger event-driven con un Job, necesitás que tu código de streaming procese todo lo pendiente y termine. Si usás Trigger.ProcessingTime("10 seconds"), tu stream queda corriendo indefinidamente y el Job nunca termina (y el Job Cluster nunca se apaga).

Trigger.AvailableNow es la solución:

Listado 4: Streaming con Trigger.AvailableNow: procesa todo y termina
(spark.readStream
    .format("cloudFiles")
    .option("cloudFiles.format", "json")
    .option("cloudFiles.schemaLocation", checkpoint_path)
    .load(source_path)
    .writeStream
    .option("checkpointLocation", checkpoint_path)
    .trigger(availableNow=True)    # <-- procesa todo y termina
    .toTable("catalog.schema.mi_tabla_bronze")
)

Comparativa

Trigger Comportamiento Job Cluster se apaga? Serverless?
availableNow=True Procesa todo lo pendiente → termina
processingTime="10s" Ejecuta micro-batches cada 10s → nunca termina No No
once=True Procesa un solo micro-batch → termina
continuous Procesamiento continuo, latencia ~1ms No No

Trigger.Once está deprecado: solo procesa un micro-batch, no todo lo pendiente. Si llegaron 10.000 archivos, once=True procesa un batch y deja el resto para la próxima ejecución. AvailableNow procesa todo.

Por qué AvailableNow es perfecto para Jobs

El flujo es:

  1. Llegan archivos / se actualiza tabla → trigger dispara
  2. Job Cluster se crea (~2-5 min, o ~1 min con Instance Pools)
  3. Tu notebook ejecuta el stream con availableNow=True
  4. Procesa todo lo acumulado desde el último checkpoint
  5. Stream termina → Job Cluster se destruye → dejás de pagar

Esto es streaming en modo batch eficiente: tenés las garantías de exactly-once de Structured Streaming (checkpoints), pero sin el costo de un cluster 24/7.

NotaNota: Serverless

En serverless compute, solo Trigger.AvailableNow funciona para Structured Streaming. ProcessingTime y Trigger.Continuous no están soportados. Otro motivo más para usar AvailableNow.

Continuous mode + Streaming

Si necesitás latencia sub-minuto, el modo continuous es otra opción. Configurás el Job como Continuous y Databricks reinicia automáticamente el run cuando termina (con un delay de < 60 segundos).

Listado 5: Continuous mode con AvailableNow: restart automático al terminar
# Con continuous mode, usás AvailableNow
# El Job Cluster se reinicia automáticamente al terminar
(spark.readStream
    .format("delta")
    .table("catalog.schema.bronze")
    .writeStream
    .option("checkpointLocation", checkpoint_path)
    .trigger(availableNow=True)
    .toTable("catalog.schema.silver")
)

El retry usa exponential backoff: si el task falla, reintenta con delays crecientes (máximo 3 reintentos por task). Si sigue fallando, cancela el run y arranca uno nuevo.

Continuous vs AvailableNow + Trigger event-driven

Continuous + AvailableNow File Arrival/Table Update + AvailableNow
Latencia Sub-60 seg (delay entre runs) 1-5 min (detección + cluster startup)
Costo Alto (cluster siempre vivo*) Bajo (cluster solo cuando hay datos)
Complejidad Baja (no configurás trigger) Media (configurás trigger + file events)
Ideal para Streams de alta frecuencia Datos que llegan esporádicamente

* En continuous mode el cluster está siempre activo porque los runs se reinician automáticamente.

Regla práctica: si tus datos llegan cada 5+ minutos, usá trigger event-driven. Si llegan constantemente (Kafka, IoT), usá continuous o directamente Lakeflow Declarative Pipelines.

Ejemplo: AutoLoader + File Arrival + AvailableNow

El escenario más común: archivos JSON llegan a un Volume de Unity Catalog, querés ingestarlos en una tabla Delta.

Código del notebook

Listado 6: AutoLoader con file events y AvailableNow para ingesta bronze
# Configuración
volume_path = "/Volumes/mi_catalogo/mi_schema/bronze/clientes/"
checkpoint_path = "/Volumes/mi_catalogo/mi_schema/checkpoints/clientes_bronze/"
target_table = "mi_catalogo.mi_schema.clientes_bronze"

# AutoLoader: descubre archivos nuevos incrementalmente
(spark.readStream
    .format("cloudFiles")
    .option("cloudFiles.format", "json")
    .option("cloudFiles.schemaLocation", checkpoint_path)
    .option("cloudFiles.useManagedFileEvents", "true")  # usa file events
    .option("cloudFiles.inferColumnTypes", "true")
    .load(volume_path)
    .writeStream
    .option("checkpointLocation", checkpoint_path)
    .option("mergeSchema", "true")   # maneja schema evolution
    .trigger(availableNow=True)
    .toTable(target_table)
)

¿Qué pasa acá?

  1. El file arrival trigger detecta archivos nuevos en el Volume
  2. Dispara el Job → se crea un Job Cluster
  3. AutoLoader (cloudFiles) descubre los archivos nuevos desde el último checkpoint
  4. availableNow=True procesa todo lo pendiente y termina
  5. El Job Cluster se destruye → dejás de pagar

El checkpoint garantiza exactly-once: si el Job falla a mitad, al reiniciar retoma desde donde quedó.

Variante con foreachBatch (lógica custom)

Si necesitás hacer algo más que escribir en una tabla (ej: llamar una API, validar datos, merge):

Listado 7: foreachBatch con validación y escritura a bronze y quarantine
def procesar_batch(batch_df, batch_id):
    # Validaciones
    df_valido = batch_df.filter("email IS NOT NULL AND cantidad > 0")
    df_invalido = batch_df.filter("email IS NULL OR cantidad <= 0")

    # Escribir válidos en bronze
    df_valido.write.mode("append").saveAsTable("catalog.schema.clientes_bronze")

    # Escribir rechazados en quarantine
    df_invalido.write.mode("append").saveAsTable("catalog.schema.clientes_quarantine")

(spark.readStream
    .format("cloudFiles")
    .option("cloudFiles.format", "json")
    .option("cloudFiles.schemaLocation", checkpoint_path)
    .option("cloudFiles.useManagedFileEvents", "true")
    .load(volume_path)
    .writeStream
    .foreachBatch(procesar_batch)
    .option("checkpointLocation", checkpoint_path)
    .trigger(availableNow=True)
    .start()
)
AdvertenciaOjo con foreachBatch

foreachBatch da at-least-once guarantees, no exactly-once. Si el Job falla y reinicia, puede reprocesar un batch. Diseñá tu lógica para ser idempotente (ej: usá MERGE en vez de INSERT).

Ejemplo: Microbatch con Table Update

Escenario: la tabla bronze se actualiza (por el pipeline del ejemplo anterior, o por otro proceso). Querés transformar los datos nuevos y escribirlos en silver.

Paso 1: Habilitar Change Data Feed en la tabla bronze

Listado 8: Habilitar Change Data Feed en la tabla bronze
ALTER TABLE mi_catalogo.mi_schema.clientes_bronze
SET TBLPROPERTIES (delta.enableChangeDataFeed = true);

Paso 2: Configurar el table update trigger

En el Job de silver, agregá un trigger Table Update que monitoree mi_catalogo.mi_schema.clientes_bronze con modo Any table is updated.

Paso 3: Notebook de transformación

Listado 9: Transformación bronze a silver con Change Data Feed (CDF)
checkpoint_path = "/Volumes/mi_catalogo/mi_schema/checkpoints/clientes_silver/"

(spark.readStream
    .format("delta")
    .option("readChangeFeed", "true")   # lee solo los cambios (CDF)
    .table("mi_catalogo.mi_schema.clientes_bronze")
    .filter("_change_type IN ('insert', 'update_postimage')")
    .drop("_change_type", "_commit_version", "_commit_timestamp")
    .withColumn("nombre_upper", F.upper(F.col("nombre")))
    .withColumn("procesado_at", F.current_timestamp())
    .writeStream
    .option("checkpointLocation", checkpoint_path)
    .trigger(availableNow=True)
    .toTable("mi_catalogo.mi_schema.clientes_silver")
)

Usando los parámetros dinámicos del trigger

Opcionalmente, podés usar los parámetros que Databricks inyecta para logging o auditoría:

Listado 10: Uso de parámetros dinámicos del trigger para logging
# Qué tablas se actualizaron
updated_tables = dbutils.widgets.get("job.trigger.table_update.updated_tables")
print(f"Tablas actualizadas: {updated_tables}")

# Versión del commit que disparó el trigger
trigger_version = dbutils.widgets.get(
    "job.trigger.table_update.mi_catalogo.mi_schema.clientes_bronze.version"
)
print(f"Procesando desde versión: {trigger_version}")

Gotchas

1. El startup del cluster mata la latencia. Un Job Cluster tarda ~3-5 minutos en arrancar. Si tu trigger detecta datos en 1 minuto pero el cluster tarda 5 en prender, tu latencia real es de 6 minutos. Solución: Instance Pools o serverless compute.

2. File events no habilitados = modo degradado. Sin file events, estás limitado a 50 triggers y 10.000 archivos. Y la detección es por polling (listing de archivos), no por notificación. Habilitá file events en la external location — es gratis y es un one-time setup.

3. Overwrites son invisibles. Si tu proceso upstream sobreescribe archivos en vez de crear nuevos, el file arrival trigger no se entera. Cambiá la estrategia a archivos con timestamp en el nombre, o usá table update trigger sobre la tabla destino.

4. Views que disparan de más. Ya lo vimos: si monitoreás una view con table update trigger, cualquier cambio en las tablas subyacentes dispara el job, aunque la view no muestre datos nuevos. Usá tablas directas.

5. Serverless no soporta ProcessingTime. Si migrás a serverless compute y tu notebook usa trigger(processingTime="10 seconds"), va a fallar. Cambialo a trigger(availableNow=True).

6. No uses All-Purpose Clusters para esto. Ya lo dijimos, pero vale repetir: si tu trigger dispara 4 veces al día y cada run tarda 15 minutos, con Job Cluster pagás 1 hora. Con All-Purpose pagás 24. La diferencia a fin de mes duele.

Cuándo NO usar Jobs para streaming

Jobs con triggers event-driven son ideales para streaming micro-batch: datos que llegan cada minutos/horas, latencia de minutos es aceptable.

Pero no son la mejor opción siempre:

Escenario Mejor opción
Latencia sub-segundo (Kafka, IoT) Lakeflow Declarative Pipelines (continuous mode)
Pipeline con quality gates y expectations Lakeflow Declarative Pipelines (expectations)
Orquestación cross-workspace o multi-cloud Airflow / orquestador externo
ETL simple y frecuente (cada 5 min) Jobs con trigger scheduled (más simple)

Referencias

Otros posts de la serie

Si te sirvió este post, mirá los anteriores de Databricks Tips:


Próxima semana: SQL Warehouses & Serverless — compute on-demand que escala solo para analytics y BI.