relay_workflow_validate
Validate workflow structure by checking DAG cycles, dependency references, and model ID format without LLM calls to ensure proper configuration before execution.
Instructions
Validate workflow structure without making any LLM calls (free). Checks DAG structure (no cycles), dependency references, and model ID format. Does NOT validate schema compatibility between steps or prompt effectiveness - use relay_workflow_run for full validation.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| steps | Yes | Steps to validate (same format as relay_workflow_run) |
Implementation Reference
- The core handler function implementing the relay_workflow_validate tool. Validates workflow steps for duplicates, model/prompt consistency, dependency existence and cycles, MCP format, and computes execution order and parallel groups.export async function relayWorkflowValidate( input: RelayWorkflowValidateInput ): Promise<RelayWorkflowValidateResponse> { const errors: ValidationError[] = []; const warnings: ValidationWarning[] = []; const stepNames = new Set(input.steps.map(s => s.name)); // Check for duplicate step names const nameCount = new Map<string, number>(); for (const step of input.steps) { nameCount.set(step.name, (nameCount.get(step.name) || 0) + 1); } for (const [name, count] of nameCount) { if (count > 1) { errors.push({ step: name, field: 'name', message: `Duplicate step name: "${name}" appears ${count} times`, }); } } // Validate each step for (const step of input.steps) { // Check step name if (!step.name || step.name.trim() === '') { errors.push({ step: '(unnamed)', field: 'name', message: 'Step name is required', }); continue; } // Check for valid step configuration const hasModel = !!step.model; const hasPrompt = !!step.prompt; const hasMcp = !!step.mcp; if (hasModel && !hasPrompt) { errors.push({ step: step.name, field: 'prompt', message: 'Steps with a model must have a prompt', }); } if (hasPrompt && !hasModel) { errors.push({ step: step.name, field: 'model', message: 'Steps with a prompt must have a model', }); } if (!hasModel && !hasMcp) { warnings.push({ step: step.name, message: 'Step has no model or MCP tool - it will be a pass-through step', }); } // Validate model format if (hasModel) { if (!isValidModelFormat(step.model!)) { errors.push({ step: step.name, field: 'model', message: `Invalid model format: "${step.model}". Expected "provider:model-id" (e.g., "openai:gpt-4o")`, }); } else if (!isKnownModel(step.model!)) { warnings.push({ step: step.name, message: `Unknown model "${step.model}" - using default pricing estimate`, }); } } // Validate MCP format if (hasMcp) { const parts = step.mcp!.split(':'); if (parts.length !== 2) { errors.push({ step: step.name, field: 'mcp', message: `Invalid MCP tool format: "${step.mcp}". Expected "server:tool" (e.g., "crm:search")`, }); } } // Validate dependencies exist if (step.depends) { for (const dep of step.depends) { if (!stepNames.has(dep)) { errors.push({ step: step.name, field: 'depends', message: `Dependency "${dep}" not found in workflow steps`, }); } if (dep === step.name) { errors.push({ step: step.name, field: 'depends', message: 'Step cannot depend on itself', }); } } } } // Check for cycles const { order, hasCycle, cycleStep } = topologicalSort(input.steps); if (hasCycle) { errors.push({ step: cycleStep || '(unknown)', field: 'depends', message: `Circular dependency detected involving step: ${cycleStep}`, }); } // Calculate parallel groups const parallelGroups = hasCycle ? [] : calculateParallelGroups(input.steps); return { valid: errors.length === 0, errors, warnings, structure: { totalSteps: input.steps.length, executionOrder: hasCycle ? [] : order, parallelGroups, }, }; }
- Zod schema for input validation of the tool, defining structure for workflow steps.const workflowStepSchema = z.object({ name: z.string(), model: z.string().optional(), prompt: z.string().optional(), systemPrompt: z.string().optional(), depends: z.array(z.string()).optional(), mcp: z.string().optional(), params: z.object({}).passthrough().optional(), schema: z.object({}).passthrough().optional(), }); export const relayWorkflowValidateSchema = z.object({ steps: z.array(workflowStepSchema).describe('Steps to validate (same format as relay_workflow_run)'), });
- src/tools/relay-workflow-validate.ts:294-323 (registration)MCP tool definition including name, description, and input schema for registration.export const relayWorkflowValidateDefinition = { name: 'relay_workflow_validate', description: 'Validate workflow structure without making any LLM calls (free). Checks DAG structure (no cycles), dependency references, and model ID format. Does NOT validate schema compatibility between steps or prompt effectiveness - use relay_workflow_run for full validation.', inputSchema: { type: 'object' as const, properties: { steps: { type: 'array', description: 'Steps to validate (same format as relay_workflow_run)', items: { type: 'object', properties: { name: { type: 'string' }, model: { type: 'string' }, prompt: { type: 'string' }, systemPrompt: { type: 'string' }, depends: { type: 'array', items: { type: 'string' } }, mcp: { type: 'string' }, params: { type: 'object' }, schema: { type: 'object' }, }, required: ['name'], }, }, }, required: ['steps'], }, };
- src/server.ts:59-67 (registration)Registration of all tool definitions, including relayWorkflowValidateDefinition, used in listTools response.const TOOLS = [ relayModelsListDefinition, relayRunDefinition, relayWorkflowRunDefinition, relayWorkflowValidateDefinition, relaySkillsListDefinition, relayRunsListDefinition, relayRunGetDefinition, ];
- src/server.ts:124-127 (registration)Dispatch handler in callToolRequest that parses input with schema and invokes the relayWorkflowValidate function.case 'relay_workflow_validate': { const parsed = relayWorkflowValidateSchema.parse(args); result = await relayWorkflowValidate(parsed); break;