Saltar al contenido principal

Mantén tus datos frescos

Un panel solo vale su URL si los números que muestra son actuales. Esa es la parte que una sesión de agente no puede dejar atrás: tu agente construye el panel y se va, y los datos debajo tienen que seguir llegando en una programación, durante meses, sin que nadie esté pendiente.

La frescura es, por tanto, estado de plataforma, no código de panel. La configuras una vez contra un conjunto de datos; sigue ejecutándose tenga o no alguien una sesión abierta.

Las tres formas en que los datos avanzan​

Qué esQuién lo dispara
Actualización bajo demandaVuelve a extraer un conjunto de datos ahoraTú, tu agente, o tu CI — POST /v1/datasets/{dataset_id}/refresh
Carga incrementalTrae solo lo que cambió desde la última ejecuciónLa misma llamada, con mode: "incremental"
Actualización programadaUna expresión cron que la plataforma ejecuta por tiNadie — el poller cron de la plataforma

Actualiza un conjunto de datos ahora​

result = client.datasets.refresh(dataset_id)
print(result.completed, result.effective_mode, result.message)
const result = await client.datasets.refresh(datasetId);
console.log(result.completed, result.effectiveMode, result.message);

Dos resultados posibles, y la diferencia importa:

  • 200 OK — el trabajo terminó de verdad dentro de la petición. completed es true y dataset es la fila final. Esto es lo que hace una redescarga de un dataset público.
  • 202 Accepted — la actualización se ha iniciado. La fila queda bloqueada y marcada PROCESSING, y un conector corre segundos después. completed es false. Sondea el conjunto de datos, o escucha el websocket, hasta que processingStatus deje de ser PROCESSING.

Un 202 no es un fallo ni un éxito — es una promesa. Trata "la llamada devolvió respuesta" y "los datos están al día" como dos hechos distintos, y comprueba el segundo antes de decirle a alguien que el panel está actualizado. La llamada que lo comprueba es get_data_freshness — todo lo anterior a esa sección trata de hacer que los datos se muevan, no de cuán antiguos son.

Cargas incrementales, y la ejecución que todavía no es incremental​

Pasa un modo para traer solo las filas nuevas:

from vibedasher.models import RefreshRequest

result = client.datasets.refresh(dataset_id, RefreshRequest(mode="incremental"))
print(result.requested_mode, "->", result.effective_mode)

requestedMode refleja lo que pediste. effectiveMode es lo que realmente se ejecutó, y no siempre coinciden. Una petición incremental contra un conjunto de datos sin un checkpoint todavía informa effectiveMode: "bootstrap": se ejecuta como una sobrescritura completa y registra la marca de agua, así que la ejecución siguiente es la primera genuinamente incremental.

Ramifica según effectiveMode, nunca según lo que pediste. (Es la misma regla que client.query, que informa del carril de ejecución que realmente usó en lugar del que pediste — un patrón que vale la pena interiorizar: la plataforma te dice lo que ocurrió, y es tu trabajo leerlo.)

Para forzar que la próxima ejecución vuelva a traer todo, limpia la marca de agua:

client.datasets.reset_checkpoint(dataset_id)

Actualización programada​

Una programación es una fila que creas una vez. Lleva una expresión cron y el proyecto dbt que actualiza:

from vibedasher.models import RefreshCreate

schedule = client.refreshes.create_refresh(
RefreshCreate(name="nightly", project_id=project_id, cron="0 3 * * *")
)
print(schedule.refresh.next_run, schedule.schedule_registration.registered)

La respuesta lleva un bloque scheduleRegistration. Comprueba registered. Es true solo cuando la programación está genuinamente en vivo; cuando es false, reason y message dicen por qué, y nextRun es null.

Ese campo existe por una interrupción real, y la historia es el mejor argumento a favor del diseño actual. La implementación original registraba cada programación como su propia regla en la nube, envolvía la llamada en un except amplio, y respondía 201 Created con un nextRun confiado. El rol de la API no tenía permiso para crear esas reglas, así que nunca se registró nada — y a cada cliente se le decía que sus datos se actualizarían. Se descubrió el 2026‑09‑05: no existía ninguna regla, y la única programación en la base de datos tenía un nextRun cinco meses en el pasado y un contador de ejecuciones de cero.

La solución no fue un mejor manejo de errores. Fue eliminar lo que podía fallar: la fila es la programación. Un poller cron corre cada cinco minutos, reclama cada fila vencida con un compare-and-set, y la ejecuta. No hay ningún registro externo que pueda tener éxito o fallar, así que lo único que puede salir mal al crearla es una expresión cron que no se pueda evaluar — y eso ahora se rechaza de antemano, no al momento de dispararse.

Dos consecuencias que vale la pena tener en cuenta:

  • Una programación se dispara dentro de unos cinco minutos de su hora prevista, no en el segundo exacto. Cron aquí significa "no antes de", no "exactamente a".
  • scheduleRegistration.backend muestra db_cron. Se mantiene en la respuesta porque los clientes ramifican según él.

Leer qué ha pasado​

Cada fila de programación lleva su propio historial — lastRun, lastStatus, lastError, executionCount, nextRun:

for r in client.refreshes.iter_all():
print(r.name, r.last_status, r.last_run, r.execution_count, r.last_error)

executionCount es el campo que hay que vigilar. Una programación que ha existido durante una semana con executionCount: 0 no va lenta — no se está ejecutando, y ese es exactamente el fallo que el diseño anterior estaba pensado para hacer visible en lugar de silencioso.

¿Qué antigüedad tienen estas cifras?​

Todo lo anterior responde a «¿funciona el pipeline?». No responde a «¿está al día lo que muestra este panel?», y ambas cosas se separan en las dos direcciones:

  • Una programación puede ejecutarse perfectamente y no cargar nada nuevo. executionCount sube, lastStatus es success, y la fila más reciente sigue siendo de marzo.
  • Un conjunto de datos puede no tener ninguna programación, y entonces executionCount no tiene fila que leer. Una subida estática es el caso habitual: sus filas cambian cuando alguien sube otras nuevas y en ningún otro momento, así que una lista de programaciones sana puede convivir con un conjunto de datos de nueve días de antigüedad sin decir nada al respecto. La ausencia de programación no es una programación con mal contador.

Una sola llamada responde a la pregunta real del lector, medida sobre las filas y no sobre la contabilidad interna de la plataforma:

f = client.datasets.get_freshness(dataset_id).freshness
print(f.as_of, f.age_days, f.status)
const { freshness } = await client.datasets.getFreshness(datasetId);
console.log(freshness.asOf, freshness.ageDays, freshness.status);

En MCP es get_data_freshness, que toma exactamente uno de viz_id o dataset_id. Con un viz_id resuelve cada conjunto de datos que lee el panel y devuelve el más antiguo: un panel solo está tan al día como su entrada más vieja. La ruta REST es GET /v1/datasets/{dataset_id}/freshness.

Los tres campos, y por qué son tres​

CampoQué es
asOfEl valor más reciente en la propia columna temporal de los datos. Esta es la respuesta, y no puede halagarte: se lee de las filas. ageDays es su antigüedad; un valor negativo significa datos con fecha futura.
lastLoadedAtCuándo aterrizaron las filas por última vez. Un hecho del pipeline, no una antigüedad — un archivo cargado esta mañana puede no contener nada más reciente que marzo. Nunca lo cites como fecha de referencia.
refreshSi hay algo programado para actualizar las cifras, incluida la respuesta «nada, y nada puede haberlo» para un archivo plano. Este es el campo que cubre el caso «sin programación» descrito arriba.

status dice cuánto fiarse de asOf: MEASURED (es real), NO_TIME_COLUMN (los datos no llevan columna de fecha, así que no tienen fecha de referencia que tener), o UNKNOWN — que significa que el hecho no se estableció y nunca es un aprobado.

lastLoadedSource nombra qué reloj respondió a lastLoadedAt. iceberg_snapshot es la hora de escritura física de la instantánea actual de la tabla: el único instante de aterrizaje verdadero, y uno que un renombrado no puede mover. not_established significa que lastLoadedAt es null a propósito: ese camino de lectura no consultó la instantánea, así que en lugar de responder con el updatedAt de la fila — un reloj de escritura que cualquier edición adelanta, y que una vez informó de filas aterrizando nueve días después de haberlo hecho — el campo declina. Un campo que declina es estrictamente mejor que uno que responde con el reloj equivocado. La ruta de arriba es la llamada que lee el correcto. get_viz y list_vizzes llevan un dataFreshness gratuito, definido sobre la postura de actualización y UNKNOWN sobre la fecha de referencia; esta llamada es la que resuelve ese UNKNOWN.

Lo que no es la antigüedad de los datos​

Ni publishStatus, ni lastBuild.createdAt, ni processingStatus, ni updatedAt responden a esta pregunta. Son, en orden: si existe un bundle, cuándo se compiló el código, si el archivo se analizó una vez, y cuándo cambió la fila por última vez. Cada uno de ellos se lee como al día en un panel cuyas cifras tienen semanas. Si estás a punto de informar de actualidad a partir de cualquiera de ellos, llama a la ruta de frescura en su lugar.

Cuesta un SELECT MAX(<columna de fecha>) por conjunto de datos — la misma sonda que el panel renderizado ya ejecuta al montarse.

Dónde vive cada gesto​

  • Las planificaciones son la superficie MCP. create_refresh, list_refreshes, get_refresh y delete_refresh cubren todo el ciclo de vida de una actualización recurrente desde tu agente.
  • «Actualiza ahora» es el SDK y la REST API. Una ejecución puntual es una sola llamada allí — mira la API Reference.
  • Los proyectos dbt llevan sus propias rutas de ejecución y actualización (/v1/dbt/projects/{project_id}/run, /v1/dbt/projects/{project_id}/refreshes).