# How Template Variables and Custom Functions Work in Buzz Workflow Definitions

> Learn how Buzz workflow definitions use template variables and custom Rust functions for dynamic data injection and conditional step execution with the evalexpr library.

- Repository: [Block Open Source/buzz](https://github.com/block/buzz)
- Tags: internals
- Published: 2026-08-27

---

**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`](https://github.com/block/buzz/blob/main/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:

```yaml
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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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.