Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions apps/sim/app/api/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { executeWorkflowJob, type WorkflowExecutionPayload } from '@/background/
import { normalizeName } from '@/executor/constants'
import { ExecutionSnapshot } from '@/executor/execution/snapshot'
import type {
ChildWorkflowContext,
ExecutionMetadata,
IterationContext,
SerializableExecutionState,
Expand Down Expand Up @@ -742,7 +743,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
blockName: string,
blockType: string,
executionOrder: number,
iterationContext?: IterationContext
iterationContext?: IterationContext,
childWorkflowContext?: ChildWorkflowContext
) => {
logger.info(`[${requestId}] 🔷 onBlockStart called:`, { blockId, blockName, blockType })
sendEvent({
Expand All @@ -761,6 +763,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
iterationType: iterationContext.iterationType,
iterationContainerId: iterationContext.iterationContainerId,
}),
...(childWorkflowContext && {
childWorkflowBlockId: childWorkflowContext.parentBlockId,
childWorkflowName: childWorkflowContext.workflowName,
}),
},
})
}
Expand All @@ -770,9 +776,20 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
blockName: string,
blockType: string,
callbackData: any,
iterationContext?: IterationContext
iterationContext?: IterationContext,
childWorkflowContext?: ChildWorkflowContext
) => {
const hasError = callbackData.output?.error
const childWorkflowData = childWorkflowContext
? {
childWorkflowBlockId: childWorkflowContext.parentBlockId,
childWorkflowName: childWorkflowContext.workflowName,
}
: {}

const instanceData = callbackData.childWorkflowInstanceId
? { childWorkflowInstanceId: callbackData.childWorkflowInstanceId }
: {}

if (hasError) {
logger.info(`[${requestId}] ✗ onBlockComplete (error) called:`, {
Expand Down Expand Up @@ -802,6 +819,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
iterationType: iterationContext.iterationType,
iterationContainerId: iterationContext.iterationContainerId,
}),
...childWorkflowData,
...instanceData,
},
})
} else {
Expand Down Expand Up @@ -831,6 +850,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
iterationType: iterationContext.iterationType,
iterationContainerId: iterationContext.iterationContainerId,
}),
...childWorkflowData,
...instanceData,
},
})
}
Expand Down Expand Up @@ -898,12 +919,34 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
selectedOutputs
)

const onChildWorkflowInstanceReady = (
blockId: string,
childWorkflowInstanceId: string,
iterationContext?: IterationContext
) => {
sendEvent({
type: 'block:childWorkflowStarted',
timestamp: new Date().toISOString(),
executionId,
workflowId,
data: {
blockId,
childWorkflowInstanceId,
...(iterationContext && {
iterationCurrent: iterationContext.iterationCurrent,
iterationContainerId: iterationContext.iterationContainerId,
}),
},
})
}

const result = await executeWorkflowCore({
snapshot,
callbacks: {
onBlockStart,
onBlockComplete,
onStream,
onChildWorkflowInstanceReady,
},
loggingSession,
abortSignal: timeoutController.signal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks'
import { ROW_STYLES } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types'
import {
collectExpandableNodeIds,
type EntryNode,
type ExecutionGroup,
flattenBlockEntriesOnly,
Expand All @@ -67,6 +68,21 @@ const MIN_HEIGHT = TERMINAL_HEIGHT.MIN
const DEFAULT_EXPANDED_HEIGHT = TERMINAL_HEIGHT.DEFAULT
const MIN_OUTPUT_PANEL_WIDTH_PX = OUTPUT_PANEL_WIDTH.MIN

/** Returns true if any node in the subtree has an error */
function hasErrorInTree(nodes: EntryNode[]): boolean {
return nodes.some((n) => Boolean(n.entry.error) || hasErrorInTree(n.children))
}

/** Returns true if any node in the subtree is currently running */
function hasRunningInTree(nodes: EntryNode[]): boolean {
return nodes.some((n) => Boolean(n.entry.isRunning) || hasRunningInTree(n.children))
}

/** Returns true if any node in the subtree was canceled */
function hasCanceledInTree(nodes: EntryNode[]): boolean {
return nodes.some((n) => Boolean(n.entry.isCanceled) || hasCanceledInTree(n.children))
}

/**
* Block row component for displaying actual block entries
*/
Expand Down Expand Up @@ -338,6 +354,122 @@ const SubflowNodeRow = memo(function SubflowNodeRow({
)
})

/**
* Workflow node component - shows workflow block header with nested child blocks
*/
const WorkflowNodeRow = memo(function WorkflowNodeRow({
node,
selectedEntryId,
onSelectEntry,
expandedNodes,
onToggleNode,
}: {
node: EntryNode
selectedEntryId: string | null
onSelectEntry: (entry: ConsoleEntry) => void
expandedNodes: Set<string>
onToggleNode: (nodeId: string) => void
}) {
const { entry, children } = node
const BlockIcon = getBlockIcon(entry.blockType)
const bgColor = getBlockColor(entry.blockType)
const nodeId = entry.id
const isExpanded = expandedNodes.has(nodeId)
const hasChildren = children.length > 0
const isSelected = selectedEntryId === entry.id

const hasError = useMemo(
() => Boolean(entry.error) || hasErrorInTree(children),
[entry.error, children]
)
const hasRunningDescendant = useMemo(
() => Boolean(entry.isRunning) || hasRunningInTree(children),
[entry.isRunning, children]
)
const hasCanceledDescendant = useMemo(
() => (Boolean(entry.isCanceled) || hasCanceledInTree(children)) && !hasRunningDescendant,
[entry.isCanceled, children, hasRunningDescendant]
)

return (
<div className='flex min-w-0 flex-col'>
{/* Workflow Block Header */}
<div
className={clsx(
ROW_STYLES.base,
'h-[26px]',
isSelected ? ROW_STYLES.selected : ROW_STYLES.hover
)}
onClick={(e) => {
e.stopPropagation()
if (!isSelected) onSelectEntry(entry)
if (hasChildren) onToggleNode(nodeId)
}}
>
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
<div
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
style={{ background: bgColor }}
>
{BlockIcon && <BlockIcon className='h-[9px] w-[9px] text-white' />}
</div>
<span
className={clsx(
'min-w-0 truncate font-medium text-[13px]',
hasError
? 'text-[var(--text-error)]'
: isSelected || isExpanded
? 'text-[var(--text-primary)]'
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
)}
>
{entry.blockName}
</span>
{hasChildren && (
<ChevronDown
className={clsx(
'h-[8px] w-[8px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]',
!isExpanded && '-rotate-90'
)}
/>
)}
</div>
<span
className={clsx(
'flex-shrink-0 font-medium text-[13px]',
!hasRunningDescendant &&
(hasCanceledDescendant
? 'text-[var(--text-secondary)]'
: 'text-[var(--text-tertiary)]')
)}
>
<StatusDisplay
isRunning={hasRunningDescendant}
isCanceled={hasCanceledDescendant}
formattedDuration={formatDuration(entry.durationMs, { precision: 2 }) ?? '-'}
/>
</span>
</div>

{/* Nested Child Blocks — rendered through EntryNodeRow for full loop/parallel support */}
{isExpanded && hasChildren && (
<div className={ROW_STYLES.nested}>
{children.map((child) => (
<EntryNodeRow
key={child.entry.id}
node={child}
selectedEntryId={selectedEntryId}
onSelectEntry={onSelectEntry}
expandedNodes={expandedNodes}
onToggleNode={onToggleNode}
/>
))}
</div>
)}
</div>
)
})

/**
* Entry node component - dispatches to appropriate component based on node type
*/
Expand Down Expand Up @@ -368,6 +500,18 @@ const EntryNodeRow = memo(function EntryNodeRow({
)
}

if (nodeType === 'workflow') {
return (
<WorkflowNodeRow
node={node}
selectedEntryId={selectedEntryId}
onSelectEntry={onSelectEntry}
expandedNodes={expandedNodes}
onToggleNode={onToggleNode}
/>
)
}

if (nodeType === 'iteration') {
return (
<IterationNodeRow
Expand Down Expand Up @@ -659,27 +803,15 @@ export const Terminal = memo(function Terminal() {
])

/**
* Auto-expand subflows and iterations when new entries arrive.
* Auto-expand subflows, iterations, and workflow nodes when new entries arrive.
* Recursively walks the full tree so nested nodes (e.g. a workflow block inside
* a loop iteration) are also expanded automatically.
* This always runs regardless of autoSelectEnabled - new runs should always be visible.
*/
useEffect(() => {
if (executionGroups.length === 0) return

const newestExec = executionGroups[0]

// Collect all node IDs that should be expanded (subflows and their iterations)
const nodeIdsToExpand: string[] = []
for (const node of newestExec.entryTree) {
if (node.nodeType === 'subflow' && node.children.length > 0) {
nodeIdsToExpand.push(node.entry.id)
// Also expand all iteration children
for (const iterNode of node.children) {
if (iterNode.nodeType === 'iteration') {
nodeIdsToExpand.push(iterNode.entry.id)
}
}
}
}
const nodeIdsToExpand = collectExpandableNodeIds(executionGroups[0].entryTree)

if (nodeIdsToExpand.length > 0) {
setExpandedNodes((prev) => {
Expand Down
Loading