Validation
canvasToDefinitionV2 and gate-handle-to-branch footgun.
Validation runs whenever a workflow is saved or published, converting the canvas graph into a validated WorkflowDefinition before anything is persisted or deployed. Understanding the pipeline helps you debug surprising publish failures.
Validation pipeline
1. canvasToDefinitionV2
Converts the canvas graph (React Flow nodes and edges) into the WorkflowDefinition v2 format. This is where the gate handle → branch serialisation happens - see the footgun section below.
2. validateWorkflowFull
Runs the full Zod v3 schema against the definition, plus cross-resource invariants:
- All step IDs are unique
- All
branchesvalues point at existing step IDs - Trigger nodes exist and have valid
outputSchema - Group definitions pass scope-isolation checks
metadata.stateMachine(when present) passesvalidateStateMachinespec.lifecycle(when present) passesvalidateLifecycleConfig
3. Connection validation
validateAllConnections checks every edge against the typed port system:
- Source port type is compatible with target port type
- No edges cross group scope boundaries without going through interface ports
- Required input ports have at least one incoming edge
4. Struct type resolution
ValidationContext resolves kind: 'struct' port references to their workspace struct definitions. An unresolved struct ID (e.g. a deleted struct type) is a hard error.
Gate handle → branch footgun
This is the most common cause of gate nodes silently routing to the wrong step after save/reload.
Gate nodes (gate.human_query) have three output handles: approved, rejected, and timeout. These handles only route correctly when canvasToDefinitionV2 serialises them as step.branches - not as step.next.
Correct (canvas serialised with canvasToDefinitionV2):
steps:
- id: approval-gate
type: gate
name: "Approve PR"
config: { nodeId: gate.human_query, ... }
branches:
approved: deploy-step
rejected: notify-rejection
timeout: auto-approve-stepBroken (serialised via the old canvasToWorkflowSteps path or hand-authored YAML):
steps:
- id: approval-gate
type: gate
branches: {} # empty - handles were not serialised
next: deploy-stepIf you see a gate node that always takes the same path regardless of the approval outcome, check the YAML pane. The branches map must have entries for each handle (approved, rejected, timeout). If any key is missing, the executor uses the next fallback or skips the step entirely.
Fix: Re-save the workflow from the canvas editor. The canvasToDefinitionV2 path runs on every save and will regenerate the correct branches map from the current canvas edges.
Validation result shape
The validator returns a ValidationResult with a structured issues array:
interface ValidationResult {
valid: boolean
issues: Array<{
path: string // dot-path into the definition, e.g. "steps[2].branches.approved"
message: string
severity: "error" | "warning"
}>
}Warnings do not block publish. Errors do.
Common errors and fixes
YAML import validation
When you import a workflow from YAML or JSON (POST /api/workflows/{id}/import), the same validation pipeline runs before any changes are applied to the canvas. Import fails atomically - the canvas is not modified if the imported definition is invalid.
Cross-resource invariants
Beyond the structural checks, validateWorkflowFull also enforces workspace-level invariants:
- Foundational node refs (
agent-definition,llm-model,capacity-pool, etc.) must point at existing rows for the workspace credential-providerrefs must be connected integrations- Subscription projects must belong to the workspace
These are surfaced as validation issues with a path like steps[0].config.agentDefinitionId.
Related pages
- Groups - scope-isolation validation
- State Machine -
validateStateMachinerules - Lifecycle Config -
validateLifecycleConfigrules - Runtime: Gates - the gate system that reads
branchesat runtime - Testing - mock validation during test runs