How the Buzz Workflow Engine Works: Architecture and Execution Flow
The Buzz workflow engine is a lightweight, channel-scoped automation system that processes declarative YAML/JSON workflows through a four-layer architecture consisting of an engine core, trigger handler, execution engine, and side-effect sink.
The Buzz workflow engine powers community automations in the block/buzz repository, a Nostr-based communication platform. This Rust-based engine enables channel administrators to define event-driven and scheduled automations using simple YAML definitions, executing actions with strict security guarantees and concurrency controls.
Architecture Overview
The engine consists of four distinct layers that handle workflow processing from trigger to completion:
-
Engine core (
crates/buzz-workflow/src/lib.rs): Holds the database pool, runtime configuration, concurrency control via semaphores, and in-memory caches. Provides the public API throughWorkflowEngine::new,on_event,run, andfinalize_run. -
Trigger handling (
lib.rslines 320-442): Matches incoming Nostr events or scheduled timers to workflow definitions.WorkflowEngine::on_eventevaluates per-channel triggers, whileWorkflowEngine::runmanages cron and interval schedules. -
Execution engine (
crates/buzz-workflow/src/executor.rs): Contains the sequential step runner, condition evaluator, template resolver, and action dispatcher. Key functions includeexecute_run,execute_steps,evaluate_condition,resolve_template, anddispatch_action. -
Side-effect sink (
crates/buzz-workflow/src/action_sink.rs): Abstracted backend that performs actual operations. Implemented via theActionSinktrait and registered throughset_action_sink, this layer handles message publishing, reactions, and webhook calls.
Engine Initialization and Configuration
Construction begins in crates/buzz-workflow/src/lib.rs (lines 75-89) where WorkflowEngine::new accepts a database pool and WorkflowConfig:
let engine = WorkflowEngine::new(db, WorkflowConfig::default());
engine.set_action_sink(Arc::new(MyActionSink));
The WorkflowConfig struct controls concurrency limits via max_concurrent and per-step timeouts through default_timeout_secs. The engine stores a semaphore (run_semaphore) to enforce these limits across asynchronous workflow executions. The ActionSink trait abstraction allows swapping backend implementations without modifying core logic.
Trigger Matching and Workflow Activation
The engine supports two activation modes processed through distinct code paths.
Event-Driven Triggers
WorkflowEngine::on_event processes real-time Nostr events such as MessagePosted, ReactionAdded, or DiffPosted. The method (lines 320-442 in lib.rs) performs the following sequence:
- Extracts
channel_idfrom the stored event and skips workflow-execution events to prevent infinite loops. - Queries the in-memory
workflow_cachefor enabled workflows targeting that channel. - Parses the YAML definition into
WorkflowDefand verifiesdef.enabledis true. - Validates the event kind matches the trigger configuration via
trigger_matches_event. - Runs owner-authority verification (
check_owner_authority) to confirm the workflow owner retains required roles in the channel. - Evaluates optional filters through
should_fire_workflowfor emoji or NIP-10 expressions. - Creates a workflow run row in the database and spawns an async task calling
executor::execute_run.
Scheduled Triggers
The background loop in WorkflowEngine::run (lines 489-544) handles Schedule triggers with cron or interval definitions. Every 60 seconds, the engine loads all enabled workflows, computes next fire instants, and obtains an at-most-once claim via claim_scheduled_workflow_fire to guarantee single execution across distributed pods. After the same owner-authority verification used in event-driven paths, it creates a run record and dispatches execution.
Workflow Execution Flow
executor::execute_run in crates/buzz-workflow/src/executor.rs (lines 35-91) acquires a semaphore permit, marks the run status as Running, and invokes execute_steps.
Step Processing and Conditions
For each step in def.steps, the executor:
- Evaluates optional
if:conditions viaevaluate_condition, which runsevalexprboolean expressions in a sandboxedspawn_blockingthread with a 100-millisecond timeout and 4-kilobyte length limit. - Resumes from saved indices for interrupted workflows, maintaining deterministic state.
Template Resolution
The resolve_template function processes all {{…}} placeholders within action fields. The template engine supports:
{{trigger.X}}for trigger event data{{steps.ID.output.Y}}for accessing previous step outputs- Filters like
| truncate(N)or| npubfor data transformation
Action Dispatching
dispatch_action sends concrete actions to the configured ActionSink. On success, the step's JSON output is stored in step_outputs for downstream steps. For RequestApproval steps, the function returns early with an approval_token, pausing execution until manual continuation.
After completion, WorkflowEngine::finalize_run writes the final status (Completed or Failed) and full execution trace to the database.
Security Guarantees and Safeguards
The engine implements multiple protection layers:
- Owner authority re-verification:
check_owner_authorityruns immediately before run creation to prevent removed members from triggering actions. - At-most-once firing: Database-backed claims prevent duplicate scheduled executions across horizontally scaled deployments.
- SSRF protection: Webhook actions validate DNS resolution and reject private IP addresses via
check_ssrf. - Expression sandboxing: Condition evaluations run in isolated threads with strict resource limits to prevent denial-of-service attacks.
Extending the Engine
The architecture supports community-driven extensions:
- New trigger types: Extend
TriggerDefincrates/buzz-workflow/src/schema.rsand updatetrigger_matches_eventlogic. - New actions: Add variants to
ActionDefinschema.rsand implement handling indispatch_action. - Community scoping: Workflow UUIDs are stored with
community_idprefixes, ensuring identical IDs across different communities never collide.
Practical Implementation Examples
Defining a Workflow Schema
id: 6e6f7d3a-1c4b-4c9e-9e1d-2d2b8b3c5f8a
channel: a4d3c5e1-7f1b-45c9-8123-13f5c9d7b5e2
owner_pubkey: e17e5abf7b1dbd363f0ed6fbda2455609727b2555428dea251388c542cd2f03f
enabled: true
trigger:
MessagePosted:
filter: "trigger_text contains 'incident'"
steps:
- id: post_alert
action:
SendMessage:
text: "🚨 Incident reported: {{trigger.text}}"
channel: null
Parsed by WorkflowEngine::parse_yaml using structures defined in schema.rs.
Processing Real-Time Events
// Inside the Relay's post-store hook
engine.on_event(community_id, &stored_event).await?;
This triggers the full matching and execution pipeline described in the event-driven triggers section.
Running Scheduled Execution
let engine = Arc::new(engine);
tokio::spawn(async move {
engine.run().await;
});
Starts the background loop handling cron parsing, interval bucketing, and distributed claim logic.
Implementing Custom Action Sinks
use buzz_workflow::{ActionSink, ActionSinkError};
#[derive(Default)]
struct MySink;
#[async_trait::async_trait]
impl ActionSink for MySink {
async fn send_message(
&self,
community_id: CommunityId,
channel_id: &str,
text: &str,
author_pubkey_hex: &str,
reply_to: Option<&str>,
) -> Result<String, ActionSinkError> {
// Custom message-publishing logic
Ok("generated-event-id".to_string())
}
}
// Register during startup
engine.set_action_sink(Arc::new(MySink));
Summary
- The Buzz workflow engine uses a four-layer architecture (core, triggers, execution, sink) to process channel-scoped automations.
- Event-driven workflows activate through
on_eventwith comprehensive owner-authority checks, while scheduled workflows use at-most-once database claims for reliability. - Execution occurs in
executor.rswith sandboxed condition evaluation, template resolution supporting previous step data, and pluggable action sinks. - Security features include SSRF protection, expression sandboxing with timeouts, and runtime authority verification.
- The engine is fully extensible through
schema.rsdefinitions and operates with community-scoped isolation to prevent ID collisions.
Frequently Asked Questions
What programming language is the Buzz workflow engine written in?
Rust. The engine is implemented in the block/buzz repository as part of the buzz-workflow crate, utilizing Tokio for async execution and SQLite via buzz_db for persistence.
How does the Buzz workflow engine prevent duplicate scheduled workflow executions?
The engine uses an at-most-once claim mechanism via claim_scheduled_workflow_fire in the database. When the background loop in WorkflowEngine::run processes cron or interval triggers, it atomically claims the right to fire that specific workflow instance, ensuring only one pod in a distributed deployment executes the scheduled run.
Can workflow steps access data from previous steps?
Yes. The template resolution system in resolve_template supports accessing previous step outputs using the {{steps.ID.output.Y}} syntax. The executor stores each step's JSON output in a step_outputs map that subsequent steps can reference through template expressions.
What security measures protect against malicious workflow expressions?
The engine sandboxes conditional expressions by running evalexpr evaluations in a spawn_blocking thread with a strict 100-millisecond timeout and a 4-kilobyte length limit. Additionally, webhook actions undergo SSRF protection through DNS resolution checks that reject private IP addresses via the check_ssrf function.
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 →