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 anddecide.jsfor your rule
Generate it
rflow new --template command-decision
# or into an existing project:
rflow add workflow command-decisionInputs
| key | type | default |
|---|---|---|
project_name | string | command-decision |
network / chain_id / rpc_env / rpc_url | network / chain_id / env_var / string | ethereum / 1 / ETH_RPC / a public RPC |
token_name / token_address / token_decimals | contract / address / int | USDC / USDC mainnet / 6 |
check_cron | cron | */15 * * * * * (seconds field supported) |
warn_supply / critical_supply | token_amount | 1000000 / 5000000 (whole tokens) |
report_url / alert_url | string | http://localhost:9098/report / /alert |
hmac_env | env_var | REPORT_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_letterThe 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 startProduction checklist
- point
report_url/alert_urlat real sinks and rotate the HMAC secret - tune
warn_supply/critical_supplyto your token's real bands - slow
check_crondown to something production-shaped (e.g.*/5 * * * *) - pin the node version your ops hosts run (
decide.jsuses BigInt β any supported node works, but pin it anyway)
Common modifications
- swap
totalSupply()for any view function (health factor, oracle priceβ¦) - rewrite
decide.jsin 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 β seecommand-trade-prepfor the money-moving version of this pattern