Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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) }}"
FieldRequiredDescription
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)
modethresholdthreshold | 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 of condition:. 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

PathDescription
trigger.args.outputThe shaped query output at fire time
trigger.query.observed_atObservation 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

  • ${{ }} inside sql: is a hard validation error β€” dynamic values belong in args: (see the injection guard).
  • The statement runs with the default 5s statement_timeout inside a READ ONLY transaction on rflow's own Postgres β€” keep polled aggregates indexed/cheap, or widen every:.
  • The trigger only sees what the project indexes: event-trigger tables and contracts.<name>.index_events tables (plus the rflow.* journal). rflow tables lists what is queryable.