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
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,21 @@ function transformBlockData(data: any, blockType: string, isInput: boolean) {
interface CollapsibleInputOutputProps {
span: TraceSpan
spanId: string
depth: number
}

function CollapsibleInputOutput({ span, spanId }: CollapsibleInputOutputProps) {
function CollapsibleInputOutput({ span, spanId, depth }: CollapsibleInputOutputProps) {
const [inputExpanded, setInputExpanded] = useState(false)
const [outputExpanded, setOutputExpanded] = useState(false)

// Calculate the left margin based on depth to match the parent span's indentation
const leftMargin = depth * 16 + 8 + 24 // Base depth indentation + icon width + extra padding

return (
<div className='mt-2 mr-4 mb-4 ml-8 space-y-3 overflow-hidden'>
<div
className='mt-2 mr-4 mb-4 space-y-3 overflow-hidden'
style={{ marginLeft: `${leftMargin}px` }}
>
{/* Input Data - Collapsible */}
{span.input && (
<div>
Expand Down Expand Up @@ -162,26 +169,30 @@ function BlockDataDisplay({
if (value === undefined) return <span className='text-muted-foreground italic'>undefined</span>

if (typeof value === 'string') {
return <span className='break-all text-green-700 dark:text-green-400'>"{value}"</span>
return <span className='break-all text-emerald-700 dark:text-emerald-400'>"{value}"</span>
}

if (typeof value === 'number') {
return <span className='text-blue-700 dark:text-blue-400'>{value}</span>
return <span className='font-mono text-blue-700 dark:text-blue-400'>{value}</span>
}

if (typeof value === 'boolean') {
return <span className='text-purple-700 dark:text-purple-400'>{value.toString()}</span>
return (
<span className='font-mono text-amber-700 dark:text-amber-400'>{value.toString()}</span>
)
}

if (Array.isArray(value)) {
if (value.length === 0) return <span className='text-muted-foreground'>[]</span>
return (
<div className='space-y-1'>
<div className='space-y-0.5'>
<span className='text-muted-foreground'>[</span>
<div className='ml-4 space-y-1'>
<div className='ml-2 space-y-0.5'>
{value.map((item, index) => (
<div key={index} className='flex min-w-0 gap-2'>
<span className='flex-shrink-0 text-muted-foreground text-xs'>{index}:</span>
<div key={index} className='flex min-w-0 gap-1.5'>
<span className='flex-shrink-0 font-mono text-slate-600 text-xs dark:text-slate-400'>
{index}:
</span>
<div className='min-w-0 flex-1 overflow-hidden'>{renderValue(item)}</div>
</div>
))}
Expand All @@ -196,10 +207,10 @@ function BlockDataDisplay({
if (entries.length === 0) return <span className='text-muted-foreground'>{'{}'}</span>

return (
<div className='space-y-1'>
<div className='space-y-0.5'>
{entries.map(([objKey, objValue]) => (
<div key={objKey} className='flex min-w-0 gap-2'>
<span className='flex-shrink-0 font-medium text-orange-700 dark:text-orange-400'>
<div key={objKey} className='flex min-w-0 gap-1.5'>
<span className='flex-shrink-0 font-medium text-indigo-700 dark:text-indigo-400'>
{objKey}:
</span>
<div className='min-w-0 flex-1 overflow-hidden'>{renderValue(objValue, objKey)}</div>
Expand Down Expand Up @@ -227,12 +238,12 @@ function BlockDataDisplay({
{transformedData &&
Object.keys(transformedData).filter((key) => key !== 'error' && key !== 'success')
.length > 0 && (
<div className='space-y-1'>
<div className='space-y-0.5'>
{Object.entries(transformedData)
.filter(([key]) => key !== 'error' && key !== 'success')
.map(([key, value]) => (
<div key={key} className='flex gap-2'>
<span className='font-medium text-orange-700 dark:text-orange-400'>{key}:</span>
<div key={key} className='flex gap-1.5'>
<span className='font-medium text-indigo-700 dark:text-indigo-400'>{key}:</span>
{renderValue(value, key)}
</div>
))}
Expand Down Expand Up @@ -592,7 +603,9 @@ function TraceSpanItem({
{expanded && (
<div>
{/* Block Input/Output Data - Collapsible */}
{(span.input || span.output) && <CollapsibleInputOutput span={span} spanId={spanId} />}
{(span.input || span.output) && (
<CollapsibleInputOutput span={span} spanId={spanId} depth={depth} />
)}

{/* Children and tool calls */}
{/* Render child spans */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,21 +720,47 @@ export function Sidebar() {
`[data-workflow-id="${workflowId}"]`
) as HTMLElement
if (activeWorkflow) {
activeWorkflow.scrollIntoView({
block: 'start',
})

// Adjust scroll position to eliminate the small gap at the top
const scrollViewport = scrollContainer.querySelector(
'[data-radix-scroll-area-viewport]'
) as HTMLElement
if (scrollViewport && scrollViewport.scrollTop > 0) {
scrollViewport.scrollTop = Math.max(0, scrollViewport.scrollTop - 8)
// Check if this is a newly created workflow (created within the last 5 seconds)
const currentWorkflow = workflows[workflowId]
const isNewlyCreated =
currentWorkflow &&
currentWorkflow.lastModified instanceof Date &&
Date.now() - currentWorkflow.lastModified.getTime() < 5000 // 5 seconds

if (isNewlyCreated) {
// For newly created workflows, use the original behavior - scroll to top
activeWorkflow.scrollIntoView({
block: 'start',
})

// Adjust scroll position to eliminate the small gap at the top
const scrollViewport = scrollContainer.querySelector(
'[data-radix-scroll-area-viewport]'
) as HTMLElement
if (scrollViewport && scrollViewport.scrollTop > 0) {
scrollViewport.scrollTop = Math.max(0, scrollViewport.scrollTop - 8)
}
} else {
// For existing workflows, check if already visible and scroll minimally
const containerRect = scrollContainer.getBoundingClientRect()
const workflowRect = activeWorkflow.getBoundingClientRect()

// Only scroll if the workflow is not fully visible
const isFullyVisible =
workflowRect.top >= containerRect.top && workflowRect.bottom <= containerRect.bottom

if (!isFullyVisible) {
// Use 'nearest' to scroll minimally - only bring into view, don't force to top
activeWorkflow.scrollIntoView({
block: 'nearest',
behavior: 'smooth',
})
}
}
}
}
}
}, [workflowId, isLoading])
}, [workflowId, isLoading, workflows])

const [showSettings, setShowSettings] = useState(false)
const [showHelp, setShowHelp] = useState(false)
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/executor/handlers/workflow/workflow-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ describe('WorkflowBlockHandler', () => {
success: true,
childWorkflowName: 'Child Workflow',
result: { data: 'test result' },
childTraceSpans: [],
})
})

Expand Down Expand Up @@ -248,6 +249,7 @@ describe('WorkflowBlockHandler', () => {
success: true,
childWorkflowName: 'Child Workflow',
result: { nested: 'data' },
childTraceSpans: [],
})
})
})
Expand Down
118 changes: 93 additions & 25 deletions apps/sim/executor/handlers/workflow/workflow-handler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { generateInternalToken } from '@/lib/auth/internal'
import { createLogger } from '@/lib/logs/console/logger'
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
import { getBaseUrl } from '@/lib/urls/utils'
import type { BlockOutput } from '@/blocks/types'
import { Executor } from '@/executor'
Expand Down Expand Up @@ -104,18 +105,17 @@ export class WorkflowBlockHandler implements BlockHandler {
// 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`)

// Map child workflow output to parent block output
const childTraceSpans = this.captureChildWorkflowLogs(result, childWorkflowName, context)
const mappedResult = this.mapChildOutputToParent(
result,
workflowId,
childWorkflowName,
duration
duration,
childTraceSpans
)

// If the child workflow failed, throw an error to trigger proper error handling in the parent
if ((mappedResult as any).success === false) {
const childError = (mappedResult as any).error || 'Unknown error'
throw new Error(`Error in child workflow "${childWorkflowName}": ${childError}`)
Expand All @@ -125,19 +125,13 @@ 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}_${block.id}`
WorkflowBlockHandler.executionStack.delete(executionId)

// Get workflow name for error reporting
const { workflows } = useWorkflowRegistry.getState()
const workflowMetadata = workflows[workflowId]
const childWorkflowName = workflowMetadata?.name || workflowId

// Enhance error message with child workflow context
const originalError = error.message || 'Unknown error'

// Check if error message already has child workflow context to avoid duplication
if (originalError.startsWith('Error in child workflow')) {
throw error // Re-throw as-is to avoid duplication
}
Expand All @@ -151,12 +145,9 @@ export class WorkflowBlockHandler implements BlockHandler {
*/
private async loadChildWorkflow(workflowId: string) {
try {
// Fetch workflow from API with internal authentication header
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}

// Add internal auth header for server-side calls
if (typeof window === 'undefined') {
const token = await generateInternalToken()
headers.Authorization = `Bearer ${token}`
Expand All @@ -182,16 +173,12 @@ export class WorkflowBlockHandler implements BlockHandler {
}

logger.info(`Loaded child workflow: ${workflowData.name} (${workflowId})`)

// Extract the workflow state (API returns normalized data in state field)
const workflowState = workflowData.state

if (!workflowState || !workflowState.blocks) {
logger.error(`Child workflow ${workflowId} has invalid state`)
return null
}

// Use blocks directly since API returns data from normalized tables
const serializedWorkflow = this.serializer.serializeWorkflow(
workflowState.blocks,
workflowState.edges || [],
Expand Down Expand Up @@ -222,17 +209,101 @@ export class WorkflowBlockHandler implements BlockHandler {
}

/**
* Maps child workflow output to parent block output format
* Captures and transforms child workflow logs into trace spans
*/
private captureChildWorkflowLogs(
childResult: any,
childWorkflowName: string,
parentContext: ExecutionContext
): any[] {
try {
if (!childResult.logs || !Array.isArray(childResult.logs)) {
return []
}

const { traceSpans } = buildTraceSpans(childResult)

if (!traceSpans || traceSpans.length === 0) {
return []
}

const transformedSpans = traceSpans.map((span: any) => {
return this.transformSpanForChildWorkflow(span, childWorkflowName)
})

return transformedSpans
} catch (error) {
logger.error(`Error capturing child workflow logs for ${childWorkflowName}:`, error)
return []
}
}

/**
* Transforms trace span for child workflow context
*/
private transformSpanForChildWorkflow(span: any, childWorkflowName: string): any {
const transformedSpan = {
...span,
name: this.cleanChildSpanName(span.name, childWorkflowName),
metadata: {
...span.metadata,
isFromChildWorkflow: true,
childWorkflowName,
},
}

if (span.children && Array.isArray(span.children)) {
transformedSpan.children = span.children.map((childSpan: any) =>
this.transformSpanForChildWorkflow(childSpan, childWorkflowName)
)
}

if (span.output?.childTraceSpans) {
transformedSpan.output = {
...transformedSpan.output,
childTraceSpans: span.output.childTraceSpans,
}
}

return transformedSpan
}

/**
* Cleans up child span names for readability
*/
private cleanChildSpanName(spanName: string, childWorkflowName: string): string {
if (spanName.includes(`${childWorkflowName}:`)) {
const cleanName = spanName.replace(`${childWorkflowName}:`, '').trim()

if (cleanName === 'Workflow Execution') {
return `${childWorkflowName} workflow`
}

if (cleanName.startsWith('Agent ')) {
return `${cleanName}`
}

return `${cleanName}`
}

if (spanName === 'Workflow Execution') {
return `${childWorkflowName} workflow`
}

return `${spanName}`
}

/**
* Maps child workflow output to parent block output
*/
private mapChildOutputToParent(
childResult: any,
childWorkflowId: string,
childWorkflowName: string,
duration: number
duration: number,
childTraceSpans?: any[]
): BlockOutput {
const success = childResult.success !== false

// If child workflow failed, return minimal output
if (!success) {
logger.warn(`Child workflow ${childWorkflowName} failed`)
return {
Expand All @@ -241,18 +312,15 @@ export class WorkflowBlockHandler implements BlockHandler {
error: childResult.error || 'Child workflow execution failed',
} as Record<string, any>
}

// Extract the actual result content from the flattened structure
let result = childResult
if (childResult?.output) {
result = childResult.output
}

// Return a properly structured response with all required fields
return {
success: true,
childWorkflowName,
result,
childTraceSpans: childTraceSpans || [],
} as Record<string, any>
}
}
Loading