New to Kestra?
Use blueprints to kickstart your first workflows.
Check a Weaviate class's object count via GraphQL Aggregate and alert Slack when it drops below a minimum threshold.
A Weaviate class that gets accidentally emptied - a bad Delete call with too
broad a filter, an ingest pipeline that silently stopped, a collection dropped and
never rebuilt - still answers every query without error. It just returns nothing
useful, and the first person to notice is usually whoever is on the receiving end
of a RAG pipeline that suddenly has no grounding data. This blueprint runs
Weaviate's own GraphQL Aggregate meta { count } query against a named class on
a schedule and alerts Slack the moment the count drops below a minimum you set.
periodic_population_check (io.kestra.plugin.core.trigger.Schedule) runs every 30 minutes. Shipped disabled so you can validate a manual run first.check_class_population (io.kestra.plugin.weaviate.Query, fetchType: FETCH_ONE) runs { Aggregate { class_name { meta { count } } } } against the Weaviate GraphQL endpoint, landing the result at row.meta.count.evaluate_population (io.kestra.plugin.core.flow.If) branches on row.meta.count >= min_object_count.then (HEALTHY): log_population_healthy records a clean check.else (AT_RISK): alert_population_at_risk reports the measured count against the minimum. There is deliberately no remediation action here - see Pitfalls for why.log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.errors block alerts Slack separately if the flow itself fails outright - an unreachable instance, a bad API key, or class_name not existing at all, since Weaviate's GraphQL layer errors on an undefined class instead of returning a zero count.Aggregate query, not an estimate from a dashboard or a downstream symptom report.errors block) for a class that does not exist at all, so that case is never silently read as "zero objects, below threshold" - it is a different, more severe condition.min_object_count) that works equally as an always-populated canary (default 1) or a partial-data-loss detector on a class expected to hold far more.Delete call with a filter that matched more than intended and want a scheduled check that would have caught it within the hour instead of the next incident review.Query running a raw GraphQL Aggregate request, not just Get-style data retrieval.An Aggregate query run by hand answers one moment in time; it does not decide a cadence, compare the result against a threshold, or notify anyone. Kestra supplies the schedule, the threshold comparison as a first-class branch, and an execution history that shows exactly when a class's population dropped and by how much.
class_name already created and expected to be populated.Local testing:
docker run -d --name weaviate-canary -p 8081:8080 -p 50052:50051 -e QUERY_DEFAULTS_LIMIT=25 -e AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true -e PERSISTENCE_DATA_PATH=/var/lib/weaviate -e DEFAULT_VECTORIZER_MODULE=none cr.weaviate.io/semitechnologies/weaviate:1.25.0
docker network connect YOUR_KESTRA_NETWORK weaviate-canary (check existing networks first with docker network ls; only needed if the Kestra Worker runs in a separate Docker network than this container; set weaviate_url to http://weaviate-canary:8080 from inside that network, or http://localhost:8081 from the host). Host port 8081 is used here specifically so it never collides with Kestra's own UI on 8080. With AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true, WEAVIATE_API_KEY can be set to any placeholder value - the instance does not check it.
WEAVIATE_API_KEY: API key used by check_class_population's apiKey property.SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_population_at_risk and the errors block.SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_WEAVIATE_API_KEY=$(echo -n '<your-local-key>' | base64). This keeps credentials out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.weaviate_url (STRING, default http://localhost:8081): Weaviate instance URL.class_name (STRING, default Article): class (collection) audited.min_object_count (INT, default 1): count floor before the class counts as AT_RISK.outputs.check_class_population.row.meta.count: the measured object count, every successful run.Article class with a handful of objects (via SchemaCreate and BatchCreate, or the Weaviate console).WEAVIATE_API_KEY and SLACK_WEBHOOK_URL secrets.log_population_healthy fires with the correct count.min_object_count above the actual count and re-run to confirm alert_population_at_risk fires.class_name at a class that does not exist and re-run to confirm the errors block fires instead of either branch.periodic_population_check once you trust the check.class_name values with io.kestra.plugin.core.flow.Loop to run the same canary across every class a RAG pipeline depends on in one flow.Aggregate query filtered with a where clause on a recency field (if the class has one) to check not just total population but how many objects were added in the last run window.row.meta.count to the KV store after each run and compare against the previous value, so a slow leak (not just a sudden wipe) also trips an alert.Subflow call to a reingestion pipeline on the AT_RISK branch, if one exists, instead of leaving remediation as alert-only.BatchCreate, Delete, Query, SchemaCreate exist under io.kestra.plugin.weaviate - nothing to reindex or restore lost objects, which is why the AT_RISK branch only alerts, unlike this repo's other governance gates that gate a remediation action behind auto_remediate/dry_run.Aggregate on an undefined class_name returns a GraphQL error, which this plugin's Query task turns into a thrown exception - caught by the errors block, not by evaluate_population. A typo'd class_name reads as a flow failure, not as AT_RISK.FETCH_ONE's flattening means row is keyed by the query's structure, not a fixed path. outputs.check_class_population.row.meta.count only works because the GraphQL query asks for Aggregate { class_name { meta { count } } } } specifically - verified directly from the plugin's extractRow logic, which takes the first class's first row-map, not a documented, stable output field name.class_name is spliced directly into the GraphQL query text. There is no bind-parameter mechanism for a GraphQL type name in this query; treat class_name as a trusted input, not one sourced from unescaped external user input.Aggregate/meta { count } query shape is taken verbatim from Weaviate's own GraphQL API documentation, and the Query task's properties (url, apiKey, query, fetchType) and response-flattening behavior are taken verbatim from the plugin's source on main - but no Docker container or Kestra engine was run to execute this flow end to end.