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

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   # optional

Input 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:

syntaxmeaning
[[ 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

  1. mkdir crates/core/templates/<id>/ and fill in the layout above (copy a seed like large-transfer-alert for the shapes). Every key in answers.sample.yaml must be a declared input.

  2. Generate the snapshot, then review and commit it:

    RFLOW_REGENERATE_TEMPLATE_SNAPSHOTS=1 \
      cargo test -p rflow_core regenerate_template_snapshots
  3. 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"); }
  4. Write the docs page at documentation/docs/pages/use-cases/<id>.mdx (matching docs.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.

  5. Verify. include_dir does 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.