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é es | Quién lo dispara | |
|---|---|---|
| Actualización bajo demanda | Vuelve a extraer un conjunto de datos ahora | Tú, tu agente, o tu CI — POST /v1/datasets/{dataset_id}/refresh |
| Carga incremental | Trae solo lo que cambió desde la última ejecución | La misma llamada, con mode: "incremental" |
| Actualización programada | Una expresión cron que la plataforma ejecuta por ti | Nadie — 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.
completedestrueydatasetes 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.completedesfalse. Sondea el conjunto de datos, o escucha el websocket, hasta queprocessingStatusdeje de serPROCESSING.
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.backendmuestradb_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.
executionCountsube,lastStatusessuccess, y la fila más reciente sigue siendo de marzo. - Un conjunto de datos puede no tener ninguna programación, y entonces
executionCountno 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
| Campo | Qué es |
|---|---|
asOf | El 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. |
lastLoadedAt | Cuá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. |
refresh | Si 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_refreshydelete_refreshcubren 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).