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 contentauthor– The user ID who triggered the eventchannel_id– The target channel identifiertimestamp– Event timeemoji– Reaction emoji (for reaction triggers)message_id– Unique message identifieris_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:
- The engine iterates over the
TriggerContextfields - It inserts values into the context using the flattened naming convention
- 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 substringstr_starts_with(haystack, needle)– Returns true if the haystack starts with the needlestr_ends_with(haystack, needle)– Returns true if the haystack ends with the needlestr_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:
- Constructs the context via
build_eval_context - Enforces length limits – Expressions exceeding
MAX_EXPR_LEN(4096 characters) are rejected immediately - Spawns blocking tasks – Uses
tokio::task::spawn_blockingto run the CPU-bound expression evaluation off the main async thread - Applies timeouts – Wraps evaluation in
tokio::time::timeoutwithEVAL_TIMEOUTset to 100 milliseconds, preventing malicious or accidental infinite loops from stalling the workflow engine - Returns boolean results – The step runs if the expression evaluates to
true, skips iffalse, and surfacesWorkflowError::ConditionErrorfor 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_contextwithinbuzz-workflow/src/executor.rs, creating aHashMapContextwith 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_conditionreturnstrue, 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →