Garder vos données fraîches
Un tableau de bord ne vaut son URL que si les chiffres qu'il affiche sont à jour. C'est la partie qu'une session d'agent ne peut pas laisser derrière elle : votre agent construit le tableau de bord puis s'en va, et les données sous-jacentes doivent continuer d'arriver selon un planning, pendant des mois, sans que personne ne soit dans la boucle.
La fraîcheur est donc un état de plateforme, pas du code de tableau de bord. Vous la configurez une fois contre un jeu de données ; elle continue de fonctionner que quelqu'un maintienne ou non une session ouverte.
Les trois façons de faire avancer les données
| Ce que c'est | Qui le déclenche | |
|---|---|---|
| Actualisation à la demande | Retirer un jeu de données maintenant | Vous, votre agent, ou votre CI — POST /v1/datasets/{dataset_id}/refresh |
| Chargement incrémental | Ne récupérer que ce qui a changé depuis la dernière exécution | Le même appel, avec mode: "incremental" |
| Actualisation planifiée | Une expression cron que la plateforme exécute pour vous | Personne — le poller cron de la plateforme |
Actualiser un jeu de données maintenant
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);
Deux résultats possibles, et la différence compte :
- 200 OK — le travail s'est réellement terminé au cours de la requête.
completedesttrueetdatasetest la ligne finale. C'est ce qui se passe pour un re-téléchargement de jeu de données public. - 202 Accepted — l'actualisation a été démarrée. La ligne est verrouillée et marquée
PROCESSING, et un connecteur s'exécute quelques secondes plus tard.completedestfalse. Sondez le jeu de données, ou écoutez le websocket, jusqu'à ce queprocessingStatussorte dePROCESSING.
Un 202 n'est ni un échec ni une réussite — c'est une promesse. Traitez « l'appel est
revenu » et « les données sont à jour » comme deux faits différents, et vérifiez le second
avant de dire à quelqu'un que le tableau de bord est à jour. L'appel qui le vérifie est
get_data_freshness — tout ce qui précède cette section
concerne la circulation des données, pas leur ancienneté.
Chargements incrémentaux, et l'exécution qui n'est pas encore incrémentale
Passez un mode pour ne récupérer que les nouvelles lignes :
from vibedasher.models import RefreshRequest
result = client.datasets.refresh(dataset_id, RefreshRequest(mode="incremental"))
print(result.requested_mode, "->", result.effective_mode)
requestedMode renvoie ce que vous avez demandé. effectiveMode est ce qui s'est
réellement exécuté, et ce n'est pas toujours la même chose. Une demande incrémentale
contre un jeu de données sans point de contrôle existant renvoie
effectiveMode: "bootstrap" : elle s'exécute comme un écrasement complet et enregistre le
filigrane, si bien que l'exécution suivante est la première véritablement
incrémentale.
Basez votre logique sur effectiveMode, jamais sur ce que vous avez demandé. (C'est la
même règle que pour client.query, qui remonte la voie d'exécution réellement utilisée
plutôt que celle que vous avez demandée — un principe qui mérite d'être intériorisé : la
plateforme vous dit ce qui s'est passé, et c'est à vous de le lire.)
Pour forcer la prochaine exécution à tout récupérer à nouveau, réinitialisez le filigrane :
client.datasets.reset_checkpoint(dataset_id)
Actualisation planifiée
Une planification est une ligne que vous créez une fois. Elle porte une expression cron et le projet dbt qu'elle actualise :
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 réponse porte un bloc scheduleRegistration. Vérifiez registered. Il est true
seulement quand la planification est réellement active ; quand il est false, reason et
message expliquent pourquoi, et nextRun est nul.
Ce champ existe à cause d'une vraie panne, et l'histoire est le meilleur argument en faveur
de la conception actuelle. L'implémentation d'origine enregistrait chaque planification
comme sa propre règle cloud, enveloppait l'appel dans un except large, et répondait
201 Created avec un nextRun confiant. Le rôle de l'API n'avait pas la permission de
créer ces règles, si bien que rien n'a jamais été enregistré — et chaque client a été
informé que ses données seraient actualisées. Cela a été découvert le 2026‑09‑05 : aucune
règle n'existait, et la seule planification en base avait un nextRun vieux de cinq mois
et un nombre d'exécutions de zéro.
Le correctif n'a pas été une meilleure gestion des erreurs. Ce fut la suppression de la chose qui pouvait échouer : la ligne est la planification. Un poller cron s'exécute toutes les cinq minutes, réclame chaque ligne due par compare-and-set, et l'exécute. Il n'y a pas d'enregistrement externe qui puisse réussir ou échouer, donc la seule chose qui peut mal se passer à la création est une expression cron qui ne peut pas être évaluée — et c'est désormais rejeté d'emblée, pas au moment du déclenchement.
Deux conséquences à prévoir :
- Une planification se déclenche dans environ cinq minutes après son heure prévue, pas à la seconde près. Cron signifie ici « pas avant », pas « exactement à ».
scheduleRegistration.backendaffichedb_cron. Il est conservé dans la réponse parce que des clients s'appuient dessus.
Lire ce qui s'est passé
Chaque ligne de planification porte son propre historique — 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 est le champ à surveiller. Une planification qui existe depuis une semaine
avec executionCount: 0 n'est pas lente — elle ne s'exécute pas, et c'est exactement
l'échec que la conception ci-dessus a été construite pour rendre visible plutôt que
silencieux.
Quel âge ont ces chiffres ?
Tout ce qui précède répond à « le pipeline fonctionne-t-il ? ». Cela ne répond pas à « ce que montre ce tableau de bord est-il à jour ? », et les deux divergent dans les deux sens :
- Une planification peut s'exécuter parfaitement et ne rien charger de nouveau.
executionCountaugmente,lastStatusvautsuccess, et la ligne la plus récente date toujours de mars. - Un jeu de données peut n'avoir aucune planification, et alors
executionCountn'a aucune ligne à lire. Un import statique est le cas courant : ses lignes ne changent que lorsque quelqu'un en téléverse de nouvelles, donc une liste de planifications en bonne santé peut coexister avec un jeu de données vieux de neuf jours sans rien en dire. L'absence de planification n'est pas une planification au compteur défaillant.
Un seul appel répond à la vraie question du lecteur, mesurée sur les lignes plutôt que sur la comptabilité interne de la plateforme :
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 il s'agit de get_data_freshness, qui prend exactement un de viz_id ou
dataset_id. Avec un viz_id, il résout chaque jeu de données lu par le tableau de bord et
renvoie le plus ancien — un tableau de bord n'est jamais plus frais que son entrée la
plus vieille. La route REST est GET /v1/datasets/{dataset_id}/freshness.
Les trois champs, et pourquoi ils sont trois
| Champ | Ce que c'est |
|---|---|
asOf | La valeur la plus récente dans la colonne temporelle des données elles-mêmes. C'est la réponse, et elle ne peut pas flatter : elle est lue dans les lignes. ageDays en donne l'ancienneté ; une valeur négative signifie des données post-datées. |
lastLoadedAt | Quand les lignes ont atterri pour la dernière fois. Un fait de pipeline, pas un âge — un fichier chargé ce matin peut ne rien contenir de plus récent que mars. Ne le citez jamais comme date de référence. |
refresh | Si quelque chose est planifié pour rafraîchir les chiffres, y compris la réponse « rien, et rien ne peut l'être » pour un fichier plat. C'est le champ qui couvre le cas « aucune planification » décrit plus haut. |
status indique quelle confiance accorder à asOf : MEASURED (il est réel),
NO_TIME_COLUMN (les données ne portent aucune colonne de date, donc n'ont pas de date de
référence à avoir), ou UNKNOWN — qui signifie que le fait n'a pas été établi et n'est
jamais un succès.
lastLoadedSource nomme quelle horloge a répondu à lastLoadedAt. iceberg_snapshot est
l'heure d'écriture physique de l'instantané courant de la table — le seul véritable instant
d'atterrissage, et qu'un renommage ne peut pas déplacer. not_established signifie que
lastLoadedAt vaut null volontairement : ce chemin de lecture n'a pas consulté
l'instantané, donc plutôt que de répondre avec l'updatedAt de la ligne — une horloge
d'écriture que toute modification fait avancer, et qui a une fois signalé des lignes
atterries neuf jours après les faits — le champ renonce. Un champ qui renonce vaut
strictement mieux qu'un champ qui répond avec la mauvaise horloge. La route ci-dessus est
l'appel qui lit la bonne. get_viz et list_vizzes portent un dataFreshness gratuit,
précis sur la posture de rafraîchissement et UNKNOWN sur la date de référence ; cet appel
est ce qui résout cet UNKNOWN.
Ce qui n'est pas l'âge des données
Ni publishStatus, ni lastBuild.createdAt, ni processingStatus, ni updatedAt ne
répondent à cette question. Ils indiquent, dans l'ordre : si un bundle existe, quand le
code a été compilé, si le fichier s'est analysé une fois, et quand la ligne a changé
pour la dernière fois. Chacun d'eux paraît à jour sur un tableau de bord dont les chiffres
ont des semaines. Si vous êtes sur le point de rapporter une fraîcheur à partir de l'un
d'eux, appelez plutôt la route de fraîcheur.
Cela coûte un SELECT MAX(<colonne de date>) par jeu de données — la même sonde que le
tableau de bord rendu exécute déjà à son montage.
Où vit chaque geste
- Les planifications sont la surface MCP.
create_refresh,list_refreshes,get_refreshetdelete_refreshcouvrent tout le cycle de vie d'une actualisation récurrente depuis votre agent. - « Actualise maintenant » passe par le SDK et l'API REST. Une exécution ponctuelle y tient en un seul appel — voir la Référence API.
- Les projets dbt portent leurs propres routes d'exécution et d'actualisation
(
/v1/dbt/projects/{project_id}/run,/v1/dbt/projects/{project_id}/refreshes).