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

Command decision

Template: command-decision Β· category: examples Β· risk: monitor_only

A cron reads a token's totalSupply() on-chain, a project-owned node script decides a severity band, and rflow POSTs the decision to an HTTP endpoint (HMAC-signed), escalating non-ok severities to a second endpoint. This is the canonical command: step shape: rflow reads the chain, runs the step durably and makes every HTTP call; your script reads JSON on stdin and prints one JSON decision on stdout; it never signs or sends and has no RPC handle. Runnable sibling: examples/command-decision.

When to use it

  • put YOUR risk logic (any language, here node) inside a durable rflow workflow
  • classify an on-chain reading into bands and branch later steps on the result
  • the starting point for command-driven monitors: swap totalSupply() for any view function and decide.js for your rule

Generate it

rflow new --template command-decision
# or into an existing project:
rflow add workflow command-decision

Inputs

keytypedefault
project_namestringcommand-decision
network / chain_id / rpc_env / rpc_urlnetwork / chain_id / env_var / stringethereum / 1 / ETH_RPC / a public RPC
token_name / token_address / token_decimalscontract / address / intUSDC / USDC mainnet / 6
check_croncron*/15 * * * * * (seconds field supported)
warn_supply / critical_supplytoken_amount1000000 / 5000000 (whole tokens)
report_url / alert_urlstringhttp://localhost:9098/report / /alert
hmac_envenv_varREPORT_HMAC_SECRET

Generated YAML (the shape)

# recipe: partial
rflow_version: 1
name: command-decision
 
config:
  port: 3940
  db_connection: ${DATABASE_URL}
 
networks:
  - name: ethereum
    chain_id: 1
    rpc: ${ETH_RPC}
 
contracts:
  USDC:
    abi: ./abis/erc20.json
    addresses:
      ethereum: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
 
secrets:
  # signs every report/alert body - the receiver verifies the signature
  report_hmac: ${REPORT_HMAC_SECRET}
 
workflows:
  command-decision:
    trigger:
      cron:
        expression: "*/15 * * * * *"
    steps: 
      - id: supply          # rflow reads the chain 
        read: { contract: USDC, network: ethereum, function: "totalSupply()" } 
      - id: decide          # your script decides 
        command: 
          run: "node ./scripts/decide.js"
          timeout: 10s
          output: json
          input: 
            supply: "${{ steps.supply.output }}"
            warn_above: "1000000"
            critical_above: "5000000"
      - id: report          # always POSTed, HMAC-signed by rflow 
        http_call: 
          url: http://localhost:9098/report
          hmac: "${{ secrets.report_hmac }}"
          body: 
            severity: "${{ steps.decide.output.severity }}"
            reason: "${{ steps.decide.output.reason }}"
      - id: escalate        # later steps branch on what the command decided 
        if: "${{ steps.decide.output.severity != 'ok' }}"
        http_call: 
          url: http://localhost:9098/alert
          hmac: "${{ secrets.report_hmac }}"
          body: 
            alert: "${{ steps.decide.output.severity }}"
    on_failure: dead_letter

The thresholds travel through the input: map, so the rules are configured in rflow.yaml and scripts/decide.js stays generic.

Required env vars

DATABASE_URL, the RPC env var (default ETH_RPC), and the HMAC secret (default REPORT_HMAC_SECRET), all listed in the generated .env.example. node must be on PATH for the decide step.

Safety notes

Monitor-only: no signer, no relayer, no transactions, so the workflow cannot move money even if the script misbehaves. The script gets no secrets unless you pass them via command.env, and its stdout is journaled, so keep secrets out of the decision object. on_failure: dead_letter keeps failed runs replayable.

Run it locally

docker compose up -d
rflow validate
# rehearse without waiting for the cron:
rflow test command-decision --fixture fixtures/cron-tick.json
rflow start

Production checklist

  1. point report_url / alert_url at real sinks and rotate the HMAC secret
  2. tune warn_supply / critical_supply to your token's real bands
  3. slow check_cron down to something production-shaped (e.g. */5 * * * *)
  4. pin the node version your ops hosts run (decide.js uses BigInt; any supported node works, but pin it anyway)

Common modifications

  • swap totalSupply() for any view function (health factor, oracle price…)
  • rewrite decide.js in any language: the stdin/stdout JSON contract is all that matters
  • add a notify: step for human-visible escalations
  • feed the decision into a send_transaction: step; see command-trade-prep for the money-moving version of this pattern