How to Implement a Custom Workflow Action in Buzz's YAML Automation Engine
Extend the ActionDef enum in crates/buzz-workflow/src/schema.rs, add the execution branch in 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. 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:
#[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. Locate the match step.action clause (around line 350) inside the WorkflowEngine implementation and add a branch for your variant:
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). Return WorkflowError::InvalidDefinition when constraints fail:
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. 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 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:
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, asserting that commands run and populate step_outputs correctly. Copy patterns from the existing CallWebhook tests in crates/buzz-workflow/tests/workflow.rs.
Run the full suite before submitting:
just ci # lint, fmt, and unit tests
just test # full integration suite (requires Postgres + Redis)
Summary
- Define the action structure in
ActionDef(schema.rs) using serde attributes for JSON compatibility. - Implement the runtime behavior in the
match step.actionblock (lib.rs) around line 350. - Optionally enforce rules in
WorkflowDef::validateto restrict when the action may run. - The desktop UI automatically recognizes new actions via WorkflowWire; customize
ActionEditor.tsxonly for complex inputs. - Test parsing in
tests/parse.rsand execution intests/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 (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 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 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). This error variant signals a static configuration problem, preventing the workflow from starting rather than failing mid-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 →