with one click
hom-workflow-authoring
// Use when creating, modifying, or debugging YAML workflow definitions or the workflow engine
// Use when creating, modifying, or debugging YAML workflow definitions or the workflow engine
Use this skill when building, reviewing, or refactoring Rust code that requires strong maintainability discipline: SRP, DRY, OCP, explicit dependency injection, TDD/ATDD workflow, architecture review, and clean project structure. Complements the project's Rust CLAUDE.md with process rigor.
Use when adding a new harness adapter or modifying an existing one in hom-adapters
Use when working on terminal emulation, the TerminalBackend trait, libghostty integration, or PTY management
Use when modifying TUI rendering, input routing, layout, command bar, or pane management
Use before making meaningful code changes in HOM to load repository architecture, workflow rules, crate boundaries, feature flags, and verification requirements
Use when building, reviewing, or refactoring Rust code in HOM that requires strong maintainability, testing discipline, and explicit dependency boundaries
| name | hom-workflow-authoring |
| description | Use when creating, modifying, or debugging YAML workflow definitions or the workflow engine |
Invoke this skill when:
workflows/crates/hom-workflow/src/parser.rscrates/hom-workflow/src/dag.rscrates/hom-workflow/src/executor.rscrates/hom-workflow/src/condition.rscrates/hom-workflow/src/checkpoint.rsEvery workflow file lives in workflows/ or ~/.config/hom/workflows/ and must follow this schema:
name: <unique-kebab-case-name>
description: <one-line description>
variables:
<key>: <default-value-or-empty-string>
steps:
- id: <unique-step-id>
harness: <harness-name> # Must match HarnessType::from_str_loose()
model: <optional-model-name>
prompt: |
<minijinja template — can reference top-level vars like {{ task }} and step outputs like {{ steps.plan.output }}>
depends_on: [<step-id>, ...] # Optional — steps with no deps run first
timeout: <duration> # Optional — e.g., "300s", "5m", or bare seconds like "300"
condition: '<expression>' # Optional — evaluated before execution
retry: # Optional
max_attempts: <n>
backoff: exponential|linear|fixed
on_failure: skip # Optional — default is abort
# Or:
# on_failure:
# fallback: <step-id>
The parser in parser.rs and dag.rs enforce:
iddepends_on reference must point to an existing step idpetgraph::algo::toposort must succeed; cycles are rejectedharness field must parse via HarnessType::from_str_loose()parse_timeout() accepts 300s, 5m, or bare seconds like 300Prompts are minijinja templates. Available variables:
{{ task }} — Runtime variables from --var key=value are injected at the top level by name{{ steps.<step-id>.output }} — Output captured from a completed stepGood prompt:
prompt: |
Review this codebase and create a detailed implementation plan for:
{{ task }}
Output a numbered list of steps with file paths and changes needed.
Bad prompt:
prompt: "do the thing" # Too vague — harness won't know what to do
Think of the DAG as a data flow graph. Each step should:
depends_on)plan ──→ implement ──→ validate ──→ security-review
↑
(condition: PASS)
Conditions are evaluated before a step runs. If false, the step is skipped.
Supported operators:
steps.<id>.output contains "<substring>"steps.<id>.status == "completed"steps.<id>.status != "failed"expr1 && expr2expr1 || expr2Notes:
&& binds tighter than ||| Strategy | Behavior |
|---|---|
abort (default) | Stop entire workflow, report failure |
skip | Mark step as skipped, continue to dependents |
on_failure: { fallback: <step-id> } | Run an alternative step instead |
#[test]
fn test_parse_valid_workflow() {
let yaml = r#"
name: test-workflow
description: A test
steps:
- id: step1
harness: claude-code
prompt: "hello"
"#;
let def = WorkflowDef::from_yaml(yaml).unwrap();
assert_eq!(def.name, "test-workflow");
assert_eq!(def.steps.len(), 1);
}
#[test]
fn test_reject_duplicate_step_ids() {
let yaml = r#"
name: bad
steps:
- id: dup
harness: claude-code
prompt: "a"
- id: dup
harness: codex
prompt: "b"
"#;
let def = WorkflowDef::from_yaml(yaml).unwrap();
assert!(def.validate().is_err());
}
#[test]
fn test_cycle_detection() {
let steps = vec![
StepDef {
id: "a".into(),
harness: "claude".into(),
model: None,
prompt: "a".into(),
depends_on: vec!["b".into()],
timeout: None,
condition: None,
retry: None,
on_failure: None,
},
StepDef {
id: "b".into(),
harness: "codex".into(),
model: None,
prompt: "b".into(),
depends_on: vec!["a".into()],
timeout: None,
condition: None,
retry: None,
on_failure: None,
},
];
assert!(WorkflowDag::from_steps(&steps).is_err());
}
#[test]
fn test_topo_order() {
let steps = vec![
StepDef {
id: "plan".into(),
harness: "claude".into(),
model: None,
prompt: "plan".into(),
depends_on: vec![],
timeout: None,
condition: None,
retry: None,
on_failure: None,
},
StepDef {
id: "implement".into(),
harness: "codex".into(),
model: None,
prompt: "implement".into(),
depends_on: vec!["plan".into()],
timeout: None,
condition: None,
retry: None,
on_failure: None,
},
];
let dag = WorkflowDag::from_steps(&steps).unwrap();
let order = dag.topo_order().unwrap();
assert_eq!(order, vec!["plan".to_string(), "implement".to_string()]);
}
#[test]
fn test_contains_condition() {
let mut outputs = HashMap::new();
outputs.insert("test".to_string(), "All 42 tests PASS".to_string());
assert!(evaluate_condition(
r#"steps.test.output contains "PASS""#,
&outputs, &HashMap::new()
));
}
WorkflowDef::from_yaml()def.validate()WorkflowDag::from_steps()HarnessType::from_str_loose()variables: sectioncargo test -p hom-workflow passes