Skip to content

Commit 8b5cac8

Browse files
committed
add workflow in workflow block func
1 parent 136bc13 commit 8b5cac8

9 files changed

Lines changed: 213 additions & 2 deletions

File tree

apps/sim/app/api/chat/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,7 @@ export async function executeWorkflowForChat(
610610
target: e.target,
611611
})),
612612
onStream,
613+
isDeployedContext: true,
613614
},
614615
})
615616

apps/sim/app/api/schedules/execute/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ export async function GET() {
379379
contextExtensions: {
380380
executionId,
381381
workspaceId: workflowRecord.workspaceId || '',
382+
isDeployedContext: true,
382383
},
383384
})
384385

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ async function executeWorkflow(
281281
contextExtensions: {
282282
executionId,
283283
workspaceId: workflow.workspaceId,
284+
isDeployedContext: true,
284285
},
285286
})
286287

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
2323
import { useCurrentWorkflow } from '../../hooks'
2424
import { ActionBar } from './components/action-bar/action-bar'
2525
import { ConnectionBlocks } from './components/connection-blocks/connection-blocks'
26+
import { useSubBlockValue } from './components/sub-block/hooks/use-sub-block-value'
2627
import { SubBlock } from './components/sub-block/sub-block'
2728

2829
const logger = createLogger('WorkflowBlock')
@@ -589,6 +590,80 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
589590
const shouldShowScheduleBadge =
590591
type === 'schedule' && !isLoadingScheduleInfo && scheduleInfo !== null
591592
const userPermissions = useUserPermissionsContext()
593+
const registryDeploymentStatuses = useWorkflowRegistry((state) => state.deploymentStatuses)
594+
const [childActiveVersion, setChildActiveVersion] = useState<number | null>(null)
595+
const [childIsDeployed, setChildIsDeployed] = useState<boolean>(false)
596+
const [isLoadingChildVersion, setIsLoadingChildVersion] = useState(false)
597+
598+
// Use the store directly for real-time updates when workflow dropdown changes
599+
const [workflowIdFromStore] = useSubBlockValue<string>(id, 'workflowId')
600+
601+
// Determine if this is a workflow block (child workflow selector) and fetch child status
602+
const isWorkflowSelector = type === 'workflow'
603+
let childWorkflowId: string | undefined
604+
if (!data.isPreview) {
605+
// Use store value for real-time updates
606+
const val = workflowIdFromStore
607+
if (typeof val === 'string' && val.trim().length > 0) {
608+
childWorkflowId = val
609+
}
610+
} else if (data.isPreview && data.subBlockValues?.workflowId?.value) {
611+
const val = data.subBlockValues.workflowId.value
612+
if (typeof val === 'string' && val.trim().length > 0) childWorkflowId = val
613+
}
614+
615+
const childDeployment = childWorkflowId ? registryDeploymentStatuses[childWorkflowId] : null
616+
617+
// Fetch active deployment version for the selected child workflow
618+
useEffect(() => {
619+
let cancelled = false
620+
const fetchActiveVersion = async (wfId: string) => {
621+
try {
622+
setIsLoadingChildVersion(true)
623+
const res = await fetch(`/api/workflows/${wfId}/deployments`, {
624+
cache: 'no-store',
625+
headers: { 'Cache-Control': 'no-cache' },
626+
})
627+
if (!res.ok) {
628+
if (!cancelled) {
629+
setChildActiveVersion(null)
630+
setChildIsDeployed(false)
631+
}
632+
return
633+
}
634+
const json = await res.json()
635+
const versions = Array.isArray(json?.data?.versions)
636+
? json.data.versions
637+
: Array.isArray(json?.versions)
638+
? json.versions
639+
: []
640+
const active = versions.find((v: any) => v.isActive)
641+
if (!cancelled) {
642+
const v = active ? Number(active.version) : null
643+
setChildActiveVersion(v)
644+
setChildIsDeployed(v != null)
645+
}
646+
} catch {
647+
if (!cancelled) {
648+
setChildActiveVersion(null)
649+
setChildIsDeployed(false)
650+
}
651+
} finally {
652+
if (!cancelled) setIsLoadingChildVersion(false)
653+
}
654+
}
655+
656+
// Always fetch when childWorkflowId changes
657+
if (childWorkflowId) {
658+
void fetchActiveVersion(childWorkflowId)
659+
} else {
660+
setChildActiveVersion(null)
661+
setChildIsDeployed(false)
662+
}
663+
return () => {
664+
cancelled = true
665+
}
666+
}, [childWorkflowId])
592667

593668
return (
594669
<div className='group relative'>
@@ -702,6 +777,31 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
702777
</div>
703778
</div>
704779
<div className='flex flex-shrink-0 items-center gap-2'>
780+
{isWorkflowSelector && childWorkflowId && (
781+
<Tooltip>
782+
<TooltipTrigger asChild>
783+
<div className='relative mr-1 flex items-center justify-center'>
784+
<div
785+
className={cn(
786+
'h-2.5 w-2.5 rounded-full',
787+
childIsDeployed ? 'bg-green-500' : 'bg-red-500'
788+
)}
789+
/>
790+
</div>
791+
</TooltipTrigger>
792+
<TooltipContent side='top' className='py-2 px-3'>
793+
<span className='text-sm'>
794+
{childIsDeployed
795+
? isLoadingChildVersion
796+
? 'Deployed'
797+
: childActiveVersion != null
798+
? `Deployed (v${childActiveVersion})`
799+
: 'Deployed'
800+
: 'Not Deployed'}
801+
</span>
802+
</TooltipContent>
803+
</Tooltip>
804+
)}
705805
{!isEnabled && (
706806
<Badge variant='secondary' className='bg-gray-100 text-gray-500 hover:bg-gray-100'>
707807
Disabled

apps/sim/background/webhook-execution.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ export async function executeWebhookJob(payload: WebhookExecutionPayload) {
174174
contextExtensions: {
175175
executionId,
176176
workspaceId: workspaceId || '',
177+
isDeployedContext: true,
177178
},
178179
})
179180

@@ -286,6 +287,7 @@ export async function executeWebhookJob(payload: WebhookExecutionPayload) {
286287
contextExtensions: {
287288
executionId,
288289
workspaceId: workspaceId || '',
290+
isDeployedContext: true,
289291
},
290292
})
291293

apps/sim/background/workflow-execution.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export async function executeWorkflowJob(payload: WorkflowExecutionPayload) {
127127
contextExtensions: {
128128
executionId,
129129
workspaceId: workspaceId || '',
130+
isDeployedContext: true,
130131
},
131132
})
132133

apps/sim/executor/handlers/workflow/workflow-handler.ts

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,20 @@ export class WorkflowBlockHandler implements BlockHandler {
5656
// Add current execution to stack
5757
WorkflowBlockHandler.executionStack.add(executionId)
5858

59-
// Load the child workflow from API
60-
const childWorkflow = await this.loadChildWorkflow(workflowId)
59+
// In deployed contexts, enforce that child workflow has an active deployment
60+
if (context.isDeployedContext) {
61+
const hasActiveDeployment = await this.checkChildDeployment(workflowId)
62+
if (!hasActiveDeployment) {
63+
throw new Error(
64+
`Child workflow is not deployed. Please deploy the workflow before invoking it.`
65+
)
66+
}
67+
}
68+
69+
// Load the child workflow
70+
const childWorkflow = context.isDeployedContext
71+
? await this.loadChildWorkflowDeployed(workflowId)
72+
: await this.loadChildWorkflow(workflowId)
6173

6274
if (!childWorkflow) {
6375
throw new Error(`Child workflow ${workflowId} not found`)
@@ -93,6 +105,8 @@ export class WorkflowBlockHandler implements BlockHandler {
93105
workflowVariables: childWorkflow.variables || {},
94106
contextExtensions: {
95107
isChildExecution: true, // Prevent child executor from managing global state
108+
// Propagate deployed context down to child execution so nested children obey constraints
109+
isDeployedContext: context.isDeployedContext === true,
96110
},
97111
})
98112

@@ -214,6 +228,91 @@ export class WorkflowBlockHandler implements BlockHandler {
214228
}
215229
}
216230

231+
/**
232+
* Checks if a workflow has an active deployed version
233+
*/
234+
private async checkChildDeployment(workflowId: string): Promise<boolean> {
235+
try {
236+
const headers: Record<string, string> = {
237+
'Content-Type': 'application/json',
238+
}
239+
if (typeof window === 'undefined') {
240+
const token = await generateInternalToken()
241+
headers.Authorization = `Bearer ${token}`
242+
}
243+
const response = await fetch(`${getBaseUrl()}/api/workflows/${workflowId}/deployed`, {
244+
headers,
245+
cache: 'no-store',
246+
})
247+
if (!response.ok) return false
248+
const json = await response.json()
249+
// API returns { deployedState: state | null }
250+
return !!json?.data?.deployedState || !!json?.deployedState
251+
} catch (e) {
252+
logger.error(`Failed to check child deployment for ${workflowId}:`, e)
253+
return false
254+
}
255+
}
256+
257+
/**
258+
* Loads child workflow using deployed state (for API/webhook/schedule/chat executions)
259+
*/
260+
private async loadChildWorkflowDeployed(workflowId: string) {
261+
try {
262+
const headers: Record<string, string> = {
263+
'Content-Type': 'application/json',
264+
}
265+
if (typeof window === 'undefined') {
266+
const token = await generateInternalToken()
267+
headers.Authorization = `Bearer ${token}`
268+
}
269+
270+
// Fetch deployed state
271+
const deployedRes = await fetch(`${getBaseUrl()}/api/workflows/${workflowId}/deployed`, {
272+
headers,
273+
cache: 'no-store',
274+
})
275+
if (!deployedRes.ok) {
276+
return null
277+
}
278+
const deployedJson = await deployedRes.json()
279+
const deployedState = deployedJson?.data?.deployedState || deployedJson?.deployedState
280+
if (!deployedState || !deployedState.blocks) {
281+
return null
282+
}
283+
284+
// Fetch variables and name from live metadata (variables are not stored in deployments)
285+
const metaRes = await fetch(`${getBaseUrl()}/api/workflows/${workflowId}`, {
286+
headers,
287+
cache: 'no-store',
288+
})
289+
if (!metaRes.ok) {
290+
return null
291+
}
292+
const metaJson = await metaRes.json()
293+
const wfData = metaJson?.data
294+
295+
const serializedWorkflow = this.serializer.serializeWorkflow(
296+
deployedState.blocks,
297+
deployedState.edges || [],
298+
deployedState.loops || {},
299+
deployedState.parallels || {},
300+
true
301+
)
302+
303+
const workflowVariables = (wfData?.variables as Record<string, any>) || {}
304+
305+
return {
306+
name: wfData?.name || 'Workflow',
307+
serializedState: serializedWorkflow,
308+
variables: workflowVariables,
309+
}
310+
} catch (error) {
311+
logger.error(`Error loading deployed child workflow ${workflowId}:`, error)
312+
return null
313+
}
314+
}
315+
217316
/**
218317
* Captures and transforms child workflow logs into trace spans
219318
*/

apps/sim/executor/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ export class Executor {
9999
executionId?: string
100100
workspaceId?: string
101101
isChildExecution?: boolean
102+
// Marks executions that must use deployed constraints (API/webhook/schedule/chat)
103+
isDeployedContext?: boolean
102104
}
103105
},
104106
private initialBlockStates: Record<string, BlockOutput> = {},
@@ -718,6 +720,7 @@ export class Executor {
718720
workflowId,
719721
workspaceId: this.contextExtensions.workspaceId,
720722
executionId: this.contextExtensions.executionId,
723+
isDeployedContext: this.contextExtensions.isDeployedContext || false,
721724
blockStates: new Map(),
722725
blockLogs: [],
723726
metadata: {

apps/sim/executor/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ export interface ExecutionContext {
103103
workflowId: string // Unique identifier for this workflow execution
104104
workspaceId?: string // Workspace ID for file storage scoping
105105
executionId?: string // Unique execution ID for file storage scoping
106+
// Whether this execution is running against deployed state (API/webhook/schedule/chat)
107+
// Manual executions in the builder should leave this undefined/false
108+
isDeployedContext?: boolean
106109
blockStates: Map<string, BlockState>
107110
blockLogs: BlockLog[] // Chronological log of block executions
108111
metadata: ExecutionMetadata // Timing metadata for the execution

0 commit comments

Comments
 (0)