ContinuousQueries bundle
Use this bundle for query authoring, query-language selection, result contracts, joins, static validation, and query troubleshooting.
Query authoring rules
- Set
mode: querywhen authoring Drasi for Kubernetes ContinuousQuery resources unless current docs say otherwise. - Set
queryLanguageexplicitly for every query when the schema supports it. Do not rely on defaults. - Prefer Cypher for new queries. Use GQL only when ISO compliance or multi-platform GQL interoperability is an explicit requirement and the pinned runtime's GQL parser/function support is verified.
- Define the result contract before attaching reactions.
- Use labels and property names that match the source data model exactly.
- Avoid implicit Cartesian joins. Use provider-supported join configuration for cross-source relationships.
- Keep queries small, observable, and testable.
- Do not mix examples from Drasi Server REST payloads with Drasi for Kubernetes YAML without adapting shape.
- Validate query behavior with expected added, updated, and deleted result changes.
- Avoid comma-separated node patterns in
MATCHfor portability. Prefer oneMATCHper pattern and explicitWHEREjoins. - For enum-like string states, prefer exact equality (
=) over pattern operators unless operator support is proven on the pinned runtime version.
Parser-compatibility preflight (required)
Before applying full production queries, compile a minimal query using the same operator classes (=, IN, CONTAINS, date/window functions) on the pinned runtime.
If the minimal query fails parser validation, do not keep retrying full manifests. Replace the unsupported operator/function with a runtime-supported alternative and re-apply.
Result contract
Every query should declare or document:
- Query id/name.
- Runtime form: Server API payload or Kubernetes resource.
- Query language.
- Source names and expected labels.
- Returned fields and types.
- Meaning of an added result.
- Meaning of an updated result.
- Meaning of a deleted result.
- Reaction consumers and downstream schema expectations.
- Known latency, window, or aggregation assumptions.
Join guardrails
For cross-source or relational joins:
- Define synthetic relationships or join mappings explicitly where Drasi requires them.
- Avoid
MATCH (a), (b) WHERE a.id = b.idpatterns unless current docs confirm the runtime supports the form. - Keep join keys stable, indexed in source systems where applicable, and consistently typed.
- Test with missing keys, duplicate keys, and late-arriving changes.
Function and language verification
Before using a function, window, duration, aggregation, or temporal expression:
- Check current Drasi Continuous Query syntax docs for the selected language.
- Prefer a documented Drasi function over a similar Cypher/GQL function from another engine.
- Create a minimal query proving the function compiles.
- Add a small input-change test proving the runtime result.
Do not assume Neo4j, openCypher, ISO GQL, or another graph engine has identical function support.
Static review checklist
Reject or revise queries with:
- Missing
queryLanguagewhen supported. - Source names that do not exist.
- Labels that do not match the source model.
- Reactions expecting fields not returned by the query.
- Broad unbounded queries without a clear reason.
- Implicit joins where explicit join configuration is required.
- Non-deterministic result shape.
- No test data or expected output.
Validation commands
For Drasi for Kubernetes:
drasi list query -n "$DRASI_NAMESPACE"
drasi describe query "$QUERY_NAME" -n "$DRASI_NAMESPACE"For Drasi Server, use the documented REST endpoints for listing queries, creating queries, retrieving query details, and polling query results.
Cross-runtime portability
When a ContinuousQuery is intended to run unchanged across more than one Drasi runtime form (for example, a query developed against drasi-lib for low-latency in-process evaluation that must later run on Drasi Server or Drasi for Kubernetes), restrict authoring to the runtime-neutral feature subset and verify with a smoke test on each target before pinning.
Portability matrix
| Feature | Drasi Server | Drasi for Kubernetes | drasi-lib | Portability stance |
|---|---|---|---|---|
MATCH over a single Source |
Yes | Yes | Yes | Runtime-neutral. Author once, expect identical result contract. |
Cypher arithmetic, comparison, and WHERE predicates documented in Drasi syntax docs |
Yes | Yes | Yes | Runtime-neutral. Verify each function exists in the pinned drasi-core line. |
Cross-source joins via explicit subscriptions mapping |
Yes | Yes | Verify at pinned drasi-lib version - embedded API surface may differ before/after Replayable Sources rewrite | Author with the join shape documented in bundles/sources/guide.md; do not rely on implicit Cartesian shapes. |
| Time-window or temporal aggregation functions | Verify per release | Verify per release | Verify per release | Runtime-specific. Confirm the function exists in the syntax docs for each target before sharing the query. |
| GQL ISO mode | Verify per release | Verify per release | Verify per release | Not all runtimes ship GQL at the same release cadence - pin language explicitly and confirm. |
| Result-contract field names and types | REST payload | CRD resource | In-process callback | Runtime-neutral if the query returns scalar-typed projections. Re-shape only at the edge consumer, never inside the query. |
changeKinds: [added, updated, deleted] semantics |
Yes | Yes | Yes | Runtime-neutral. Reactions and embedded consumers MUST handle all three identically. |
Runtime-neutral query authoring pattern
- Project only scalar fields (
RETURN n.id AS id, n.email AS email); avoid runtime-specific projections (paths, list-valued aggregations) until you have confirmed support on every target. - Pin language explicitly (
queryLanguage: CypherorqueryLanguage: GQL) - never rely on defaults that may differ across runtimes. - Run a smoke test that asserts identical result rows for the same source change on each target runtime before promoting the query.
- If a target runtime cannot evaluate the query, prefer re-modelling the query over forking it. A forked query becomes two queries to maintain, and divergence is hard to detect from telemetry.
This portability discipline pairs with the mixed-runtime guidance in SKILL.md and the cross-runtime compatibility section in templates/drasi-version-pinning.md.
Common anti-patterns (don't / do instead)
| Don't | Do instead | Why |
|---|---|---|
MATCH (a), (b) WHERE a.id = b.id (implicit Cartesian join) |
Use explicit subscriptions mapping or a runtime-supported join form |
Implicit cross products fan out catastrophically and are usually not what the author meant. |
| Mix Drasi Server REST payload shape with Drasi for Kubernetes CRD shape in one example | Pick the runtime form and stick to it; cross-reference, do not copy | Field paths (spec.sources.subscriptions[] vs sources[].sourceId) differ between runtimes. |
Omit queryLanguage |
Set queryLanguage explicitly on every query |
Defaults vary across runtimes and across pre-1.0 platform releases. |
Run drasi-lib 0.4.x in a project whose Drasi Server is pinned to an older drasi-core line without recording the decision |
Pin deliberately; record the rationale in templates/drasi-version-pinning.md |
Undocumented drift makes upgrade evidence unverifiable and turns a routine bump into an incident. |
| Author a query against fields the source data model does not return | Verify labels and property names against the Source schema before authoring | "Query active but empty" is almost always a label/property mismatch and wastes triage time. |
Treat deleted change kind as a downstream delete without idempotency |
Make reactions idempotent for all three change kinds (added, updated, deleted) |
At-least-once delivery means duplicate deleted events can arrive after a deleted already processed. |
Cypher and GQL feature support
Drasi ContinuousQueries are a subset of openCypher 9, not the full
language. The same engine also accepts GQL (ISO/IEC 39075:2024 - the
ISO graph query language, not GraphQL); both parsers are peers over a
shared AST in drasi-core/query-ast/. Selection: Query::cypher(id) /
Query::gql(id) for drasi-lib, queryLanguage: Cypher | GQL for
Drasi Server REST and Drasi for Kubernetes CRDs.
New-project default: start with Cypher for all new Drasi ContinuousQuery work, unless you have a specific reason for GQL (ISO compliance requirement, multi-system GQL interoperability). Reasoning:
- Cypher is the primary target for new features. The
drasi.*temporal namespace, custom-plugin authoring patterns, and all official Drasi examples use Cypher. GQL support was added later (drasi-platform PR #289) and may lag on function registration parity. - Separate function registries (drasi-core issue #217): A function registered in
functions-cypheris invisible from GQL mode. Custom function authors must register in both registries, and GQL-only queries may silently resolve to a no-op for functions the author forgot to dual-register. - Drasi is pre-1.0 and GQL support is also pre-1.0. ISO GQL 39075:2024 is a new standard; Drasi's implementation is a subset. Cypher (openCypher 9) syntax is stable, with years of battle-tested parsing.
- All Drasi documentation, learning materials, and community examples use Cypher. A developer starting with Cypher will have friction-free access to the full reference corpus.
- Syntax differences are cosmetic (
elementIdvselement_id,datetime()vscurrent_datetime()). No material advantage to GQL for single-platform use.
Use GQL only when ISO compliance or multi-platform GQL interoperability is an explicit requirement. Record the language choice in versions.md.
AST surface - what the parsers actually accept
From drasi-core/query-ast/src/ast.rs on main:
| AST node | Role |
|---|---|
Query |
Top level; holds parts: Vec<QueryPart> |
QueryPart |
One clause in a multi-part query (WITH boundary) |
MatchClause |
Pattern + WHERE |
ProjectionClause |
RETURN / WITH |
Expression |
Enum, dispatch root |
UnaryExpression |
Unary ops |
BinaryExpression |
Arithmetic + comparison + logical |
FunctionExpression |
Named function call (resolved against FunctionRegistry) |
CaseExpression |
CASE WHEN ... THEN ... ELSE ... END |
ListExpression |
List literal |
IteratorExpression |
List comprehension / range |
That the AST has dedicated nodes for Case, List, and Iterator is
the strongest single signal of which Cypher subset is parseable.
Whether the evaluator supports every shape on main is a separate
question - verify against the SDK version pinned in versions.md.
Clauses
| Clause | Cypher | GQL | Notes |
|---|---|---|---|
MATCH (single + multi-pattern) |
Supported | Supported | Returns nodes and relationships across multiple sources without explicit join syntax. |
WHERE |
Supported | Supported | Filtering on properties + element identity. |
RETURN with AS aliasing |
Supported | Supported | Drives the result-set schema (the "result contract"). |
WITH (multi-part) |
Supported | Supported | QueryPart is the WITH-delimited unit. Canonical workaround for DISTINCT. |
OPTIONAL MATCH |
Not confirmed on main |
Not confirmed | AST currently has only MatchClause. Verify against query-cypher/src/ before relying. |
UNWIND |
Not confirmed | Not confirmed | IteratorExpression exists in the AST but no dedicated UNWIND clause node observed. |
CREATE / MERGE / DELETE / SET |
Not applicable | Not applicable | ContinuousQueries are read-only over source-driven graphs. The graph is mutated by SourceChange events emitted by Sources, not by query mutation. |
CALL (procedures) |
Not confirmed | Not confirmed | No procedure registry observed in the public AST surface. |
UNION |
Not confirmed | Not confirmed | Verify before relying. |
Predicates and operators
| Class | Examples | AST node |
|---|---|---|
| Comparison | =, <>, <, <=, >, >= |
BinaryExpression |
| Boolean | AND, OR, NOT |
BinaryExpression / UnaryExpression |
| Set membership | IN ['critical', 'extreme'] |
BinaryExpression |
| NULL | IS NULL, IS NOT NULL |
UnaryExpression |
| String operators | merged via "String Operators (#386)" | BinaryExpression |
| Arithmetic | + - * / |
BinaryExpression |
| Case | CASE WHEN ... THEN ... ELSE ... END |
CaseExpression |
Pattern syntax
- Nodes:
(v:Label {prop: value}). - Relations:
-[:TYPE]->; anonymous middle nodes (:Building) and multi-segment paths joined with commas inMATCHare supported. - Identifier escaping with backticks for special characters:
MATCH (n:`Special-Label`). - Element identity:
- Cypher mode:
elementId(e) - GQL mode:
element_id(e)
- Cypher mode:
- Both modes share the same MATCH / WHERE / RETURN shape.
Aggregating functions (eight built-ins on main)
From drasi-core/core/src/evaluation/functions/aggregation/:
| Function | File | Accumulator |
|---|---|---|
sum |
aggregation/sum.rs |
ValueAccumulator::Sum { value: f64 } |
avg |
aggregation/avg.rs |
ValueAccumulator::Avg { sum: f64, count: i64 } |
count |
aggregation/count.rs |
ValueAccumulator::Count { value: i64 } |
min |
aggregation/min.rs |
Accumulator::LazySortedSet(LazySortedSet) |
max |
aggregation/max.rs |
Accumulator::LazySortedSet(LazySortedSet) |
collect |
aggregation/collect.rs |
ValueAccumulator::Value(ElementValue::List(...)) |
linearGradient |
aggregation/linear_gradient.rs |
ValueAccumulator::LinearGradient { count, mean_x, mean_y, m2, cov } |
| (AggregatingLast) | aggregation/last.rs |
ValueAccumulator::Value(...) |
Aggregators are incremental, not batch - see
bundles/custom-plugins/guide.md for the AggregatingFunction trait
(initialize_accumulator / apply / revert / snapshot /
accumulator_is_lazy). Values are individually applied when a row
newly matches and reverted when a row newly stops matching; the engine
does not recompute from scratch.
Note on third-party "unsupported features" lists. Some community guides list
collect()as "not in the supported subset". The repo contradicts this -aggregation/collect.rsis implemented with the full apply / revert lifecycle. Treat third-party subset lists as starting hypotheses, not facts; verify against the repo.
drasi.* namespace (temporal logic via functions, not new clauses)
Names and signatures below are verified against the official
Drasi Custom Functions reference
(checked 2026-07-21). This is the authoritative list, do not invent
drasi.* names (e.g. drasi.age, drasi.minutes, drasi.now do not exist;
express elapsed-time logic with drasi.trueFor or by comparing
drasi.changeDateTime() to datetime()).
| Function | Signature | Purpose |
|---|---|---|
drasi.changeDateTime |
drasi.changeDateTime(element) |
ZONED DATETIME of when the element last changed (windowing / recency). |
drasi.previousValue |
drasi.previousValue(expression, default) |
Value of expression before the current change (any change between revisions). |
drasi.previousDistinctValue |
drasi.previousDistinctValue(expression, default) |
Like previousValue but ignores repeated identical values. |
drasi.listMin |
drasi.listMin(list) |
Minimum element of a LIST. |
drasi.listMax |
drasi.listMax(list) |
Maximum element of a LIST. |
drasi.getVersionByTimestamp |
drasi.getVersionByTimestamp(element, timestamp) |
Historical snapshot lookup; requires a temporal index. |
drasi.getVersionsByTimeRange |
drasi.getVersionsByTimeRange(element, from, to, include_initial_version) |
Historical range lookup; requires a temporal index. |
drasi.trueFor |
drasi.trueFor(expression, duration) |
expression must stay true continuously for duration from the change time before an added result is emitted. |
drasi.trueLater |
drasi.trueLater(expression, timestamp) |
Evaluates expression at a future timestamp; returns drasi.awaiting and re-schedules until then. |
drasi.trueUntil |
drasi.trueUntil(expression, timestamp) |
expression must remain true until timestamp (confirmed in the reference). |
drasi.slidingWindow |
drasi.slidingWindow(duration, aggregation_expression) |
Applies an aggregation over a trailing time window. |
drasi.linearGradient |
drasi.linearGradient(x, y) |
Fits a line over paired points and returns the slope (paired with the aggregating LinearGradient). |
Code-registered but not yet in the official docs page (verified against
both functions-cypher/src/lib.rs and functions-gql/src/lib.rs on
drasi-core main, 2026-08-24). Treat as undocumented surface: verify
behaviour against source before relying on it, and re-check whether the
docs page has caught up:
| Function | Kind | Purpose |
|---|---|---|
drasi.stdevp |
Scalar | Population standard deviation of the input values. Registered in both Cypher and GQL sets. |
drasi.last |
Aggregating | Returns the most recent value seen (AggregatingLast accumulator). Registered in both Cypher and GQL sets. Useful with drasi.slidingWindow. |
retainHistory |
Context mutator | Retains element history in the evaluation context (not part of the drasi.* namespace; Cypher and GQL). |
drasi.trueLater returns the special value drasi.awaiting while it waits for
the scheduled timestamp, reactions must treat "no row yet" as a valid pending
state, not an error. These are dispatched through FunctionExpression AST nodes in the
same way as Cypher built-ins; they are registered against the same
FunctionRegistry. New temporal logic should follow this pattern
- it is materially easier than adding a clause (see
bundles/custom-plugins/guide.mdquery-language extension points).
GQL mode differences
- Identity function naming:
elementId(e)(Cypher) vselement_id(e)(GQL). - Same MATCH / WHERE / RETURN shape; same pattern syntax.
- Separate function set (
functions-gqlcrate). A function registered infunctions-cypheris invisible from GQL mode unless also registered infunctions-gql(drasi-core issue #217). - GQL was added to drasi-platform via PR #289.
Known unsupported or hedged features
| Feature | Status | Workaround |
|---|---|---|
DISTINCT |
Hedged unsupported - not contradicted by repo evidence | Use WITH ... count() to emulate the deduplication. |
ORDER BY |
Hedged unsupported | Order downstream in the Reaction or consumer. |
LIMIT |
Hedged unsupported | Cap fan-out at the join level; gate at the Reaction. |
Mutation clauses (CREATE / MERGE / SET / DELETE) |
Not applicable by design | The graph is mutated only by SourceChange events from Sources. |
Cartesian joins (MATCH (a), (b) without a relationship pattern) |
Parses, but scales poorly | Express the join through an explicit relationship or split into multiple queries. |
| Cross-language function reuse | Not automatic | Register the function in both functions-cypher and functions-gql. |
Function reference
The following tables document the built-in functions available in Drasi's Cypher and GQL parsers, sourced from the drasi-core evaluation engine. Each function is registered in the FunctionRegistry and dispatched through FunctionExpression AST nodes.
All functions shown below are available in both Cypher and GQL modes unless noted. The aggregating functions (already documented above) and drasi.* temporal namespace (also above) are not repeated here.
[VERIFY] EvidenceType = Source WhereToCheck = drasi-core/core/src/evaluation/functions/, function registry by category; verify function names and signatures against the pinned drasi-core commit [/VERIFY]
Cypher scalar functions
| Function | Signature | Description | Example |
|---|---|---|---|
char_length |
char_length(str) |
Number of Unicode characters in a string | char_length(n.name) |
coalesce |
coalesce(val1, val2, ...) |
Returns the first non-null argument | coalesce(n.nickname, n.name) |
head |
head(list) |
Returns the first element of a list | head(n.tags) |
is_empty |
is_empty(list_or_str) |
Returns true if the list or string is empty | is_empty(n.items) |
last |
last(list) |
Returns the last element of a list | last(n.readings) |
null_if |
null_if(val1, val2) |
Returns null if val1 equals val2 | null_if(n.status, 'inactive') |
size |
size(list_or_str) |
Returns the number of elements in a list or characters in a string | size(n.items) |
timestamp |
timestamp() |
Returns the current time as epoch milliseconds | timestamp() |
to_boolean |
to_boolean(val) |
Converts a value to boolean | to_boolean(n.is_active) |
to_float |
to_float(val) |
Converts a value to a 64-bit float | to_float(n.temperature) |
to_integer |
to_integer(val) |
Converts a value to a 64-bit integer | to_integer(n.quantity) |
Numeric functions
| Function | Signature | Description | Example |
|---|---|---|---|
abs |
abs(val) |
Absolute value | abs(n.temperature - n.baseline) |
ceil |
ceil(val) |
Round up to nearest integer | ceil(n.value) |
floor |
floor(val) |
Round down to nearest integer | floor(n.value) |
round |
round(val) |
Round to nearest integer | round(n.avg_rating) |
rand |
rand() |
Returns a random float in [0, 1) | rand() |
sign |
sign(val) |
Returns -1, 0, or 1 | sign(n.change) |
Trigonometric functions
| Function | Signature | Description | Example |
|---|---|---|---|
sin |
sin(radians) |
Sine of an angle in radians | sin(n.angle) |
cos |
cos(radians) |
Cosine of an angle in radians | cos(n.angle) |
tan |
tan(radians) |
Tangent of an angle in radians | tan(n.angle) |
pi |
pi() |
The mathematical constant pi | pi() |
degrees |
degrees(radians) |
Converts radians to degrees | degrees(n.angle) |
radians |
radians(degrees) |
Converts degrees to radians | radians(n.heading) |
Text/string functions
Text functions are registered from core/src/evaluation/functions/text/text.rs and include string inspection, transformation, and pattern matching. Verify the exact function name and argument count against the pinned release before authoring. Common patterns include:
- Lowercase/uppercase conversion
- Substring extraction
- String trimming
- Pattern matching (CONTAINS, STARTS WITH, ENDS WITH are supported as operators, see predicates table above)
[VERIFY] EvidenceType = Source WhereToCheck = drasi-core/core/src/evaluation/functions/text/text.rs, exact function names and signatures [/VERIFY]
List functions
| Function | Signature | Description | Example |
|---|---|---|---|
distinct |
distinct(list) |
Returns a list with duplicate elements removed | distinct(n.tags) |
index_of |
index_of(list, val) |
Returns the index of the first occurrence of val, or -1 | index_of(n.tags, 'urgent') |
insert |
insert(list, idx, val) |
Inserts val at position idx, returns new list | insert(n.items, 0, n.new_item) |
range |
range(start, end) |
Returns a list of integers from start to end (inclusive) | range(1, 10) |
reduce |
reduce(list, init, f) |
Reduces a list using a function/expression (syntax varies by release) | reduce(n.values, 0, (acc, x) -> acc + x) |
tail |
tail(list) |
Returns all elements of a list except the first | tail(n.readings) |
Temporal instant functions
Functions for creating, parsing, and comparing date/time instants. Verify exact signatures against the pinned release as the temporal function surface is actively evolving alongside the drasi.* temporal namespace.
| Function | Category | Description |
|---|---|---|
datetime() |
Construction | Creates a datetime value from string or components |
localdatetime() |
Construction | Creates a datetime without timezone |
date() |
Construction | Creates a date value |
time() |
Construction | Creates a time value |
[VERIFY] EvidenceType = Source WhereToCheck = drasi-core/core/src/evaluation/functions/temporal_instant/, constructor and accessor function signatures [/VERIFY]
Temporal duration functions
Functions for creating durations and computing time between instants.
| Function | Description | Example |
|---|---|---|
duration() |
Creates a duration value from components or ISO string | duration('PT5M') |
duration.between() |
Duration between two temporal instants | duration.between(n.start, n.end) |
duration.in_* |
Accessor components (inSeconds, inMinutes, inHours, inDays) | duration.in_seconds(d) |
[VERIFY] EvidenceType = Source WhereToCheck = drasi-core/core/src/evaluation/functions/temporal_duration/, duration function names and accessors [/VERIFY]
Metadata functions
| Function | Signature | Description | Example |
|---|---|---|---|
elementId (Cypher) / element_id (GQL) |
elementId(element) |
Returns the unique element identifier of a node or relationship | elementId(n) |
changeDateTime |
changeDateTime(element) |
Returns the last-change timestamp of the element as a zoned datetime | changeDateTime(n) |
The element identity functions are the canonical way to reference individual graph elements across changes. changeDateTime is useful for recency-checking without maintaining a separate timestamp property, it reads the Drasi-internal effective_from metadata on the element.
Context mutator functions
[VERIFY] EvidenceType = Source WhereToCheck = drasi-core/core/src/evaluation/functions/context_mutators.rs, context mutation function signatures [/VERIFY]
Context mutators allow queries to influence the evaluation context for downstream query parts. Verify exact function names against the pinned release.
Future / pending temporal functions
Functions from core/src/evaluation/functions/future/ that complement the drasi.* temporal namespace. These overlap with the drasi.* functions already documented above and are registered under both paths. Key additional functions not documented in the drasi.* table:
| Function | Description |
|---|---|
awaiting |
Evaluates whether an element is in a pending/awaiting state |
future_element |
References an element that will exist at a future time |
true_now_or_later |
Returns true if the predicate is true now or will become true |
Past / historical temporal functions
Functions from core/src/evaluation/functions/past/ for querying element state history. These enable queries over the temporal index when it is configured on the Source.
| Function | Description |
|---|---|
past.getVersionByTimestamp |
Retrieve element state at a specific past point in time |
past.getVersionsByTimeRange |
Retrieve element state over a time range |
Re-verify each function against query-cypher/src/ and the functions-* crate on
the drasi-core commit pinned in versions.md before authoring around
the limitation. The list is conservative - features may have landed
since the last currency check.
Common symptoms
| Symptom | Likely cause | First action |
|---|---|---|
| Query fails to start | Source unavailable, syntax error, unsupported function | Describe source and query, then validate syntax against docs |
| Query active but empty | Label mismatch, property mismatch, source has no matching data | Inspect source data model and run minimal query |
| Reaction receives malformed payload | Query result contract differs from reaction template | Compare returned fields with reaction mapping |
| Cross-source query fails | Missing explicit join config or incompatible join keys | Simplify to one source, then add join back |
Join cardinality and result-contract example
Cardinality budgets
ContinuousQueries materialize joined state. Plan cardinality before authoring:
| Join shape | Approximate safe ceiling | Mitigation if you exceed it |
|---|---|---|
| 1 : 1 | Effectively unbounded | None needed beyond standard limits. |
| 1 : N (small fan-out, N ≤ 100) | ~100k driving entities | Pre-filter the N side with a WHERE clause before the join. |
| 1 : N (large fan-out, N > 1k) | ~10k driving entities | Split into two queries: one for the driving entity, one fed by the first's reaction. |
| M : N | Avoid in production | Re-model the source data or pre-aggregate. |
If you exceed the ceiling for your runtime form, expect:
- Query evaluation latency degrading at the p99 first, then p95.
- Memory pressure on the query container; possible OOM kills.
- Inconsistent result updates as the engine prioritises new changes over backfill.
Stress-test with realistic cardinalities before pinning a production query.
Result contract - annotated example
Source (PostgreSQL users table):
| id | tier | status | |
|---|---|---|---|
| u-1 | u1@example.com | gold | active |
| u-2 | u2@example.com | silver | active |
Query:
MATCH (u:User)
WHERE u.tier = 'gold' AND u.status = 'active'
RETURN u.id AS userId, u.email AS userEmailResult contract documented for downstream reactions:
queryId: gold-active-users
queryLanguage: Cypher
sources: [users-pg]
fields:
- name: userId
type: string
classification: Internal
nullable: false
- name: userEmail
type: string
classification: Confidential # see security/guide.md classification matrix
nullable: false
changeKinds: [added, updated, deleted]
ordering: best-effort (not strictly ordered across keys)
duplicateSemantics: at-least-once # reactions must be idempotentPublish this contract alongside the query YAML so reaction authors know exactly what to expect and what redaction is required.
Non-events / absence-of-change pattern
Drasi can detect when expected changes do not occur within a defined time window. This is the "absence of change" or "non-events" pattern, a common requirement for watchdog, heartbeat, and deadline monitoring.
When to use
- Alert when a sensor stops reporting (temperature, pressure, motion).
- Notify when a process has not completed by a deadline.
- Trigger escalation when expected state transitions do not happen (order not shipped, payment not received).
- Detect stale connections or silent components in a distributed system.
How it works
The pattern uses a two-query approach:
- State query: Captures the current state of entities with their last-known timestamps or sequence numbers.
- Absence query: Projects the state through a time window and filters for entities whose last activity timestamp is older than the threshold.
Cypher example
// Step 1: State query -- track sensors and their last heartbeat
MATCH (s:Sensor)
RETURN s.id AS sensorId, s.name AS sensorName, s.lastHeartbeat AS lastHeartbeat
// Step 2: Absence query -- sensors with no heartbeat in the last 5 minutes
MATCH (s:Sensor)
WHERE s.lastHeartbeat < datetime() - duration('PT5M')
RETURN s.id AS sensorId, s.name AS sensorName, s.lastHeartbeat AS lastHeartbeat,
datetime() AS currentTimeGQL example
MATCH (s:Sensor)
WHERE s.lastHeartbeat < current_datetime() - INTERVAL '5' MINUTE
RETURN s.id AS sensorId, s.name AS sensorName, s.lastHeartbeat AS lastHeartbeat,
current_datetime() AS currentTimeNon-events via drasi.* temporal functions
The idiomatic Drasi way to react to absence of change is drasi.trueFor, it emits
an added result only when a condition has held continuously for a duration, which is
exactly "this has been true (silent / over threshold) long enough to act on". Do not
reach for invented helpers like drasi.age() or drasi.minutes(), those functions do
not exist (see the verified drasi.* table above).
// Freezer temperature over threshold continuously for 10 seconds
// (canonical drasi.trueFor pattern from the official reference)
MATCH (f:Freezer)
WHERE drasi.trueFor(f.temp > 32, duration({ seconds: 10 }))
RETURN f.id AS id, f.temp AS tempIf instead you track a lastHeartbeat timestamp on the entity, compare it against the
current time with real functions (no drasi.age): drasi.changeDateTime() gives the
element's last-change time, and datetime() / duration() express the window.
// Sensors whose last change is older than 5 minutes
MATCH (s:Sensor)
WHERE s.lastHeartbeat < datetime() - duration({ minutes: 5 })
RETURN s.id AS sensorId, s.name AS sensorName, s.lastHeartbeat AS lastHeartbeatConfirm drasi.trueFor / duration() availability and exact duration() map syntax
against the pinned runtime; both are documented in the
Drasi Custom Functions reference
(checked 2026-07-21).
Result contract for non-event queries
queryId: stale-sensors
queryLanguage: Cypher
sources: [sensors-source]
fields:
- name: sensorId
type: string
nullable: false
- name: sensorName
type: string
nullable: false
- name: lastHeartbeat
type: datetime
nullable: false
changeKinds: [added, deleted] # added when a sensor falls behind threshold; deleted when it recovers
ordering: best-effortThe result contract is unusual because the query fires on a time-based evaluation rather than a data change. Results are:
- added: A sensor crossed the staleness threshold (was reporting, now silent).
- deleted: A sensor recovered (started reporting again, so it no longer matches the absence condition).
- updated: Rare, a sensor that is already in the stale set gets an even older timestamp.
Design considerations
- Evaluation cadence: Absence queries rely on periodic re-evaluation by the Drasi runtime. Confirm the evaluation interval is appropriate for your deadline requirements. A 5-minute staleness check evaluated every 60 seconds is safe; a 5-second staleness check evaluated every 60 seconds will miss transitions.
- Threshold tuning: Set the absence window to at least 2x the expected heartbeat interval to avoid flapping from clock skew or transient delays.
- Recovery handling: When a stale entity recovers (sends a heartbeat again), it is removed from the result set (deleted). Reactions should handle the delete event by clearing the alert or notifying that the entity is back online.
- False positives: Test absence queries with realistic data patterns to verify that normal processing delays do not trigger false alerts.
- Cross-reference: See
tutorial/absence-of-changein thedrasi-project/learningrepository for a full worked example with deployment instructions.
Known runtime limitations
[VERIFY] EvidenceType = Docs + ReleaseNotes WhereToCheck = drasi-platform release notes and drasi-context.yaml for time-window function support at the pinned version Claim = drasi.trueFor / drasi.trueUntil / drasi.slidingWindow, duration() map syntax, and GQL INTERVAL syntax behave identically at the pinned runtime version [/VERIFY]
- The temporal
drasi.*functions andduration()are documented (see the verifieddrasi.*table above), but availability and exactduration()argument shape can differ across pre-1.0 releases, verify against the pinned release before relying on absence-of-change queries in production. - The
datetime()(Cypher) /current_datetime()(GQL) constructor availability varies. Test on the target runtime.