Workflow Editor Specification

Visual workflow editor for creating and editing DAG-based workflow templates.

Status: Planned (not yet implemented) Source: Architecture Advisory Section 12.2 Related Modules: Workflow Engine, Workflow Templates Sprint: 111_004 Workflow Editor

Overview

The workflow editor provides a visual graph editor for creating and editing workflow templates. It supports drag-and-drop node placement, connection creation, real-time run visualization, and bidirectional YAML synchronization.


Graph Editor Component

Editor State

interface WorkflowEditorState {
  template: WorkflowTemplate;
  selectedNode: string | null;
  selectedEdge: string | null;
  zoom: number;
  pan: { x: number; y: number };
  mode: "select" | "pan" | "connect";
  clipboard: StepNode[] | null;
  undoStack: WorkflowTemplate[];
  redoStack: WorkflowTemplate[];
}

interface WorkflowEditorProps {
  template: WorkflowTemplate;
  stepTypes: StepType[];
  readOnly: boolean;
  onSave: (template: WorkflowTemplate) => void;
  onValidate: (template: WorkflowTemplate) => ValidationResult;
}

Node Renderer

interface NodeRendererProps {
  node: StepNode;
  stepType: StepType;
  status?: StepRunStatus;       // For run visualization
  selected: boolean;
  onSelect: () => void;
  onMove: (position: Position) => void;
  onConnect: (sourceHandle: string) => void;
}

const NodeRenderer: React.FC<NodeRendererProps> = ({
  node, stepType, status, selected
}) => {
  const statusColor = getStatusColor(status);

  return (
    <div className={`workflow-node ${selected ? 'selected' : ''}`}
         style={{ borderColor: statusColor }}>

      {/* Node header */}
      <div className="node-header" style={{ backgroundColor: stepType.color }}>
        <Icon name={stepType.icon} />
        <span className="node-name">{node.name}</span>
        {status && <StatusBadge status={status} />}
      </div>

      {/* Node body */}
      <div className="node-body">
        <span className="node-type">{stepType.name}</span>
        {node.timeout && <span className="node-timeout">T {node.timeout}s</span>}
      </div>

      {/* Connection handles */}
      <Handle type="target" position="top" />
      <Handle type="source" position="bottom" />

      {/* Conditional indicator */}
      {node.condition && (
        <div className="condition-badge" title={node.condition}>
          <Icon name="condition" />
        </div>
      )}
    </div>
  );
};

Run Visualization Overlay

Real-Time Execution Display

interface RunVisualizationProps {
  template: WorkflowTemplate;
  workflowRun: WorkflowRun;
  stepRuns: StepRun[];
  onNodeClick: (nodeId: string) => void;
}

const RunVisualization: React.FC<RunVisualizationProps> = ({
  template, workflowRun, stepRuns, onNodeClick
}) => {
  // WebSocket for real-time updates
  const { subscribe, unsubscribe } = useWorkflowStream(workflowRun.id);

  useEffect(() => {
    const handlers = {
      'step_started': (data) => updateStepStatus(data.nodeId, 'running'),
      'step_completed': (data) => updateStepStatus(data.nodeId, data.status),
      'step_log': (data) => appendLog(data.nodeId, data.line),
    };

    subscribe(handlers);
    return () => unsubscribe();
  }, [workflowRun.id]);

  return (
    <div className="run-visualization">
      {/* Workflow graph with status overlay */}
      <WorkflowGraph
        template={template}
        nodeRenderer={(node) => (
          <NodeRenderer
            node={node}
            stepType={getStepType(node.type)}
            status={getStepRunStatus(node.id)}
            selected={selectedNode === node.id}
            onSelect={() => setSelectedNode(node.id)}
          />
        )}
        edgeRenderer={(edge) => (
          <EdgeRenderer
            edge={edge}
            animated={isEdgeActive(edge)}
          />
        )}
      />

      {/* Log panel */}
      {selectedNode && (
        <LogPanel
          stepRun={getStepRun(selectedNode)}
          streaming={isStepRunning(selectedNode)}
        />
      )}

      {/* Progress bar */}
      <ProgressBar
        completed={completedSteps}
        total={totalSteps}
        status={workflowRun.status}
      />
    </div>
  );
};

Status Indicators

StatusVisual
PendingGray circle
RunningBlue spinner
SuccessGreen checkmark
FailedRed X
SkippedYellow dash

Canvas Operations

Drag and Drop

Validation

The editor performs real-time validation:

Zoom and Pan

ActionControl
Zoom InCtrl + Mouse Wheel Up
Zoom OutCtrl + Mouse Wheel Down
Fit ViewCtrl + 0
PanMiddle Mouse Drag / Space + Drag
ResetCtrl + R

YAML Editor Mode

Monaco Editor Integration

The editor supports a bidirectional YAML mode for power users:

interface YAMLEditorProps {
  template: WorkflowTemplate;
  onChange: (template: WorkflowTemplate) => void;
  onValidate: (yaml: string) => ValidationResult;
}

const YAMLEditor: React.FC<YAMLEditorProps> = ({ template, onChange, onValidate }) => {
  const [yaml, setYaml] = useState(templateToYaml(template));

  return (
    <MonacoEditor
      language="yaml"
      value={yaml}
      onChange={(value) => {
        setYaml(value);
        const result = onValidate(value);
        if (result.valid) {
          onChange(yamlToTemplate(value));
        }
      }}
      options={{
        minimap: { enabled: false },
        lineNumbers: 'on',
        scrollBeyondLastLine: false,
      }}
    />
  );
};

Bidirectional Sync

Changes in either view (graph or YAML) are synchronized:


Step Palette

Available Step Types

The palette shows all available step types from core and plugins:

interface StepPaletteProps {
  stepTypes: StepType[];
  onDragStart: (stepType: string) => void;
  filter: string;
}

const categories = [
  { name: "Deployment", types: ["deploy", "rollback"] },
  { name: "Gates", types: ["security-gate", "approval", "freeze-window-gate"] },
  { name: "Utility", types: ["script", "wait", "notify"] },
  { name: "Plugins", types: [] }, // Dynamically loaded
];

Keyboard Shortcuts

ShortcutAction
Ctrl + SSave template
Ctrl + ZUndo
Ctrl + Shift + ZRedo
DeleteDelete selected
Ctrl + CCopy selected
Ctrl + VPaste
Ctrl + ASelect all
EscapeDeselect / Cancel

See Also