Skip to content

Commit 2ff39d5

Browse files
committed
fix(workflows): scope the canonical sub-block index to the active surface
A block that is both an action and a trigger holds one `subBlocks` array — its own fields plus its trigger's, spread in after them. The two sets routinely share a `canonicalParamId` under different ids, so indexing them together collapses a trigger field into an action pair whose `basicId` it can never be. Every group-relative question about that field then answers for the dormant surface. The serializer was never affected: `shouldSerializeSubBlock` drops the inactive surface before the canonical collapse reads it, so it resolves against a value map the dormant surface cannot appear in. Every other caller resolves against the block's full value map, so the scoping has to live in the index. - add `getCanonicalSubBlocksForSurface` / `buildCanonicalIndexForSurface`, and move the three sites that already had the filter inline onto them - `getCardSubBlocks` derives its own index instead of accepting one; it already took `triggerMode`, and accepting an index is what let all three callers pass one built for the other surface - scope the remaining consumers that resolve against a full value map: the canvas card, autolayout, both preview surfaces, the dependsOn gate, the canonical value hook, reactive conditions, and the copilot selector lint - keep a canonical group with no advanced member out of the legacy `advancedMode` path, which deleted its basic member and republished nothing - merge legacy type-scoped tool modes as a baseline under index-scoped ones, so the first re-toggle stops reverting the ids the user has not touched
1 parent 7173a3f commit 2ff39d5

19 files changed

Lines changed: 404 additions & 65 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-canonical-sub-block-value.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { useCallback, useMemo } from 'react'
22
import { isEqual } from 'es-toolkit'
33
import { useStoreWithEqualityFn } from 'zustand/traditional'
4-
import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility'
4+
import {
5+
buildCanonicalIndexForSurface,
6+
resolveDependencyValue,
7+
} from '@/lib/workflows/subblocks/visibility'
58
import { getBlock } from '@/blocks/registry'
69
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
710
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -22,9 +25,10 @@ export function useCanonicalSubBlockValue<T = unknown>(
2225
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
2326
const blockState = useWorkflowStore((state) => state.blocks[blockId])
2427
const blockConfig = blockState?.type ? getBlock(blockState.type) : null
28+
const triggerSurface = blockState?.triggerMode === true
2529
const canonicalIndex = useMemo(
26-
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
27-
[blockConfig?.subBlocks]
30+
() => buildCanonicalIndexForSurface(blockConfig?.subBlocks || [], triggerSurface),
31+
[blockConfig?.subBlocks, triggerSurface]
2832
)
2933
const canonicalModeOverrides = blockState?.data?.canonicalModes
3034

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react'
44
import { isEqual } from 'es-toolkit'
55
import { useStoreWithEqualityFn } from 'zustand/traditional'
66
import {
7-
buildCanonicalIndex,
7+
buildCanonicalIndexForSurface,
88
isNonEmptyValue,
99
normalizeDependencyValue,
1010
parseDependsOn,
@@ -41,9 +41,15 @@ export function useDependsOnGate(
4141
: blockState?.type
4242
? getBlock(blockState.type)
4343
: null
44+
/**
45+
* A nested tool's params are always the ACTION surface — `dependencyBlockType` means
46+
* `blockConfig` describes the tool, not the host block, so the host's trigger mode says
47+
* nothing about which of the tool's fields are live.
48+
*/
49+
const triggerSurface = !dependencyBlockType && blockState?.triggerMode === true
4450
const canonicalIndex = useMemo(
45-
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
46-
[blockConfig?.subBlocks]
51+
() => buildCanonicalIndexForSurface(blockConfig?.subBlocks || [], triggerSurface),
52+
[blockConfig?.subBlocks, triggerSurface]
4753
)
4854
const canonicalModeOverrides = blockState?.data?.canonicalModes
4955

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ import { isRetryEligibleBlock } from '@/lib/workflows/blocks/retry-eligibility'
2323
import {
2424
buildCanonicalIndex,
2525
evaluateSubBlockCondition,
26+
getCanonicalSubBlocksForSurface,
2627
hasAdvancedValues,
2728
isCanonicalPair,
2829
isStandaloneAdvancedMode,
2930
resolveCanonicalMode,
30-
shouldUseSubBlockForTriggerModeCanonicalIndex,
3131
} from '@/lib/workflows/subblocks/visibility'
3232
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
3333
import {
@@ -157,11 +157,10 @@ export function Editor() {
157157
isEqual
158158
)
159159

160-
const subBlocksForCanonical = useMemo(() => {
161-
const subBlocks = blockConfig?.subBlocks || []
162-
if (!triggerMode) return subBlocks
163-
return subBlocks.filter(shouldUseSubBlockForTriggerModeCanonicalIndex)
164-
}, [blockConfig?.subBlocks, triggerMode])
160+
const subBlocksForCanonical = useMemo(
161+
() => getCanonicalSubBlocksForSurface(blockConfig?.subBlocks || [], triggerMode),
162+
[blockConfig?.subBlocks, triggerMode]
163+
)
165164

166165
const canonicalIndex = useMemo(
167166
() => buildCanonicalIndex(subBlocksForCanonical),

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { useCallback, useMemo } from 'react'
22
import {
3-
buildCanonicalIndex,
3+
buildCanonicalIndexForSurface,
44
evaluateSubBlockCondition,
55
isSubBlockFeatureEnabled,
66
isSubBlockHidden,
77
isSubBlockVisibleForMode,
88
isSubBlockVisibleForTriggerMode,
99
isToolInputOnlySubBlock,
10-
shouldUseSubBlockForTriggerModeCanonicalIndex,
1110
} from '@/lib/workflows/subblocks/visibility'
1211
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
1312
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -48,7 +47,8 @@ export function useEditorSubblockLayout(
4847
config?.subBlocks || [],
4948
blockId,
5049
activeWorkflowId,
51-
blockDataFromStore?.canonicalModes
50+
blockDataFromStore?.canonicalModes,
51+
displayTriggerMode
5252
)
5353

5454
return useMemo(() => {
@@ -102,10 +102,7 @@ export function useEditorSubblockLayout(
102102
{}
103103
)
104104

105-
const subBlocksForCanonical = displayTriggerMode
106-
? (config.subBlocks || []).filter(shouldUseSubBlockForTriggerModeCanonicalIndex)
107-
: config.subBlocks || []
108-
const canonicalIndex = buildCanonicalIndex(subBlocksForCanonical)
105+
const canonicalIndex = buildCanonicalIndexForSurface(config.subBlocks || [], displayTriggerMode)
109106
const effectiveAdvanced = displayAdvancedMode
110107
const canonicalModeOverrides = blockData?.canonicalModes
111108

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import {
6565
} from '@/lib/workflows/subblocks/display'
6666
import {
6767
buildCanonicalIndex,
68+
buildCanonicalIndexForSurface,
6869
hasAdvancedValues,
6970
resolveDependencyValue,
7071
} from '@/lib/workflows/subblocks/visibility'
@@ -805,14 +806,18 @@ export const WorkflowBlock = memo(function WorkflowBlock({
805806
])
806807
}
807808

808-
const canonicalIndex = useMemo(() => buildCanonicalIndex(config.subBlocks), [config.subBlocks])
809+
const canonicalIndex = useMemo(
810+
() => buildCanonicalIndexForSurface(config.subBlocks, displayTriggerMode),
811+
[config.subBlocks, displayTriggerMode]
812+
)
809813
const canonicalModeOverrides = currentStoreBlock?.data?.canonicalModes
810814

811815
const hiddenByReactiveCondition = useReactiveConditions(
812816
config.subBlocks,
813817
id,
814818
activeWorkflowId,
815-
canonicalModeOverrides
819+
canonicalModeOverrides,
820+
displayTriggerMode
816821
)
817822

818823
const subBlockRowsData = useMemo(() => {
@@ -859,7 +864,6 @@ export const WorkflowBlock = memo(function WorkflowBlock({
859864
const displayableSubBlocks = getCardSubBlocks(config, {
860865
advanced: effectiveAdvanced,
861866
values: rawValues,
862-
canonicalIndex,
863867
canonicalModeOverrides,
864868
triggerMode: effectiveTrigger,
865869
hiddenIds: hiddenByReactiveCondition,

apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { useParams } from 'next/navigation'
3030
import { ReactFlowProvider } from 'reactflow'
3131
import { extractReferencePrefixes } from '@/lib/workflows/sanitization/references'
3232
import {
33-
buildCanonicalIndex,
33+
buildCanonicalIndexForSurface,
3434
evaluateSubBlockCondition,
3535
hasAdvancedValues,
3636
isSubBlockFeatureEnabled,
@@ -1055,9 +1055,10 @@ function PreviewEditorContent({
10551055
}, {})
10561056
}, [subBlockValues])
10571057

1058+
const effectiveTrigger = block.triggerMode === true
10581059
const canonicalIndex = useMemo(
1059-
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
1060-
[blockConfig?.subBlocks]
1060+
() => buildCanonicalIndexForSurface(blockConfig?.subBlocks || [], effectiveTrigger),
1061+
[blockConfig?.subBlocks, effectiveTrigger]
10611062
)
10621063

10631064
const isSubflow = block.type === 'loop' || block.type === 'parallel'
@@ -1118,7 +1119,6 @@ function PreviewEditorContent({
11181119
hasAdvancedValues(blockConfig.subBlocks, rawValues, canonicalIndex)
11191120

11201121
const isPureTriggerBlock = blockConfig.triggers?.enabled && blockConfig.category === 'triggers'
1121-
const effectiveTrigger = block.triggerMode === true
11221122

11231123
const visibleSubBlocks = blockConfig.subBlocks.filter((subBlock) => {
11241124
if (subBlock.hidden || subBlock.hideFromPreview) return false

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
resolveWorkflowSelectionLabel,
2929
} from '@/lib/workflows/subblocks/display'
3030
import {
31-
buildCanonicalIndex,
31+
buildCanonicalIndexForSurface,
3232
evaluateSubBlockCondition,
3333
isSubBlockFeatureEnabled,
3434
isSubBlockVisibleForMode,
@@ -237,10 +237,11 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
237237
} = data
238238

239239
const blockConfig = getBlock(type)
240+
const effectiveTrigger = isTrigger || type === 'starter'
240241

241242
const canonicalIndex = useMemo(
242-
() => buildCanonicalIndex(blockConfig?.subBlocks || []),
243-
[blockConfig?.subBlocks]
243+
() => buildCanonicalIndexForSurface(blockConfig?.subBlocks || [], effectiveTrigger),
244+
[blockConfig?.subBlocks, effectiveTrigger]
244245
)
245246

246247
const rawValues = useMemo(() => {
@@ -267,7 +268,6 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
267268
if (!blockConfig?.subBlocks) return []
268269

269270
const isPureTriggerBlock = blockConfig.triggers?.enabled && blockConfig.category === 'triggers'
270-
const effectiveTrigger = isTrigger || type === 'starter'
271271

272272
return blockConfig.subBlocks.filter((subBlock) => {
273273
if (subBlock.hidden) return false
@@ -308,8 +308,7 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
308308
blockConfig?.subBlocks,
309309
blockConfig?.triggers?.enabled,
310310
blockConfig?.category,
311-
type,
312-
isTrigger,
311+
effectiveTrigger,
313312
canonicalIndex,
314313
rawValues,
315314
canvasPresentation,
@@ -348,7 +347,6 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
348347
* lightweight mode, which has no values to resolve chips from.
349348
*/
350349
const sentenceSegments = useMemo(() => {
351-
const effectiveTrigger = isTrigger || type === 'starter'
352350
if (lightweight || !blockConfig) return null
353351
if (type === 'condition' || type === 'router_v2' || type === 'starter') return null
354352

@@ -374,7 +372,7 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
374372
(subBlockId) => availableIds.has(subBlockId),
375373
(subBlockId) => onCardById.get(subBlockId) ?? null
376374
)
377-
}, [lightweight, blockConfig, type, isTrigger, visibleSubBlocks, onCardById, rawValues])
375+
}, [lightweight, blockConfig, type, effectiveTrigger, visibleSubBlocks, onCardById, rawValues])
378376

379377
/**
380378
* Compute condition rows for condition blocks.

apps/sim/hooks/use-reactive-conditions.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { useCallback, useMemo } from 'react'
22
import type { CanonicalModeOverrides } from '@/lib/workflows/subblocks/visibility'
3-
import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility'
3+
import {
4+
buildCanonicalIndexForSurface,
5+
resolveDependencyValue,
6+
} from '@/lib/workflows/subblocks/visibility'
47
import type { SubBlockConfig } from '@/blocks/types'
58
import { useWorkspaceCredential } from '@/hooks/queries/credentials'
69
import { EMPTY_BLOCK_SUBBLOCK_VALUES, useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -15,12 +18,22 @@ export function useReactiveConditions(
1518
subBlocks: SubBlockConfig[],
1619
blockId: string,
1720
activeWorkflowId: string | null,
18-
canonicalModeOverrides?: CanonicalModeOverrides
21+
canonicalModeOverrides?: CanonicalModeOverrides,
22+
triggerSurface = false
1923
): Set<string> {
2024
const reactiveSubBlock = useMemo(() => subBlocks.find((sb) => sb.reactiveCondition), [subBlocks])
2125
const reactiveCond = reactiveSubBlock?.reactiveCondition
2226

23-
const canonicalIndex = useMemo(() => buildCanonicalIndex(subBlocks), [subBlocks])
27+
/**
28+
* Scoped so a trigger-mode block watches its own credential. The only shipped reactive
29+
* condition (`SERVICE_ACCOUNT_SUBBLOCKS`) watches `oauthCredential`, which on Gmail, Drive,
30+
* Sheets, Forms and Calendar spans both surfaces under different ids — unscoped, trigger mode
31+
* resolves it to the dormant action credential and fetches the wrong one.
32+
*/
33+
const canonicalIndex = useMemo(
34+
() => buildCanonicalIndexForSurface(subBlocks, triggerSurface),
35+
[subBlocks, triggerSurface]
36+
)
2437

2538
// Resolve watchFields through canonical index to get the active credential value
2639
const watchedCredentialId = useSubBlockStore(

apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getSkillById } from '@/lib/workflows/skills/operations'
1111
import {
1212
buildCanonicalIndex,
1313
buildSubBlockValues,
14+
getCanonicalSubBlocksForSurface,
1415
isCanonicalPair,
1516
resolveCanonicalMode,
1617
} from '@/lib/workflows/subblocks/visibility'
@@ -1034,11 +1035,18 @@ function collectSelectorFields(
10341035
const blockConfig = getBlock(blockType)
10351036
if (!blockConfig) continue
10361037

1037-
const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks)
1038+
// Scoped to the block's active surface: a trigger field sharing a `canonicalParamId` with an
1039+
// action pair matches neither of its members, so an unscoped index skipped every trigger
1040+
// selector as "inactive" while still validating the dormant action ones.
1041+
const activeSubBlocks = getCanonicalSubBlocksForSurface(
1042+
blockConfig.subBlocks,
1043+
blockData.triggerMode === true
1044+
)
1045+
const canonicalIndex = buildCanonicalIndex(activeSubBlocks)
10381046
const allValues = buildSubBlockValues(blockData.subBlocks || {})
10391047
const canonicalModeOverrides = blockData.data?.canonicalModes
10401048

1041-
for (const subBlockConfig of blockConfig.subBlocks) {
1049+
for (const subBlockConfig of activeSubBlocks) {
10421050
if (!SELECTOR_TYPES.has(subBlockConfig.type)) continue
10431051

10441052
// oauth-input credentials are only validated when explicitly requested

apps/sim/lib/workflows/autolayout/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { calculateWorkflowBlockDimensions } from '@/lib/workflows/blocks/determi
2828
import { getConditionRows, getRouterRows } from '@/lib/workflows/dynamic-handle-topology'
2929
import { getDisplayValue, hasDisplayableRowValue } from '@/lib/workflows/subblocks/display'
3030
import {
31-
buildCanonicalIndex,
31+
buildCanonicalIndexForSurface,
3232
buildSubBlockValues,
3333
type CanonicalModeOverrides,
3434
evaluateSubBlockCondition,
@@ -198,9 +198,9 @@ function getVisiblePreviewSubBlocks(block: BlockState): {
198198
rawValues.__canonicalModes = canonicalModeOverrides
199199
}
200200

201-
const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks)
202201
const effectiveAdvanced = Boolean(block.advancedMode)
203202
const effectiveTrigger = Boolean(block.triggerMode)
203+
const canonicalIndex = buildCanonicalIndexForSurface(blockConfig.subBlocks, effectiveTrigger)
204204
const isPureTriggerBlock = blockConfig.triggers?.enabled && blockConfig.category === 'triggers'
205205

206206
const visibleSubBlocks = blockConfig.subBlocks.filter((subBlock) => {

0 commit comments

Comments
 (0)