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

state_set, list_add & list_remove

Three small steps that give workflows durable memory: a Postgres-backed key/value store (state_set) and runtime mutation of your lists: watchlists (list_add / list_remove). Plus the previous_run context root for comparing against the last run.

state_set

rflow_version: 1
name: oracle-watch
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
networks:
  - name: ethereum
    chain_id: 1
    rpc: ${ETH_RPC}
 
contracts:
  Oracle:
    abi: ./abis/chainlink-aggregator.json
    addresses:
      ethereum: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"
 
notifications:
  channels:
    ops:
      console: {}
 
workflows:
  price-move-alert:
    trigger:
      interval:
        every: 1m
    steps: 
      - id: read-price
        read: { contract: Oracle, network: ethereum, function: "latestAnswer()" } 
      - id: alert-on-move
        if: "${{ state['oracle.last_price'] is defined and mul_div(steps.read-price.output, 100, state['oracle.last_price']) > 105 }}"
        notify: { channel: ops, message: "price moved >5% since the last tick" } 
      - id: remember
        state_set: 
          key: "oracle.last_price"
          value: "${{ steps.read-price.output }}"
FieldDescription
keyThe state key (may be an expression)
valueThe value (may be an expression; numbers stay exact)

Values persist in rflow.state and are readable from any expression as state.<key> (or state['dotted.key']). State written earlier in the same run is visible to the next step's expressions.

list_add / list_remove

rflow_version: 1
name: swap-guard
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
networks:
  - name: ethereum
    chain_id: 1
    rpc: ${ETH_RPC}
 
contracts:
  UniV3Factory:
    abi: ./abis/univ3-factory.json
    network: ethereum
    address: "0x1F98431c8aD98523631AE4a59f267346ea31F984"
  AnyPool:
    abi: ./abis/univ3-pool.json
    factory:
      contract: UniV3Factory
      event: PoolCreated
      network: ethereum
 
lists: 
  blocked: { seed: [] } 
 
workflows: 
  auto-block: 
    trigger: 
      event: { contract: AnyPool, name: Swap, network: ethereum } 
    steps: 
      - id: block-trader
        if: "${{ trigger.args.amount0 > wei('1000000', 18) }}"
        list_add: 
          list: blocked
          value: "${{ trigger.args.sender }}"
FieldDescription
listA list declared under lists: (validated)
valueThe member (may be an expression; addresses are stored lowercased)

The classic factory pattern: one workflow watches PoolCreated and list_adds each child; other workflows filter with ${{ trigger.address in lists.pools }}.

What a list is at runtime

lists.<name> in expressions is the union of three provenances (visible with rflow list show):

ProvenanceWritten byRemoved by
seedrflow.yamlediting rflow.yaml (list_remove/CLI cannot remove seeds)
manualrflow list addrflow list remove / list_remove:
workflowlist_add: stepslist_remove: / CLI
sourcelists.<name>.source refreshthe next refresh (atomically replaced)

A mutation is visible to the very next where: evaluation without a restart.

previous_run — compare against last time

Every step expression can read the workflow's last succeeded run:

PathDescription
previous_run.id / previous_run.statusThe run (status is always succeeded)
previous_run.steps.<id>.outputThat run's journaled step output
previous_run.steps.<id>.statusThat step's terminal status
rflow_version: 1
name: rate-watch
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
notifications:
  channels:
    ops:
      console: {}
 
workflows:
  rate-change-alert:
    trigger:
      interval:
        every: 5m
    steps: 
      - id: fetch
        http_call: { url: "https://api.example.com/rate" } 
      - id: only-on-change
        if: "${{ previous_run.steps.fetch.output.body.rate != steps.fetch.output.body.rate }}"
        notify: { channel: ops, message: "rate changed: ${{ steps.fetch.output.body.rate }}" } 

On the workflow's first run, previous_run is undefined. Strict evaluation makes an unguarded reference a step error (never a silent false). Guard it:

if: "${{ previous_run is not defined or previous_run.steps.fetch.output.body.rate != steps.fetch.output.body.rate }}"

Dry-run behaviour

In rflow replay / rflow test sessions, state_set / list_add / list_remove skip their writes and journal would_set / would_mutate outputs instead: reads see live state, writes never land, and there is nothing to clean up after a rehearsal.