Repository navigation
feat(workflow): Loop outputs, Loop retry and flow outputs (parity package #6) - #840
Merged
Merged
Conversation
…try and flow outputs Survey, spec and plan (EN + VI for spec and plan) for roadmap row #6 as one PR: Loop outputs/fetchType evaluated per iteration, Loop retry inherited by iterations, flow-level outputs with migration 0211 (provisional), SECRET outputs sealed and masked, FILE outputs scoped to the execution, a 1 MiB / 16 MiB cap. Corrects the roadmap's premises (8 banks, the four unattributed Loop entries named). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Ratchet 301 / 35 / 35 / 0 and census 295 / 25 / 21 on the merge 271b1c1, as predicted; the differing field of each of the eight entries the package targets; migration 0211 still free on every local ref. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t save (package #6 A1) models::Loop reads `outputs` (the flow's FlowOutput bean) and `fetchType` (LoopFetchType, AUTO by default), and writes them back. The not_honoured warning for both keys is gone; property-treatment.json moves them to "honoured", and the schema README says what "honoured" measures. A fence test holds every honoured key of the Flow and Loop rows to being run by egret or warned. Save-time checks: a Loop output needs a type (run-time gate, as the flow's), output ids are unique on the flow (Kestra's rule) and on a Loop, a SECRET Loop output under STORE, or AUTO with a string values:, is refused, and a CREATE_NEW_EXECUTION retry that reaches a Loop iteration warns, since worker::replay refuses it at run. The last three are save-only. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…age #6 A2) New `outputs::evaluate(cx, &[FlowOutput]) -> Result<Typed, OutputError>`, a port of Kestra's renderOutputs + typedOutputs (spec D2): required outputs render together and one failure fails them all; each optional output renders alone and a failure becomes null plus a warning line; every value is then coerced by its type with package #3's coercer (`inputs_check::coerce_output`, Kestra's parseType). Every error names the output. SECRET ids are reported for the caller to seal (D9); FILE is checked for shape only (D10 is B4's). Not wired into the worker yet (A3, B1), so the ratchet does not move. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…l (package #6 A3) In the outbox phase of the settle that terminates a LOOP sub-execution, read the Loop from the parent's source and, when it declares outputs:, evaluate them with outputs::evaluate on the iteration's own surface (after its errors:/finally:). A failure, or a FETCH map over 1 MiB, logs an ERROR line on the sub-execution, appends FAILED after the terminal it reached (CREATED RUNNING SUCCESS FAILED), persists it, and reports FAILED to the Loop. Optional-output warnings are logged at WARN; every line is masked with the render context's redactions. The typed map travels on the terminal report to on_execution_terminal (A4 appends it). Executor ratchet 301 / 35 / 35 / 0 -> 301 / 34 / 34 / 0: valids/loop-outputs-failed-render banked; derived constants, handler-surface.txt and the README's register re-measured. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…arent lock (package #6 A4) on_execution_terminal now writes the Loop run's task_runs.outputs under the parent row lock: iterationCount, runningIterations and terminatedIterations on every Loop run (initialised by the first batch, {0,0,{}} for a Loop with no values), and {item: {value, iteration, key?}, outputs} appended in termination order when the iteration's map is non-empty and it was not a failure under transmitFailed. An append that would take the list past 16 MiB fails the iteration (logged on the Loop run); the limit can be lowered per workspace in tests. Security, brought forward from B4 so A4 never stores either unchecked: SECRET output values are stored as the mask and sealed with the secret_inputs cipher into task_runs.secret_outputs (migration 0211, the whole D11 migration), revealed only by the render surface and masked in every line; a deployment with no master key fails the output naming secrets.master_key. A FILE output must be a readable file of the execution's loop family, else "egret refuses output <id>: <uri> is not a file of this execution". The 1 MiB per-iteration cap now applies to every fetchType until A5 writes STORE maps to storage. Executor ratchet 301 / 34 / 34 / 0 -> 301 / 30 / 30 / 0: if-in-parallel and loop-outputs (predicted), loop-outputs-auto and loop-outputs-store (ahead of A5, states only) banked; derived constants, handler-surface.txt and the README re-measured. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ation and resolve AUTO (package #6 A5) fetchType STORE (and AUTO when the Loop run's values rendered to a URI, Kestra's guessOutputMode, decided once per Loop run) writes the typed map as Ion to kestra:///{ns}/{flow}/executions/{iteration}/outputs.ion, indexes it in the iteration's transaction, and the list entry holds {uri}. The STORE branch comes before the 1 MiB per-iteration cap, which is now FETCH's only; the 16 MiB total still applies to the {uri} entries. The write never happens inside a transaction: Prefetch::upload records it, the transaction owner rolls back, writes the object with nothing held and runs the attempt again (the prefetch rule applied to a write). A SECRET output under STORE is refused at evaluation, before rendering, naming the output and how STORE was reached (covers versions stored before #6). STORE with no internal storage fails the iteration naming [storage]. Ratchet unchanged at 301/30/30/0; loop-outputs-auto and loop-outputs-store now match the artifact's content too. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…kage #6 A6) sub_flow_source now attaches the nearest retry: of the Loop or of its ancestors in the parent flow (else what an enclosing iteration inherited) to the iteration's flow as x-egret-inherited-retry, a field distinct from the flow's own retry. retry::policy_for consults it after the run chain and before the flow's retry: own > ancestor inside the iteration > inherited > flow, Kestra's searchForParentTaskWithRetry precedence. An inherited CREATE_NEW_EXECUTION stays refused at run by worker::replay. The key is refused at save as unknown_property. Survey R3 pinned first: an iteration that goes FAILED then RETRYING in one settle reports nothing to its Loop (passed before the change). Also fixed, found by the precedence test: flowable::current_list asked policy_for for a failed FLOWABLE run (a Loop under a Sequential with a retry:, or any flowable under a flow-level retry) and held its list in RetryPending for a retry retry::scan never schedules, so the execution stalled RUNNING. "Will be retried" now uses scan's own gate (Runnable/Executable only, ExecutorService.java:572). Executor ratchet 301 / 30 / 30 / 0 -> 301 / 29 / 29 / 0: valids/retry-flowable-parallel banked, as predicted. Derived constants, handler-surface.txt (ceiling unchanged at 287) and README re-measured. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rminal (package #6 B1) Evaluate a flow's `outputs:` with the shared evaluator in the settle that makes the execution terminal, before `afterExecution:` runs, and store them in `workflow.executions.outputs` (migration 0211, landed in A4). A failure logs `Failed to render output values` and appends FAILED after the terminal; any terminal of an execution that ran evaluates (FAILED and KILLED included, as Kestra's onEnd), never a Loop iteration (`sub_flow_source` clears the parent's outputs, spec D6). SECRET values are masked and sealed into `secret_outputs`, refused with no master key; FILE values must be files of the execution's loop family (D9, D10 brought forward from B4). `fail_after_terminal` no longer appends FAILED to FAILED (Kestra's `State.withState` is a no-op there). Banks `valids/flow-with-outputs-failed` and, unpredicted, `valids/subflow-secret-output-child` (no master key on either harness): ratchet 301/29/29/0 -> 301/27/27/0; census 295/25/21 unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… (package #6 B2) `worker::render::Surface::load` reads `workflow.executions.outputs` and reveals the sealed SECRET values (`secret_outputs`) to the execution's own renders, adding them to the redactions so every line and failure message masks them; NULL still renders `{}`. A Flow trigger's render context over the execution (`execution_render_context`, whose rendered inputs become another execution's) gets the masked map. Controller decision 2026-10-29: flow-level outputs are capped at 16 MiB serialised per execution (`MAX_FLOW_OUTPUTS_BYTES`); beyond it the evaluation fails at the terminal pointing to a FILE output. Recorded in spec 2.2 items 10-11 and section 6 (EN + VI). Banks `valids/after-execution`: ratchet 301/27/27/0 -> 301/26/26/0; census 295/25/21 unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lish trigger.outputs and return outputs on the execution API (package #6 B3) A waiting Subflow run now carries the child's stored flow outputs as outputs.<sub>.outputs, with the child's sealed SECRET values copied into task_runs.secret_outputs and revealed to the parent's own renders. A child whose declared outputs were not stored fails the run as Kestra's failSubflowDueToOutput: the child's state, then FAILED once, and one fresh attempt [CREATED, FAILED] (new Command::FailSubflowOutputs). This banks valids/subflow-secret-output-parent: ratchet 301/25/25/0. A Flow trigger's inputs render with trigger: {outputs} bound when the source's outputs are non-empty; the fired execution's trigger record carries variables.outputs; a dependsOn window keeps its creator's outputs and the fire deep-merges them as Kestra's MapUtils.deepMerge. SECRET values stay masked throughout. The Flow trigger render context now also hides a Loop run's and a Subflow run's sealed outputs (controller decision); SECRET inputs there are recorded as a pre-existing KI-112 limit in spec 2.2 item 12. The execution API (get and list) returns outputs, masked; no statement selects secret_outputs. openapi.json and the generated client are regenerated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… and pin the SECRET/FILE output surface (package #6 B4) A read (the render snapshot and the FILE-input check) now also admits a URI a waited-for Subflow child published as a flow output: the child row must name the Subflow run that copied it, and the URI must lie in the child's own loop family. The FILE-output rule (D10) and the files API keep the plain readability decision, so a parent cannot re-publish the file. Adversarial tests pin the rest of the B4 surface: a replay source's file, a non-execution kestra:// URI and nsfile:// refused as flow FILE outputs; a sibling iteration's file accepted as a Loop FILE output; secret() in an output value refused at save and at run. Spec section 2.2 item 14 and section 6 item 2 record it (EN and VI). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d the fence-test sweep (package #6 Z) - Spec section 7 "As built" and release notes (EN + VI); Q3/D9 corrected: both subflow-secret-output-* entries banked because the oracle's egret has no master key, as Kestra's harness; section 4 gets the measured sequence 35 -> 34 -> 30 -> 30 -> 29 -> 27 -> 26 -> 25 -> 25; section 6 gains duplicate_output_id on a Loop, outputs.ion, nextOffset. - Plan status (EN + VI) with the measured ratchet and gates; D12's networked migration re-check is still owed before merge. - Roadmap row #6 done (EN + VI), note 6 closed, Appendix A corrected, #11 gains binding `trigger` in a fired flow's task renders. - Oracle README and baseline.txt sweep for stale counts. - keys.rs fence test: Loop.outputs/fetchType/retry and Flow.outputs move from the PACKAGE_6 list into RUN; still checked honoured. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Loop list appends no longer rewrite the list: SQL append with a running byte total in task_runs.outputs_bytes; SECRET entries sealed one by one. - The inherited-run query reads terminal runs only. - The 16 MiB cap counts SECRET values at their clear size. - Execution outputs are on GET detail only; dependsOn-merged trigger outputs and window outputs are capped at 16 MiB. - Flow-trigger context loads sealed columns masked; a child with an unparsable source fails the Subflow run unless KILLED. - OpenAPI and the frontend client regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 0211 to 0214 Main reached 0213 (SP2b). The migration is renamed under D12 and every mention updated; the API client digest is regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Workflow parity package #6 runs three things a stored flow already declared but egret never ran:
Loop'soutputs:andfetchType:;Loop'sretry:, now inherited by each iteration;outputs:.Each now behaves as Kestra does, except for the egret-only limits listed below.
Spec:
docs/en/specs/2026-10-29-workflow-loop-outputs-design.md(+ VI). Plan:docs/en/plans/2026-10-29-workflow-loop-outputs.md. Roadmap row #6.Changes
Loopoutputs.Looprun'soutputslist as{item: {value, iteration, key?}, outputs}.Looprun carries the countersiterationCount,runningIterationsandterminatedIterations.fetchType: STORE(orAUTOover a filevalues:) writes each iteration's outputs tooutputs.ion, and the list holds{uri}.transmitFailed: true, the default, that fails theLoop.Loopretry. An iteration's tasks andSubflows inherit the nearestretry:, in this order: their own, an ancestor inside the iteration, theLoop's or its ancestors', then the flow's.afterExecution:. A failure appendsFAILED.{{ execution.outputs }}holds them, and aSubflowparent readsoutputs.<sub>.outputs.inputs:can use{{ trigger.outputs }}, merged with the window's fordependsOn.GET /executions/{id}gainsoutputs, with SECRET values shown as******.outputs, because they can reach 16 MiB per execution.retry:now ends its parentFAILED. Before, the parent stayedRUNNINGforever.0214_workflow_execution_outputs.sql. It was built as 0211. Under D12 it was renumbered at merge time, because main had reached 0213 and no PR was open.workflow.executions.outputs jsonbandsecret_outputs bytea, andtask_runs.outputs_bytes.ADD COLUMN IF NOT EXISTS.Loopappend moves one entry, not the whole list, using a SQL append and a running byte total.Loop's list.egret-only limits (Kestra has none)
Loopiteration's FETCH outputs may be at most 1 MiB, and the whole list at most 16 MiB. A flow's outputs may be at most 16 MiB. Each overflow fails with a message pointing to STORE or to a FILE output.type: FILEoutput must name a file of the execution itself or of itsLoopfamily. Anything else is refused.secrets.master_key.Loop'soutputs:are refused at save, and so issecret()in an output'svalue:.Behaviour change for stored flows. A flow saved before this release can now give a different answer. The full before/after table is in spec §7, "Release notes (#6)", and on the docs upgrade page.
Test plan
subflow-secret-output-*.cmp-checked. For example,the_loop_book_moves_one_entry_per_iteration_end_at_any_list_lengthmeasures 298–366 bytes per iteration end at N = 2 000 and at N = 10 000.origin/mainand the renumber (2026-11-07):cargo fmt --checkand workspaceclippy --all-targets -D warningsare clean.egret-workflow: 3 590 passed. The 7 failures are allsql_starrocks_live, which needs a live StarRocks (EGRET_TEST_STARROCKS_URL); the same 7 failed before the merge.egret-server --libpassed, including the openapi currency and client digest gates.egret-metastore --libpassed, including the migration-version uniqueness test.Documentation
egret-data/docs#67: Loop outputs, Loop retry and flow outputs. It updates inputs and outputs, flow control, error handling, execution detail, Flow triggers, the YAML reference and the REST reference. It also adds an upgrade note and marks KI-117 fixed.
Checklist
🤖 Generated with Claude Code