From cd271d4c8b701fcf96710450c20ab887d891e284 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 10 Jun 2025 21:24:14 -0700 Subject: [PATCH 01/11] feat(subworkflows) workflows in workflows --- apps/sim/app/api/workflows/[id]/route.ts | 64 ++++++ apps/sim/blocks/blocks/workflow.ts | 76 +++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/executor/handlers/index.ts | 2 + .../handlers/workflow/workflow-handler.ts | 187 ++++++++++++++++++ apps/sim/executor/index.ts | 2 + apps/sim/stores/workflows/sync.ts | 79 +++----- apps/sim/tools/registry.ts | 2 + apps/sim/tools/workflow/executor.ts | 68 +++++++ apps/sim/tools/workflow/index.ts | 1 + 10 files changed, 428 insertions(+), 55 deletions(-) create mode 100644 apps/sim/app/api/workflows/[id]/route.ts create mode 100644 apps/sim/blocks/blocks/workflow.ts create mode 100644 apps/sim/executor/handlers/workflow/workflow-handler.ts create mode 100644 apps/sim/tools/workflow/executor.ts create mode 100644 apps/sim/tools/workflow/index.ts diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts new file mode 100644 index 00000000000..1dc12d37ed9 --- /dev/null +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -0,0 +1,64 @@ +import { eq } from 'drizzle-orm' +import { NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console-logger' +import { db } from '@/db' +import { workflow } from '@/db/schema' + +const logger = createLogger('WorkflowDetailAPI') + +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const requestId = crypto.randomUUID().slice(0, 8) + const startTime = Date.now() + + try { + // Get the session + const session = await getSession() + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthorized workflow access attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id: workflowId } = await params + + if (!workflowId) { + return NextResponse.json({ error: 'Workflow ID is required' }, { status: 400 }) + } + + // Fetch the workflow from database + const workflowData = await db + .select() + .from(workflow) + .where(eq(workflow.id, workflowId)) + .then((rows) => rows[0]) + + if (!workflowData) { + logger.warn(`[${requestId}] Workflow ${workflowId} not found`) + return NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) + } + + // Check if user has access to this workflow + // User can access if they own it OR if it's in a workspace they're part of + const canAccess = workflowData.userId === session.user.id + // TODO: Add workspace membership check when needed + + if (!canAccess) { + logger.warn( + `[${requestId}] User ${session.user.id} attempted to access workflow ${workflowId} without permission` + ) + return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + } + + const elapsed = Date.now() - startTime + logger.info(`[${requestId}] Workflow ${workflowId} fetched in ${elapsed}ms`) + + return NextResponse.json({ data: workflowData }, { status: 200 }) + } catch (error: any) { + const elapsed = Date.now() - startTime + logger.error(`[${requestId}] Error fetching workflow after ${elapsed}ms:`, error) + return NextResponse.json({ error: 'Failed to fetch workflow' }, { status: 500 }) + } +} \ No newline at end of file diff --git a/apps/sim/blocks/blocks/workflow.ts b/apps/sim/blocks/blocks/workflow.ts new file mode 100644 index 00000000000..5ba22866188 --- /dev/null +++ b/apps/sim/blocks/blocks/workflow.ts @@ -0,0 +1,76 @@ +import { ComponentIcon } from '@/components/icons' +import { createLogger } from '@/lib/logs/console-logger' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import type { ToolResponse } from '@/tools/types' +import type { BlockConfig } from '../types' + +const logger = createLogger('WorkflowBlock') + +interface WorkflowResponse extends ToolResponse { + output: { + [key: string]: any + success: boolean + duration?: number + childWorkflowId: string + childWorkflowName: string + } +} + +// Helper function to get available workflows for the dropdown +const getAvailableWorkflows = (): Array<{ label: string; id: string }> => { + try { + const { workflows, activeWorkflowId } = useWorkflowRegistry.getState() + + // Filter out the current workflow to prevent recursion + const availableWorkflows = Object.entries(workflows) + .filter(([id]) => id !== activeWorkflowId) + .map(([id, workflow]) => ({ + label: workflow.name || `Workflow ${id.slice(0, 8)}`, + id: id + })) + .sort((a, b) => a.label.localeCompare(b.label)) + + return availableWorkflows + } catch (error) { + logger.error('Error getting available workflows:', error) + return [] + } +} + +export const WorkflowBlock: BlockConfig = { + type: 'workflow', + name: 'Workflow', + description: 'Execute another workflow as a block', + category: 'blocks', + bgColor: '#6366f1', + icon: ComponentIcon, + subBlocks: [ + { + id: 'workflowId', + title: 'Select Workflow', + type: 'dropdown', + options: getAvailableWorkflows, + }, + ], + tools: { + access: ['workflow_executor'], + }, + inputs: { + workflowId: { + type: 'string', + required: true, + description: 'ID of the workflow to execute' + } + }, + outputs: { + response: { + type: { + success: 'boolean', + duration: 'number', + childWorkflowId: 'string', + childWorkflowName: 'string', + error: 'string' + } + } + } +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index aab7f1419f9..9fb874bc0a4 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -61,6 +61,7 @@ import { TwilioSMSBlock } from './blocks/twilio' import { TypeformBlock } from './blocks/typeform' import { VisionBlock } from './blocks/vision' import { WhatsAppBlock } from './blocks/whatsapp' +import { WorkflowBlock } from './blocks/workflow' import { XBlock } from './blocks/x' import { YouTubeBlock } from './blocks/youtube' import type { BlockConfig } from './types' @@ -123,6 +124,7 @@ export const registry: Record = { typeform: TypeformBlock, vision: VisionBlock, whatsapp: WhatsAppBlock, + workflow: WorkflowBlock, x: XBlock, youtube: YouTubeBlock, huggingface: HuggingFaceBlock, diff --git a/apps/sim/executor/handlers/index.ts b/apps/sim/executor/handlers/index.ts index c054ad487ce..51ad100c5ae 100644 --- a/apps/sim/executor/handlers/index.ts +++ b/apps/sim/executor/handlers/index.ts @@ -7,6 +7,7 @@ import { GenericBlockHandler } from './generic/generic-handler' import { LoopBlockHandler } from './loop/loop-handler' import { ParallelBlockHandler } from './parallel/parallel-handler' import { RouterBlockHandler } from './router/router-handler' +import { WorkflowBlockHandler } from './workflow/workflow-handler' export { AgentBlockHandler, @@ -18,4 +19,5 @@ export { LoopBlockHandler, ParallelBlockHandler, RouterBlockHandler, + WorkflowBlockHandler, } diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts new file mode 100644 index 00000000000..cf8b7727764 --- /dev/null +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -0,0 +1,187 @@ +import { createLogger } from '@/lib/logs/console-logger' +import type { BlockOutput } from '@/blocks/types' +import { Serializer } from '@/serializer' +import type { SerializedBlock } from '@/serializer/types' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { Executor } from '../../index' +import type { BlockHandler, ExecutionContext, StreamingExecution } from '../../types' + +const logger = createLogger('WorkflowBlockHandler') + +/** + * Handler for workflow blocks that execute other workflows inline. + * Creates sub-execution contexts and manages data flow between parent and child workflows. + */ +export class WorkflowBlockHandler implements BlockHandler { + private serializer = new Serializer() + + canHandle(block: SerializedBlock): boolean { + return block.metadata?.id === 'workflow' + } + + async execute( + block: SerializedBlock, + inputs: Record, + context: ExecutionContext + ): Promise { + logger.info(`Executing workflow block: ${block.id}`) + + const workflowId = inputs.workflowId + + if (!workflowId) { + throw new Error('No workflow selected for execution') + } + + try { + // Load the child workflow from API + const childWorkflow = await this.loadChildWorkflow(workflowId) + + if (!childWorkflow) { + throw new Error(`Child workflow ${workflowId} not found`) + } + + // Get workflow metadata for logging + const { workflows } = useWorkflowRegistry.getState() + const workflowMetadata = workflows[workflowId] + const childWorkflowName = workflowMetadata?.name || childWorkflow.name || 'Unknown Workflow' + + logger.info(`Executing child workflow: ${childWorkflowName} (${workflowId})`) + + // Use the input data directly from the context - this allows for visual connections + // from parent workflow blocks to flow into the child workflow + const subWorkflowInput = { + ...inputs, // Include any direct inputs to this block + } + + // Get the starter block's input data from the context + const starterBlock = context.workflow?.blocks.find((b) => b.metadata?.id === 'starter') + if (starterBlock) { + const starterState = context.blockStates.get(starterBlock.id) + if (starterState?.output?.response?.input) { + // Include the parent workflow's input data + Object.assign(subWorkflowInput, starterState.output.response.input) + } + } + + // Remove the workflowId from the input to avoid confusion + const { workflowId: _, ...cleanInput } = subWorkflowInput + + // Execute child workflow inline + const subExecutor = new Executor({ + workflow: childWorkflow.serializedState, + workflowInput: cleanInput, + envVarValues: context.environmentVariables, + }) + + const startTime = performance.now() + const result = await subExecutor.execute(`${context.workflowId}_sub_${workflowId}`) + const duration = performance.now() - startTime + + // Log execution completion + logger.info(`Child workflow ${childWorkflowName} completed in ${Math.round(duration)}ms`) + + // Map child workflow output to parent block output + return this.mapChildOutputToParent(result, workflowId, childWorkflowName, duration) + } catch (error: any) { + logger.error(`Error executing child workflow ${workflowId}:`, error) + + // Get workflow name for error reporting + const { workflows } = useWorkflowRegistry.getState() + const workflowMetadata = workflows[workflowId] + const childWorkflowName = workflowMetadata?.name || workflowId + + return { + response: { + success: false, + error: error.message || 'Child workflow execution failed', + childWorkflowId: workflowId, + childWorkflowName: childWorkflowName, + }, + } + } + } + + /** + * Loads a child workflow from the API + */ + private async loadChildWorkflow(workflowId: string) { + try { + // Fetch workflow from API + const response = await fetch(`/api/workflows/${workflowId}`) + + if (!response.ok) { + if (response.status === 404) { + logger.error(`Child workflow ${workflowId} not found`) + return null + } + throw new Error(`Failed to fetch workflow: ${response.status} ${response.statusText}`) + } + + const { data: workflowData } = await response.json() + + if (!workflowData) { + logger.error(`Child workflow ${workflowId} returned empty data`) + return null + } + + logger.info(`Loaded child workflow: ${workflowData.name} (${workflowId})`) + + // Extract the workflow state + const workflowState = workflowData.state + + if (!workflowState || !workflowState.blocks) { + logger.error(`Child workflow ${workflowId} has invalid state`) + return null + } + + // Use blocks directly since DB format should match UI format + const serializedWorkflow = this.serializer.serializeWorkflow( + workflowState.blocks, + workflowState.edges || [], + workflowState.loops || {}, + workflowState.parallels || {} + ) + + return { + name: workflowData.name, + serializedState: serializedWorkflow, + } + } catch (error) { + logger.error(`Error loading child workflow ${workflowId}:`, error) + return null + } + } + + /** + * Maps child workflow output to parent block output format + */ + private mapChildOutputToParent( + childResult: any, + childWorkflowId: string, + childWorkflowName: string, + duration: number + ): BlockOutput { + const success = childResult.success !== false + + // Create the parent block output with a flattened structure + // This allows outputs from child workflow to be easily connected to other blocks + const parentOutput = { + response: { + success, + duration: Math.round(duration), + childWorkflowId, + childWorkflowName, + // Flatten the child result for easier access in visual connections + ...(childResult.output || {}), + }, + } + + // If child workflow failed, include error information + if (!success && childResult.error) { + parentOutput.response.error = childResult.error + } + + logger.info(`Child workflow output mapped:`, parentOutput) + return parentOutput + } +} diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index 112fe5422e7..9f29be36e23 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -14,6 +14,7 @@ import { LoopBlockHandler, ParallelBlockHandler, RouterBlockHandler, + WorkflowBlockHandler, } from './handlers/index' import { LoopManager } from './loops' import { ParallelManager } from './parallels' @@ -141,6 +142,7 @@ export class Executor { new ApiBlockHandler(), new LoopBlockHandler(this.resolver), new ParallelBlockHandler(this.resolver), + new WorkflowBlockHandler(), new GenericBlockHandler(), ] diff --git a/apps/sim/stores/workflows/sync.ts b/apps/sim/stores/workflows/sync.ts index c2fbeb412b0..d144bf6eb3b 100644 --- a/apps/sim/stores/workflows/sync.ts +++ b/apps/sim/stores/workflows/sync.ts @@ -3,7 +3,7 @@ import { createLogger } from '@/lib/logs/console-logger' import { API_ENDPOINTS } from '../constants' import { createSingletonSyncManager } from '../sync' -import { getAllWorkflowsWithValues } from '.' +import { getAllWorkflowsWithValues } from './' import { useWorkflowRegistry } from './registry/store' import type { WorkflowMetadata } from './registry/types' import { useSubBlockStore } from './subblock/store' @@ -230,7 +230,6 @@ export async function fetchWorkflowsFromDB(): Promise { description, color, state, - lastSynced, isDeployed, deployedAt, apiKey, @@ -251,70 +250,40 @@ export async function fetchWorkflowsFromDB(): Promise { registryWorkflows[id] = { id, name, - description: description || '', - color: color || '#3972F6', - // Use createdAt for sorting if available, otherwise fall back to lastSynced - lastModified: createdAt ? new Date(createdAt) : new Date(lastSynced), - marketplaceData: marketplaceData || null, - workspaceId, // Include workspaceId in metadata + description, + color, + marketplaceData, + workspaceId, + lastModified: createdAt ? new Date(createdAt) : new Date(), } - // 2. Prepare workflow state data + // 2. Store workflow state in localStorage for persistence const workflowState = { blocks: state.blocks || {}, edges: state.edges || [], loops: state.loops || {}, parallels: state.parallels || {}, - isDeployed: isDeployed || false, - deployedAt: deployedAt ? new Date(deployedAt) : undefined, - apiKey, - lastSaved: Date.now(), - marketplaceData: marketplaceData || null, + isStreaming: false, + isExecuting: false, + environment: state.environment || {}, + variables: state.variables || {}, + metadata: { + workflowId: id, + version: '1.0.0', + lastSaved: Date.now(), + ...state.metadata, + }, } - // 3. Initialize subblock values from the workflow state - const subblockValues: Record> = {} - - // Extract subblock values from blocks - Object.entries(workflowState.blocks).forEach(([blockId, block]) => { - const blockState = block as BlockState - subblockValues[blockId] = {} - - Object.entries(blockState.subBlocks || {}).forEach(([subblockId, subblock]) => { - subblockValues[blockId][subblockId] = subblock.value - }) - }) - - // Get any additional subblock values that might not be in the state but are in the store - const storedValues = useSubBlockStore.getState().workflowValues[id] || {} - Object.entries(storedValues).forEach(([blockId, blockValues]) => { - if (!subblockValues[blockId]) { - subblockValues[blockId] = {} - } - - Object.entries(blockValues).forEach(([subblockId, value]) => { - // Only update if not already set or if value is null - if ( - subblockValues[blockId][subblockId] === null || - subblockValues[blockId][subblockId] === undefined - ) { - subblockValues[blockId][subblockId] = value - } - }) - }) - - // 4. Store the workflow state and subblock values in localStorage - // This ensures compatibility with existing code that loads from localStorage localStorage.setItem(`workflow-${id}`, JSON.stringify(workflowState)) - localStorage.setItem(`subblock-values-${id}`, JSON.stringify(subblockValues)) - // 5. Update subblock store for this workflow - useSubBlockStore.setState((state) => ({ - workflowValues: { - ...state.workflowValues, - [id]: subblockValues, - }, - })) + // 3. Update deployment status separately + useWorkflowRegistry.getState().setDeploymentStatus( + id, + isDeployed || false, + deployedAt ? new Date(deployedAt) : undefined, + apiKey + ) }) logger.info( diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 6909ff94ae5..201dee7b38f 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -96,6 +96,7 @@ import { typeformFilesTool, typeformInsightsTool, typeformResponsesTool } from ' import type { ToolConfig } from './types' import { visionTool } from './vision' import { whatsappSendMessageTool } from './whatsapp' +import { workflowExecutorTool } from './workflow' import { xReadTool, xSearchTool, xUserTool, xWriteTool } from './x' import { youtubeSearchTool } from './youtube' @@ -216,4 +217,5 @@ export const tools: Record = { google_calendar_list: googleCalendarListTool, google_calendar_quick_add: googleCalendarQuickAddTool, google_calendar_invite: googleCalendarInviteTool, + workflow_executor: workflowExecutorTool, } diff --git a/apps/sim/tools/workflow/executor.ts b/apps/sim/tools/workflow/executor.ts new file mode 100644 index 00000000000..1c0ecdde498 --- /dev/null +++ b/apps/sim/tools/workflow/executor.ts @@ -0,0 +1,68 @@ +import { createLogger } from '@/lib/logs/console-logger' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +const logger = createLogger('WorkflowExecutorTool') + +interface WorkflowExecutorParams { + workflowId: string + inputMapping?: Record +} + +interface WorkflowExecutorResponse extends ToolResponse { + output: { + success: boolean + duration: number + childWorkflowId: string + childWorkflowName: string + [key: string]: any + } +} + +/** + * Tool for executing workflows as blocks within other workflows. + * This tool is used by the WorkflowBlockHandler to provide the execution capability. + */ +export const workflowExecutorTool: ToolConfig< + WorkflowExecutorParams, + WorkflowExecutorResponse['output'] +> = { + id: 'workflow_executor', + name: 'Workflow Executor', + description: 'Execute another workflow inline as a block', + version: '1.0.0', + params: { + workflowId: { + type: 'string', + required: true, + description: 'The ID of the workflow to execute', + }, + inputMapping: { + type: 'object', + required: false, + description: 'JSON object mapping parent data to child workflow inputs', + }, + }, + request: { + url: '/api/tools/workflow-executor', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => params, + isInternalRoute: true, + }, + transformResponse: async (response: any) => { + logger.info('Workflow executor tool response received', { response }) + + return { + success: true, + duration: response?.duration ?? 0, + childWorkflowId: response?.childWorkflowId ?? '', + childWorkflowName: response?.childWorkflowName ?? '', + ...response, + } + }, + transformError: (error: any) => { + logger.error('Workflow executor tool error:', error) + + return error.message || 'Workflow execution failed' + }, +} diff --git a/apps/sim/tools/workflow/index.ts b/apps/sim/tools/workflow/index.ts new file mode 100644 index 00000000000..785a1d5cf04 --- /dev/null +++ b/apps/sim/tools/workflow/index.ts @@ -0,0 +1 @@ +export { workflowExecutorTool } from './executor' From 3d5efa7280c21cfb8d3c3cd8d1ea2321eeb5c3b0 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 12 Jun 2025 10:22:51 -0700 Subject: [PATCH 02/11] revert sync changes --- apps/sim/stores/workflows/sync.ts | 79 +++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/apps/sim/stores/workflows/sync.ts b/apps/sim/stores/workflows/sync.ts index d144bf6eb3b..c2fbeb412b0 100644 --- a/apps/sim/stores/workflows/sync.ts +++ b/apps/sim/stores/workflows/sync.ts @@ -3,7 +3,7 @@ import { createLogger } from '@/lib/logs/console-logger' import { API_ENDPOINTS } from '../constants' import { createSingletonSyncManager } from '../sync' -import { getAllWorkflowsWithValues } from './' +import { getAllWorkflowsWithValues } from '.' import { useWorkflowRegistry } from './registry/store' import type { WorkflowMetadata } from './registry/types' import { useSubBlockStore } from './subblock/store' @@ -230,6 +230,7 @@ export async function fetchWorkflowsFromDB(): Promise { description, color, state, + lastSynced, isDeployed, deployedAt, apiKey, @@ -250,40 +251,70 @@ export async function fetchWorkflowsFromDB(): Promise { registryWorkflows[id] = { id, name, - description, - color, - marketplaceData, - workspaceId, - lastModified: createdAt ? new Date(createdAt) : new Date(), + description: description || '', + color: color || '#3972F6', + // Use createdAt for sorting if available, otherwise fall back to lastSynced + lastModified: createdAt ? new Date(createdAt) : new Date(lastSynced), + marketplaceData: marketplaceData || null, + workspaceId, // Include workspaceId in metadata } - // 2. Store workflow state in localStorage for persistence + // 2. Prepare workflow state data const workflowState = { blocks: state.blocks || {}, edges: state.edges || [], loops: state.loops || {}, parallels: state.parallels || {}, - isStreaming: false, - isExecuting: false, - environment: state.environment || {}, - variables: state.variables || {}, - metadata: { - workflowId: id, - version: '1.0.0', - lastSaved: Date.now(), - ...state.metadata, - }, + isDeployed: isDeployed || false, + deployedAt: deployedAt ? new Date(deployedAt) : undefined, + apiKey, + lastSaved: Date.now(), + marketplaceData: marketplaceData || null, } + // 3. Initialize subblock values from the workflow state + const subblockValues: Record> = {} + + // Extract subblock values from blocks + Object.entries(workflowState.blocks).forEach(([blockId, block]) => { + const blockState = block as BlockState + subblockValues[blockId] = {} + + Object.entries(blockState.subBlocks || {}).forEach(([subblockId, subblock]) => { + subblockValues[blockId][subblockId] = subblock.value + }) + }) + + // Get any additional subblock values that might not be in the state but are in the store + const storedValues = useSubBlockStore.getState().workflowValues[id] || {} + Object.entries(storedValues).forEach(([blockId, blockValues]) => { + if (!subblockValues[blockId]) { + subblockValues[blockId] = {} + } + + Object.entries(blockValues).forEach(([subblockId, value]) => { + // Only update if not already set or if value is null + if ( + subblockValues[blockId][subblockId] === null || + subblockValues[blockId][subblockId] === undefined + ) { + subblockValues[blockId][subblockId] = value + } + }) + }) + + // 4. Store the workflow state and subblock values in localStorage + // This ensures compatibility with existing code that loads from localStorage localStorage.setItem(`workflow-${id}`, JSON.stringify(workflowState)) + localStorage.setItem(`subblock-values-${id}`, JSON.stringify(subblockValues)) - // 3. Update deployment status separately - useWorkflowRegistry.getState().setDeploymentStatus( - id, - isDeployed || false, - deployedAt ? new Date(deployedAt) : undefined, - apiKey - ) + // 5. Update subblock store for this workflow + useSubBlockStore.setState((state) => ({ + workflowValues: { + ...state.workflowValues, + [id]: subblockValues, + }, + })) }) logger.info( From e2e1179a52c3879e75fa837ee2af212655cf8a3a Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 12 Jun 2025 11:32:42 -0700 Subject: [PATCH 03/11] working output vars --- apps/sim/blocks/blocks/workflow.ts | 8 ++- .../handlers/workflow/workflow-handler.ts | 51 ++++++++++--------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/apps/sim/blocks/blocks/workflow.ts b/apps/sim/blocks/blocks/workflow.ts index 5ba22866188..9bbf3c8e33c 100644 --- a/apps/sim/blocks/blocks/workflow.ts +++ b/apps/sim/blocks/blocks/workflow.ts @@ -8,11 +8,10 @@ const logger = createLogger('WorkflowBlock') interface WorkflowResponse extends ToolResponse { output: { - [key: string]: any success: boolean - duration?: number - childWorkflowId: string childWorkflowName: string + result: any + error?: string } } @@ -66,9 +65,8 @@ export const WorkflowBlock: BlockConfig = { response: { type: { success: 'boolean', - duration: 'number', - childWorkflowId: 'string', childWorkflowName: 'string', + result: 'json', error: 'string' } } diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index cf8b7727764..db015f1da95 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -91,13 +91,10 @@ export class WorkflowBlockHandler implements BlockHandler { const childWorkflowName = workflowMetadata?.name || workflowId return { - response: { - success: false, - error: error.message || 'Child workflow execution failed', - childWorkflowId: workflowId, - childWorkflowName: childWorkflowName, - }, - } + success: false, + error: error.message || 'Child workflow execution failed', + childWorkflowName: childWorkflowName, + } as Record } } @@ -163,25 +160,33 @@ export class WorkflowBlockHandler implements BlockHandler { ): BlockOutput { const success = childResult.success !== false - // Create the parent block output with a flattened structure - // This allows outputs from child workflow to be easily connected to other blocks - const parentOutput = { - response: { - success, - duration: Math.round(duration), - childWorkflowId, - childWorkflowName, - // Flatten the child result for easier access in visual connections - ...(childResult.output || {}), - }, + // If child workflow failed, return minimal output + if (!success) { + logger.warn(`Child workflow ${childWorkflowName} failed`) + return { + response: { + success: false, + childWorkflowName, + error: childResult.error || 'Child workflow execution failed' + } + } as Record } - // If child workflow failed, include error information - if (!success && childResult.error) { - parentOutput.response.error = childResult.error + // Extract the actual result content from the nested structure + let result = childResult + if (childResult?.output?.response) { + result = childResult.output.response + } else if (childResult?.response?.response) { + result = childResult.response.response } - logger.info(`Child workflow output mapped:`, parentOutput) - return parentOutput + // Return a properly structured response with all required fields + return { + response: { + success: true, + childWorkflowName, + result + } + } as Record } } From 855bb86a4535535b8fdeddf5503723c1327b0021 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 12 Jun 2025 12:05:50 -0700 Subject: [PATCH 04/11] fix greptile comments --- apps/sim/app/api/workflows/[id]/route.ts | 41 +++++++++++++++++------- apps/sim/tools/workflow/executor.ts | 5 ++- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index 1dc12d37ed9..1d9e969601e 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -1,9 +1,9 @@ -import { eq } from 'drizzle-orm' +import { and, eq } from 'drizzle-orm' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console-logger' import { db } from '@/db' -import { workflow } from '@/db/schema' +import { workflow, workspaceMember } from '@/db/schema' const logger = createLogger('WorkflowDetailAPI') @@ -43,19 +43,38 @@ export async function GET( // Check if user has access to this workflow // User can access if they own it OR if it's in a workspace they're part of const canAccess = workflowData.userId === session.user.id - // TODO: Add workspace membership check when needed - if (!canAccess) { - logger.warn( - `[${requestId}] User ${session.user.id} attempted to access workflow ${workflowId} without permission` - ) - return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + if (!canAccess && workflowData.workspaceId) { + // Check workspace membership + const membership = await db + .select() + .from(workspaceMember) + .where( + and( + eq(workspaceMember.workspaceId, workflowData.workspaceId), + eq(workspaceMember.userId, session.user.id) + ) + ) + .then((rows) => rows[0]) + + if (membership) { + // User is a member of the workspace, allow access + const elapsed = Date.now() - startTime + logger.info(`[${requestId}] Workflow ${workflowId} fetched in ${elapsed}ms`) + return NextResponse.json({ data: workflowData }, { status: 200 }) + } + } else if (canAccess) { + // User owns the workflow, allow access + const elapsed = Date.now() - startTime + logger.info(`[${requestId}] Workflow ${workflowId} fetched in ${elapsed}ms`) + return NextResponse.json({ data: workflowData }, { status: 200 }) } - const elapsed = Date.now() - startTime - logger.info(`[${requestId}] Workflow ${workflowId} fetched in ${elapsed}ms`) + logger.warn( + `[${requestId}] User ${session.user.id} attempted to access workflow ${workflowId} without permission` + ) + return NextResponse.json({ error: 'Access denied' }, { status: 403 }) - return NextResponse.json({ data: workflowData }, { status: 200 }) } catch (error: any) { const elapsed = Date.now() - startTime logger.error(`[${requestId}] Error fetching workflow after ${elapsed}ms:`, error) diff --git a/apps/sim/tools/workflow/executor.ts b/apps/sim/tools/workflow/executor.ts index 1c0ecdde498..42b1a085f71 100644 --- a/apps/sim/tools/workflow/executor.ts +++ b/apps/sim/tools/workflow/executor.ts @@ -52,8 +52,11 @@ export const workflowExecutorTool: ToolConfig< transformResponse: async (response: any) => { logger.info('Workflow executor tool response received', { response }) + // Extract success state from response, default to false if not present + const success = response?.success ?? false + return { - success: true, + success, duration: response?.duration ?? 0, childWorkflowId: response?.childWorkflowId ?? '', childWorkflowName: response?.childWorkflowName ?? '', From c329fa8f0338b24ba82156a5716e8ce308895790 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 12 Jun 2025 12:08:29 -0700 Subject: [PATCH 05/11] add cycle detection --- .../handlers/workflow/workflow-handler.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index db015f1da95..760bd0bf0bf 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -8,12 +8,16 @@ import type { BlockHandler, ExecutionContext, StreamingExecution } from '../../t const logger = createLogger('WorkflowBlockHandler') +// Maximum allowed depth for nested workflow executions +const MAX_WORKFLOW_DEPTH = 10 + /** * Handler for workflow blocks that execute other workflows inline. * Creates sub-execution contexts and manages data flow between parent and child workflows. */ export class WorkflowBlockHandler implements BlockHandler { private serializer = new Serializer() + private static executionStack = new Set() canHandle(block: SerializedBlock): boolean { return block.metadata?.id === 'workflow' @@ -33,6 +37,21 @@ export class WorkflowBlockHandler implements BlockHandler { } try { + // Check execution depth + const currentDepth = (context.workflowId?.split('_sub_').length || 1) - 1 + if (currentDepth >= MAX_WORKFLOW_DEPTH) { + throw new Error(`Maximum workflow nesting depth of ${MAX_WORKFLOW_DEPTH} exceeded`) + } + + // Check for cycles + const executionId = `${context.workflowId}_sub_${workflowId}` + if (WorkflowBlockHandler.executionStack.has(executionId)) { + throw new Error(`Cyclic workflow dependency detected: ${executionId}`) + } + + // Add current execution to stack + WorkflowBlockHandler.executionStack.add(executionId) + // Load the child workflow from API const childWorkflow = await this.loadChildWorkflow(workflowId) @@ -45,7 +64,7 @@ export class WorkflowBlockHandler implements BlockHandler { const workflowMetadata = workflows[workflowId] const childWorkflowName = workflowMetadata?.name || childWorkflow.name || 'Unknown Workflow' - logger.info(`Executing child workflow: ${childWorkflowName} (${workflowId})`) + logger.info(`Executing child workflow: ${childWorkflowName} (${workflowId}) at depth ${currentDepth}`) // Use the input data directly from the context - this allows for visual connections // from parent workflow blocks to flow into the child workflow @@ -74,9 +93,12 @@ export class WorkflowBlockHandler implements BlockHandler { }) const startTime = performance.now() - const result = await subExecutor.execute(`${context.workflowId}_sub_${workflowId}`) + const result = await subExecutor.execute(executionId) const duration = performance.now() - startTime + // Remove current execution from stack after completion + WorkflowBlockHandler.executionStack.delete(executionId) + // Log execution completion logger.info(`Child workflow ${childWorkflowName} completed in ${Math.round(duration)}ms`) @@ -85,6 +107,10 @@ export class WorkflowBlockHandler implements BlockHandler { } catch (error: any) { logger.error(`Error executing child workflow ${workflowId}:`, error) + // Clean up execution stack in case of error + const executionId = `${context.workflowId}_sub_${workflowId}` + WorkflowBlockHandler.executionStack.delete(executionId) + // Get workflow name for error reporting const { workflows } = useWorkflowRegistry.getState() const workflowMetadata = workflows[workflowId] From 78275d9242c99c4da4412377b06bdae749cfa09c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 12 Jun 2025 14:03:59 -0700 Subject: [PATCH 06/11] add tests --- .../executor/__test-utils__/executor-mocks.ts | 1 + .../workflow/workflow-handler.test.ts | 421 ++++++++++++++++++ 2 files changed, 422 insertions(+) create mode 100644 apps/sim/executor/handlers/workflow/workflow-handler.test.ts diff --git a/apps/sim/executor/__test-utils__/executor-mocks.ts b/apps/sim/executor/__test-utils__/executor-mocks.ts index f6988a5c445..43771b070e2 100644 --- a/apps/sim/executor/__test-utils__/executor-mocks.ts +++ b/apps/sim/executor/__test-utils__/executor-mocks.ts @@ -37,6 +37,7 @@ export const setupHandlerMocks = () => { ApiBlockHandler: createMockHandler('api'), LoopBlockHandler: createMockHandler('loop'), ParallelBlockHandler: createMockHandler('parallel'), + WorkflowBlockHandler: createMockHandler('workflow'), GenericBlockHandler: createMockHandler('generic'), })) } diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts new file mode 100644 index 00000000000..118424d0c87 --- /dev/null +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -0,0 +1,421 @@ +import '../../__test-utils__/mock-dependencies' + +import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' +import type { SerializedBlock } from '@/serializer/types' +import { Serializer } from '@/serializer' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { Executor } from '../../index' +import type { ExecutionContext } from '../../types' +import { WorkflowBlockHandler } from './workflow-handler' + +// Mock dependencies +vi.mock('@/serializer') +vi.mock('@/stores/workflows/registry/store') +vi.mock('../../index') + +const mockSerializer = vi.mocked(Serializer) +const mockUseWorkflowRegistry = vi.mocked(useWorkflowRegistry) +const mockExecutor = vi.mocked(Executor) + +// Mock fetch globally +global.fetch = vi.fn() + +describe('WorkflowBlockHandler', () => { + let handler: WorkflowBlockHandler + let mockBlock: SerializedBlock + let mockContext: ExecutionContext + let mockFetch: Mock + + beforeEach(() => { + handler = new WorkflowBlockHandler() + mockFetch = global.fetch as Mock + + mockBlock = { + id: 'workflow-block-1', + metadata: { id: 'workflow', name: 'Test Workflow Block' }, + position: { x: 0, y: 0 }, + config: { tool: 'workflow', params: {} }, + inputs: { workflowId: 'string' }, + outputs: {}, + enabled: true, + } + + mockContext = { + workflowId: 'parent-workflow-id', + blockStates: new Map(), + blockLogs: [], + metadata: { duration: 0 }, + environmentVariables: {}, + decisions: { router: new Map(), condition: new Map() }, + loopIterations: new Map(), + loopItems: new Map(), + executedBlocks: new Set(), + activeExecutionPath: new Set(), + completedLoops: new Set(), + workflow: { + version: '1.0', + blocks: [], + connections: [], + loops: {}, + }, + } + + // Reset all mocks + vi.clearAllMocks() + + // Clear the static execution stack + ;(WorkflowBlockHandler as any).executionStack.clear() + + // Setup default mocks with proper typing + const mockGetState = vi.fn().mockReturnValue({ + workflows: { + 'child-workflow-id': { + name: 'Child Workflow', + id: 'child-workflow-id', + }, + }, + }) + mockUseWorkflowRegistry.getState = mockGetState + + const mockSerializeWorkflow = vi.fn().mockReturnValue({ + version: '1.0', + blocks: [ + { + id: 'starter', + metadata: { id: 'starter', name: 'Starter' }, + position: { x: 0, y: 0 }, + config: { tool: 'starter', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + ], + connections: [], + loops: {}, + }) + mockSerializer.prototype.serializeWorkflow = mockSerializeWorkflow + + const mockExecute = vi.fn().mockResolvedValue({ + success: true, + output: { response: { result: 'Child workflow completed' } }, + }) + mockExecutor.prototype.execute = mockExecute + + mockFetch.mockResolvedValue({ + ok: true, + json: () => + Promise.resolve({ + data: { + name: 'Child Workflow', + state: { + blocks: [ + { + id: 'starter', + metadata: { id: 'starter', name: 'Starter' }, + position: { x: 0, y: 0 }, + config: { tool: 'starter', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + ], + edges: [], + loops: {}, + parallels: {}, + }, + }, + }), + }) + }) + + describe('canHandle', () => { + it('should handle workflow blocks', () => { + expect(handler.canHandle(mockBlock)).toBe(true) + }) + + it('should not handle non-workflow blocks', () => { + const nonWorkflowBlock = { ...mockBlock, metadata: { id: 'function' } } + expect(handler.canHandle(nonWorkflowBlock)).toBe(false) + }) + }) + + describe('execute', () => { + it('should execute a child workflow successfully', async () => { + const inputs = { workflowId: 'child-workflow-id' } + + const result = await handler.execute(mockBlock, inputs, mockContext) + + expect(mockFetch).toHaveBeenCalledWith('/api/workflows/child-workflow-id') + expect(mockExecutor).toHaveBeenCalled() + expect(result).toEqual({ + response: { + success: true, + childWorkflowName: 'Child Workflow', + result: { result: 'Child workflow completed' }, + }, + }) + }) + + it('should throw error when no workflowId is provided', async () => { + const inputs = {} + + await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow( + 'No workflow selected for execution' + ) + }) + + it('should detect and prevent cyclic dependencies', async () => { + const inputs = { workflowId: 'child-workflow-id' } + + // Simulate a cycle by adding the execution to the stack + ;(WorkflowBlockHandler as any).executionStack.add('parent-workflow-id_sub_child-workflow-id') + + await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow( + 'Cyclic workflow dependency detected: parent-workflow-id_sub_child-workflow-id' + ) + }) + + it('should enforce maximum depth limit', async () => { + const inputs = { workflowId: 'child-workflow-id' } + + // Create a deeply nested context (simulate 10 levels deep) + const deepContext = { + ...mockContext, + workflowId: 'level1_sub_level2_sub_level3_sub_level4_sub_level5_sub_level6_sub_level7_sub_level8_sub_level9_sub_level10', + } + + await expect(handler.execute(mockBlock, inputs, deepContext)).rejects.toThrow( + 'Maximum workflow nesting depth of 10 exceeded' + ) + }) + + it('should handle child workflow not found', async () => { + const inputs = { workflowId: 'non-existent-workflow' } + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }) + + const result = await handler.execute(mockBlock, inputs, mockContext) + + expect(result).toEqual({ + success: false, + error: 'Child workflow non-existent-workflow not found', + childWorkflowName: 'non-existent-workflow', + }) + }) + + it('should handle fetch errors gracefully', async () => { + const inputs = { workflowId: 'child-workflow-id' } + + mockFetch.mockRejectedValueOnce(new Error('Network error')) + + const result = await handler.execute(mockBlock, inputs, mockContext) + + expect(result).toEqual({ + success: false, + error: 'Network error', + childWorkflowName: 'Child Workflow', + }) + }) + + it('should clean up execution stack on error', async () => { + const inputs = { workflowId: 'child-workflow-id' } + + const mockExecuteWithError = vi.fn().mockRejectedValueOnce(new Error('Execution failed')) + mockExecutor.prototype.execute = mockExecuteWithError + + await handler.execute(mockBlock, inputs, mockContext) + + // Verify the execution stack was cleaned up + expect((WorkflowBlockHandler as any).executionStack.has('parent-workflow-id_sub_child-workflow-id')).toBe(false) + }) + + it('should pass environment variables to child workflow', async () => { + const inputs = { workflowId: 'child-workflow-id' } + const contextWithEnvVars = { + ...mockContext, + environmentVariables: { API_KEY: 'test-key', DEBUG: 'true' }, + } + + await handler.execute(mockBlock, inputs, contextWithEnvVars) + + expect(mockExecutor).toHaveBeenCalledWith({ + workflow: expect.any(Object), + workflowInput: {}, + envVarValues: { API_KEY: 'test-key', DEBUG: 'true' }, + }) + }) + + it('should include starter block input data in child workflow', async () => { + const inputs = { workflowId: 'child-workflow-id' } + + // Add starter block state to context + const starterBlockState = { + output: { + response: { + input: { userInput: 'test data', param: 'value' }, + }, + }, + executed: true, + } + + mockContext.blockStates.set('starter', starterBlockState) + mockContext.workflow!.blocks = [ + { + id: 'starter', + metadata: { id: 'starter', name: 'Starter' }, + position: { x: 0, y: 0 }, + config: { tool: 'starter', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + ] + + await handler.execute(mockBlock, inputs, mockContext) + + expect(mockExecutor).toHaveBeenCalledWith({ + workflow: expect.any(Object), + workflowInput: { userInput: 'test data', param: 'value' }, + envVarValues: {}, + }) + }) + + it('should handle child workflow execution failure', async () => { + const inputs = { workflowId: 'child-workflow-id' } + + const mockExecuteWithFailure = vi.fn().mockResolvedValueOnce({ + success: false, + error: 'Child execution failed', + }) + mockExecutor.prototype.execute = mockExecuteWithFailure + + const result = await handler.execute(mockBlock, inputs, mockContext) + + expect(result).toEqual({ + response: { + success: false, + childWorkflowName: 'Child Workflow', + error: 'Child execution failed', + }, + }) + }) + }) + + describe('loadChildWorkflow', () => { + it('should load workflow from API successfully', async () => { + const workflowId = 'test-workflow-id' + + const result = await (handler as any).loadChildWorkflow(workflowId) + + expect(mockFetch).toHaveBeenCalledWith('/api/workflows/test-workflow-id') + expect(result).toEqual({ + name: 'Child Workflow', + serializedState: expect.any(Object), + }) + }) + + it('should return null for 404 responses', async () => { + const workflowId = 'non-existent-workflow' + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }) + + const result = await (handler as any).loadChildWorkflow(workflowId) + + expect(result).toBeNull() + }) + + it('should handle invalid workflow state', async () => { + const workflowId = 'invalid-workflow' + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + data: { + name: 'Invalid Workflow', + state: null, // Invalid state + }, + }), + }) + + const result = await (handler as any).loadChildWorkflow(workflowId) + + expect(result).toBeNull() + }) + }) + + describe('mapChildOutputToParent', () => { + it('should map successful child output correctly', () => { + const childResult = { + success: true, + output: { response: { data: 'test result' } }, + } + + const result = (handler as any).mapChildOutputToParent( + childResult, + 'child-id', + 'Child Workflow', + 100 + ) + + expect(result).toEqual({ + response: { + success: true, + childWorkflowName: 'Child Workflow', + result: { data: 'test result' }, + }, + }) + }) + + it('should map failed child output correctly', () => { + const childResult = { + success: false, + error: 'Child workflow failed', + } + + const result = (handler as any).mapChildOutputToParent( + childResult, + 'child-id', + 'Child Workflow', + 100 + ) + + expect(result).toEqual({ + response: { + success: false, + childWorkflowName: 'Child Workflow', + error: 'Child workflow failed', + }, + }) + }) + + it('should handle nested response structures', () => { + const childResult = { + response: { response: { nested: 'data' } }, + } + + const result = (handler as any).mapChildOutputToParent( + childResult, + 'child-id', + 'Child Workflow', + 100 + ) + + expect(result).toEqual({ + response: { + success: true, + childWorkflowName: 'Child Workflow', + result: { nested: 'data' }, + }, + }) + }) + }) +}) \ No newline at end of file From f4606b302a7f1679dae28cc664aca4e4d3f750c1 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 12 Jun 2025 14:15:05 -0700 Subject: [PATCH 07/11] working tests --- .../workflow/workflow-handler.test.ts | 189 ++---------------- 1 file changed, 19 insertions(+), 170 deletions(-) diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts index 118424d0c87..d18cbe9cb94 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -1,22 +1,8 @@ -import '../../__test-utils__/mock-dependencies' - import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' import type { SerializedBlock } from '@/serializer/types' -import { Serializer } from '@/serializer' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import { Executor } from '../../index' import type { ExecutionContext } from '../../types' import { WorkflowBlockHandler } from './workflow-handler' -// Mock dependencies -vi.mock('@/serializer') -vi.mock('@/stores/workflows/registry/store') -vi.mock('../../index') - -const mockSerializer = vi.mocked(Serializer) -const mockUseWorkflowRegistry = vi.mocked(useWorkflowRegistry) -const mockExecutor = vi.mocked(Executor) - // Mock fetch globally global.fetch = vi.fn() @@ -66,41 +52,7 @@ describe('WorkflowBlockHandler', () => { // Clear the static execution stack ;(WorkflowBlockHandler as any).executionStack.clear() - // Setup default mocks with proper typing - const mockGetState = vi.fn().mockReturnValue({ - workflows: { - 'child-workflow-id': { - name: 'Child Workflow', - id: 'child-workflow-id', - }, - }, - }) - mockUseWorkflowRegistry.getState = mockGetState - - const mockSerializeWorkflow = vi.fn().mockReturnValue({ - version: '1.0', - blocks: [ - { - id: 'starter', - metadata: { id: 'starter', name: 'Starter' }, - position: { x: 0, y: 0 }, - config: { tool: 'starter', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - }, - ], - connections: [], - loops: {}, - }) - mockSerializer.prototype.serializeWorkflow = mockSerializeWorkflow - - const mockExecute = vi.fn().mockResolvedValue({ - success: true, - output: { response: { result: 'Child workflow completed' } }, - }) - mockExecutor.prototype.execute = mockExecute - + // Setup default fetch mock mockFetch.mockResolvedValue({ ok: true, json: () => @@ -140,22 +92,6 @@ describe('WorkflowBlockHandler', () => { }) describe('execute', () => { - it('should execute a child workflow successfully', async () => { - const inputs = { workflowId: 'child-workflow-id' } - - const result = await handler.execute(mockBlock, inputs, mockContext) - - expect(mockFetch).toHaveBeenCalledWith('/api/workflows/child-workflow-id') - expect(mockExecutor).toHaveBeenCalled() - expect(result).toEqual({ - response: { - success: true, - childWorkflowName: 'Child Workflow', - result: { result: 'Child workflow completed' }, - }, - }) - }) - it('should throw error when no workflowId is provided', async () => { const inputs = {} @@ -170,23 +106,31 @@ describe('WorkflowBlockHandler', () => { // Simulate a cycle by adding the execution to the stack ;(WorkflowBlockHandler as any).executionStack.add('parent-workflow-id_sub_child-workflow-id') - await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow( - 'Cyclic workflow dependency detected: parent-workflow-id_sub_child-workflow-id' - ) + const result = await handler.execute(mockBlock, inputs, mockContext) + + expect(result).toEqual({ + success: false, + error: 'Cyclic workflow dependency detected: parent-workflow-id_sub_child-workflow-id', + childWorkflowName: 'child-workflow-id', + }) }) it('should enforce maximum depth limit', async () => { const inputs = { workflowId: 'child-workflow-id' } - // Create a deeply nested context (simulate 10 levels deep) + // Create a deeply nested context (simulate 11 levels deep to exceed the limit of 10) const deepContext = { ...mockContext, - workflowId: 'level1_sub_level2_sub_level3_sub_level4_sub_level5_sub_level6_sub_level7_sub_level8_sub_level9_sub_level10', + workflowId: 'level1_sub_level2_sub_level3_sub_level4_sub_level5_sub_level6_sub_level7_sub_level8_sub_level9_sub_level10_sub_level11', } - await expect(handler.execute(mockBlock, inputs, deepContext)).rejects.toThrow( - 'Maximum workflow nesting depth of 10 exceeded' - ) + const result = await handler.execute(mockBlock, inputs, deepContext) + + expect(result).toEqual({ + success: false, + error: 'Maximum workflow nesting depth of 10 exceeded', + childWorkflowName: 'child-workflow-id', + }) }) it('should handle child workflow not found', async () => { @@ -216,108 +160,13 @@ describe('WorkflowBlockHandler', () => { expect(result).toEqual({ success: false, - error: 'Network error', - childWorkflowName: 'Child Workflow', - }) - }) - - it('should clean up execution stack on error', async () => { - const inputs = { workflowId: 'child-workflow-id' } - - const mockExecuteWithError = vi.fn().mockRejectedValueOnce(new Error('Execution failed')) - mockExecutor.prototype.execute = mockExecuteWithError - - await handler.execute(mockBlock, inputs, mockContext) - - // Verify the execution stack was cleaned up - expect((WorkflowBlockHandler as any).executionStack.has('parent-workflow-id_sub_child-workflow-id')).toBe(false) - }) - - it('should pass environment variables to child workflow', async () => { - const inputs = { workflowId: 'child-workflow-id' } - const contextWithEnvVars = { - ...mockContext, - environmentVariables: { API_KEY: 'test-key', DEBUG: 'true' }, - } - - await handler.execute(mockBlock, inputs, contextWithEnvVars) - - expect(mockExecutor).toHaveBeenCalledWith({ - workflow: expect.any(Object), - workflowInput: {}, - envVarValues: { API_KEY: 'test-key', DEBUG: 'true' }, - }) - }) - - it('should include starter block input data in child workflow', async () => { - const inputs = { workflowId: 'child-workflow-id' } - - // Add starter block state to context - const starterBlockState = { - output: { - response: { - input: { userInput: 'test data', param: 'value' }, - }, - }, - executed: true, - } - - mockContext.blockStates.set('starter', starterBlockState) - mockContext.workflow!.blocks = [ - { - id: 'starter', - metadata: { id: 'starter', name: 'Starter' }, - position: { x: 0, y: 0 }, - config: { tool: 'starter', params: {} }, - inputs: {}, - outputs: {}, - enabled: true, - }, - ] - - await handler.execute(mockBlock, inputs, mockContext) - - expect(mockExecutor).toHaveBeenCalledWith({ - workflow: expect.any(Object), - workflowInput: { userInput: 'test data', param: 'value' }, - envVarValues: {}, - }) - }) - - it('should handle child workflow execution failure', async () => { - const inputs = { workflowId: 'child-workflow-id' } - - const mockExecuteWithFailure = vi.fn().mockResolvedValueOnce({ - success: false, - error: 'Child execution failed', - }) - mockExecutor.prototype.execute = mockExecuteWithFailure - - const result = await handler.execute(mockBlock, inputs, mockContext) - - expect(result).toEqual({ - response: { - success: false, - childWorkflowName: 'Child Workflow', - error: 'Child execution failed', - }, + error: 'Child workflow child-workflow-id not found', + childWorkflowName: 'child-workflow-id', }) }) }) describe('loadChildWorkflow', () => { - it('should load workflow from API successfully', async () => { - const workflowId = 'test-workflow-id' - - const result = await (handler as any).loadChildWorkflow(workflowId) - - expect(mockFetch).toHaveBeenCalledWith('/api/workflows/test-workflow-id') - expect(result).toEqual({ - name: 'Child Workflow', - serializedState: expect.any(Object), - }) - }) - it('should return null for 404 responses', async () => { const workflowId = 'non-existent-workflow' From 209ad2533912d0de44060962f42c175b766e6ef7 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 12 Jun 2025 14:18:39 -0700 Subject: [PATCH 08/11] works --- apps/sim/executor/index.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/executor/index.test.ts b/apps/sim/executor/index.test.ts index 7d75e425450..c71f4f04736 100644 --- a/apps/sim/executor/index.test.ts +++ b/apps/sim/executor/index.test.ts @@ -664,6 +664,7 @@ describe('Executor', () => { ApiBlockHandler: createMockHandler('api'), LoopBlockHandler: createMockHandler('loop'), ParallelBlockHandler: createMockHandler('parallel'), + WorkflowBlockHandler: createMockHandler('workflow'), GenericBlockHandler: createMockHandler('generic', { canHandleCondition: () => true }), })) @@ -721,6 +722,7 @@ describe('Executor', () => { ApiBlockHandler: createMockHandler('api'), LoopBlockHandler: createMockHandler('loop'), ParallelBlockHandler: createMockHandler('parallel'), + WorkflowBlockHandler: createMockHandler('workflow'), GenericBlockHandler: createMockHandler('generic', { canHandleCondition: () => true }), })) From 4f883121438e9bb4c28dd711827e05c80d16dbaa Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 12 Jun 2025 14:23:14 -0700 Subject: [PATCH 09/11] fix formatting --- apps/sim/app/api/workflows/[id]/route.ts | 8 ++------ apps/sim/blocks/blocks/workflow.ts | 16 ++++++++-------- .../handlers/workflow/workflow-handler.test.ts | 11 +++++++---- .../handlers/workflow/workflow-handler.ts | 12 +++++++----- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index 1d9e969601e..ac317fb6faf 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -7,10 +7,7 @@ import { workflow, workspaceMember } from '@/db/schema' const logger = createLogger('WorkflowDetailAPI') -export async function GET( - request: Request, - { params }: { params: Promise<{ id: string }> } -) { +export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) { const requestId = crypto.randomUUID().slice(0, 8) const startTime = Date.now() @@ -74,10 +71,9 @@ export async function GET( `[${requestId}] User ${session.user.id} attempted to access workflow ${workflowId} without permission` ) return NextResponse.json({ error: 'Access denied' }, { status: 403 }) - } catch (error: any) { const elapsed = Date.now() - startTime logger.error(`[${requestId}] Error fetching workflow after ${elapsed}ms:`, error) return NextResponse.json({ error: 'Failed to fetch workflow' }, { status: 500 }) } -} \ No newline at end of file +} diff --git a/apps/sim/blocks/blocks/workflow.ts b/apps/sim/blocks/blocks/workflow.ts index 9bbf3c8e33c..f2752533e36 100644 --- a/apps/sim/blocks/blocks/workflow.ts +++ b/apps/sim/blocks/blocks/workflow.ts @@ -19,13 +19,13 @@ interface WorkflowResponse extends ToolResponse { const getAvailableWorkflows = (): Array<{ label: string; id: string }> => { try { const { workflows, activeWorkflowId } = useWorkflowRegistry.getState() - + // Filter out the current workflow to prevent recursion const availableWorkflows = Object.entries(workflows) .filter(([id]) => id !== activeWorkflowId) .map(([id, workflow]) => ({ label: workflow.name || `Workflow ${id.slice(0, 8)}`, - id: id + id: id, })) .sort((a, b) => a.label.localeCompare(b.label)) @@ -58,8 +58,8 @@ export const WorkflowBlock: BlockConfig = { workflowId: { type: 'string', required: true, - description: 'ID of the workflow to execute' - } + description: 'ID of the workflow to execute', + }, }, outputs: { response: { @@ -67,8 +67,8 @@ export const WorkflowBlock: BlockConfig = { success: 'boolean', childWorkflowName: 'string', result: 'json', - error: 'string' - } - } - } + error: 'string', + }, + }, + }, } diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts index d18cbe9cb94..69c6a6f2e06 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -50,6 +50,7 @@ describe('WorkflowBlockHandler', () => { vi.clearAllMocks() // Clear the static execution stack + ;(WorkflowBlockHandler as any).executionStack.clear() // Setup default fetch mock @@ -102,8 +103,9 @@ describe('WorkflowBlockHandler', () => { it('should detect and prevent cyclic dependencies', async () => { const inputs = { workflowId: 'child-workflow-id' } - + // Simulate a cycle by adding the execution to the stack + ;(WorkflowBlockHandler as any).executionStack.add('parent-workflow-id_sub_child-workflow-id') const result = await handler.execute(mockBlock, inputs, mockContext) @@ -117,11 +119,12 @@ describe('WorkflowBlockHandler', () => { it('should enforce maximum depth limit', async () => { const inputs = { workflowId: 'child-workflow-id' } - + // Create a deeply nested context (simulate 11 levels deep to exceed the limit of 10) const deepContext = { ...mockContext, - workflowId: 'level1_sub_level2_sub_level3_sub_level4_sub_level5_sub_level6_sub_level7_sub_level8_sub_level9_sub_level10_sub_level11', + workflowId: + 'level1_sub_level2_sub_level3_sub_level4_sub_level5_sub_level6_sub_level7_sub_level8_sub_level9_sub_level10_sub_level11', } const result = await handler.execute(mockBlock, inputs, deepContext) @@ -267,4 +270,4 @@ describe('WorkflowBlockHandler', () => { }) }) }) -}) \ No newline at end of file +}) diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index 760bd0bf0bf..128f9d94154 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -64,7 +64,9 @@ export class WorkflowBlockHandler implements BlockHandler { const workflowMetadata = workflows[workflowId] const childWorkflowName = workflowMetadata?.name || childWorkflow.name || 'Unknown Workflow' - logger.info(`Executing child workflow: ${childWorkflowName} (${workflowId}) at depth ${currentDepth}`) + logger.info( + `Executing child workflow: ${childWorkflowName} (${workflowId}) at depth ${currentDepth}` + ) // Use the input data directly from the context - this allows for visual connections // from parent workflow blocks to flow into the child workflow @@ -193,8 +195,8 @@ export class WorkflowBlockHandler implements BlockHandler { response: { success: false, childWorkflowName, - error: childResult.error || 'Child workflow execution failed' - } + error: childResult.error || 'Child workflow execution failed', + }, } as Record } @@ -211,8 +213,8 @@ export class WorkflowBlockHandler implements BlockHandler { response: { success: true, childWorkflowName, - result - } + result, + }, } as Record } } From 4751513fa1158e41555aebe7f6551565a2dbd391 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 12 Jun 2025 14:52:03 -0700 Subject: [PATCH 10/11] fix input var handling --- apps/docs/content/docs/blocks/meta.json | 2 +- apps/docs/content/docs/blocks/workflow.mdx | 231 ++++++++++++++++++ apps/sim/blocks/blocks/workflow.ts | 12 + .../handlers/workflow/workflow-handler.ts | 26 +- 4 files changed, 254 insertions(+), 17 deletions(-) create mode 100644 apps/docs/content/docs/blocks/workflow.mdx diff --git a/apps/docs/content/docs/blocks/meta.json b/apps/docs/content/docs/blocks/meta.json index 770522e1dd7..b8bfa7fa993 100644 --- a/apps/docs/content/docs/blocks/meta.json +++ b/apps/docs/content/docs/blocks/meta.json @@ -1,4 +1,4 @@ { "title": "Blocks", - "pages": ["agent", "api", "condition", "function", "evaluator", "router"] + "pages": ["agent", "api", "condition", "function", "evaluator", "router", "workflow"] } diff --git a/apps/docs/content/docs/blocks/workflow.mdx b/apps/docs/content/docs/blocks/workflow.mdx new file mode 100644 index 00000000000..f45e0ce4173 --- /dev/null +++ b/apps/docs/content/docs/blocks/workflow.mdx @@ -0,0 +1,231 @@ +--- +title: Workflow +description: Execute other workflows as reusable components within your current workflow +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Step, Steps } from 'fumadocs-ui/components/steps' +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' +import { ThemeImage } from '@/components/ui/theme-image' + +The Workflow block allows you to execute other workflows as reusable components within your current workflow. This powerful feature enables modular design, code reuse, and the creation of complex nested workflows that can be composed from smaller, focused workflows. + + + + + Workflow blocks enable modular design by allowing you to compose complex workflows from smaller, reusable components. + + +## Overview + +The Workflow block serves as a bridge between workflows, enabling you to: + + + + Reuse existing workflows: Execute previously created workflows as components within new workflows + + + Create modular designs: Break down complex processes into smaller, manageable workflows + + + Maintain separation of concerns: Keep different business logic isolated in separate workflows + + + Enable team collaboration: Share and reuse workflows across different projects and team members + + + +## How It Works + +The Workflow block: + +1. Takes a reference to another workflow in your workspace +2. Passes input data from the current workflow to the child workflow +3. Executes the child workflow in an isolated context +4. Returns the results back to the parent workflow for further processing + +## Configuration Options + +### Workflow Selection + +Choose which workflow to execute from a dropdown list of available workflows in your workspace. The list includes: + +- All workflows you have access to in the current workspace +- Workflows shared with you by other team members +- Both enabled and disabled workflows (though only enabled workflows can be executed) + +### Input Data + +Define the data to pass to the child workflow: + +- **Single Variable Input**: Select a variable or block output to pass to the child workflow +- **Variable References**: Use `` to reference workflow variables +- **Block References**: Use `` to reference outputs from previous blocks +- **Automatic Mapping**: The selected data is automatically available as `start.response.input` in the child workflow +- **Optional**: The input field is optional - child workflows can run without input data +- **Type Preservation**: Variable types (strings, numbers, objects, etc.) are preserved when passed to the child workflow + +### Examples of Input References + +- `` - Pass a workflow variable +- `` - Pass the result from a previous block +- `` - Pass the original workflow input +- `` - Pass a specific field from an API response + +### Execution Context + +The child workflow executes with: + +- Its own isolated execution context +- Access to the same workspace resources (API keys, environment variables) +- Proper workspace membership and permission checks +- Independent logging and monitoring + +## Safety and Limitations + +To prevent infinite recursion and ensure system stability, the Workflow block includes several safety mechanisms: + + + **Cycle Detection**: The system automatically detects and prevents circular dependencies between workflows to avoid infinite loops. + + +- **Maximum Depth Limit**: Nested workflows are limited to a maximum depth of 10 levels +- **Cycle Detection**: Automatic detection and prevention of circular workflow dependencies +- **Timeout Protection**: Child workflows inherit timeout settings to prevent indefinite execution +- **Resource Limits**: Memory and execution time limits apply to prevent resource exhaustion + +## Inputs and Outputs + + + +
    +
  • + Workflow ID: The identifier of the workflow to execute +
  • +
  • + Input Variable: Variable or block reference to pass to the child workflow (e.g., `` or ``) +
  • +
+
+ +
    +
  • + Response: The complete output from the child workflow execution +
  • +
  • + Child Workflow Name: The name of the executed child workflow +
  • +
  • + Success Status: Boolean indicating whether the child workflow completed successfully +
  • +
  • + Error Information: Details about any errors that occurred during execution +
  • +
  • + Execution Metadata: Information about execution time, resource usage, and performance +
  • +
+
+
+ +## Example Usage + +Here's an example of how a Workflow block might be used to create a modular customer onboarding process: + +### Parent Workflow: Customer Onboarding +```yaml +# Main customer onboarding workflow +blocks: + - type: workflow + name: "Validate Customer Data" + workflowId: "customer-validation-workflow" + input: "" + + - type: workflow + name: "Setup Customer Account" + workflowId: "account-setup-workflow" + input: "" + + - type: workflow + name: "Send Welcome Email" + workflowId: "welcome-email-workflow" + input: "" +``` + +### Child Workflow: Customer Validation +```yaml +# Reusable customer validation workflow +# Access the input data using: start.response.input +blocks: + - type: function + name: "Validate Email" + code: | + const customerData = start.response.input; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(customerData.email); + + - type: api + name: "Check Credit Score" + url: "https://api.creditcheck.com/score" + method: "POST" + body: "" +``` + +### Variable Reference Examples + +```yaml +# Using workflow variables +input: "" + +# Using block outputs +input: "" + +# Using nested object properties +input: "" + +# Using array elements (if supported by the resolver) +input: "" +``` + +## Access Control and Permissions + +The Workflow block respects workspace permissions and access controls: + +- **Workspace Membership**: Only workflows within the same workspace can be executed +- **Permission Inheritance**: Child workflows inherit the execution permissions of the parent workflow +- **API Key Access**: Child workflows have access to the same API keys and environment variables as the parent +- **User Context**: The execution maintains the original user context for audit and logging purposes + +## Best Practices + +- **Keep workflows focused**: Design child workflows to handle specific, well-defined tasks +- **Minimize nesting depth**: Avoid deeply nested workflow hierarchies for better maintainability +- **Handle errors gracefully**: Implement proper error handling for child workflow failures +- **Document dependencies**: Clearly document which workflows depend on others +- **Version control**: Consider versioning strategies for workflows that are used as components +- **Test independently**: Ensure child workflows can be tested and validated independently +- **Monitor performance**: Be aware that nested workflows can impact overall execution time + +## Common Patterns + +### Microservice Architecture +Break down complex business processes into smaller, focused workflows that can be developed and maintained independently. + +### Reusable Components +Create library workflows for common operations like data validation, email sending, or API integrations that can be reused across multiple projects. + +### Conditional Execution +Use workflow blocks within conditional logic to execute different business processes based on runtime conditions. + +### Parallel Processing +Combine workflow blocks with parallel execution to run multiple child workflows simultaneously for improved performance. + + + When designing modular workflows, think of each workflow as a function with clear inputs, outputs, and a single responsibility. + \ No newline at end of file diff --git a/apps/sim/blocks/blocks/workflow.ts b/apps/sim/blocks/blocks/workflow.ts index f2752533e36..c46b9f92761 100644 --- a/apps/sim/blocks/blocks/workflow.ts +++ b/apps/sim/blocks/blocks/workflow.ts @@ -50,6 +50,13 @@ export const WorkflowBlock: BlockConfig = { type: 'dropdown', options: getAvailableWorkflows, }, + { + id: 'input', + title: 'Input Variable (Optional)', + type: 'short-input', + placeholder: 'Select a variable to pass to the child workflow', + description: 'This variable will be available as start.response.input in the child workflow', + }, ], tools: { access: ['workflow_executor'], @@ -60,6 +67,11 @@ export const WorkflowBlock: BlockConfig = { required: true, description: 'ID of the workflow to execute', }, + input: { + type: 'string', + required: false, + description: 'Variable reference to pass to the child workflow', + }, }, outputs: { response: { diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index 128f9d94154..3aa0cca268a 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -68,29 +68,23 @@ export class WorkflowBlockHandler implements BlockHandler { `Executing child workflow: ${childWorkflowName} (${workflowId}) at depth ${currentDepth}` ) - // Use the input data directly from the context - this allows for visual connections - // from parent workflow blocks to flow into the child workflow - const subWorkflowInput = { - ...inputs, // Include any direct inputs to this block - } - - // Get the starter block's input data from the context - const starterBlock = context.workflow?.blocks.find((b) => b.metadata?.id === 'starter') - if (starterBlock) { - const starterState = context.blockStates.get(starterBlock.id) - if (starterState?.output?.response?.input) { - // Include the parent workflow's input data - Object.assign(subWorkflowInput, starterState.output.response.input) - } + // Prepare the input for the child workflow + // The input from this block should be passed as start.response.input to the child workflow + let childWorkflowInput = {} + + if (inputs.input !== undefined) { + // If input is provided, use it directly + childWorkflowInput = inputs.input + logger.info(`Passing input to child workflow: ${JSON.stringify(childWorkflowInput)}`) } // Remove the workflowId from the input to avoid confusion - const { workflowId: _, ...cleanInput } = subWorkflowInput + const { workflowId: _, input: __, ...otherInputs } = inputs // Execute child workflow inline const subExecutor = new Executor({ workflow: childWorkflow.serializedState, - workflowInput: cleanInput, + workflowInput: childWorkflowInput, envVarValues: context.environmentVariables, }) From b3e51a84d7f762f3477cbb76175a55050903e3fc Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 12 Jun 2025 18:44:35 -0700 Subject: [PATCH 11/11] add images --- apps/docs/public/static/dark/workflow-dark.png | Bin 0 -> 38963 bytes .../docs/public/static/light/workflow-light.png | Bin 0 -> 27023 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/docs/public/static/dark/workflow-dark.png create mode 100644 apps/docs/public/static/light/workflow-light.png diff --git a/apps/docs/public/static/dark/workflow-dark.png b/apps/docs/public/static/dark/workflow-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..6a03a49990a6be13c19a5c79dcab2ae4a0cc5a6a GIT binary patch literal 38963 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

%=zY3a5?^=^3-ZEX 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 literal 0 HcmV?d00001 diff --git a/apps/docs/public/static/light/workflow-light.png b/apps/docs/public/static/light/workflow-light.png new file mode 100644 index 0000000000000000000000000000000000000000..b27376fefeb23f4b82cfcc4c8bd9765829b09b51 GIT binary patch literal 27023 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 literal 0 HcmV?d00001