You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(spec): ExecutionStepMetrics gains an optional failures slot; FlowRunSummary.failed is declared as the fold INCLUDING what a delegating node rolled up from its child (#15617, spec half) (#16635)
* wip(spec): ExecutionStepMetrics failure slot + FlowRunSummary.failed fold reconciliation (#15617 spec half)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
* chore(spec): regenerate authorable-surface + docs references for the ExecutionStepMetrics failure slot (#15617)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
* fix(spec): contract review R1.1 on #15617 — narrow the header roll-up clause to the totals that roll, drop the acted analogy at the failed-child boundary, state the mixed case and the third absence arm in the contract text
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
* chore(spec): regenerate docs references for the R1.1 describe wording (#15617)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(spec): `ExecutionStepMetrics` gains an optional `failures` slot, and `FlowRunSummary.failed` is declared as the fold INCLUDING what a delegating node rolled up from its child (maintainer ruling 2026-09-06 on #15617, spec half)
6
+
7
+
Additive. Nothing an author writes is renamed, retired or narrowed; no accept
8
+
set shrinks. One optional key is declared on a runtime-produced schema and the
9
+
prose of a published contract is reconciled with itself.
10
+
11
+
**What was wrong.**`FlowRunSummary` said two things about `failed`. Its
12
+
header paragraph declared that a `subflow` node rolls its child run's totals
13
+
up into the parent — "this summary answers *what did this run cause*" — while
14
+
the field itself declared `failed = Σ nodes[].failures`, a fold over the
15
+
parent's own node executions. For a parent that delegates its rows to a
16
+
`subflow` (or a `map` item) those give different answers, and the engine could
17
+
only satisfy the second one: `ExecutionStepMetrics` carried `selected` /
18
+
`acted` / `unmeasuredEffect` and no failure slot, so a child's contained
19
+
failures had no path into the parent's fold. Measured on the real engine by
Copy file name to clipboardExpand all lines: content/docs/references/automation/execution.mdx
+10-8Lines changed: 10 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -144,7 +144,7 @@ const result = CheckpointSchema.parse(data);
144
144
|**iteration**|`integer`| optional | Zero-based iteration of the enclosing `loop`, carried through any nesting — a step inside a `parallel` branch that is itself inside a loop body carries the loop's iteration here and its branch index on `branch`. A step inside a `try` / `catch` region that is itself inside a loop body carries the enclosing loop's iteration — a try/catch region has no index of its own — while `regionKind` stays `try` / `catch`. |
145
145
|**branch**|`integer`| optional | Zero-based index of the enclosing `parallel` branch. Present only on a step inside a parallel branch; absent everywhere else. When the parallel node is itself inside a loop body, the loop iteration is reported through `iteration`, never here. |
146
146
|**regionKind**|`string`| optional | Region kind the step ran in: loop-body \| parallel-branch \| try \| catch. Stays `try` / `catch` for a step inside a try/catch region nested in a loop body; the loop is reported through `iteration`. For `parallel-branch` the branch index is reported through `branch`, and the enclosing loop iteration — when the parallel node sits inside a loop body — through `iteration`. |
147
-
|**metrics**|`{ selected?: integer; acted?: integer; unmeasuredEffect?: boolean}`| optional | Records this step selected / acted on, as reported by the node executor |
147
+
|**metrics**|`{ selected?: integer; acted?: integer; unmeasuredEffect?: boolean; failures?: integer }`| optional | Records this step selected / acted on — and, for a step that delegated to a child run (`subflow`, a `map` item), the failures that child contained — as reported by the node executor |
148
148
|**skippedBy**|`{ nodeId: string; edgeId?: string; label?: string }`| optional | The gate that closed, when `status` is `skipped`|
149
149
150
150
### Nested Shape: `ExecutionLog.summary`
@@ -155,7 +155,7 @@ const result = CheckpointSchema.parse(data);
155
155
|**acted**|`integer`| ✅ | Total records written / effects dispatched by the run |
156
156
|**skipped**|`integer`| ✅ | Total node executions a closed gate prevented |
157
157
|**unmeasured**|`integer`| optional | Total executions that may have caused an effect the platform cannot count. Absent = not tracked (an older run), which is not the same as zero. |
158
-
|**failed**|`integer`| optional | Total node executions that failed — a fold of `nodes[].failures`. On a run that completed every one of them was contained (caught by a `try_catch` or routed down a `fault` edge) and the run went on. Absent = not tracked (an older run), which is not the same as zero. |
158
+
|**failed**|`integer`| optional | Total node executions that failed — a fold of `nodes[].failures`, INCLUDING what a delegating node (`subflow` / `map`) rolled up from a child run that COMPLETED while containing failures: this total answers "what did this run cause", subflows included, so a parent whose child lost rows does not read `failed: 0`. A child that FAILED — whether or not it also contained failures before it failed — counts once, as the delegating step's own failure, and its own `failed` stays on its row (unlike `acted`, which carries a failed child's writes). On a run that completed every one of them was contained (caught by a `try_catch` or routed down a `fault` edge) and the run went on. Absent = not tracked (an older run), which is not the same as zero. |
|**gates**|`{ nodeId: string; targetNodeId: string; edgeId?: string; label?: string; … }[]`| ✅ | Gates that closed during the run, most-skipped first |
161
161
|**detailOmitted**|`boolean`| optional | Set when persistence dropped `nodes`/`gates` to keep the stored row bounded — the totals are still exact. Declared so empty arrays are never mistaken for "nothing ran". |
@@ -201,7 +201,7 @@ const result = CheckpointSchema.parse(data);
201
201
|**iteration**|`integer`| optional | Zero-based iteration of the enclosing `loop`, carried through any nesting — a step inside a `parallel` branch that is itself inside a loop body carries the loop's iteration here and its branch index on `branch`. A step inside a `try` / `catch` region that is itself inside a loop body carries the enclosing loop's iteration — a try/catch region has no index of its own — while `regionKind` stays `try` / `catch`. |
202
202
|**branch**|`integer`| optional | Zero-based index of the enclosing `parallel` branch. Present only on a step inside a parallel branch; absent everywhere else. When the parallel node is itself inside a loop body, the loop iteration is reported through `iteration`, never here. |
203
203
|**regionKind**|`string`| optional | Region kind the step ran in: loop-body \| parallel-branch \| try \| catch. Stays `try` / `catch` for a step inside a try/catch region nested in a loop body; the loop is reported through `iteration`. For `parallel-branch` the branch index is reported through `branch`, and the enclosing loop iteration — when the parallel node sits inside a loop body — through `iteration`. |
204
-
|**metrics**|`{ selected?: integer; acted?: integer; unmeasuredEffect?: boolean}`| optional | Records this step selected / acted on, as reported by the node executor |
204
+
|**metrics**|`{ selected?: integer; acted?: integer; unmeasuredEffect?: boolean; failures?: integer }`| optional | Records this step selected / acted on — and, for a step that delegated to a child run (`subflow`, a `map` item), the failures that child contained — as reported by the node executor |
205
205
|**skippedBy**|`{ nodeId: string; edgeId?: string; label?: string }`| optional | The gate that closed, when `status` is `skipped`|
206
206
207
207
### Nested Shape: `ExecutionStepLog.error`
@@ -219,6 +219,7 @@ const result = CheckpointSchema.parse(data);
219
219
|**selected**|`integer`| optional | Records this node READ or matched (a `get_record` query, a lookup) |
220
220
|**acted**|`integer`| optional | Records this node WROTE (created / updated / deleted) or effects it dispatched (notifications delivered) |
221
221
|**unmeasuredEffect**|`boolean`| optional | This execution may have caused an effect the platform cannot count (an external write through a connector). NOT interchangeable with `acted: 0` — it says the count is unknown, not that it is zero. |
222
+
|**failures**|`integer`| optional | Node executions that failed inside a child run this execution delegated to and went on from — a `subflow` child or a `map` item whose run COMPLETED while containing failures: its `summary.failed`, rolled up so the parent answers "what did this run cause". Folds into this node's `failures` and so into the run-level `failed`. NOT this execution's own outcome: a step that failed is `status: 'failure'` and counts once through `nodes[].failures`, and a child that FAILED — whether or not it also contained failures before it failed — is exactly that step failure: its own `failed`, contained and fatal alike, stays on the child's run row and nothing rides up here (unlike `acted`, which does carry a failed child's writes). Absent = delegated nothing, or the child tracked no count, or the producer did not track it; never zero. |
222
223
223
224
### Nested Shape: `ExecutionStepLog.skippedBy`
224
225
@@ -240,6 +241,7 @@ const result = CheckpointSchema.parse(data);
240
241
|**selected**|`integer`| optional | Records this node READ or matched (a `get_record` query, a lookup) |
241
242
|**acted**|`integer`| optional | Records this node WROTE (created / updated / deleted) or effects it dispatched (notifications delivered) |
242
243
|**unmeasuredEffect**|`boolean`| optional | This execution may have caused an effect the platform cannot count (an external write through a connector). NOT interchangeable with `acted: 0` — it says the count is unknown, not that it is zero. |
244
+
|**failures**|`integer`| optional | Node executions that failed inside a child run this execution delegated to and went on from — a `subflow` child or a `map` item whose run COMPLETED while containing failures: its `summary.failed`, rolled up so the parent answers "what did this run cause". Folds into this node's `failures` and so into the run-level `failed`. NOT this execution's own outcome: a step that failed is `status: 'failure'` and counts once through `nodes[].failures`, and a child that FAILED — whether or not it also contained failures before it failed — is exactly that step failure: its own `failed`, contained and fatal alike, stays on the child's run row and nothing rides up here (unlike `acted`, which does carry a failed child's writes). Absent = delegated nothing, or the child tracked no count, or the producer did not track it; never zero. |
243
245
244
246
245
247
---
@@ -281,9 +283,9 @@ const result = CheckpointSchema.parse(data);
281
283
|**nodeId**|`string`| ✅ | Node ID |
282
284
|**nodeType**|`string`| ✅ | Node action type (e.g., "get_record", "decision") |
|**status**|`Enum<'success' \| 'failure' \| 'skipped'>`| ✅ | Terminal status of the node across the run — `failure` if any execution failed, else `success` if any succeeded, else `skipped`|
286
+
|**status**|`Enum<'success' \| 'failure' \| 'skipped'>`| ✅ | Terminal status of the node across the run — `failure` if any execution failed, else `success` if any succeeded, else `skipped`. Judged on this node's OWN executions: a delegating node (`subflow` / `map`) whose child completed while containing failures reads `success` here with `failures > 0`|
285
287
|**runs**|`integer`| ✅ | Times the node executed (loop iterations and parallel branches each count) |
286
-
|**failures**|`integer`| ✅ | Executions that failed — a failure a `try_catch` caught or a `fault` edge routed counts here too; the run-level `failed` is the sum of this across `nodes`|
288
+
|**failures**|`integer`| ✅ | Executions that failed — a failure a `try_catch` caught or a `fault` edge routed counts here too — plus what a delegating execution rolled up from a child run that COMPLETED (`metrics.failures`: the contained failures of a `subflow` child or a `map` item). On a delegating node this may therefore exceed `runs` and is no longer only this node's own failed executions; a child that FAILED adds only the step's own failure (unlike `acted`, which carries a failed child's writes too); the run-level `failed` is the sum of this across `nodes`|
287
289
|**skipped**|`integer`| ✅ | Times a closed gate kept this node from running at all |
288
290
|**selected**|`integer`| optional | Records read across every execution — omitted for a node that reads none |
289
291
|**acted**|`integer`| optional | Records written / effects dispatched across every execution — omitted for a node that writes none |
@@ -302,7 +304,7 @@ const result = CheckpointSchema.parse(data);
302
304
|**acted**|`integer`| ✅ | Total records written / effects dispatched by the run |
303
305
|**skipped**|`integer`| ✅ | Total node executions a closed gate prevented |
304
306
|**unmeasured**|`integer`| optional | Total executions that may have caused an effect the platform cannot count. Absent = not tracked (an older run), which is not the same as zero. |
305
-
|**failed**|`integer`| optional | Total node executions that failed — a fold of `nodes[].failures`. On a run that completed every one of them was contained (caught by a `try_catch` or routed down a `fault` edge) and the run went on. Absent = not tracked (an older run), which is not the same as zero. |
307
+
|**failed**|`integer`| optional | Total node executions that failed — a fold of `nodes[].failures`, INCLUDING what a delegating node (`subflow` / `map`) rolled up from a child run that COMPLETED while containing failures: this total answers "what did this run cause", subflows included, so a parent whose child lost rows does not read `failed: 0`. A child that FAILED — whether or not it also contained failures before it failed — counts once, as the delegating step's own failure, and its own `failed` stays on its row (unlike `acted`, which carries a failed child's writes). On a run that completed every one of them was contained (caught by a `try_catch` or routed down a `fault` edge) and the run went on. Absent = not tracked (an older run), which is not the same as zero. |
|**gates**|`{ nodeId: string; targetNodeId: string; edgeId?: string; label?: string; … }[]`| ✅ | Gates that closed during the run, most-skipped first |
308
310
|**detailOmitted**|`boolean`| optional | Set when persistence dropped `nodes`/`gates` to keep the stored row bounded — the totals are still exact. Declared so empty arrays are never mistaken for "nothing ran". |
@@ -314,9 +316,9 @@ const result = CheckpointSchema.parse(data);
314
316
|**nodeId**|`string`| ✅ | Node ID |
315
317
|**nodeType**|`string`| ✅ | Node action type (e.g., "get_record", "decision") |
|**status**|`Enum<'success' \| 'failure' \| 'skipped'>`| ✅ | Terminal status of the node across the run — `failure` if any execution failed, else `success` if any succeeded, else `skipped`|
319
+
|**status**|`Enum<'success' \| 'failure' \| 'skipped'>`| ✅ | Terminal status of the node across the run — `failure` if any execution failed, else `success` if any succeeded, else `skipped`. Judged on this node's OWN executions: a delegating node (`subflow` / `map`) whose child completed while containing failures reads `success` here with `failures > 0`|
318
320
|**runs**|`integer`| ✅ | Times the node executed (loop iterations and parallel branches each count) |
319
-
|**failures**|`integer`| ✅ | Executions that failed — a failure a `try_catch` caught or a `fault` edge routed counts here too; the run-level `failed` is the sum of this across `nodes`|
321
+
|**failures**|`integer`| ✅ | Executions that failed — a failure a `try_catch` caught or a `fault` edge routed counts here too — plus what a delegating execution rolled up from a child run that COMPLETED (`metrics.failures`: the contained failures of a `subflow` child or a `map` item). On a delegating node this may therefore exceed `runs` and is no longer only this node's own failed executions; a child that FAILED adds only the step's own failure (unlike `acted`, which carries a failed child's writes too); the run-level `failed` is the sum of this across `nodes`|
320
322
|**skipped**|`integer`| ✅ | Times a closed gate kept this node from running at all |
321
323
|**selected**|`integer`| optional | Records read across every execution — omitted for a node that reads none |
322
324
|**acted**|`integer`| optional | Records written / effects dispatched across every execution — omitted for a node that writes none |
0 commit comments