# How the Buzz Workflow Engine Works: Architecture and Execution Flow

> Discover how the Buzz workflow engine works. Explore its four-layer architecture including core, trigger handler, execution engine, and side-effect sink for efficient automation.

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

---

**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](https://github.com/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/crates/buzz-workflow/src/lib.rs) (lines 75-89) where `WorkflowEngine::new` accepts a database pool and `WorkflowConfig`:

```rust
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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/crates/buzz-workflow/src/schema.rs) and update `trigger_matches_event` logic.
- **New actions**: Add variants to `ActionDef` in [`schema.rs`](https://github.com/block/buzz/blob/main/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

```yaml
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`](https://github.com/block/buzz/blob/main/schema.rs).

### Processing Real-Time Events

```rust
// 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

```rust
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

```rust
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`](https://github.com/block/buzz/blob/main/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`](https://github.com/block/buzz/blob/main/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.