Creating templates
Templates are data, not code: a package is a manifest plus files. There
are no generation hooks and no template-supplied code execution — rendering is
plain substitution, and every install path ends in rflow validate. Adding a
template to the registry is adding a directory; no code or index edit is
required (the registry discovers packages at build time).
This page covers first-party embedded templates, which live in
crates/core/templates/and compile into the binary. To share a template with everyone WITHOUT waiting for a release, submit it to the community registry instead —rflow templates scaffold <id>writes the skeleton, and a merged PR is installable immediately by every existing binary.
Package layout
crates/core/templates/<id>/
template.yaml # the manifest — id MUST equal the directory name
README.md # for humans browsing the repo (not generated)
answers.sample.yaml # doctor/snapshot answers (not generated)
rflow.yaml.tmpl # rendered -> rflow.yaml
env.example.tmpl # rendered -> .env.example
gitignore # copied -> .gitignore
docker-compose.yml # copied -> docker-compose.yml
abis/<name>.json # copied verbatim (one per requires.abis entry)
scripts/*.js # copied verbatim (command-step templates)
fixtures/*.json # copied verbatim (rflow test / rflow replay inputs)
tests/expected-rflow.yaml # committed snapshot (not generated)Everything except template.yaml, README.md, answers.sample.yaml and
tests/ is an output: *.tmpl files render, everything else copies
byte-for-byte. outputs.files in the manifest must list exactly the generated
paths (post-rename: rflow.yaml, .env.example, .gitignore, …) — doctor
fails on any mismatch.
The manifest
id: my-template # = directory name
title: My Template
category: monitoring # treasury | monitoring | keeper | intents | bridge-ops
# | governance | security | relayers | offchain | examples
rflow_min_version: "0.1.0"
template_version: "1.0.0"
risk: monitor_only # monitor_only | prepares_tx | money_moving | admin | experimental
summary: "One line for the ls table."
aliases: [] # optional extra ids that resolve here
inputs: # declaration order = prompt order
project_name:
type: string
prompt: "Project name"
default: my-template
pattern: "^[a-z0-9][a-z0-9-]*quot;
token_address:
type: address
prompt: "ERC20 token address"
required: true
requires:
database: true
signer: false
relayers: [] # names the template's yaml declares
abis: [] # names matching abis/<name>.json
secrets:
- name: TG_BOT_TOKEN
optional: true
outputs:
files: [rflow.yaml, .env.example, .gitignore, docker-compose.yml]
safety: # honest metadata shown by `templates show`
requires_simulation: false
requires_approval: false
docs:
page: /use-cases/my-template # must exist under documentation/docs/pages
example: examples/my-example # optionalInput types
string, bool, int, decimal, duration, cron, address (EIP-55
checked on mixed case), tx_hash, network, chain_id, contract,
abi_path, token_amount, choice (with options:), list, secret_name,
env_var — plus per-input required, default, pattern and prompt.
Validation runs before rendering, and the rendered project is validated again
through normal rflow validate.
Rendering rules
Templates render with minijinja configured with custom delimiters so rflow's own syntax passes through untouched:
| syntax | meaning |
|---|---|
[[ input_key ]] | template placeholder (strict — an unknown key fails the render) |
[% if ... %] … [% endif %] | template logic (e.g. a choice switching a channel block) |
[# ... #] | template comment |
${{ ... }} | an rflow expression — written literally, passes through |
${VAR} | an env placeholder — written literally, passes through |
Gotcha: minijinja's whitespace trim markers ([%- / -%]) strip the next
line's indentation too, which corrupts YAML. Put conditional tags inline at
the end of a line instead (see workflow-error-pager's templates for the
pattern).
Money templates must render the full safety rails into the YAML:
simulation + assert_sim, a gas cap, recheck:, trigger confirmations +
run_on: confirmed, wait_for: confirmed, on_failure: dead_letter, a
concurrency group — and an approval: gate for treasury/admin flows.
The recipe
-
mkdir crates/core/templates/<id>/and fill in the layout above (copy a seed likelarge-transfer-alertfor the shapes). Every key inanswers.sample.yamlmust be a declared input. -
Generate the snapshot, then review and commit it:
RFLOW_REGENERATE_TEMPLATE_SNAPSHOTS=1 \ cargo test -p rflow_core regenerate_template_snapshots -
Add a one-line snapshot test to your group file (
crates/core/src/template_registry/tests_group_<x>.rs):#[test] fn my_template_snapshot() { super::assert_template_snapshot("my-template"); } -
Write the docs page at
documentation/docs/pages/use-cases/<id>.mdx(matchingdocs.page) and link it from/use-cases. The page shape: what it does, when to use it, the generated YAML, inputs, env vars, safety notes, local run, production checklist, common modifications. -
Verify.
include_dirdoes not trigger rebuilds when only template files change — touch the registry first:touch crates/core/src/template_registry/registry.rs cargo test -p rflow_core template_registry cargo run --bin rflow -- templates doctor <id> cargo run --bin rflow -- new --template <id> --yes --output /tmp/x cargo run --bin rflow -- validate -p /tmp/x
Definition of done: doctor passes (which includes the sample-answers render + validate, the outputs check and the snapshot), and the docs page exists.