From 2731d98fbf7220277cc54b592d12840fb8e3dcdb Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 27 Jan 2026 12:09:35 -0800 Subject: [PATCH] fix(hitl): add missing fields to block configs --- .../[executionId]/resume-page-client.tsx | 16 ++--- .../components/tag-dropdown/tag-dropdown.tsx | 35 +---------- apps/sim/blocks/blocks/agent.ts | 10 +++- apps/sim/blocks/blocks/human_in_the_loop.ts | 16 +++++ apps/sim/blocks/blocks/router.ts | 1 + apps/sim/blocks/blocks/workflow.ts | 5 ++ apps/sim/blocks/blocks/workflow_input.ts | 5 ++ apps/sim/blocks/types.ts | 5 ++ apps/sim/executor/constants.ts | 11 ++++ apps/sim/executor/execution/block-executor.ts | 58 +++---------------- .../handlers/trigger/trigger-handler.ts | 9 ++- apps/sim/executor/utils/output-filter.ts | 54 +++++++++++++++++ .../sim/lib/workflows/blocks/block-outputs.ts | 58 +++++++------------ .../executor/human-in-the-loop-manager.ts | 13 ++--- 14 files changed, 153 insertions(+), 143 deletions(-) create mode 100644 apps/sim/executor/utils/output-filter.ts diff --git a/apps/sim/app/resume/[workflowId]/[executionId]/resume-page-client.tsx b/apps/sim/app/resume/[workflowId]/[executionId]/resume-page-client.tsx index 0c0a5ece3..3edd059aa 100644 --- a/apps/sim/app/resume/[workflowId]/[executionId]/resume-page-client.tsx +++ b/apps/sim/app/resume/[workflowId]/[executionId]/resume-page-client.tsx @@ -6,7 +6,6 @@ import { useRouter } from 'next/navigation' import { Badge, Button, - Code, Input, Label, Table, @@ -777,15 +776,6 @@ export default function ResumeExecutionPage({ refreshSelectedDetail, ]) - const pauseResponsePreview = useMemo(() => { - if (!selectedDetail?.pausePoint.response?.data) return '{}' - try { - return JSON.stringify(selectedDetail.pausePoint.response.data, null, 2) - } catch { - return String(selectedDetail.pausePoint.response.data) - } - }, [selectedDetail]) - const isFormComplete = useMemo(() => { if (!isHumanMode || !hasInputFormat) return true return inputFormatFields.every((field) => { @@ -1155,10 +1145,12 @@ export default function ResumeExecutionPage({ borderBottom: '1px solid var(--border)', }} > - +
- +

+ No display data configured +

)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx index b58752dee..17b50aad2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx @@ -1183,19 +1183,6 @@ export const TagDropdown: React.FC = ({ const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks, true) blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) } - } else if (sourceBlock.type === 'approval') { - const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks) - - const isSelfReference = activeSourceBlockId === blockId - - if (dynamicOutputs.length > 0) { - const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags - } else { - const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks) - const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags - } } else if (sourceBlock.type === 'human_in_the_loop') { const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks) @@ -1400,13 +1387,8 @@ export const TagDropdown: React.FC = ({ if (!accessibleBlock) continue // Skip the current block - blocks cannot reference their own outputs - // Exception: approval and human_in_the_loop blocks can reference their own outputs - if ( - accessibleBlockId === blockId && - accessibleBlock.type !== 'approval' && - accessibleBlock.type !== 'human_in_the_loop' - ) - continue + // Exception: human_in_the_loop blocks can reference their own outputs (url, resumeEndpoint) + if (accessibleBlockId === blockId && accessibleBlock.type !== 'human_in_the_loop') continue const blockConfig = getBlock(accessibleBlock.type) @@ -1520,19 +1502,6 @@ export const TagDropdown: React.FC = ({ const outputPaths = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks, true) blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) } - } else if (accessibleBlock.type === 'approval') { - const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks) - - const isSelfReference = accessibleBlockId === blockId - - if (dynamicOutputs.length > 0) { - const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags - } else { - const outputPaths = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks) - const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags - } } else if (accessibleBlock.type === 'human_in_the_loop') { const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks) diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts index 6b4fd0efb..5606406c8 100644 --- a/apps/sim/blocks/blocks/agent.ts +++ b/apps/sim/blocks/blocks/agent.ts @@ -769,7 +769,13 @@ Example 3 (Array Input): outputs: { content: { type: 'string', description: 'Generated response content' }, model: { type: 'string', description: 'Model used for generation' }, - tokens: { type: 'any', description: 'Token usage statistics' }, - toolCalls: { type: 'any', description: 'Tool calls made' }, + tokens: { type: 'json', description: 'Token usage statistics' }, + toolCalls: { type: 'json', description: 'Tool calls made' }, + providerTiming: { + type: 'json', + description: 'Provider timing information', + hiddenFromDisplay: true, + }, + cost: { type: 'number', description: 'Cost of the API call', hiddenFromDisplay: true }, }, } diff --git a/apps/sim/blocks/blocks/human_in_the_loop.ts b/apps/sim/blocks/blocks/human_in_the_loop.ts index df3d24a48..e1765ee5b 100644 --- a/apps/sim/blocks/blocks/human_in_the_loop.ts +++ b/apps/sim/blocks/blocks/human_in_the_loop.ts @@ -162,5 +162,21 @@ export const HumanInTheLoopBlock: BlockConfig = { type: 'string', description: 'Resume API endpoint URL for direct curl requests', }, + response: { + type: 'json', + description: 'Display data shown to the approver', + hiddenFromDisplay: true, + }, + submission: { + type: 'json', + description: 'Form submission data from the approver', + hiddenFromDisplay: true, + }, + resumeInput: { + type: 'json', + description: 'Raw input data submitted when resuming', + hiddenFromDisplay: true, + }, + submittedAt: { type: 'string', description: 'ISO timestamp when the workflow was resumed' }, }, } diff --git a/apps/sim/blocks/blocks/router.ts b/apps/sim/blocks/blocks/router.ts index fa086becb..7da50ed98 100644 --- a/apps/sim/blocks/blocks/router.ts +++ b/apps/sim/blocks/blocks/router.ts @@ -247,6 +247,7 @@ export const RouterBlock: BlockConfig = { tokens: { type: 'json', description: 'Token usage' }, cost: { type: 'json', description: 'Cost information' }, selectedPath: { type: 'json', description: 'Selected routing path' }, + selectedRoute: { type: 'string', description: 'Selected route ID' }, }, } diff --git a/apps/sim/blocks/blocks/workflow.ts b/apps/sim/blocks/blocks/workflow.ts index b1a5056b1..d30ab4c6d 100644 --- a/apps/sim/blocks/blocks/workflow.ts +++ b/apps/sim/blocks/blocks/workflow.ts @@ -44,6 +44,11 @@ export const WorkflowBlock: BlockConfig = { childWorkflowName: { type: 'string', description: 'Child workflow name' }, result: { type: 'json', description: 'Workflow execution result' }, error: { type: 'string', description: 'Error message' }, + childTraceSpans: { + type: 'json', + description: 'Child workflow trace spans', + hiddenFromDisplay: true, + }, }, hideFromToolbar: true, } diff --git a/apps/sim/blocks/blocks/workflow_input.ts b/apps/sim/blocks/blocks/workflow_input.ts index 73e3c833b..24c3b3f67 100644 --- a/apps/sim/blocks/blocks/workflow_input.ts +++ b/apps/sim/blocks/blocks/workflow_input.ts @@ -43,5 +43,10 @@ export const WorkflowInputBlock: BlockConfig = { childWorkflowName: { type: 'string', description: 'Child workflow name' }, result: { type: 'json', description: 'Workflow execution result' }, error: { type: 'string', description: 'Error message' }, + childTraceSpans: { + type: 'json', + description: 'Child workflow trace spans', + hiddenFromDisplay: true, + }, }, } diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index cbfd20ba3..099dcd8b6 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -157,6 +157,11 @@ export type OutputFieldDefinition = * Uses the same condition format as subBlocks. */ condition?: OutputCondition + /** + * If true, this output is hidden from display in the tag dropdown and logs, + * but still available for resolution and execution. + */ + hiddenFromDisplay?: boolean } export interface ParamConfig { diff --git a/apps/sim/executor/constants.ts b/apps/sim/executor/constants.ts index ba2c2fc23..8cfbaed58 100644 --- a/apps/sim/executor/constants.ts +++ b/apps/sim/executor/constants.ts @@ -1,5 +1,16 @@ import type { LoopType, ParallelType } from '@/lib/workflows/types' +/** + * Runtime-injected keys for trigger blocks that should be hidden from logs/display. + * These are added during execution but aren't part of the block's static output schema. + */ +export const TRIGGER_INTERNAL_KEYS = ['webhook', 'workflowId'] as const +export type TriggerInternalKey = (typeof TRIGGER_INTERNAL_KEYS)[number] + +export function isTriggerInternalKey(key: string): key is TriggerInternalKey { + return TRIGGER_INTERNAL_KEYS.includes(key as TriggerInternalKey) +} + export enum BlockType { PARALLEL = 'parallel', LOOP = 'loop', diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 2cc37a77e..f159e4db0 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -11,8 +11,6 @@ import { DEFAULTS, EDGE, isSentinelBlockType, - isTriggerBehavior, - isWorkflowBlockType, } from '@/executor/constants' import type { DAGNode } from '@/executor/dag/builder' import { ChildWorkflowError } from '@/executor/errors/child-workflow-error' @@ -30,6 +28,7 @@ import type { } from '@/executor/types' import { streamingResponseFormatProcessor } from '@/executor/utils' import { buildBlockExecutionError, normalizeError } from '@/executor/utils/errors' +import { filterOutputForLog } from '@/executor/utils/output-filter' import { validateBlockType } from '@/executor/utils/permission-check' import type { VariableResolver } from '@/executor/variables/resolver' import type { SerializedBlock } from '@/serializer/types' @@ -149,13 +148,15 @@ export class BlockExecutor { blockLog.endedAt = new Date().toISOString() blockLog.durationMs = duration blockLog.success = true - blockLog.output = this.filterOutputForLog(block, normalizedOutput) + blockLog.output = filterOutputForLog(block.metadata?.id || '', normalizedOutput, { block }) } this.state.setBlockOutput(node.id, normalizedOutput, duration) if (!isSentinel) { - const displayOutput = this.filterOutputForDisplay(block, normalizedOutput) + const displayOutput = filterOutputForLog(block.metadata?.id || '', normalizedOutput, { + block, + }) this.callOnBlockComplete(ctx, node, block, resolvedInputs, displayOutput, duration) } @@ -233,7 +234,7 @@ export class BlockExecutor { blockLog.success = false blockLog.error = errorMessage blockLog.input = input - blockLog.output = this.filterOutputForLog(block, errorOutput) + blockLog.output = filterOutputForLog(block.metadata?.id || '', errorOutput, { block }) } logger.error( @@ -246,7 +247,7 @@ export class BlockExecutor { ) if (!isSentinel) { - const displayOutput = this.filterOutputForDisplay(block, errorOutput) + const displayOutput = filterOutputForLog(block.metadata?.id || '', errorOutput, { block }) this.callOnBlockComplete(ctx, node, block, input, displayOutput, duration) } @@ -335,51 +336,6 @@ export class BlockExecutor { return { result: output } } - private filterOutputForLog( - block: SerializedBlock, - output: NormalizedBlockOutput - ): NormalizedBlockOutput { - const blockType = block.metadata?.id - - if (blockType === BlockType.HUMAN_IN_THE_LOOP) { - const filtered: NormalizedBlockOutput = {} - for (const [key, value] of Object.entries(output)) { - if (key.startsWith('_')) continue - if (key === 'response') continue - filtered[key] = value - } - return filtered - } - - if (isTriggerBehavior(block)) { - const filtered: NormalizedBlockOutput = {} - const internalKeys = ['webhook', 'workflowId'] - for (const [key, value] of Object.entries(output)) { - if (internalKeys.includes(key)) continue - filtered[key] = value - } - return filtered - } - - return output - } - - private filterOutputForDisplay( - block: SerializedBlock, - output: NormalizedBlockOutput - ): NormalizedBlockOutput { - const filtered = this.filterOutputForLog(block, output) - - if (isWorkflowBlockType(block.metadata?.id)) { - const { childTraceSpans: _, ...displayOutput } = filtered as { - childTraceSpans?: unknown - } & Record - return displayOutput - } - - return filtered - } - private callOnBlockStart(ctx: ExecutionContext, node: DAGNode, block: SerializedBlock): void { const blockId = node.id const blockName = block.metadata?.name ?? blockId diff --git a/apps/sim/executor/handlers/trigger/trigger-handler.ts b/apps/sim/executor/handlers/trigger/trigger-handler.ts index d9be91d23..e8d14f8a7 100644 --- a/apps/sim/executor/handlers/trigger/trigger-handler.ts +++ b/apps/sim/executor/handlers/trigger/trigger-handler.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { BlockType, isTriggerBehavior } from '@/executor/constants' +import { BlockType, isTriggerBehavior, isTriggerInternalKey } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' @@ -33,7 +33,12 @@ export class TriggerBlockHandler implements BlockHandler { const starterOutput = starterState.output if (starterOutput.webhook?.data) { - const { webhook, workflowId, ...cleanOutput } = starterOutput + const cleanOutput: Record = {} + for (const [key, value] of Object.entries(starterOutput)) { + if (!isTriggerInternalKey(key)) { + cleanOutput[key] = value + } + } return cleanOutput } diff --git a/apps/sim/executor/utils/output-filter.ts b/apps/sim/executor/utils/output-filter.ts new file mode 100644 index 000000000..6d205a710 --- /dev/null +++ b/apps/sim/executor/utils/output-filter.ts @@ -0,0 +1,54 @@ +import { getBlock } from '@/blocks' +import { isTriggerBehavior, isTriggerInternalKey } from '@/executor/constants' +import type { NormalizedBlockOutput } from '@/executor/types' +import type { SerializedBlock } from '@/serializer/types' + +/** + * Filters block output for logging/display purposes. + * Removes internal fields and fields marked with hiddenFromDisplay. + * + * @param blockType - The block type string (e.g., 'human_in_the_loop', 'workflow') + * @param output - The raw block output to filter + * @param options - Optional configuration + * @param options.block - Full SerializedBlock for trigger behavior detection + * @param options.additionalHiddenKeys - Extra keys to filter out (e.g., 'resume') + */ +export function filterOutputForLog( + blockType: string, + output: NormalizedBlockOutput, + options?: { + block?: SerializedBlock + additionalHiddenKeys?: string[] + } +): NormalizedBlockOutput { + const blockConfig = blockType ? getBlock(blockType) : undefined + const filtered: NormalizedBlockOutput = {} + const additionalHiddenKeys = options?.additionalHiddenKeys ?? [] + + for (const [key, value] of Object.entries(output)) { + // Skip internal keys (underscore prefix) + if (key.startsWith('_')) continue + + // Skip fields marked as hidden in block config + if (blockConfig?.outputs) { + const outputDef = blockConfig.outputs[key] + if (outputDef && typeof outputDef === 'object' && outputDef.hiddenFromDisplay) { + continue + } + } + + // Skip runtime-injected trigger keys not in block config + if (options?.block && isTriggerBehavior(options.block) && isTriggerInternalKey(key)) { + continue + } + + // Skip additional hidden keys specified by caller + if (additionalHiddenKeys.includes(key)) { + continue + } + + filtered[key] = value + } + + return filtered +} diff --git a/apps/sim/lib/workflows/blocks/block-outputs.ts b/apps/sim/lib/workflows/blocks/block-outputs.ts index b00156e0c..05eb96487 100644 --- a/apps/sim/lib/workflows/blocks/block-outputs.ts +++ b/apps/sim/lib/workflows/blocks/block-outputs.ts @@ -86,8 +86,8 @@ function evaluateOutputCondition( } /** - * Filters outputs based on their conditions. - * Returns a new OutputDefinition with only the outputs whose conditions are met. + * Filters outputs based on their conditions and hiddenFromDisplay flag. + * Returns a new OutputDefinition with only the outputs that should be shown. */ function filterOutputsByCondition( outputs: OutputDefinition, @@ -96,6 +96,16 @@ function filterOutputsByCondition( const filtered: OutputDefinition = {} for (const [key, value] of Object.entries(outputs)) { + // Skip fields marked as hidden from display + if ( + value && + typeof value === 'object' && + 'hiddenFromDisplay' in value && + value.hiddenFromDisplay + ) { + continue + } + if (!value || typeof value !== 'object' || !('condition' in value)) { filtered[key] = value continue @@ -105,7 +115,7 @@ function filterOutputsByCondition( const passes = !condition || evaluateOutputCondition(condition, subBlocks) if (passes) { - const { condition: _, ...rest } = value + const { condition: _, hiddenFromDisplay: __, ...rest } = value filtered[key] = rest } } @@ -259,50 +269,26 @@ export function getBlockOutputs( } if (blockType === 'human_in_the_loop') { - const hitlOutputs: OutputDefinition = { - url: { type: 'string', description: 'Resume UI URL' }, - resumeEndpoint: { - type: 'string', - description: 'Resume API endpoint URL for direct curl requests', - }, - } + // Start with block config outputs (respects hiddenFromDisplay via filterOutputsByCondition) + const baseOutputs = filterOutputsByCondition( + { ...(blockConfig.outputs || {}) } as OutputDefinition, + subBlocks + ) + // Add inputFormat fields (resume form fields) const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value) for (const field of normalizedInputFormat) { const fieldName = field?.name?.trim() if (!fieldName) continue - hitlOutputs[fieldName] = { + baseOutputs[fieldName] = { type: (field?.type || 'any') as any, - description: `Field from resume form`, + description: field?.description || `Field from resume form`, } } - return hitlOutputs - } - - if (blockType === 'approval') { - // Start with only url (apiUrl commented out - not accessible as output) - const pauseResumeOutputs: OutputDefinition = { - url: { type: 'string', description: 'Resume UI URL' }, - // apiUrl: { type: 'string', description: 'Resume API URL' }, // Commented out - not accessible as output - } - - const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value) - - // Add each input format field as a top-level output - for (const field of normalizedInputFormat) { - const fieldName = field?.name?.trim() - if (!fieldName) continue - - pauseResumeOutputs[fieldName] = { - type: (field?.type || 'any') as any, - description: `Field from input format`, - } - } - - return pauseResumeOutputs + return baseOutputs } if (startPath === StartBlockPath.LEGACY_STARTER) { diff --git a/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts b/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts index fe936920e..479ead99a 100644 --- a/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts +++ b/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts @@ -9,6 +9,7 @@ import { LoggingSession } from '@/lib/logs/execution/logging-session' import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core' import { ExecutionSnapshot } from '@/executor/execution/snapshot' import type { ExecutionResult, PausePoint, SerializedSnapshot } from '@/executor/types' +import { filterOutputForLog } from '@/executor/utils/output-filter' import type { SerializedConnection } from '@/serializer/types' const logger = createLogger('HumanInTheLoopManager') @@ -576,13 +577,11 @@ export class PauseResumeManager { log.blockId === contextId ) if (blockLogIndex !== -1) { - // Filter output for logging (exclude internal fields and response) - const filteredOutput: Record = {} - for (const [key, value] of Object.entries(mergedOutput)) { - if (key.startsWith('_')) continue - if (key === 'response') continue - filteredOutput[key] = value - } + // Filter output for logging using shared utility + // 'resume' is redundant with url/resumeEndpoint so we filter it out + const filteredOutput = filterOutputForLog('human_in_the_loop', mergedOutput, { + additionalHiddenKeys: ['resume'], + }) stateCopy.blockLogs[blockLogIndex] = { ...stateCopy.blockLogs[blockLogIndex], blockId: stateBlockKey,