# How to Implement a Custom Workflow Action in Buzz's YAML Automation Engine

> Learn to implement a custom workflow action in Buzz's YAML automation engine. Extend ActionDef, add execution, and Buzz automatically exposes your new capability via JSON schema.

- Repository: [Block Open Source/buzz](https://github.com/block/buzz)
- Tags: how-to-guide
- Published: 2026-08-29

---

**Extend the `ActionDef` enum in [`crates/buzz-workflow/src/schema.rs`](https://github.com/block/buzz/blob/main/crates/buzz-workflow/src/schema.rs), add the execution branch in [`crates/buzz-workflow/src/lib.rs`](https://github.com/block/buzz/blob/main/crates/buzz-workflow/src/lib.rs), and the engine automatically exposes the new capability through the existing JSON schema.**

The Buzz automation engine (block/buzz) processes declarative workflow definitions written in YAML. To implement a custom workflow action in Buzz, you extend the core Rust enums and runtime match arms that drive the execution loop, enabling any new capability to participate in the engine’s validation, execution, and UI rendering pipeline.

## Step 1: Extend the Action Definition in schema.rs

The canonical schema for workflow actions lives in [`crates/buzz-workflow/src/schema.rs`](https://github.com/block/buzz/blob/main/crates/buzz-workflow/src/schema.rs). The **`ActionDef`** enum (lines 92‑100) uses internal tagging with the JSON key `action` and snake_case renaming for variants.

Add your custom variant to this enum:

```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum ActionDef {
    // … existing variants …
    /// Run an OS command on the server hosting the relay.
    RunCommand {
        /// The command to execute (e.g. “ls -la”).
        cmd: String,
        /// Optional working directory.
        #[serde(default)]
        cwd: Option<String>,
        /// Optional environment variables.
        #[serde(default)]
        env: Option<std::collections::HashMap<String, String>>,
    },
}

```

Use **`#[serde(default)]`** for optional fields to maintain backward compatibility with existing YAML definitions.

## Step 2: Add Execution Logic in lib.rs

The runtime engine iterates over workflow steps in [`crates/buzz-workflow/src/lib.rs`](https://github.com/block/buzz/blob/main/crates/buzz-workflow/src/lib.rs). Locate the `match step.action` clause (around line 350) inside the `WorkflowEngine` implementation and add a branch for your variant:

```rust
match step.action {
    // … existing branches …
    ActionDef::RunCommand { cmd, cwd, env } => {
        // Build the command.
        let mut command = tokio::process::Command::new("sh");
        command.arg("-c").arg(&cmd);
        if let Some(dir) = cwd {
            command.current_dir(dir);
        }
        if let Some(map) = env {
            command.envs(map);
        }
        // Run asynchronously and capture the output.
        let output = command.output().await?;
        // Store the result in the step’s output map for later evalexpr use.
        step_outputs.insert(
            format!("step_{}_output", step.id),
            json!({ "stdout": String::from_utf8_lossy(&output.stdout),
                    "stderr": String::from_utf8_lossy(&output.stderr),
                    "status": output.status.code() })
        );
    }
}

```

This pattern mirrors the built-in `CallWebhook` action. The **`step_outputs`** map preserves results for downstream steps using the naming convention `step_{id}_output`.

## Step 3: Implement Validation Constraints (Optional)

For actions requiring additional safety checks, edit `WorkflowDef::validate` (lines 71‑84 in [`schema.rs`](https://github.com/block/buzz/blob/main/schema.rs)). Return **`WorkflowError::InvalidDefinition`** when constraints fail:

```rust
if matches!(step.action, ActionDef::RunCommand { .. }) && !self.enabled {
    return Err(WorkflowError::InvalidDefinition(
        "run_command cannot be used in a disabled workflow".into()
    ));
}

```

Validation runs before any execution, ensuring malformed workflows fail fast.

## Step 4: Expose the Action in the Desktop UI

The Tauri front-end consumes workflow definitions via the **WorkflowWire** JSON format marshaled in [`desktop/src-tauri/src/commands/workflows.rs`](https://github.com/block/buzz/blob/main/desktop/src-tauri/src/commands/workflows.rs). Because the JSON already includes the `action` field, the desktop dropdown automatically displays your new variant without changes.

If you need custom input controls, modify **[`ActionEditor.tsx`](https://github.com/block/buzz/blob/main/ActionEditor.tsx)** in `desktop/src/features/workflows/` to render specialized fields for your variant’s parameters.

## Step 5: Test Your Implementation

Add coverage in `crates/buzz-workflow/tests/` to prevent regressions.

Unit-test the YAML parsing in [`parse.rs`](https://github.com/block/buzz/blob/main/parse.rs):

```rust
let yaml = r#"
action: run_command
cmd: "echo hello"
"#;
let def: ActionDef = serde_yaml::from_str(yaml).unwrap();
assert!(matches!(def, ActionDef::RunCommand { .. }));

```

Integration-test the execution path in [`exec.rs`](https://github.com/block/buzz/blob/main/exec.rs), asserting that commands run and populate `step_outputs` correctly. Copy patterns from the existing `CallWebhook` tests in [`crates/buzz-workflow/tests/workflow.rs`](https://github.com/block/buzz/blob/main/crates/buzz-workflow/tests/workflow.rs).

Run the full suite before submitting:

```bash
just ci          # lint, fmt, and unit tests

just test        # full integration suite (requires Postgres + Redis)

```

## Summary

- Define the action structure in **`ActionDef`** ([`schema.rs`](https://github.com/block/buzz/blob/main/schema.rs)) using serde attributes for JSON compatibility.
- Implement the runtime behavior in the **`match step.action`** block ([`lib.rs`](https://github.com/block/buzz/blob/main/lib.rs)) around line 350.
- Optionally enforce rules in **`WorkflowDef::validate`** to restrict when the action may run.
- The desktop UI automatically recognizes new actions via **WorkflowWire**; customize [`ActionEditor.tsx`](https://github.com/block/buzz/blob/main/ActionEditor.tsx) only for complex inputs.
- Test parsing in [`tests/parse.rs`](https://github.com/block/buzz/blob/main/tests/parse.rs) and execution in [`tests/exec.rs`](https://github.com/block/buzz/blob/main/tests/exec.rs).

## Frequently Asked Questions

### Where is the action schema defined in the Buzz repository?

The **`ActionDef`** enum in [`crates/buzz-workflow/src/schema.rs`](https://github.com/block/buzz/blob/main/crates/buzz-workflow/src/schema.rs) (lines 92‑100) defines the schema. It uses `#[serde(tag = "action", rename_all = "snake_case")]` for JSON representation, ensuring YAML tags map directly to Rust variants.

### How does the Buzz engine execute custom workflow steps?

The **`WorkflowEngine`** in [`crates/buzz-workflow/src/lib.rs`](https://github.com/block/buzz/blob/main/crates/buzz-workflow/src/lib.rs) iterates over steps and matches `step.action` against `ActionDef` variants. Your implementation runs inside this match arm, typically producing output stored in the `step_outputs` map for later expression evaluation.

### Do I need to modify the front-end code to display a new action?

No. The desktop and web UIs read the **WorkflowWire** JSON from the relay, which automatically includes your new variant. The action appears in the dropdown immediately. Only add a case to **[`ActionEditor.tsx`](https://github.com/block/buzz/blob/main/ActionEditor.tsx)** if your action requires custom input widgets beyond the standard form fields.

### What error type should validation use for invalid action configurations?

Return **`WorkflowError::InvalidDefinition`** from `WorkflowDef::validate` (lines 71‑84 in [`schema.rs`](https://github.com/block/buzz/blob/main/schema.rs)). This error variant signals a static configuration problem, preventing the workflow from starting rather than failing mid-execution.