fix(condition): stop shipping all block outputs in every evaluation (#6468)

ConditionBlockHandler forwarded collectBlockData's full blockData — every
block output accumulated so far in the run — to function_execute on each
condition evaluation. The resolver already inlines every <block.field>
reference into the expression before the handler runs, so that payload was
never read; it only inflated the request body.

Inside a wide subflow one flat blockStates map holds every branch's outputs,
so a 91-branch parallel pushed the body past the 10MB cap and failed the
gate with "Request body size limit exceeded" even though the expression was
just a boolean compare. Per-value large-value offload does not catch this:
its threshold is 8MB for a single value, while this is an aggregate of many
medium ones.

Mirrors FunctionBlockHandler, which moved to blockData: {} in #4560 and left
the condition handler on the old path.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vikhyath Mondreti
2026-08-08 18:36:39 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent 220495c196
commit 19c3171560
2 changed files with 27 additions and 3 deletions
@@ -179,7 +179,7 @@ describe('ConditionBlockHandler', () => {
timeout: 5000,
envVars: mockContext.environmentVariables,
workflowVariables: mockContext.workflowVariables,
blockData: { 'source-block-1': { value: 10, text: 'hello' } },
blockData: {},
blockNameMapping: { sourceblock: 'source-block-1' },
_context: {
workflowId: 'test-workflow-id',
@@ -190,6 +190,24 @@ describe('ConditionBlockHandler', () => {
)
})
it('should never forward collected block outputs in the request body', async () => {
mockCollectBlockData.mockReturnValueOnce({
blockData: { 'huge-block': { payload: 'x'.repeat(1024) } },
blockNameMapping: { hugeblock: 'huge-block' },
})
mockExecuteTool.mockResolvedValueOnce({ success: true, output: { result: true } })
const conditions = [
{ id: 'cond1', title: 'if', value: 'true' },
{ id: 'else1', title: 'else', value: '' },
]
await handler.execute(mockContext, mockBlock, { conditions: JSON.stringify(conditions) })
const [, toolParams] = mockExecuteTool.mock.calls[0]
expect(toolParams.blockData).toEqual({})
})
it('should select the else path if other conditions fail', async () => {
mockExecuteTool.mockResolvedValueOnce({ success: true, output: { result: false } })
@@ -22,6 +22,12 @@ const CONDITION_TIMEOUT_MS = 5000
* Evaluates a single condition expression.
* The resolver preserves legacy Condition expression substitution before this function executes the
* resulting JavaScript through the shared function execution boundary.
*
* `blockData` is deliberately empty: the resolver already inlines every `<block.field>` reference
* into the expression before this runs, so shipping the run's accumulated block outputs would only
* inflate the request body. Sending them blew the 10MB body cap on wide subflows, where a single
* flat `blockStates` map holds every branch's outputs.
*
* Returns true if condition is met, false otherwise.
*/
async function evaluateConditionExpression(
@@ -36,7 +42,7 @@ async function evaluateConditionExpression(
const contextSetup = `const context = ${JSON.stringify(evalContext)};`
const code = `${contextSetup}\nreturn Boolean(${conditionExpression})`
const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx, currentNodeId)
const { blockNameMapping, blockOutputSchemas } = collectBlockData(ctx, currentNodeId)
const result = await executeTool(
'function_execute',
@@ -45,7 +51,7 @@ async function evaluateConditionExpression(
timeout: CONDITION_TIMEOUT_MS,
envVars: normalizeStringRecord(ctx.environmentVariables),
workflowVariables: normalizeWorkflowVariables(ctx.workflowVariables),
blockData,
blockData: {},
blockNameMapping,
blockOutputSchemas,
_context: {