# Orquesta-Style Task Transition Model **Date**: 2026-02-04 ## Overview Refactored the workflow builder's task transition model from flat `on_success`/`on_failure`/`on_complete`/`on_timeout` fields to an Orquesta-style ordered `next` array of transitions. Each transition can specify a `when` condition, `publish` directives, and multiple `do` targets — enabling far more expressive workflow definitions. Also added visual drag handles to task nodes in the workflow builder for creating transitions via drag-and-drop. ## Motivation The previous model only allowed a single target task per transition type and had no way to: - Route to multiple tasks from a single transition - Attach per-transition variable publishing - Use custom condition expressions beyond the four fixed types - Publish variables without transitioning to another task The Orquesta model (from StackStorm) solves all of these with a simple, ordered list of conditional transitions. ## Changes ### Frontend (`web/`) #### `web/src/types/workflow.ts` - **Added** `TaskTransition` type: `{ when?: string; publish?: PublishDirective[]; do?: string[] }` - **Added** `TransitionPreset` type and constants (`PRESET_WHEN`, `PRESET_LABELS`) for the three common quick-access patterns: succeeded, failed, always - **Added** `classifyTransitionWhen()` and `transitionLabel()` for edge visualization - **Added** `EdgeType` — simplified to `"success" | "failure" | "complete" | "custom"` - **Added** helper functions: `findOrCreateTransition()`, `addTransitionTarget()`, `removeTaskFromTransitions()` - **Removed** `on_success`, `on_failure`, `on_complete`, `on_timeout`, `decision`, `publish` fields from `WorkflowTask` - **Removed** `DecisionBranch` type (subsumed by `TaskTransition.when`) - **Updated** `WorkflowYamlTask` to use `next?: WorkflowYamlTransition[]` - **Updated** `builderStateToDefinition()` to serialize `next` array - **Updated** `definitionToBuilderState()` to load both new `next` format and legacy flat fields (auto-converts) - **Updated** `deriveEdges()` to iterate `task.next[].do[]` - **Updated** `validateWorkflow()` to validate `next[].do[]` targets #### `web/src/components/workflows/TaskNode.tsx` - **Redesigned output handles**: Three color-coded drag handles at bottom (green=succeeded, red=failed, gray=always) - **Added input handle**: Neutral circle at top center as drop target, highlights purple during active connection - **Removed** old footer link-icon buttons - **Added** transition summary in node body (e.g., "2 targets via 1 transition") - **Added** custom transitions badge #### `web/src/components/workflows/WorkflowEdges.tsx` - Updated edge colors for new `EdgeType` values - Preview line color now uses `TransitionPreset` mapping - Dynamic label width based on text content #### `web/src/components/workflows/WorkflowCanvas.tsx` - Updated to use `TransitionPreset` instead of `TransitionType` - Added `onMouseUp` handler for drag-to-cancel on canvas background #### `web/src/components/workflows/TaskInspector.tsx` - **Replaced** four fixed `TransitionField` dropdowns with a dynamic transition list editor - Each transition card shows: `when` expression (editable), `do` target list (add/remove), `publish` key-value pairs (add/remove) - Quick-set buttons for common `when` presets (On Success, On Failure, Always) - Add transition buttons: "On Success", "On Failure", "Custom transition" - **Moved** publish variables from task-level section to per-transition - **Removed** old `TransitionField` component - **Added** Join section for barrier configuration #### `web/src/pages/actions/WorkflowBuilderPage.tsx` - Updated `handleSetConnection` to use `addTransitionTarget()` with `TransitionPreset` - Updated `handleDeleteTask` to use `removeTaskFromTransitions()` ### Backend (`crates/`) #### `crates/common/src/workflow/parser.rs` - **Added** `TaskTransition` struct: `{ when, publish, do }` - **Added** `Task::normalize_transitions()` — converts legacy fields into `next` array - **Added** `Task::all_transition_targets()` — collects all referenced task names - **Updated** `parse_workflow_yaml()` to call `normalize_all_transitions()` after parsing - **Updated** `validate_task()` to use `all_transition_targets()` instead of checking individual fields - Legacy fields (`on_success`, `on_failure`, `on_complete`, `on_timeout`, `decision`) retained for deserialization but cleared after normalization - **Added** 12 new tests covering both new and legacy formats #### `crates/common/src/workflow/validator.rs` - Updated `build_graph()` and `find_entry_points()` to use `task.all_transition_targets()` #### `crates/common/src/workflow/mod.rs` - Exported new `TaskTransition` type #### `crates/executor/src/workflow/graph.rs` - **Replaced** `TaskTransitions` struct (flat fields) with `Vec` - **Added** `GraphTransition`: `{ when, publish: Vec, do_tasks: Vec }` - **Added** `PublishVar`: `{ name, expression }` — preserves both key and value - **Added** `TransitionKind` enum and `GraphTransition::kind()` classifier - **Added** `TaskGraph::matching_transitions()` — returns full transition objects for coordinators - **Added** `TaskGraph::all_transition_targets()` — all target names from a task - **Updated** `next_tasks()` to evaluate transitions by `TransitionKind` - **Updated** `compute_inbound_edges()` to iterate `GraphTransition.do_tasks` - **Updated** `extract_publish_vars()` to return `Vec` instead of `Vec` - **Added** 12 new tests #### `crates/executor/src/workflow/task_executor.rs` - Updated variable publishing to extract from matching transitions instead of removed `task.publish` field ## YAML Format ### New (canonical) ```yaml tasks: - name: task1 action: core.echo next: - when: "{{ succeeded() }}" publish: - result: "{{ result() }}" do: - task2 - log - when: "{{ failed() }}" do: - error_handler ``` ### Legacy (still parsed, auto-converted) ```yaml tasks: - name: task1 action: core.echo on_success: task2 on_failure: error_handler ``` ## Test Results - **Parser tests**: 37 passed (includes 12 new) - **Graph tests**: 12 passed (includes 10 new) - **TypeScript**: Zero errors - **Rust workspace**: Zero warnings