Found while implementing #9510 (the missing isSuspendSignal arm on the same method). Filed rather than fixed there: different defect class — a divergent run environment, not a lost durable pause — and repairing it changes what every retry attempt does, pausing or not, which #9510's ruling does not cover.
Measured
packages/services/service-automation/src/engine.ts, origin/main @ ed4ca5999.
execute() seeds the run's variable map with five things executeWithoutRetry() does not:
// execute() only:variables.set('record',context.record);// + every record field flattened,// so bare `status` / `budget` resolvevariables.set('previous',context?.previous??null);variables.set('$runId',runId);variables.set('$flowName',flowName);variables.set('$flowLabel',flow.label??flowName);executeWithoutRetry() — the method retryExecution re-runs the flow through on every retry attempt — seeds only seedDeclaredVariables(flow, context) and $record.
Observed directly: two runs of one flow, pausing on attempt 1 vs on attempt 2, produce continuations whose variable snapshots differ by exactly $runId, $flowName, $flowLabel, previous — present on the first-attempt run, absent on the retry.
Why it matters
Conditions are strict CEL, where reading an unbound name ABORTS the predicate (Unknown variable: X) rather than yielding false — the mechanism #4697 documents on seedDeclaredVariables. So on a retry attempt:
Every one of these is reachable exactly where retry is most used: errorHandling.strategy: 'retry' on a record-change flow.
Not decided here
Whether the repair is to hoist the shared seeding into one helper both methods call (the obvious reading — the two methods already duplicate the disabled / no-start-node / failure exits and have drifted once per card: #9378, #9415, #9414, #9510), or to make executeWithoutRetry take the already-seeded map from its caller. The first is more in keeping with buildRunTrigger's chokepoint precedent, but it touches the first-attempt path too, so it wants a ruling rather than a guess.
Refs
Found while implementing #9510 (the missing
isSuspendSignalarm on the same method). Filed rather than fixed there: different defect class — a divergent run environment, not a lost durable pause — and repairing it changes what every retry attempt does, pausing or not, which #9510's ruling does not cover.Measured
packages/services/service-automation/src/engine.ts,origin/main@ed4ca5999.execute()seeds the run's variable map with five thingsexecuteWithoutRetry()does not:executeWithoutRetry()— the methodretryExecutionre-runs the flow through on every retry attempt — seeds onlyseedDeclaredVariables(flow, context)and$record.Observed directly: two runs of one flow, pausing on attempt 1 vs on attempt 2, produce continuations whose variable snapshots differ by exactly
$runId,$flowName,$flowLabel,previous— present on the first-attempt run, absent on the retry.Why it matters
Conditions are strict CEL, where reading an unbound name ABORTS the predicate (
Unknown variable: X) rather than yieldingfalse— the mechanism #4697 documents onseedDeclaredVariables. So on a retry attempt:record-after-updateflow whose start condition or edge predicate readsprevious(the create-vs-update discriminator record_change flow start node binds to a single lifecycle event — no create-OR-update in one flow #3427 bound deliberately, "always, even tonull") aborts — the retry fails for a reason attempt 1 never hit, which reads as a flaky flow rather than a defect;status/budgetreference to a triggering-record field aborts for the same reason (the flattening isexecute()-only);$runIdis undefined for a pausing node on a retry attempt. Its documented purpose is ADR-0019 mapping: "a pausing node (e.g. Approval) reads$runIdto map its external state back to this run for resume". An approval request row minted on a retry attempt therefore carries no run id — the pause is stored (since bug(service-automation): a retry attempt that PAUSES is recorded as failed and its suspension is never persisted —executeWithoutRetryhas noisSuspendSignalarm #9510) but the external record cannot point back at it.Every one of these is reachable exactly where retry is most used:
errorHandling.strategy: 'retry'on a record-change flow.Not decided here
Whether the repair is to hoist the shared seeding into one helper both methods call (the obvious reading — the two methods already duplicate the disabled / no-start-node / failure exits and have drifted once per card: #9378, #9415, #9414, #9510), or to make
executeWithoutRetrytake the already-seeded map from its caller. The first is more in keeping withbuildRunTrigger's chokepoint precedent, but it touches the first-attempt path too, so it wants a ruling rather than a guess.Refs
executeWithoutRetryhas noisSuspendSignalarm #9510 — the suspend-arm restoration on the same method; its parity pin (packages/services/service-automation/src/retry-attempt-pause.test.ts) asserts this divergence explicitly so it cannot be repaired silentlypreviousbinding this breaks$runIdcontract for pausing nodes