@@ -6,12 +6,20 @@ import {
66 StartBlockPath ,
77} from '@/lib/workflows/triggers/triggers'
88import type { InputFormatField } from '@/lib/workflows/types'
9- import type { NormalizedBlockOutput , UserFile } from '@/executor/types'
9+ import {
10+ EXECUTION_CONTROL_OUTPUT_FIELD_NAMES ,
11+ type NormalizedBlockOutput ,
12+ type UserFile ,
13+ } from '@/executor/types'
1014import type { SerializedBlock } from '@/serializer/types'
1115import { safeAssign } from '@/tools/safe-assign'
1216
1317type ExecutionKind = 'chat' | 'manual' | 'api' | 'external'
1418
19+ const EXECUTION_CONTROL_OUTPUT_FIELD_NAME_SET = new Set < string > (
20+ EXECUTION_CONTROL_OUTPUT_FIELD_NAMES
21+ )
22+
1523export interface ExecutorStartResolution {
1624 blockId : string
1725 block : SerializedBlock
@@ -133,6 +141,66 @@ function extractInputFormat(block: SerializedBlock): InputFormatField[] {
133141 . map ( ( field ) => field )
134142}
135143
144+ function readInputFormatFieldName ( field : InputFormatField ) : string | undefined {
145+ return typeof field . name === 'string' ? field . name . trim ( ) : undefined
146+ }
147+
148+ function collectExecutionControlFieldNames ( fieldNames : Iterable < string | undefined > ) : string [ ] {
149+ const reservedFieldNames : string [ ] = [ ]
150+
151+ for ( const fieldName of fieldNames ) {
152+ if ( ! fieldName || ! EXECUTION_CONTROL_OUTPUT_FIELD_NAME_SET . has ( fieldName ) ) {
153+ continue
154+ }
155+
156+ if ( ! reservedFieldNames . includes ( fieldName ) ) {
157+ reservedFieldNames . push ( fieldName )
158+ }
159+ }
160+
161+ return reservedFieldNames
162+ }
163+
164+ function throwReservedStartOutputFieldsError (
165+ block : SerializedBlock ,
166+ reservedFieldNames : string [ ] ,
167+ source : 'input format' | 'runtime input'
168+ ) : never {
169+ const blockName = block . metadata ?. name ?? block . id
170+
171+ throw new Error (
172+ `Start block "${ blockName } " cannot use reserved ${ source } field name(s): ${ reservedFieldNames . join ( ', ' ) } . These names control workflow execution and cannot be used as Start outputs. Rename these fields before running the workflow. Reserved names are: ${ EXECUTION_CONTROL_OUTPUT_FIELD_NAMES . join ( ', ' ) } .`
173+ )
174+ }
175+
176+ function assertNoReservedInputFormatFields (
177+ inputFormat : InputFormatField [ ] ,
178+ block : SerializedBlock
179+ ) : void {
180+ const reservedFieldNames = collectExecutionControlFieldNames (
181+ inputFormat . map ( readInputFormatFieldName )
182+ )
183+
184+ if ( reservedFieldNames . length === 0 ) {
185+ return
186+ }
187+
188+ throwReservedStartOutputFieldsError ( block , reservedFieldNames , 'input format' )
189+ }
190+
191+ function assertNoReservedStartOutputFields (
192+ output : NormalizedBlockOutput ,
193+ block : SerializedBlock
194+ ) : void {
195+ const reservedFieldNames = collectExecutionControlFieldNames ( Object . keys ( output ) )
196+
197+ if ( reservedFieldNames . length === 0 ) {
198+ return
199+ }
200+
201+ throwReservedStartOutputFieldsError ( block , reservedFieldNames , 'runtime input' )
202+ }
203+
136204export function coerceValue ( type : string | null | undefined , value : unknown ) : unknown {
137205 if ( value === undefined || value === null ) {
138206 return value
@@ -190,7 +258,7 @@ function deriveInputFromFormat(
190258 }
191259
192260 for ( const field of inputFormat ) {
193- const fieldName = field . name ?. trim ( )
261+ const fieldName = readInputFormatFieldName ( field )
194262 if ( ! fieldName ) continue
195263
196264 let fieldValue : unknown
@@ -436,35 +504,48 @@ export interface StartBlockOutputOptions {
436504export function buildStartBlockOutput ( options : StartBlockOutputOptions ) : NormalizedBlockOutput {
437505 const { resolution, workflowInput } = options
438506 const inputFormat = extractInputFormat ( resolution . block )
507+ assertNoReservedInputFormatFields ( inputFormat , resolution . block )
439508 const { finalInput, structuredInput, hasStructured } = deriveInputFromFormat (
440509 inputFormat ,
441510 workflowInput
442511 )
443512
513+ let output : NormalizedBlockOutput
514+
444515 switch ( resolution . path ) {
445516 case StartBlockPath . UNIFIED :
446- return buildUnifiedStartOutput ( workflowInput , structuredInput , hasStructured )
517+ output = buildUnifiedStartOutput ( workflowInput , structuredInput , hasStructured )
518+ break
447519
448520 case StartBlockPath . SPLIT_API :
449521 case StartBlockPath . SPLIT_INPUT :
450- return buildApiOrInputOutput ( finalInput , workflowInput )
522+ output = buildApiOrInputOutput ( finalInput , workflowInput )
523+ break
451524
452525 case StartBlockPath . SPLIT_CHAT :
453- return buildChatOutput ( workflowInput )
526+ output = buildChatOutput ( workflowInput )
527+ break
454528
455529 case StartBlockPath . SPLIT_MANUAL :
456- return buildManualTriggerOutput ( finalInput , workflowInput )
530+ output = buildManualTriggerOutput ( finalInput , workflowInput )
531+ break
457532
458533 case StartBlockPath . EXTERNAL_TRIGGER :
459- return buildIntegrationTriggerOutput ( workflowInput , structuredInput , hasStructured )
534+ output = buildIntegrationTriggerOutput ( workflowInput , structuredInput , hasStructured )
535+ break
460536
461537 case StartBlockPath . LEGACY_STARTER :
462- return buildLegacyStarterOutput (
538+ output = buildLegacyStarterOutput (
463539 finalInput ,
464540 workflowInput ,
465541 getLegacyStarterMode ( { subBlocks : extractSubBlocks ( resolution . block ) } )
466542 )
543+ break
544+
467545 default :
468- return buildManualTriggerOutput ( finalInput , workflowInput )
546+ output = buildManualTriggerOutput ( finalInput , workflowInput )
469547 }
548+
549+ assertNoReservedStartOutputFields ( output , resolution . block )
550+ return output
470551}
0 commit comments