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 through WorkflowEngine::new, on_event, run, and finalize_run.

  • Trigger handling (lib.rs lines 320-442): Matches incoming Nostr events or scheduled timers to workflow definitions. WorkflowEngine::on_event evaluates per-channel triggers, while WorkflowEngine::run manages 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 include execute_run, execute_steps, evaluate_condition, resolve_template, and dispatch_action.

  • Side-effect sink (crates/buzz-workflow/src/action_sink.rs): Abstracted backend that performs actual operations. Implemented via the ActionSink trait and registered through set_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:

  1. Extracts channel_id from the stored event and skips workflow-execution events to prevent infinite loops.
  2. Queries the in-memory workflow_cache for enabled workflows targeting that channel.
  3. Parses the YAML definition into WorkflowDef and verifies def.enabled is true.
  4. Validates the event kind matches the trigger configuration via trigger_matches_event.
  5. Runs owner-authority verification (check_owner_authority) to confirm the workflow owner retains required roles in the channel.
  6. Evaluates optional filters through should_fire_workflow for emoji or NIP-10 expressions.
  7. 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:

  1. Evaluates optional if: conditions via evaluate_condition, which runs evalexpr boolean expressions in a sandboxed spawn_blocking thread with a 100-millisecond timeout and 4-kilobyte length limit.
  2. 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 | npub for 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_authority runs 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 TriggerDef in crates/buzz-workflow/src/schema.rs and update trigger_matches_event logic.
  • New actions: Add variants to ActionDef in schema.rs and implement handling in dispatch_action.
  • Community scoping: Workflow UUIDs are stored with community_id prefixes, 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_event with comprehensive owner-authority checks, while scheduled workflows use at-most-once database claims for reliability.
  • Execution occurs in executor.rs with 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.rs definitions 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:

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 →