How Template Variables and Custom Functions Work in Buzz Workflow Definitions

Buzz's workflow engine uses Jinja-like {{}} syntax for dynamic data injection and registers custom Rust helper functions with the evalexpr library to power conditional step execution.

When building automation workflows in the block/buzz repository, you embed data from triggering events and previous step outputs directly into YAML definitions using template variables, while custom functions enable complex boolean logic for step conditions. This architecture allows developers to create dynamic, branching workflows without leaving the declarative YAML format.

Resolving Template Variables in Workflow Steps

The template resolution engine lives in crates/buzz-workflow/src/executor.rs. The resolve_template function (lines 66‑78) scans strings for {{…}} placeholders and replaces them with values from the runtime context.

Trigger Context Variables

Workflows access incoming event data through the trigger context. The TriggerContext struct (lines 25‑46) exposes fields as template variables using the pattern {{trigger.FIELD}}:

  • {{trigger.text}} – The message or payload text
  • {{trigger.author}} – The user identifier
  • {{trigger.channel_id}} – The source channel
  • {{trigger.timestamp}}, {{trigger.emoji}}, {{trigger.message_id}}

The helper method TriggerContext::get_field retrieves these values during variable resolution. If a variable name does not exist in the context, Buzz leaves the template text untouched rather than failing, ensuring malformed templates do not break execution.

Step Output Variables

Steps can consume outputs from previous steps using the dot-notated path {{steps.STEP_ID.output.FIELD}}. Internally, resolve_variable (lines 30‑53) parses this pattern against a HashMap<String, JsonValue> that stores each completed step's JSON output. For example, if a step with id: greet returns {"success": true, "message_id": "123"}, subsequent steps access these via {{steps.greet.output.success}} and {{steps.greet.output.message_id}}.

Applying Output Filters

After resolution, the apply_filter function (lines 80‑100) transforms string values. Buzz ships with built-in filters:

  • | truncate(N) – Shortens text to N characters
  • | npub – Converts hex-encoded public keys to bech32 npub format

These filters chain to variables: {{trigger.author | npub}} or {{trigger.text | truncate(30)}}.

Custom Functions for Conditional Logic

Workflow steps support optional if: clauses evaluated by the evalexpr expression library. Because evalexpr v11 lacks native string manipulation, Buzz extends it with custom Rust functions.

The evalexpr Integration

In build_eval_context (lines 40‑81), Buzz creates a fresh evaluation context for each workflow execution. This function maps trigger fields and step outputs to underscored identifiers (trigger_text, steps_STEP_ID_output_FIELD) because evalexpr does not support dotted variable names. The evaluate_condition function (lines 57‑98) then parses the boolean expression against this context.

Available String Helper Functions

Buzz registers four string utility functions in the evaluation context:

  1. str_contains(haystack, needle) – Returns true if the haystack contains the needle substring
  2. str_starts_with(s, prefix) – Checks if string s begins with prefix
  3. str_ends_with(s, suffix) – Checks if string s ends with suffix
  4. str_len(s) – Returns the string length as an integer

These enable complex conditions like checking for keywords or verifying output lengths before executing steps.

Complete Workflow Example

The following YAML demonstrates template variables and custom functions working together:

name: Greet & Log
trigger: message_posted
steps:
  - id: greet
    action:
      type: post_message
      content: "👋 Hello {{trigger.author | npub}}! You said: {{trigger.text | truncate(30)}}"
  - id: log
    if: "str_contains(trigger_text, \"important\") && !steps_greet_output_success"
    action:
      type: webhook
      url: "https://example.com/log"
      body:
        author: "{{trigger.author}}"
        message: "{{trigger.text}}"
        channel: "{{trigger.channel_id}}"

In this workflow:

  • The greet step uses {{trigger.author | npub}} to format the author and {{trigger.text | truncate(30)}} to preview the message
  • The log step only executes if the trigger text contains "important" and the greet step failed, using the underscored variable steps_greet_output_success

Summary

  • Template variables in Buzz use双重 curly brace syntax ({{trigger.FIELD}} or {{steps.ID.output.FIELD}}) resolved by resolve_template in crates/buzz-workflow/src/executor.rs
  • Trigger context exposes event metadata like author, text, and channel_id through the TriggerContext struct
  • Step outputs persist as JSON and remain accessible to subsequent steps via dot-notation paths
  • Filters like truncate and npub transform resolved values before insertion
  • Custom functions (str_contains, str_starts_with, str_ends_with, str_len) extend evalexpr for string-based condition evaluation
  • Underscored identifiers (trigger_text, steps_ID_output_FIELD) bridge the gap between YAML dot-notation and evalexpr's variable naming constraints

Frequently Asked Questions

What happens if a template variable does not exist in Buzz?

Buzz leaves unknown variable placeholders untouched in the output string. This design choice, implemented in the variable resolution logic, ensures that a typo or missing field does not crash the entire workflow execution.

How do I reference a previous step's success status in a condition?

Use the underscored identifier pattern steps_STEP_ID_output_FIELD. For a step with id: greet, access its success boolean via steps_greet_output_success in the if: expression, as the build_eval_context function automatically maps step outputs to these snake_case names.

Can I add custom filters beyond truncate and npub?

Currently, Buzz hardcodes available filters in the apply_filter function (lines 80‑100 of executor.rs). Adding new filters requires modifying the Rust source code and rebuilding the project; there is no plugin system for user-defined filters in the current architecture.

Why must I use underscores instead of dots in if: conditions?

The underlying evalexpr library does not support dots in variable identifiers. To work around this, Buzz's build_eval_context automatically converts trigger.field to trigger_field and steps.id.output.field to steps_id_output_field when building the evaluation context for condition expressions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →