Allow run from block for triggers

This commit is contained in:
Siddharth Ganesan
2026-01-27 12:50:16 -08:00
parent 8dc45e6e7e
commit 415acda403
3 changed files with 36 additions and 30 deletions

View File

@@ -112,19 +112,13 @@ export const ActionBar = memo(
const isInsideSubflow = parentId && (parentType === 'loop' || parentType === 'parallel')
const snapshot = activeWorkflowId ? getLastExecutionSnapshot(activeWorkflowId) : null
const hasExecutionSnapshot = !!snapshot
const dependenciesSatisfied = (() => {
if (!snapshot) return false
const incomingEdges = edges.filter((edge) => edge.target === blockId)
if (incomingEdges.length === 0) return true
return incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source))
})()
const incomingEdges = edges.filter((edge) => edge.target === blockId)
const isTriggerBlock = incomingEdges.length === 0
const dependenciesSatisfied =
isTriggerBlock ||
(snapshot && incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source)))
const canRunFromBlock =
hasExecutionSnapshot &&
dependenciesSatisfied &&
!isNoteBlock &&
!isInsideSubflow &&
!isExecuting
dependenciesSatisfied && !isNoteBlock && !isInsideSubflow && !isExecuting
const handleRunFromBlockClick = useCallback(() => {
if (!activeWorkflowId || !canRunFromBlock) return
@@ -176,9 +170,8 @@ export const ActionBar = memo(
{(() => {
if (disabled) return getTooltipMessage('Run from this block')
if (isExecuting) return 'Execution in progress'
if (!hasExecutionSnapshot) return 'Run workflow first'
if (!dependenciesSatisfied) return 'Run upstream blocks first'
if (isInsideSubflow) return 'Cannot run from inside subflow'
if (!dependenciesSatisfied) return 'Run upstream blocks first'
return 'Run from this block'
})()}
</Tooltip.Content>

View File

@@ -1435,16 +1435,18 @@ export function useWorkflowExecution() {
const handleRunFromBlock = useCallback(
async (blockId: string, workflowId: string) => {
const snapshot = getLastExecutionSnapshot(workflowId)
if (!snapshot) {
const workflowEdges = useWorkflowStore.getState().edges
const incomingEdges = workflowEdges.filter((edge) => edge.target === blockId)
const isTriggerBlock = incomingEdges.length === 0
if (!snapshot && !isTriggerBlock) {
logger.error('No execution snapshot available for run-from-block', { workflowId, blockId })
return
}
const workflowEdges = useWorkflowStore.getState().edges
const incomingEdges = workflowEdges.filter((edge) => edge.target === blockId)
const dependenciesSatisfied =
incomingEdges.length === 0 ||
incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source))
isTriggerBlock ||
(snapshot && incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source)))
if (!dependenciesSatisfied) {
logger.error('Upstream dependencies not satisfied for run-from-block', {
@@ -1454,10 +1456,20 @@ export function useWorkflowExecution() {
return
}
// For trigger blocks with no snapshot, create an empty one
const effectiveSnapshot: SerializableExecutionState = snapshot || {
blockStates: {},
executedBlocks: [],
blockLogs: [],
decisions: { router: {}, condition: {} },
completedLoops: [],
activeExecutionPath: [],
}
logger.info('Starting run-from-block execution', {
workflowId,
startBlockId: blockId,
snapshotExecutedBlocks: snapshot.executedBlocks.length,
isTriggerBlock,
})
setIsExecuting(true)
@@ -1471,7 +1483,7 @@ export function useWorkflowExecution() {
await executionStream.executeFromBlock({
workflowId,
startBlockId: blockId,
sourceSnapshot: snapshot,
sourceSnapshot: effectiveSnapshot,
callbacks: {
onExecutionStarted: (data) => {
logger.info('Run-from-block execution started:', data)
@@ -1579,21 +1591,23 @@ export function useWorkflowExecution() {
onExecutionCompleted: (data) => {
if (data.success) {
const mergedBlockStates: Record<string, BlockState> = { ...snapshot.blockStates }
const mergedBlockStates: Record<string, BlockState> = {
...effectiveSnapshot.blockStates,
}
for (const [bId, state] of accumulatedBlockStates) {
mergedBlockStates[bId] = state
}
const mergedExecutedBlocks = new Set([
...snapshot.executedBlocks,
...effectiveSnapshot.executedBlocks,
...executedBlockIds,
])
const updatedSnapshot: SerializableExecutionState = {
...snapshot,
...effectiveSnapshot,
blockStates: mergedBlockStates,
executedBlocks: Array.from(mergedExecutedBlocks),
blockLogs: [...snapshot.blockLogs, ...accumulatedBlockLogs],
blockLogs: [...effectiveSnapshot.blockLogs, ...accumulatedBlockLogs],
activeExecutionPath: Array.from(mergedExecutedBlocks),
}
setLastExecutionSnapshot(workflowId, updatedSnapshot)

View File

@@ -1012,18 +1012,17 @@ const WorkflowContent = React.memo(() => {
}
const block = contextMenuBlocks[0]
const snapshot = getLastExecutionSnapshot(workflowIdParam)
if (!snapshot) return { canRun: false, reason: 'Run workflow first' }
const incomingEdges = edges.filter((edge) => edge.target === block.id)
const isTriggerBlock = incomingEdges.length === 0
const dependenciesSatisfied =
incomingEdges.length === 0 ||
incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source))
isTriggerBlock ||
(snapshot && incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source)))
const isNoteBlock = block.type === 'note'
const isInsideSubflow =
block.parentId && (block.parentType === 'loop' || block.parentType === 'parallel')
if (!dependenciesSatisfied) return { canRun: false, reason: 'Run upstream blocks first' }
if (isInsideSubflow) return { canRun: false, reason: 'Cannot run from inside subflow' }
if (!dependenciesSatisfied) return { canRun: false, reason: 'Run upstream blocks first' }
if (isNoteBlock) return { canRun: false, reason: undefined }
if (isExecuting) return { canRun: false, reason: undefined }