# How Workflow Triggers and evalexpr Condition Evaluation Work in Buzz

> Learn how Buzz workflow triggers and evalexpr condition evaluation work. Understand variable mapping, timeouts, and limits for robust conditional step execution in your YAML workflows.

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

---

**Buzz executes YAML workflow steps conditionally by evaluating `if:` expressions with the `evalexpr` crate, mapping trigger data and step outputs to flat variable names while enforcing strict timeouts and length limits to prevent runtime abuse.**

Buzz powers automated chat workflows in the `block/buzz` repository. When events fire—whether from chat messages, reactions, or webhooks—the workflow engine must decide which steps to run based on dynamic conditions. This article breaks down how **workflow triggers** supply event data and how the **evalexpr condition evaluation** system safely executes user-defined logic.

## Understanding TriggerContext and Event Data

Every workflow begins with a **trigger**. When a qualifying event occurs, Buzz constructs a `TriggerContext` containing primitive data fields from the event. According to the source code in [`crates/buzz-workflow/src/schema.rs`](https://github.com/block/buzz/blob/main/crates/buzz-workflow/src/schema.rs), this context includes:

- `text` – The message or payload content
- `author` – The user ID who triggered the event  
- `channel_id` – The target channel identifier
- `timestamp` – Event time
- `emoji` – Reaction emoji (for reaction triggers)
- `message_id` – Unique message identifier
- `is_reply` – Boolean flag indicating if the message is a reply
- Custom webhook fields (for webhook triggers)

The `TriggerContext` struct normalizes disparate event types into a consistent schema that the executor can consume uniformly.

## Mapping YAML References to evalexpr Variables

Buzz workflows use dot-notation in YAML (e.g., `trigger.text`, `steps.fetch.output.status`) to reference data. Before evaluation, the engine flattens these references into underscore-separated variable names that `evalexpr` recognizes.

| YAML Reference | evalexpr Variable |
|----------------|-------------------|
| `trigger.text` | `trigger_text` |
| `trigger.author` | `trigger_author` |
| `trigger.channel_id` | `trigger_channel_id` |
| `trigger.timestamp` | `trigger_timestamp` |
| `trigger.emoji` | `trigger_emoji` |
| `trigger.message_id` | `trigger_message_id` |
| `trigger.is_reply` | `trigger_is_reply` |
| `steps.<STEP_ID>.output.<FIELD>` | `steps_<STEP_ID>_output_<FIELD>` |

This mapping occurs during context construction, ensuring that expressions like `str_contains(trigger_text, "urgent")` resolve against the actual event data.

## Building the Evaluation Context in executor.rs

The core logic resides in [`crates/buzz-workflow/src/executor.rs`](https://github.com/block/buzz/blob/main/crates/buzz-workflow/src/executor.rs). The `build_eval_context` function (lines 30-84) constructs a `HashMapContext` and populates it with trigger fields and prior step outputs.

For each workflow execution:

1. The engine iterates over the `TriggerContext` fields
2. It inserts values into the context using the flattened naming convention
3. It appends outputs from previously executed steps using the `steps_<ID>_output_<FIELD>` pattern

This context becomes the variable namespace for all subsequent `if:` evaluations in the workflow.

## String Helper Functions for evalexpr

The default `evalexpr` library lacks string manipulation utilities critical for workflow logic. Buzz registers four custom helper functions in [`executor.rs`](https://github.com/block/buzz/blob/main/executor.rs) (lines 40-80):

- **`str_contains(haystack, needle)`** – Returns true if the haystack contains the needle substring
- **`str_starts_with(haystack, needle)`** – Returns true if the haystack starts with the needle
- **`str_ends_with(haystack, needle)`** – Returns true if the haystack ends with the needle  
- **`str_len(string)`** – Returns the character length of the string

These functions enable expressive filters without requiring regex or external processing:

```yaml
if: str_contains(trigger_text, "urgent") && trigger_is_reply == false

```

## Safe Condition Evaluation with Timeouts

The `evaluate_condition` function (lines 57-99 in [`executor.rs`](https://github.com/block/buzz/blob/main/executor.rs)) executes expressions defensively to protect the async runtime. The implementation:

1. **Constructs the context** via `build_eval_context`
2. **Enforces length limits** – Expressions exceeding `MAX_EXPR_LEN` (4096 characters) are rejected immediately
3. **Spawns blocking tasks** – Uses `tokio::task::spawn_blocking` to run the CPU-bound expression evaluation off the main async thread
4. **Applies timeouts** – Wraps evaluation in `tokio::time::timeout` with `EVAL_TIMEOUT` set to 100 milliseconds, preventing malicious or accidental infinite loops from stalling the workflow engine
5. **Returns boolean results** – The step runs if the expression evaluates to `true`, skips if `false`, and surfaces `WorkflowError::ConditionError` for evaluation failures

This architecture ensures that workflow conditions cannot block the Tokio runtime or consume excessive resources.

## Practical Workflow Examples

### Filtering Messages with Trigger Data

Filter incoming messages to respond only to non-reply greetings:

```yaml
trigger:
  type: MessagePosted
  channel: "#general"

steps:
  greet:
    if: trigger_is_reply == false && str_contains(trigger_text, "hello")
    action:
      SendMessage:
        text: "👋 Hey there, {{trigger_author}}!"
        channel: "{{trigger_channel_id}}"

```

### Chaining Steps with Output Conditions

Use results from prior HTTP calls to determine subsequent actions:

```yaml
steps:
  fetch:
    action:
      CallWebhook:
        url: "https://api.example.com/data?user={{trigger_author}}"
        
  filter:
    if: steps_fetch_output_status == 200 && str_contains(steps_fetch_output_body, "approved")
    action:
      AddReaction:
        emoji: "✅"

```

### Webhook Trigger Conditions

Access custom webhook fields directly in conditions:

```yaml
trigger:
  type: Webhook
  webhook_fields:
    event_type: "deployment"

steps:
  notify:
    if: trigger_event_type == "deployment"
    action:
      SendMessage:
        text: "🚀 Deployment started!"
        channel: "#ops"

```

## Summary

- **TriggerContext** normalizes events into primitive fields (text, author, channel_id, etc.) defined in [`schema.rs`](https://github.com/block/buzz/blob/main/schema.rs)
- **Variable mapping** converts YAML dot-notation (`trigger.text`) to evalexpr-compatible names (`trigger_text`)
- **Context building** happens in `build_eval_context` within [`buzz-workflow/src/executor.rs`](https://github.com/block/buzz/blob/main/buzz-workflow/src/executor.rs), creating a `HashMapContext` with trigger data and step outputs
- **Helper functions** (`str_contains`, `str_starts_with`, `str_ends_with`, `str_len`) extend evalexpr for practical string operations
- **Safety mechanisms** include a 4096-character expression limit, 100-millisecond timeouts, and blocking thread isolation to prevent runtime abuse
- **Step execution** proceeds only when `evaluate_condition` returns `true`, otherwise the step is skipped

## Frequently Asked Questions

### How does Buzz prevent malicious workflow expressions from hanging the system?

Buzz applies multiple safeguards in `evaluate_condition`. It spawns expression evaluation on a dedicated blocking thread using `tokio::task::spawn_blocking`, then enforces a 100-millisecond timeout with `tokio::time::timeout`. Expressions exceeding 4096 characters are rejected before evaluation begins. These measures ensure pathological expressions cannot stall the async runtime.

### What evalexpr variables are available in a workflow step?

Available variables include all `TriggerContext` fields mapped to snake_case (e.g., `trigger_text`, `trigger_author`, `trigger_is_reply`), plus any prior step outputs using the pattern `steps_<STEP_ID>_output_<FIELD>`. Custom webhook fields from the trigger also appear as `trigger_<FIELD_NAME>` variables.

### Can I use regular expressions in Buzz workflow conditions?

No, the current implementation in `block/buzz` does not expose regex functions to evalexpr. Instead, use the provided string helpers (`str_contains`, `str_starts_with`, `str_ends_with`) for substring matching. For complex pattern matching, consider preprocessing data in an earlier step that outputs a boolean or matched string.

### What happens if an evalexpr condition evaluates to an error?

If the expression is invalid, references undefined variables, or encounters a runtime error, `evaluate_condition` returns `WorkflowError::ConditionError`. This error propagates up and typically halts workflow execution, surfacing the failure to the caller rather than silently skipping the step or continuing execution.