11import { createLogger } from '@sim/logger'
22import { generateId , isValidUuid } from '@sim/utils/id'
33import { sortObjectKeysDeep } from '@sim/utils/object'
4+ import {
5+ type BlockRetryConfig ,
6+ normalizeBlockRetryTries ,
7+ normalizeBlockRetryWaitMs ,
8+ } from '@sim/workflow-types/workflow'
49import { isIntegrationDeploymentAvailableForVisibility } from '@/lib/integrations/availability.server'
510import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
611import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
12+ import { isRetryEligibleBlock } from '@/lib/workflows/blocks/retry-eligibility'
713import {
814 buildCanonicalIndex ,
915 buildDefaultCanonicalModes ,
@@ -22,6 +28,73 @@ import {
2228 validateTargetHandle ,
2329} from './validation'
2430
31+ /**
32+ * Merges a requested retry policy onto whatever the block already had, clamped
33+ * to the executor's bounds.
34+ *
35+ * `enabled` is optional and falls back to the block's current state (or `true`
36+ * for a block with no policy yet), so `{maxTries: 4}` reads as "retry four
37+ * times" rather than silently storing a disabled policy. Numbers are kept when
38+ * only `enabled` changes, matching the editor: toggling retry off and back on
39+ * restores what was configured instead of resetting to the defaults.
40+ *
41+ * Clamped through the shared normalizers rather than rejected, so a value that
42+ * drifts outside the bounds still yields a runnable policy — same contract the
43+ * editor and executor already follow.
44+ */
45+ export function resolveBlockRetryUpdate (
46+ requested : Partial < BlockRetryConfig > ,
47+ existing : BlockRetryConfig | undefined
48+ ) : BlockRetryConfig {
49+ return {
50+ enabled :
51+ typeof requested . enabled === 'boolean' ? requested . enabled : ( existing ?. enabled ?? true ) ,
52+ maxTries : normalizeBlockRetryTries ( requested . maxTries ?? existing ?. maxTries ) ,
53+ waitBetweenTriesMs : normalizeBlockRetryWaitMs (
54+ requested . waitBetweenTriesMs ?? existing ?. waitBetweenTriesMs
55+ ) ,
56+ }
57+ }
58+
59+ /**
60+ * Applies a requested retry policy to a block, or records why it could not be.
61+ *
62+ * Eligibility is checked with the same predicate the executor uses, so a policy
63+ * the runtime would ignore (triggers, human-in-the-loop, sentinels) is reported
64+ * back instead of being written as dead configuration.
65+ */
66+ export function applyBlockRetry (
67+ block : any ,
68+ requested : unknown ,
69+ context : { operationType : string ; blockId : string ; skippedItems ?: SkippedItem [ ] }
70+ ) : void {
71+ if ( requested === null ) {
72+ block . retry = undefined
73+ return
74+ }
75+ if ( typeof requested !== 'object' || Array . isArray ( requested ) ) return
76+
77+ if (
78+ ! isRetryEligibleBlock ( {
79+ blockType : block . type ,
80+ category : getBlock ( block . type ) ?. category ,
81+ triggerMode : block . triggerMode ,
82+ } )
83+ ) {
84+ if ( context . skippedItems ) {
85+ logSkippedItem ( context . skippedItems , {
86+ type : 'retry_not_supported' ,
87+ operationType : context . operationType ,
88+ blockId : context . blockId ,
89+ reason : `Block "${ context . blockId } " (${ block . type } ) cannot retry - triggers, human-in-the-loop, and container blocks always run once` ,
90+ } )
91+ }
92+ return
93+ }
94+
95+ block . retry = resolveBlockRetryUpdate ( requested as Partial < BlockRetryConfig > , block . retry )
96+ }
97+
2598/**
2699 * Helper to create a block state from operation params
27100 */
@@ -88,6 +161,16 @@ export function createBlockFromParams(
88161 locked : false ,
89162 }
90163
164+ // Block-level setting like `enabled`, not a subBlock input — the executor
165+ // reads it from block state when wrapping the run, never from the tool params.
166+ if ( params . retry !== undefined ) {
167+ applyBlockRetry ( blockState , params . retry , {
168+ operationType : 'add' ,
169+ blockId,
170+ skippedItems,
171+ } )
172+ }
173+
91174 // Add validated inputs as subBlocks
92175 if ( validatedInputs ) {
93176 Object . entries ( validatedInputs ) . forEach ( ( [ key , value ] ) => {
0 commit comments