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 β€” no signing, no sending, 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
steps:
  - id: supply          # rflow reads the chain
    read: { contract: USDC, 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", ... }
  - id: report          # always POSTed, HMAC-signed by rflow
    http_call: { url: ..., hmac: "${{ secrets.report_hmac }}", body: { severity: "${{ steps.decide.output.severity }}", ... } }
  - id: escalate        # later steps branch on what the command decided
    if: "${{ steps.decide.output.severity != 'ok' }}"
    http_call: { url: ..., ... }
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 β€” 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