@@ -119,7 +119,7 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP
draggable
onDragStart={(e) => handleDragStart(e, connection)}
onDragEnd={handleDragEnd}
- className="group flex items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 cursor-grab active:cursor-grabbing w-fit"
+ className="group flex items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 cursor-grab active:cursor-grabbing w-max"
>
diff --git a/sim/app/w/[id]/components/workflow-block/workflow-block.tsx b/sim/app/w/[id]/components/workflow-block/workflow-block.tsx
index 884913d0e8..95801922a0 100644
--- a/sim/app/w/[id]/components/workflow-block/workflow-block.tsx
+++ b/sim/app/w/[id]/components/workflow-block/workflow-block.tsx
@@ -224,13 +224,20 @@ export function WorkflowBlock({ id, data }: NodeProps) {
position={horizontalHandles ? Position.Left : Position.Top}
id="target"
className={cn(
- '!w-3.5 !h-3.5',
- '!bg-white !rounded-full !border !border-gray-200',
- 'group-hover:!border-blue-500',
+ '!w-[7px] !h-5',
+ '!bg-slate-300 dark:!bg-slate-500 !rounded-[2px] !border-none',
+ '!z-[-1]',
+ 'group-hover:!shadow-[0_0_0_3px_rgba(156,163,175,0.15)]',
+ horizontalHandles
+ ? 'hover:!w-[10px] hover:!left-[-10px] hover:!rounded-l-full hover:!rounded-r-none'
+ : 'hover:!h-[10px] hover:!translate-y-[-3px] hover:!rounded-t-full hover:!rounded-b-none',
'!cursor-crosshair',
- 'transition-[border-color] duration-150',
+ 'transition-all duration-150',
horizontalHandles ? '!left-[-7px]' : '!top-[-7px]'
)}
+ style={{
+ ...(horizontalHandles ? { top: '50%', transform: 'translateY(-50%)' } : {}),
+ }}
data-nodeid={id}
data-handleid="target"
isConnectableStart={false}
@@ -362,24 +369,74 @@ export function WorkflowBlock({ id, data }: NodeProps) {
{/* Output Handle */}
{type !== 'condition' && (
-
+ connection.target !== id}
+ />
+
+ {/* Error Handle - Don't show for starter blocks */}
+ {type !== 'starter' && (
+ connection.target !== id}
+ />
)}
- data-nodeid={id}
- data-handleid="source"
- isConnectableStart={true}
- isConnectableEnd={false}
- isValidConnection={(connection) => connection.target !== id}
- />
+ >
)}
diff --git a/sim/executor/index.ts b/sim/executor/index.ts
index ce8184aeb8..9538396c28 100644
--- a/sim/executor/index.ts
+++ b/sim/executor/index.ts
@@ -392,6 +392,10 @@ export class Executor {
} else {
const allDependenciesMet = incomingConnections.every((conn) => {
const sourceExecuted = executedBlocks.has(conn.source)
+ const sourceBlock = this.workflow.blocks.find((b) => b.id === conn.source)
+ const sourceBlockState = context.blockStates.get(conn.source)
+ const hasSourceError = sourceBlockState?.output?.error !== undefined ||
+ sourceBlockState?.output?.response?.error !== undefined
// For condition blocks, check if this is the selected path
if (conn.sourceHandle?.startsWith('condition-')) {
@@ -411,7 +415,6 @@ export class Executor {
}
// For router blocks, check if this is the selected target
- const sourceBlock = this.workflow.blocks.find((b) => b.id === conn.source)
if (sourceBlock?.metadata?.id === 'router') {
const selectedTarget = context.decisions.router.get(conn.source)
@@ -424,6 +427,16 @@ export class Executor {
return sourceExecuted && conn.target === selectedTarget
}
+ // For error connections, check if the source had an error
+ if (conn.sourceHandle === 'error') {
+ return sourceExecuted && hasSourceError
+ }
+
+ // For regular connections, check if the source was executed without error
+ if (conn.sourceHandle === 'source' || !conn.sourceHandle) {
+ return sourceExecuted && !hasSourceError
+ }
+
// If source is not in active path, consider this dependency met
// This allows blocks with multiple inputs to execute even if some inputs are from inactive paths
if (!context.activeExecutionPath.has(conn.source)) {
@@ -556,6 +569,7 @@ export class Executor {
blockLog.durationMs =
new Date(blockLog.endedAt).getTime() - new Date(blockLog.startedAt).getTime()
+ // Log the error even if we'll continue execution through error path
context.blockLogs.push(blockLog)
addConsole({
output: {},
@@ -571,6 +585,34 @@ export class Executor {
blockType: block.metadata?.id || 'unknown',
})
+ // Create error output with appropriate structure
+ const errorOutput: NormalizedBlockOutput = {
+ response: {
+ error: this.extractErrorMessage(error),
+ status: error.status || 500,
+ },
+ error: this.extractErrorMessage(error),
+ }
+
+ // Set block state with error output
+ context.blockStates.set(blockId, {
+ output: errorOutput,
+ executed: true,
+ executionTime: blockLog.durationMs,
+ })
+
+ // Check for error connections and follow them if they exist
+ const hasErrorPath = this.activateErrorPath(blockId, context)
+
+ // Console.error the error for visibility
+ logger.error(`Error executing block ${block.metadata?.name || blockId}:`, this.sanitizeError(error))
+
+ // If there are error paths to follow, return error output instead of throwing
+ if (hasErrorPath) {
+ // Return the error output to allow execution to continue along error path
+ return errorOutput
+ }
+
// Create a proper error message that is never undefined
let errorMessage = error.message
@@ -590,6 +632,39 @@ export class Executor {
}
}
+ /**
+ * Activates error paths from a block that had an error.
+ * Checks for connections from the block's "error" handle and adds them to the active execution path.
+ *
+ * @param blockId - ID of the block that had an error
+ * @param context - Current execution context
+ * @returns Whether there was an error path to follow
+ */
+ private activateErrorPath(blockId: string, context: ExecutionContext): boolean {
+ // Skip for starter blocks which don't have error handles
+ const block = this.workflow.blocks.find((b) => b.id === blockId)
+ if (block?.metadata?.id === 'starter' || block?.metadata?.id === 'condition') {
+ return false
+ }
+
+ // Look for connections from this block's error handle
+ const errorConnections = this.workflow.connections.filter(
+ (conn) => conn.source === blockId && conn.sourceHandle === 'error'
+ )
+
+ if (errorConnections.length === 0) {
+ return false
+ }
+
+ // Add all error connection targets to the active execution path
+ for (const conn of errorConnections) {
+ context.activeExecutionPath.add(conn.target)
+ logger.info(`Activated error path from ${blockId} to ${conn.target}`)
+ }
+
+ return true
+ }
+
/**
* Normalizes a block output to ensure it has the expected structure.
* Handles different block types with appropriate response formats.
@@ -599,8 +674,26 @@ export class Executor {
* @returns Normalized output with consistent structure
*/
private normalizeBlockOutput(output: any, block: SerializedBlock): NormalizedBlockOutput {
+ // Handle error outputs
+ if (output && typeof output === 'object' && output.error) {
+ return {
+ response: {
+ error: output.error,
+ status: output.status || 500,
+ },
+ error: output.error,
+ };
+ }
+
if (output && typeof output === 'object' && 'response' in output) {
- return output as NormalizedBlockOutput
+ // If response already contains an error, maintain it
+ if (output.response && output.response.error) {
+ return {
+ ...output,
+ error: output.response.error
+ };
+ }
+ return output as NormalizedBlockOutput;
}
const blockType = block.metadata?.id
@@ -830,4 +923,4 @@ export class Executor {
return { message: `Unexpected error type: ${typeof error}` }
}
-}
+}
\ No newline at end of file
diff --git a/sim/executor/path.ts b/sim/executor/path.ts
index e77c1b2fa4..61a9a4da63 100644
--- a/sim/executor/path.ts
+++ b/sim/executor/path.ts
@@ -96,13 +96,32 @@ export class PathTracker {
}
}
} else {
- // For regular blocks, activate all outgoing connections
+ // For regular blocks, activate all outgoing connections based on success or error status
+ const blockState = context.blockStates.get(blockId)
+ const hasError = blockState?.output?.error !== undefined || blockState?.output?.response?.error !== undefined
+
+ // Get all outgoing connections
const outgoingConnections = this.workflow.connections.filter(
(conn) => conn.source === blockId
)
for (const conn of outgoingConnections) {
- context.activeExecutionPath.add(conn.target)
+ // For error connections, only activate them on error
+ if (conn.sourceHandle === 'error') {
+ if (hasError) {
+ context.activeExecutionPath.add(conn.target)
+ }
+ }
+ // For regular (source) connections, only activate them if there's no error
+ else if (conn.sourceHandle === 'source' || !conn.sourceHandle) {
+ if (!hasError) {
+ context.activeExecutionPath.add(conn.target)
+ }
+ }
+ // All other types of connections (e.g., from condition blocks) follow their own rules
+ else {
+ context.activeExecutionPath.add(conn.target)
+ }
}
}
}
diff --git a/sim/executor/types.ts b/sim/executor/types.ts
index 12d95219fe..091db740c9 100644
--- a/sim/executor/types.ts
+++ b/sim/executor/types.ts
@@ -32,7 +32,9 @@ export interface NormalizedBlockOutput {
data?: any // Response data from API calls
status?: number // HTTP status code
headers?: Record // HTTP headers
+ error?: string // Error message if block execution failed
}
+ error?: string // Top-level error field for easy error checking
[key: string]: any // Additional properties
}