import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { LoggingSession } from '@/lib/logs/execution/logging-session' import { captureServerEvent } from '@/lib/posthog/server' import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core' import { handlePostExecutionPauseState } from '@/lib/workflows/executor/pause-persistence' import { ExecutionSnapshot } from '@/executor/execution/snapshot' import type { ExecutionMetadata, SerializableExecutionState } from '@/executor/execution/types' import type { ExecutionResult, StreamingExecution } from '@/executor/types' const logger = createLogger('WorkflowExecution') export interface ExecuteWorkflowOptions { enabled: boolean selectedOutputs?: string[] isSecureMode?: boolean workflowTriggerType?: 'api' | 'chat' | 'copilot' | 'table' /** * If set, the executor enters the workflow at this block instead of resolving a Start block. * Use for trigger-originated runs (webhooks, table triggers, schedules) where the entry point * is the trigger block itself. */ triggerBlockId?: string onStream?: (streamingExec: StreamingExecution) => Promise /** Fires before each block runs; lets callers track per-block lifecycle (e.g. table-cell live state). */ onBlockStart?: ( blockId: string, blockName: string, blockType: string, executionOrder: number ) => Promise onBlockComplete?: (blockId: string, output: unknown) => Promise skipLoggingComplete?: boolean includeFileBase64?: boolean base64MaxBytes?: number largeValueKeys?: string[] fileKeys?: string[] abortSignal?: AbortSignal /** Use the live/draft workflow state instead of the deployed state. Used by copilot. */ useDraftState?: boolean /** Stop execution after this block completes. Used for "run until block" feature. */ stopAfterBlockId?: string /** Run-from-block configuration using a prior execution snapshot. */ runFromBlock?: { startBlockId: string sourceSnapshot: SerializableExecutionState sourceExecutionId?: string } executionMode?: 'sync' | 'stream' | 'async' } export interface WorkflowInfo { id: string userId: string workspaceId?: string | null isDeployed?: boolean variables?: Record } export async function executeWorkflow( workflow: WorkflowInfo, requestId: string, input: unknown | undefined, actorUserId: string, streamConfig?: ExecuteWorkflowOptions, providedExecutionId?: string ): Promise { if (!workflow.workspaceId) { throw new Error(`Workflow ${workflow.id} has no workspaceId`) } const workflowId = workflow.id const workspaceId = workflow.workspaceId const executionId = providedExecutionId || generateId() const triggerType = streamConfig?.workflowTriggerType || 'api' const loggingSession = new LoggingSession(workflowId, executionId, triggerType, requestId) try { const metadata: ExecutionMetadata = { requestId, executionId, workflowId, workspaceId, userId: actorUserId, workflowUserId: workflow.userId, triggerType, triggerBlockId: streamConfig?.triggerBlockId, useDraftState: streamConfig?.useDraftState ?? false, startTime: new Date().toISOString(), isClientSession: false, largeValueExecutionIds: Array.from(new Set([executionId])), largeValueKeys: streamConfig?.largeValueKeys, fileKeys: streamConfig?.fileKeys, executionMode: streamConfig?.executionMode, } const snapshot = new ExecutionSnapshot( metadata, workflow, input, workflow.variables || {}, streamConfig?.selectedOutputs || [] ) const executionStartMs = Date.now() const result = await executeWorkflowCore({ snapshot, callbacks: { onStream: streamConfig?.onStream, onBlockStart: streamConfig?.onBlockStart ? async ( blockId: string, blockName: string, blockType: string, executionOrder: number ) => { await streamConfig.onBlockStart!(blockId, blockName, blockType, executionOrder) } : undefined, onBlockComplete: streamConfig?.onBlockComplete ? async (blockId: string, _blockName: string, _blockType: string, output: unknown) => { await streamConfig.onBlockComplete!(blockId, output) } : undefined, }, loggingSession, includeFileBase64: streamConfig?.includeFileBase64, base64MaxBytes: streamConfig?.base64MaxBytes, abortSignal: streamConfig?.abortSignal, stopAfterBlockId: streamConfig?.stopAfterBlockId, runFromBlock: streamConfig?.runFromBlock, }) const blockTypes = [ ...new Set( (result.logs ?? []) .map((log) => log.blockType) .filter((t): t is string => typeof t === 'string') ), ] if (result.status !== 'paused') { captureServerEvent( actorUserId, 'workflow_executed', { workflow_id: workflowId, workspace_id: workspaceId, trigger_type: triggerType, success: result.success, block_count: result.logs?.length ?? 0, block_types: blockTypes.join(','), duration_ms: Date.now() - executionStartMs, }, { groups: { workspace: workspaceId }, setOnce: { first_execution_at: new Date().toISOString() }, } ) } await handlePostExecutionPauseState({ result, workflowId, executionId, loggingSession }) if (streamConfig?.skipLoggingComplete) { return { ...result, _streamingMetadata: { loggingSession, processedInput: input, }, } } return result } catch (error: unknown) { logger.error(`[${requestId}] Workflow execution failed:`, error) captureServerEvent( actorUserId, 'workflow_execution_failed', { workflow_id: workflow.id, workspace_id: workspaceId, trigger_type: streamConfig?.workflowTriggerType || 'api', error_message: toError(error).message, }, { groups: { workspace: workspaceId } } ) throw error } }