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

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

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 — no restart needed.

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
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.