Files
simstudioai--sim/apps/sim/executor/execution/executor.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

629 lines
24 KiB
TypeScript

import { createLogger, type Logger } from '@sim/logger'
import { normalizeStringArray } from '@/lib/core/utils/arrays'
import { normalizeStringRecord, normalizeWorkflowVariables } from '@/lib/core/utils/records'
import { collectUserFileKeys } from '@/lib/core/utils/user-file'
import { mergeFileKeys, mergeLargeValueKeys } from '@/lib/execution/payloads/access-keys'
import { collectLargeValueKeys } from '@/lib/execution/payloads/large-execution-value'
import { StartBlockPath } from '@/lib/workflows/triggers/triggers'
import type { DAG } from '@/executor/dag/builder'
import { DAGBuilder } from '@/executor/dag/builder'
import { BlockExecutor } from '@/executor/execution/block-executor'
import { EdgeManager } from '@/executor/execution/edge-manager'
import { ExecutionEngine } from '@/executor/execution/engine'
import { ExecutionState } from '@/executor/execution/state'
import type {
ContextExtensions,
SerializableExecutionState,
WorkflowInput,
} from '@/executor/execution/types'
import { createBlockHandlers } from '@/executor/handlers/registry'
import { LoopOrchestrator } from '@/executor/orchestrators/loop'
import { NodeExecutionOrchestrator } from '@/executor/orchestrators/node'
import { ParallelOrchestrator } from '@/executor/orchestrators/parallel'
import type { BlockState, ExecutionContext, ExecutionResult } from '@/executor/types'
import { type ClonedSubflowInfo, ParallelExpander } from '@/executor/utils/parallel-expansion'
import {
computeExecutionSets,
type RunFromBlockContext,
resolveContainerToSentinelStart,
validateRunFromBlock,
} from '@/executor/utils/run-from-block'
import {
buildResolutionFromBlock,
buildStartBlockOutput,
resolveExecutorStartBlock,
} from '@/executor/utils/start-block'
import {
extractLoopIdFromSentinel,
extractParallelIdFromSentinel,
stripCloneSuffixes,
stripOuterBranchSuffix,
} from '@/executor/utils/subflow-utils'
import { VariableResolver } from '@/executor/variables/resolver'
import { navigatePathAsync } from '@/executor/variables/resolvers/reference-async.server'
import type { SerializedWorkflow } from '@/serializer/types'
import type { SubflowType } from '@/stores/workflows/workflow/types'
const logger = createLogger('DAGExecutor')
interface RestoredClonedSubflowInfo extends ClonedSubflowInfo {
parentParallelId: string
}
export interface DAGExecutorOptions {
workflow: SerializedWorkflow
envVarValues?: Record<string, string>
workflowInput?: WorkflowInput
workflowVariables?: Record<string, unknown>
contextExtensions?: ContextExtensions
}
export class DAGExecutor {
private workflow: SerializedWorkflow
private environmentVariables: Record<string, string>
private workflowInput: WorkflowInput
private workflowVariables: Record<string, unknown>
private contextExtensions: ContextExtensions
private dagBuilder: DAGBuilder
private execLogger: Logger
constructor(options: DAGExecutorOptions) {
this.workflow = options.workflow
this.environmentVariables = normalizeStringRecord(options.envVarValues)
this.workflowInput = options.workflowInput ?? {}
this.workflowVariables = normalizeWorkflowVariables(options.workflowVariables)
this.contextExtensions = options.contextExtensions ?? {}
this.dagBuilder = new DAGBuilder()
this.execLogger = logger.withMetadata({
workflowId: this.contextExtensions.metadata?.workflowId,
workspaceId: this.contextExtensions.workspaceId,
executionId: this.contextExtensions.executionId,
userId: this.contextExtensions.userId,
requestId: this.contextExtensions.metadata?.requestId,
})
}
async execute(workflowId: string, triggerBlockId?: string): Promise<ExecutionResult> {
const savedIncomingEdges = this.contextExtensions.dagIncomingEdges
const dag = this.dagBuilder.build(this.workflow, {
triggerBlockId,
savedIncomingEdges,
includeAllBlocks: this.contextExtensions.resumeFromSnapshot === true,
})
const restoredClonedSubflows = this.restoreSnapshotParallelBatches(
dag,
this.contextExtensions.snapshotState
)
this.restoreSavedIncomingEdges(dag, savedIncomingEdges)
const { context, state } = this.createExecutionContext(workflowId, triggerBlockId)
context.subflowParentMap = this.buildSubflowParentMap(dag)
this.registerRestoredClonedSubflows(context.subflowParentMap, restoredClonedSubflows)
const engine = this.buildExecutionPipeline(context, dag, state)
return await engine.run(triggerBlockId)
}
async continueExecution(
_pendingBlocks: string[],
context: ExecutionContext
): Promise<ExecutionResult> {
this.execLogger.warn(
'Debug mode (continueExecution) is not yet implemented in the refactored executor'
)
return {
success: false,
output: {},
logs: context.blockLogs ?? [],
error: 'Debug mode is not yet supported in the refactored executor',
metadata: {
duration: 0,
startTime: new Date().toISOString(),
},
}
}
/**
* Execute from a specific block using cached outputs for upstream blocks.
*/
async executeFromBlock(
workflowId: string,
startBlockId: string,
sourceSnapshot: SerializableExecutionState
): Promise<ExecutionResult> {
// Build full DAG with all blocks to compute upstream set for snapshot filtering
// includeAllBlocks is needed because the startBlockId might be a trigger not reachable from the main trigger
const dag = this.dagBuilder.build(this.workflow, { includeAllBlocks: true })
const executedBlocks = new Set(sourceSnapshot.executedBlocks)
const validation = validateRunFromBlock(startBlockId, dag, executedBlocks)
if (!validation.valid) {
throw new Error(validation.error)
}
const { dirtySet, upstreamSet, reachableUpstreamSet } = computeExecutionSets(dag, startBlockId)
const effectiveStartBlockId = resolveContainerToSentinelStart(startBlockId, dag) ?? startBlockId
// Extract container IDs from sentinel IDs in reachable upstream set
// Use reachableUpstreamSet (not upstreamSet) to preserve sibling branch outputs
// Example: A->C, B->C where C references A.result || B.result
// When running from A, B's output should be preserved for C to reference
const reachableContainerIds = new Set<string>()
for (const nodeId of reachableUpstreamSet) {
const loopId = extractLoopIdFromSentinel(nodeId)
if (loopId) reachableContainerIds.add(loopId)
const parallelId = extractParallelIdFromSentinel(nodeId)
if (parallelId) reachableContainerIds.add(parallelId)
}
// Filter snapshot to include all blocks reachable from dirty blocks
// This preserves sibling branch outputs that dirty blocks may reference
const filteredBlockStates: Record<string, any> = {}
for (const [blockId, state] of Object.entries(sourceSnapshot.blockStates)) {
const aliasBaseId = stripOuterBranchSuffix(blockId)
const isReachableOuterBranchAlias =
aliasBaseId !== blockId &&
Array.from(reachableUpstreamSet).some(
(reachableId) => stripCloneSuffixes(reachableId) === aliasBaseId
)
if (
reachableUpstreamSet.has(blockId) ||
reachableContainerIds.has(blockId) ||
isReachableOuterBranchAlias
) {
filteredBlockStates[blockId] = state
}
}
const filteredExecutedBlocks = sourceSnapshot.executedBlocks.filter((id) => {
const aliasBaseId = stripOuterBranchSuffix(id)
const isReachableOuterBranchAlias =
aliasBaseId !== id &&
Array.from(reachableUpstreamSet).some(
(reachableId) => stripCloneSuffixes(reachableId) === aliasBaseId
)
return (
reachableUpstreamSet.has(id) || reachableContainerIds.has(id) || isReachableOuterBranchAlias
)
})
// Filter loop/parallel executions to only include reachable containers
const filteredLoopExecutions: Record<string, any> = {}
if (sourceSnapshot.loopExecutions) {
for (const [loopId, execution] of Object.entries(sourceSnapshot.loopExecutions)) {
if (reachableContainerIds.has(loopId)) {
filteredLoopExecutions[loopId] = execution
}
}
}
const filteredParallelExecutions: Record<string, any> = {}
if (sourceSnapshot.parallelExecutions) {
for (const [parallelId, execution] of Object.entries(sourceSnapshot.parallelExecutions)) {
if (reachableContainerIds.has(parallelId)) {
filteredParallelExecutions[parallelId] = execution
}
}
}
const filteredSnapshot: SerializableExecutionState = {
...sourceSnapshot,
blockStates: filteredBlockStates,
executedBlocks: filteredExecutedBlocks,
loopExecutions: filteredLoopExecutions,
parallelExecutions: filteredParallelExecutions,
}
this.execLogger.info('Executing from block', {
workflowId,
startBlockId,
effectiveStartBlockId,
dirtySetSize: dirtySet.size,
upstreamSetSize: upstreamSet.size,
reachableUpstreamSetSize: reachableUpstreamSet.size,
})
// Remove incoming edges from non-dirty sources so convergent blocks don't wait for cached upstream
for (const nodeId of dirtySet) {
const node = dag.nodes.get(nodeId)
if (!node) continue
const nonDirtyIncoming: string[] = []
for (const sourceId of node.incomingEdges) {
if (!dirtySet.has(sourceId)) {
nonDirtyIncoming.push(sourceId)
}
}
for (const sourceId of nonDirtyIncoming) {
node.incomingEdges.delete(sourceId)
}
}
const runFromBlockContext = { startBlockId: effectiveStartBlockId, dirtySet }
const { context, state } = this.createExecutionContext(workflowId, undefined, {
snapshotState: filteredSnapshot,
runFromBlockContext,
})
const filteredLargeValueKeys = collectLargeValueKeys({
blockStates: filteredBlockStates,
loopExecutions: filteredLoopExecutions,
parallelExecutions: filteredParallelExecutions,
})
mergeLargeValueKeys(context, filteredLargeValueKeys)
const filteredFileKeys = collectUserFileKeys({
blockStates: filteredBlockStates,
loopExecutions: filteredLoopExecutions,
parallelExecutions: filteredParallelExecutions,
})
mergeFileKeys(context, filteredFileKeys)
context.subflowParentMap = this.buildSubflowParentMap(dag)
const engine = this.buildExecutionPipeline(context, dag, state, filteredSnapshot)
const result = await engine.run()
if (result.metadata) {
result.metadata.largeValueKeys = context.largeValueKeys
result.metadata.fileKeys = context.fileKeys
}
return result
}
private restoreSavedIncomingEdges(dag: DAG, savedIncomingEdges?: Record<string, string[]>): void {
if (!savedIncomingEdges) return
for (const [nodeId, incomingEdges] of Object.entries(savedIncomingEdges)) {
const node = dag.nodes.get(nodeId)
if (node) {
node.incomingEdges = new Set(incomingEdges)
}
}
}
private restoreSnapshotParallelBatches(
dag: DAG,
snapshotState?: SerializableExecutionState
): RestoredClonedSubflowInfo[] {
if (!snapshotState?.parallelExecutions) return []
const expander = new ParallelExpander()
const clonedSubflows: RestoredClonedSubflowInfo[] = []
for (const [parallelId, scope] of Object.entries(snapshotState.parallelExecutions)) {
const currentBatchSize = Number(scope.currentBatchSize ?? 0)
if (!Number.isFinite(currentBatchSize) || currentBatchSize <= 0) continue
const currentBatchStart = Number(scope.currentBatchStart ?? 0)
const totalBranches = Number(scope.totalBranches ?? currentBatchStart + currentBatchSize)
const items = Array.isArray(scope.items)
? scope.items.slice(currentBatchStart, currentBatchStart + currentBatchSize)
: undefined
const restoredBatch = expander.expandParallel(dag, parallelId, currentBatchSize, items, {
branchIndexOffset: currentBatchStart,
totalBranches,
})
clonedSubflows.push(
...restoredBatch.clonedSubflows.map((clone) => ({
...clone,
parentParallelId: parallelId,
}))
)
}
return clonedSubflows
}
private registerRestoredClonedSubflows(
parentMap: Map<string, { parentId: string; parentType: SubflowType; branchIndex?: number }>,
clonedSubflows: RestoredClonedSubflowInfo[]
): void {
const branchCloneMaps = new Map<string, Map<number, Map<string, string>>>()
for (const clone of clonedSubflows) {
let parallelBranchMaps = branchCloneMaps.get(clone.parentParallelId)
if (!parallelBranchMaps) {
parallelBranchMaps = new Map()
branchCloneMaps.set(clone.parentParallelId, parallelBranchMaps)
}
let cloneMap = parallelBranchMaps.get(clone.outerBranchIndex)
if (!cloneMap) {
cloneMap = new Map()
parallelBranchMaps.set(clone.outerBranchIndex, cloneMap)
}
cloneMap.set(clone.originalId, clone.clonedId)
}
for (const clone of clonedSubflows) {
const originalEntry = parentMap.get(clone.originalId)
const cloneMap = branchCloneMaps.get(clone.parentParallelId)?.get(clone.outerBranchIndex)
const clonedParentId = originalEntry ? cloneMap?.get(originalEntry.parentId) : undefined
parentMap.set(clone.clonedId, {
parentId: clonedParentId ?? clone.parentParallelId,
parentType: clonedParentId && originalEntry ? originalEntry.parentType : 'parallel',
branchIndex: clonedParentId ? 0 : clone.outerBranchIndex,
})
}
}
private buildExecutionPipeline(
context: ExecutionContext,
dag: DAG,
state: ExecutionState,
snapshotState = this.contextExtensions.snapshotState
) {
const resolver = new VariableResolver(this.workflow, this.workflowVariables, state, {
navigatePathAsync,
})
const allHandlers = createBlockHandlers()
const blockExecutor = new BlockExecutor(allHandlers, resolver, this.contextExtensions, state)
const edgeManager = new EdgeManager(dag)
const loopOrchestrator = new LoopOrchestrator(
dag,
state,
resolver,
this.contextExtensions,
edgeManager
)
const parallelOrchestrator = new ParallelOrchestrator(
dag,
state,
resolver,
this.contextExtensions,
edgeManager
)
edgeManager.restoreDeactivatedEdges(
snapshotState?.deactivatedEdges,
snapshotState?.nodesWithActivatedEdge
)
const nodeOrchestrator = new NodeExecutionOrchestrator(
dag,
state,
blockExecutor,
loopOrchestrator,
parallelOrchestrator
)
return new ExecutionEngine(context, dag, edgeManager, nodeOrchestrator)
}
private createExecutionContext(
workflowId: string,
triggerBlockId?: string,
overrides?: {
snapshotState?: SerializableExecutionState
runFromBlockContext?: RunFromBlockContext
}
): { context: ExecutionContext; state: ExecutionState } {
const snapshotState = overrides?.snapshotState ?? this.contextExtensions.snapshotState
const blockStates = snapshotState?.blockStates
? new Map(Object.entries(snapshotState.blockStates))
: new Map<string, BlockState>()
let executedBlocks = snapshotState?.executedBlocks
? new Set(snapshotState.executedBlocks)
: new Set<string>()
if (overrides?.runFromBlockContext) {
const { dirtySet } = overrides.runFromBlockContext
executedBlocks = new Set([...executedBlocks].filter((id) => !dirtySet.has(id)))
this.execLogger.info('Cleared executed status for dirty blocks', {
dirtySetSize: dirtySet.size,
remainingExecutedBlocks: executedBlocks.size,
})
}
const state = new ExecutionState(blockStates, executedBlocks)
const context: ExecutionContext = {
workflowId,
workspaceId: this.contextExtensions.workspaceId,
executionId: this.contextExtensions.executionId,
largeValueExecutionIds: this.contextExtensions.largeValueExecutionIds,
largeValueKeys: this.contextExtensions.largeValueKeys,
fileKeys: this.contextExtensions.fileKeys,
allowLargeValueWorkflowScope: this.contextExtensions.allowLargeValueWorkflowScope,
userId: this.contextExtensions.userId,
isDeployedContext: this.contextExtensions.isDeployedContext,
enforceCredentialAccess: this.contextExtensions.enforceCredentialAccess,
piiBlockOutputRedaction: this.contextExtensions.piiBlockOutputRedaction,
blockStates: state.getBlockStates(),
blockLogs: overrides?.runFromBlockContext ? [] : (snapshotState?.blockLogs ?? []),
metadata: {
...this.contextExtensions.metadata,
startTime: new Date().toISOString(),
duration: 0,
useDraftState:
this.contextExtensions.metadata?.useDraftState ??
this.contextExtensions.isDeployedContext !== true,
},
environmentVariables: this.environmentVariables,
workflowVariables: this.workflowVariables,
decisions: {
router: snapshotState?.decisions?.router
? new Map(Object.entries(snapshotState.decisions.router))
: new Map(),
condition: snapshotState?.decisions?.condition
? new Map(Object.entries(snapshotState.decisions.condition))
: new Map(),
},
completedLoops: snapshotState?.completedLoops
? new Set(snapshotState.completedLoops)
: new Set(),
loopExecutions: snapshotState?.loopExecutions
? new Map(
Object.entries(snapshotState.loopExecutions).map(([loopId, scope]) => [
loopId,
{
...scope,
currentIterationOutputs: scope.currentIterationOutputs
? new Map(Object.entries(scope.currentIterationOutputs))
: new Map(),
},
])
)
: new Map(),
parallelExecutions: snapshotState?.parallelExecutions
? new Map(
Object.entries(snapshotState.parallelExecutions).map(([parallelId, scope]) => [
parallelId,
{
...scope,
branchOutputs: scope.branchOutputs
? new Map(Object.entries(scope.branchOutputs).map(([k, v]) => [Number(k), v]))
: new Map(),
accumulatedOutputs: scope.accumulatedOutputs
? new Map(
Object.entries(scope.accumulatedOutputs).map(([k, v]) => [Number(k), v])
)
: new Map(),
},
])
)
: new Map(),
parallelBlockMapping: snapshotState?.parallelBlockMapping
? new Map(Object.entries(snapshotState.parallelBlockMapping))
: new Map(),
executedBlocks: state.getExecutedBlocks(),
activeExecutionPath: snapshotState?.activeExecutionPath
? new Set(snapshotState.activeExecutionPath)
: new Set(),
workflow: this.workflow,
stream: this.contextExtensions.stream ?? false,
selectedOutputs: normalizeStringArray(this.contextExtensions.selectedOutputs),
edges: this.contextExtensions.edges ?? [],
onStream: this.contextExtensions.onStream,
onBlockStart: this.contextExtensions.onBlockStart,
onBlockComplete: this.contextExtensions.onBlockComplete,
onChildWorkflowInstanceReady: this.contextExtensions.onChildWorkflowInstanceReady,
abortSignal: this.contextExtensions.abortSignal,
childWorkflowContext: this.contextExtensions.childWorkflowContext,
includeFileBase64: this.contextExtensions.includeFileBase64,
base64MaxBytes: this.contextExtensions.base64MaxBytes,
runFromBlockContext: overrides?.runFromBlockContext,
stopAfterBlockId: this.contextExtensions.stopAfterBlockId,
callChain: this.contextExtensions.callChain,
}
if (this.contextExtensions.resumeFromSnapshot) {
context.metadata.resumeFromSnapshot = true
this.execLogger.info('Resume from snapshot enabled', {
resumePendingQueue: this.contextExtensions.resumePendingQueue,
remainingEdges: this.contextExtensions.remainingEdges,
triggerBlockId,
})
}
if (this.contextExtensions.remainingEdges) {
;(context.metadata as any).remainingEdges = this.contextExtensions.remainingEdges
this.execLogger.info('Set remaining edges for resume', {
edgeCount: this.contextExtensions.remainingEdges.length,
})
}
if (this.contextExtensions.resumePendingQueue?.length) {
context.metadata.pendingBlocks = [...this.contextExtensions.resumePendingQueue]
this.execLogger.info('Set pending blocks from resume queue', {
pendingBlocks: context.metadata.pendingBlocks,
skipStarterBlockInit: true,
})
} else if (overrides?.runFromBlockContext) {
// In run-from-block mode, initialize the start block only if it's a regular block
// Skip for sentinels/containers (loop/parallel) which aren't real blocks
const startBlockId = overrides.runFromBlockContext.startBlockId
const isRegularBlock = this.workflow.blocks.some((b) => b.id === startBlockId)
if (isRegularBlock) {
this.initializeStarterBlock(context, state, startBlockId)
}
} else {
this.initializeStarterBlock(context, state, triggerBlockId)
}
return { context, state }
}
/**
* Builds a unified child-subflow → parent-subflow mapping that covers all nesting
* combinations: loop-in-loop, parallel-in-parallel, loop-in-parallel, parallel-in-loop.
* Used by the iteration context builder to walk the full ancestor chain for SSE events.
*/
private buildSubflowParentMap(
dag: DAG
): Map<string, { parentId: string; parentType: SubflowType; branchIndex?: number }> {
const parentMap = new Map<
string,
{ parentId: string; parentType: SubflowType; branchIndex?: number }
>()
// Scan loop configs: children can be loops or parallels
for (const [loopId, config] of dag.loopConfigs) {
for (const nodeId of config.nodes) {
if (dag.loopConfigs.has(nodeId) || dag.parallelConfigs.has(nodeId)) {
parentMap.set(nodeId, { parentId: loopId, parentType: 'loop' })
}
}
}
// Scan parallel configs: children can be parallels or loops
for (const [parallelId, config] of dag.parallelConfigs) {
for (const nodeId of config.nodes ?? []) {
if (dag.parallelConfigs.has(nodeId) || dag.loopConfigs.has(nodeId)) {
parentMap.set(nodeId, { parentId: parallelId, parentType: 'parallel', branchIndex: 0 })
}
}
}
return parentMap
}
private initializeStarterBlock(
context: ExecutionContext,
state: ExecutionState,
triggerBlockId?: string
): void {
let startResolution: ReturnType<typeof resolveExecutorStartBlock> | null = null
if (triggerBlockId) {
const triggerBlock = this.workflow.blocks.find((b) => b.id === triggerBlockId)
if (!triggerBlock) {
this.execLogger.error('Specified trigger block not found in workflow', {
triggerBlockId,
})
throw new Error(`Trigger block not found: ${triggerBlockId}`)
}
startResolution = buildResolutionFromBlock(triggerBlock)
if (!startResolution) {
startResolution = {
blockId: triggerBlock.id,
block: triggerBlock,
path: StartBlockPath.SPLIT_MANUAL,
}
}
} else {
startResolution = resolveExecutorStartBlock(this.workflow.blocks, {
execution: 'manual',
isChildWorkflow: false,
})
if (!startResolution?.block) {
this.execLogger.warn('No start block found in workflow')
return
}
}
if (state.getBlockStates().has(startResolution.block.id)) {
return
}
const blockOutput = buildStartBlockOutput({
resolution: startResolution,
workflowInput: this.workflowInput,
})
state.setBlockState(startResolution.block.id, {
output: blockOutput,
executed: false,
executionTime: 0,
})
}
}