Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
37 changes: 36 additions & 1 deletion apps/sim/app/api/logs/execution/[executionId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
workflowExecutionSnapshots,
} from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { and, eq, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
Expand Down Expand Up @@ -48,6 +48,7 @@ export async function GET(
endedAt: workflowExecutionLogs.endedAt,
totalDurationMs: workflowExecutionLogs.totalDurationMs,
cost: workflowExecutionLogs.cost,
executionData: workflowExecutionLogs.executionData,
})
.from(workflowExecutionLogs)
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
Expand Down Expand Up @@ -78,10 +79,44 @@ export async function GET(
return NextResponse.json({ error: 'Workflow state snapshot not found' }, { status: 404 })
}

const traceSpans =
(workflowLog.executionData as { traceSpans?: Array<{ [key: string]: unknown }> })
?.traceSpans || []
const childSnapshotIds = new Set<string>()
const collectSnapshotIds = (spans: Array<{ [key: string]: unknown }>) => {
spans.forEach((span) => {
const snapshotId = span.childWorkflowSnapshotId
if (typeof snapshotId === 'string') {
childSnapshotIds.add(snapshotId)
}
const children = span.children
if (Array.isArray(children)) {
collectSnapshotIds(children as Array<{ [key: string]: unknown }>)
}
})
}
if (traceSpans.length > 0) {
collectSnapshotIds(traceSpans)
}

const childWorkflowSnapshots =
childSnapshotIds.size > 0
? await db
.select()
.from(workflowExecutionSnapshots)
.where(inArray(workflowExecutionSnapshots.id, Array.from(childSnapshotIds)))
: []

const childSnapshotMap = childWorkflowSnapshots.reduce<Record<string, unknown>>((acc, snap) => {
acc[snap.id] = snap.stateData
return acc
}, {})

const response = {
executionId,
workflowId: workflowLog.workflowId,
workflowState: snapshot.stateData,
childWorkflowSnapshots: childSnapshotMap,
executionMetadata: {
trigger: workflowLog.trigger,
startedAt: workflowLog.startedAt.toISOString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ export function ExecutionSnapshot({
}, [executionId, closeMenu])

const workflowState = data?.workflowState as WorkflowState | undefined
const childWorkflowSnapshots = data?.childWorkflowSnapshots as
| Record<string, WorkflowState>
| undefined

const renderContent = () => {
if (isLoading) {
Expand Down Expand Up @@ -148,6 +151,7 @@ export function ExecutionSnapshot({
key={executionId}
workflowState={workflowState}
traceSpans={traceSpans}
childWorkflowSnapshots={childWorkflowSnapshots}
className={className}
height={height}
width={width}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,7 @@ interface ExecutionData {
output?: unknown
status?: string
durationMs?: number
childWorkflowSnapshotId?: string
}

interface WorkflowVariable {
Expand All @@ -714,6 +715,8 @@ interface PreviewEditorProps {
parallels?: Record<string, Parallel>
/** When true, shows "Not Executed" badge if no executionData is provided */
isExecutionMode?: boolean
/** Child workflow snapshots keyed by snapshot ID (execution mode only) */
childWorkflowSnapshots?: Record<string, WorkflowState>
/** Optional close handler - if not provided, no close button is shown */
onClose?: () => void
/** Callback to drill down into a nested workflow block */
Expand All @@ -739,6 +742,7 @@ function PreviewEditorContent({
loops,
parallels,
isExecutionMode = false,
childWorkflowSnapshots,
onClose,
onDrillDown,
}: PreviewEditorProps) {
Expand Down Expand Up @@ -768,17 +772,31 @@ function PreviewEditorContent({
const { data: childWorkflowState, isLoading: isLoadingChildWorkflow } = useWorkflowState(
childWorkflowId ?? undefined
)
const childWorkflowSnapshotId = executionData?.childWorkflowSnapshotId
const childWorkflowSnapshotState = childWorkflowSnapshotId
? childWorkflowSnapshots?.[childWorkflowSnapshotId]
: undefined

/** Drills down into the child workflow or opens it in a new tab */
const handleExpandChildWorkflow = useCallback(() => {
if (!childWorkflowId || !childWorkflowState) return
if (!childWorkflowId) return

if (isExecutionMode && onDrillDown) {
onDrillDown(block.id, childWorkflowState)
const resolvedChildState = childWorkflowSnapshotState ?? childWorkflowState
if (!resolvedChildState) return
onDrillDown(block.id, resolvedChildState)
} else if (workspaceId) {
window.open(`/workspace/${workspaceId}/w/${childWorkflowId}`, '_blank', 'noopener,noreferrer')
}
}, [childWorkflowId, childWorkflowState, isExecutionMode, onDrillDown, block.id, workspaceId])
}, [
childWorkflowId,
childWorkflowSnapshotState,
childWorkflowState,
isExecutionMode,
onDrillDown,
block.id,
workspaceId,
])

const contentRef = useRef<HTMLDivElement>(null)
const subBlocksRef = useRef<HTMLDivElement>(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ interface TraceSpan {
status?: string
duration?: number
children?: TraceSpan[]
childWorkflowSnapshotId?: string
childWorkflowId?: string
}

interface BlockExecutionData {
Expand All @@ -28,6 +30,7 @@ interface BlockExecutionData {
durationMs: number
/** Child trace spans for nested workflow blocks */
children?: TraceSpan[]
childWorkflowSnapshotId?: string
}

/** Represents a level in the workflow navigation stack */
Expand Down Expand Up @@ -89,6 +92,7 @@ export function buildBlockExecutions(spans: TraceSpan[]): Record<string, BlockEx
status: span.status || 'unknown',
durationMs: span.duration || 0,
children: span.children,
childWorkflowSnapshotId: span.childWorkflowSnapshotId,
}
}
}
Expand All @@ -103,6 +107,8 @@ interface PreviewProps {
traceSpans?: TraceSpan[]
/** Pre-computed block executions (optional - will be built from traceSpans if not provided) */
blockExecutions?: Record<string, BlockExecutionData>
/** Child workflow snapshots keyed by snapshot ID (execution mode only) */
childWorkflowSnapshots?: Record<string, WorkflowState>
/** Additional CSS class names */
className?: string
/** Height of the component */
Expand Down Expand Up @@ -135,6 +141,7 @@ export function Preview({
workflowState: rootWorkflowState,
traceSpans: rootTraceSpans,
blockExecutions: providedBlockExecutions,
childWorkflowSnapshots,
className,
height = '100%',
width = '100%',
Expand Down Expand Up @@ -284,6 +291,7 @@ export function Preview({
loops={workflowState.loops}
parallels={workflowState.parallels}
isExecutionMode={isExecutionMode}
childWorkflowSnapshots={childWorkflowSnapshots}
onClose={handleEditorClose}
onDrillDown={handleDrillDown}
/>
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/executor/errors/child-workflow-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface ChildWorkflowErrorOptions {
childWorkflowName: string
childTraceSpans?: TraceSpan[]
executionResult?: ExecutionResult
childWorkflowSnapshotId?: string
cause?: Error
}

Expand All @@ -16,13 +17,15 @@ export class ChildWorkflowError extends Error {
readonly childTraceSpans: TraceSpan[]
readonly childWorkflowName: string
readonly executionResult?: ExecutionResult
readonly childWorkflowSnapshotId?: string

constructor(options: ChildWorkflowErrorOptions) {
super(options.message, { cause: options.cause })
this.name = 'ChildWorkflowError'
this.childWorkflowName = options.childWorkflowName
this.childTraceSpans = options.childTraceSpans ?? []
this.executionResult = options.executionResult
this.childWorkflowSnapshotId = options.childWorkflowSnapshotId
}

static isChildWorkflowError(error: unknown): error is ChildWorkflowError {
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/executor/execution/block-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ export class BlockExecutor {
if (ChildWorkflowError.isChildWorkflowError(error)) {
errorOutput.childTraceSpans = error.childTraceSpans
errorOutput.childWorkflowName = error.childWorkflowName
if (error.childWorkflowSnapshotId) {
errorOutput.childWorkflowSnapshotId = error.childWorkflowSnapshotId
}
}

this.state.setBlockOutput(node.id, errorOutput, duration)
Expand Down
27 changes: 25 additions & 2 deletions apps/sim/executor/handlers/workflow/workflow-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { snapshotService } from '@/lib/logs/execution/snapshot/service'
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
import type { TraceSpan } from '@/lib/logs/types'
import type { BlockOutput } from '@/blocks/types'
Expand Down Expand Up @@ -57,6 +58,7 @@ export class WorkflowBlockHandler implements BlockHandler {
const workflowMetadata = workflows[workflowId]
let childWorkflowName = workflowMetadata?.name || workflowId

let childWorkflowSnapshotId: string | undefined
try {
const currentDepth = (ctx.workflowId?.split('_sub_').length || 1) - 1
if (currentDepth >= DEFAULTS.MAX_WORKFLOW_DEPTH) {
Expand Down Expand Up @@ -107,6 +109,12 @@ export class WorkflowBlockHandler implements BlockHandler {
childWorkflowInput = inputs.input
}

const childSnapshotResult = await snapshotService.createSnapshotWithDeduplication(
workflowId,
childWorkflow.workflowState
)
childWorkflowSnapshotId = childSnapshotResult.snapshot.id

const subExecutor = new Executor({
workflow: childWorkflow.serializedState,
workflowInput: childWorkflowInput,
Expand Down Expand Up @@ -139,7 +147,8 @@ export class WorkflowBlockHandler implements BlockHandler {
workflowId,
childWorkflowName,
duration,
childTraceSpans
childTraceSpans,
childWorkflowSnapshotId
)

return mappedResult
Expand Down Expand Up @@ -172,6 +181,7 @@ export class WorkflowBlockHandler implements BlockHandler {
childWorkflowName,
childTraceSpans,
executionResult,
childWorkflowSnapshotId,
cause: error instanceof Error ? error : undefined,
})
}
Expand Down Expand Up @@ -279,6 +289,10 @@ export class WorkflowBlockHandler implements BlockHandler {
)

const workflowVariables = (workflowData.variables as Record<string, any>) || {}
const workflowStateWithVariables = {
...workflowState,
variables: workflowVariables,
}

if (Object.keys(workflowVariables).length > 0) {
logger.info(
Expand All @@ -290,6 +304,7 @@ export class WorkflowBlockHandler implements BlockHandler {
name: workflowData.name,
serializedState: serializedWorkflow,
variables: workflowVariables,
workflowState: workflowStateWithVariables,
rawBlocks: workflowState.blocks,
}
}
Expand Down Expand Up @@ -358,11 +373,16 @@ export class WorkflowBlockHandler implements BlockHandler {
)

const workflowVariables = (wfData?.variables as Record<string, any>) || {}
const workflowStateWithVariables = {
...deployedState,
variables: workflowVariables,
}

return {
name: wfData?.name || DEFAULTS.WORKFLOW_NAME,
serializedState: serializedWorkflow,
variables: workflowVariables,
workflowState: workflowStateWithVariables,
rawBlocks: deployedState.blocks,
}
}
Expand Down Expand Up @@ -504,7 +524,8 @@ export class WorkflowBlockHandler implements BlockHandler {
childWorkflowId: string,
childWorkflowName: string,
duration: number,
childTraceSpans?: WorkflowTraceSpan[]
childTraceSpans?: WorkflowTraceSpan[],
childWorkflowSnapshotId?: string
): BlockOutput {
const success = childResult.success !== false
const result = childResult.output || {}
Expand All @@ -521,6 +542,8 @@ export class WorkflowBlockHandler implements BlockHandler {
return {
success: true,
childWorkflowName,
childWorkflowId,
childWorkflowSnapshotId,
result,
childTraceSpans: childTraceSpans || [],
} as Record<string, any>
Expand Down
1 change: 1 addition & 0 deletions apps/sim/hooks/queries/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export interface ExecutionSnapshotData {
executionId: string
workflowId: string
workflowState: Record<string, unknown>
childWorkflowSnapshots?: Record<string, Record<string, unknown>>
executionMetadata: {
trigger: string
startedAt: string
Expand Down
22 changes: 22 additions & 0 deletions apps/sim/lib/logs/execution/trace-spans/trace-spans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,26 @@ export function buildTraceSpans(result: ExecutionResult): {
const duration = log.durationMs || 0

let output = log.output || {}
let childWorkflowSnapshotId: string | undefined
let childWorkflowId: string | undefined

if (output && typeof output === 'object') {
const outputRecord = output as Record<string, unknown>
childWorkflowSnapshotId =
typeof outputRecord.childWorkflowSnapshotId === 'string'
? outputRecord.childWorkflowSnapshotId
: undefined
childWorkflowId =
typeof outputRecord.childWorkflowId === 'string' ? outputRecord.childWorkflowId : undefined
if (childWorkflowSnapshotId || childWorkflowId) {
const {
childWorkflowSnapshotId: _childSnapshotId,
childWorkflowId: _childWorkflowId,
...outputRest
} = outputRecord
output = outputRest
}
}

if (log.error) {
output = {
Expand All @@ -134,6 +154,8 @@ export function buildTraceSpans(result: ExecutionResult): {
blockId: log.blockId,
input: log.input || {},
output: output,
...(childWorkflowSnapshotId ? { childWorkflowSnapshotId } : {}),
...(childWorkflowId ? { childWorkflowId } : {}),
...(log.loopId && { loopId: log.loopId }),
...(log.parallelId && { parallelId: log.parallelId }),
...(log.iterationIndex !== undefined && { iterationIndex: log.iterationIndex }),
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/lib/logs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ export interface TraceSpan {
blockId?: string
input?: Record<string, unknown>
output?: Record<string, unknown>
childWorkflowSnapshotId?: string
childWorkflowId?: string
model?: string
cost?: {
input?: number
Expand Down
Loading