How Workflow Triggers and evalexpr Condition Evaluation Work in Buzz

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, 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. 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 (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:

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

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:

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:

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
  • 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, 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.

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 →