Query trigger
Poll a read-only SQL aggregate over the indexed event tables on an interval and fire when a condition is met β "page me when 24h volume crosses X", "act when cumulative deposits pass a threshold" β without an external cron + API.
workflows:
volume-alarm:
trigger:
query:
sql: >-
SELECT COALESCE(SUM(value::numeric), 0)
FROM rflow_indexer_rflow_idx_usdc.transfer
WHERE block_timestamp > now() - interval '1 hour'
every: 1m
condition: "${{ output > wei('10000000', 6) }}"
mode: threshold # threshold (default) | changed
steps:
- id: alert
notify:
channel: ops
message: "hourly USDC volume crossed 10M: ${{ format_units(trigger.args.output, 6) }}"| Field | Required | Description |
|---|---|---|
sql | β | ONE read-only statement (SELECT / WITH β¦ SELECT) β same rules and table naming as the query: step |
args | $1..$N values, rendered once at boot β only constants / secrets / lists roots (no trigger context exists yet; validated) | |
every | β | Poll interval, e.g. 1m |
condition | β | Firing condition over the shaped output (alias result) |
mode | threshold | threshold | changed β identical semantics to the read trigger |
The shaped query output (scalar / object / row array β shaped exactly like
the query: step) is what
condition: sees; NUMERIC values compare as exact numbers, so
U256-scale thresholds are precise.
Firing modes β edge-triggered, never spam
threshold(default) β fire on the falseβtrue crossing ofcondition:. A condition that stays true fires once, not every poll.changedβ fire when the shaped output differs from the previous poll's while the condition holds (the first observation only records a baseline).
The previous poll's state persists per workflow
(rflow.read_trigger_state), so a restart mid-"true" does not re-fire a
threshold trigger. A transient query failure (database hiccup, table not
created yet) logs, keeps the previous state and retries next poll.
The trigger context
| Path | Description |
|---|---|
trigger.args.output | The shaped query output at fire time |
trigger.query.observed_at | Observation timestamp (ISO) |
Exactly-once
Each fire claims query:{workflow}:{poll instant} β one run per polling
instant, and the persisted edge state carries the dedupe across restarts.
Honest notes
${{ }}insidesql:is a hard validation error β dynamic values belong inargs:(see the injection guard).- The statement runs with the default 5s
statement_timeoutinside aREAD ONLYtransaction on rflow's own Postgres β keep polled aggregates indexed/cheap, or widenevery:. - The trigger only sees what the project indexes: event-trigger tables and
contracts.<name>.index_eventstables (plus therflow.*journal).rflow tableslists what is queryable.