Skip to content

Call Saved Workflow Architecture

CallSavedWorkflowInvocation should become an engine-native workflow call boundary, not a frontend-only dynamic node and not a compile-time graph inliner.

The long-term feature goal is:

  • A parent workflow can call a saved workflow selected by ID.
  • The call node redraws in the editor based on the selected workflow’s exposed form fields.
  • Parent values and inbound connections bind to those exposed fields as call arguments.
  • Execution suspends at the call node, runs the selected workflow as a dependent workflow execution, captures explicit return values, and then resumes the parent workflow.
  • The architecture must work for Invoke frontend graphs and for externally submitted graphs that use the same node type.

This document records the current state, the target architecture, and the execution contract needed to continue development later.

Favor the architecturally correct design over the fastest implementation path.

The work may still proceed incrementally, but each increment should satisfy all of the following:

  • testable in isolation
  • compatible with the long-term architecture described here
  • non-breaking to existing code and existing workflow execution behavior

Speed is not the primary goal for this phase. The primary goal is to move toward the durable design without introducing throwaway execution semantics that would need to be unwound later.

Implemented already in the branch:

  • A real invocation exists: call_saved_workflow.
  • A real return node exists: workflow_return.
  • Named returns exist through workflow_return_value, workflow_return, and caller-side workflow_return_get.
  • workflow_return accepts one key/value return member directly or a collected list of return members, then emits a named values: dict[str, Any] map.
  • Only one workflow_return node is allowed per workflow, enforced in both frontend validation and Python validation.
  • The frontend provides a saved-workflow picker using a reusable SavedWorkflowField UI type.
  • The node redraws dynamically based on the selected saved workflow’s exposed form fields.
  • Dynamic field values persist with the parent workflow.
  • Compatible inbound edges are preserved when switching between workflows with matching exposed field identities and compatible types.
  • Incompatible or no-longer-exposed inbound edges are removed in the editor.
  • Backend validation exists for workflow_id existence and access rights.

Implemented runtime scaffolding:

  • GraphExecutionState now persists workflow-call runtime state:
    • workflow_call_stack
    • workflow_call_history
    • workflow_call_parent
    • waiting_workflow_call
    • waiting_workflow_call_execution
    • waiting_workflow_call_child_session
    • max_workflow_call_depth
  • Nested and recursive calls are represented by the stack, with a runtime depth cap of 4.
  • Parent/child workflow-call identity is now explicit in runtime state:
    • the parent tracks an active WorkflowCallExecution record while waiting
    • completed and failed calls are preserved in workflow_call_history
    • child sessions carry a workflow_call_parent reference back to the parent call relationship
  • GraphExecutionState.next() returns no runnable node while the parent session is waiting on a child workflow call.
  • GraphExecutionState.is_complete() stays false while waiting.
  • DefaultSessionRunner.run_node() now treats call_saved_workflow as a call boundary instead of a normal executable node.
  • On boundary entry, the runner:
    • validates the selected workflow
    • builds a workflow call frame
    • converts the saved workflow JSON into a backend Graph
    • validates and applies parent call arguments to the child graph
    • creates a child GraphExecutionState
    • attaches that child session to the waiting parent session
  • Workflow-call runtime responsibilities are now split:
    • WorkflowCallCoordinator handles call-specific setup:
      • build the child graph
      • apply parent call arguments
      • create the child GraphExecutionState
      • suspend the parent and enqueue the child queue item
    • WorkflowCallQueueLifecycle handles queue-visible parent/child lifecycle:
      • run child queue items
      • resume waiting parents after child success
      • complete the parent call node with the child workflow_return values
      • fail suspended parents after child failure and cascade that failure upward through parent call chains
  • Child SessionQueueItem rows now carry explicit relationship metadata:
    • workflow_call_id
    • parent_item_id
    • parent_session_id
    • root_item_id
    • workflow_call_depth
    • this metadata is now used directly by queue-visible child execution and parent resume/failure handling
  • The session_queue table now has matching durable columns for that relationship metadata:
    • workflow_call_id
    • parent_item_id
    • parent_session_id
    • root_item_id
    • workflow_call_depth
    • child workflow executions are now inserted as their own pending queue rows using those columns
  • Parent queue items now enter a real waiting status while suspended on a child workflow execution.
  • _on_after_run_session() no longer completes queue items whose sessions are incomplete but waiting.
  • Dynamic call arguments now execute end-to-end in the current runner path:
    • literal dynamic values are serialized into a hidden workflow_inputs payload on the parent node at graph-build time
    • stale hidden workflow_inputs values from recalled graphs are ignored unless a matching current dynamic field exists
    • existing dynamic input values are preserved across refresh only while the exposed field type remains compatible; if the selected child workflow changes the exposed field type at the same node/field path, the caller input resets to the child workflow’s current initial value
    • connected dynamic values are accepted as special call-boundary edges and are resolved from parent results at runtime
    • both are validated against the child workflow’s exposed form interface before being applied to the child graph
  • Queue lifecycle semantics now exist for workflow-call chains:
    • parent queue items are suspended in waiting while a child queue row runs
    • child success resumes the suspended parent and completes the parent call node with the child workflow_return values
    • child failure fails the suspended parent and cascades upward through any waiting parent chain
    • canceling a parent cancels its descendant child chain
    • canceling a child cancels the waiting parent chain upward
    • canceling remaining siblings after a batched child failure also cancels descendants of those sibling rows
    • deleting any queue row in a workflow-call chain deletes the full chain to avoid leaving orphaned parent or child rows behind
    • cancel_all_except_current and delete_all_except_current preserve the active queue item plus its workflow-call ancestors and descendants; this also covers the handoff window where a parent is waiting and its child is still pending; unrelated waiting chains are still canceled or deleted
    • retry is root-oriented rather than child-oriented; child queue rows should not be directly retried from the UI
    • the current UI policy is:
      • child queue rows keep Cancel
      • child queue rows hide Retry
    • child queue-row creation is now fail-clean:
      • if call-boundary setup fails after some child rows have already been inserted, those child rows are deleted before the parent invocation is failed
    • child queue-row fan-out is bounded by remaining queue capacity, not just the global queue-size setting:
      • a workflow call that would exceed the remaining pending capacity now fails instead of silently truncating or over-enqueuing child rows
      • child insertion rechecks pending capacity in the same database transaction as the insert

Implemented conversion helper:

  • workflow_graph_builder.py converts saved workflow JSON into an executable backend Graph.
  • It currently supports the invocation-node subset needed for this feature.
  • It flattens connector nodes and omits explicit destination field values when a connection exists, matching frontend graph-build semantics.
  • It now serves as the first explicit callable-workflow compatibility gate:
    • the selected workflow must contain exactly one workflow_return node
    • connected batch child inputs produced by ordinary non-generator upstream nodes still fail early with a clear unsupported-feature error
    • malformed batch input wiring, including multiple connected inputs to one batch field, is reported as unsupported_batch_input compatibility rather than a generic unsupported-node failure
    • child workflows that mix supported batch nodes with unrelated generator nodes are currently rejected with a clear unsupported-feature error
    • unsupported callees are rejected before any child queue row is created
  • Compatibility metadata is now exposed through workflow library API responses:
    • workflow list items and workflow detail responses include call_saved_workflow_compatibility
    • workflow list items use structural generator-backed batch checks so list/picker rendering does not enumerate every image in board-backed generators; workflow detail and runtime execution still resolve real generator values
    • the saved-workflow picker uses that metadata to disable unsupported workflows before execution
    • the picker still allows an already-selected unsupported workflow to render, with an explicit unsupported state and a localized frontend message selected from the structured backend reason
    • workflow library list items now surface an explicit unsupported badge and localized reason without blocking normal workflow viewing or editing

What is still not implemented:

  • connected batch child inputs whose batch values are produced by ordinary non-generator upstream nodes are still not supported and must fail with a clear domain error
  • child workflows that mix supported batch nodes with unrelated generator nodes are still not supported and must fail with a clear domain error
  • broader child-workflow compatibility coverage still needs to be expanded from real unsupported shapes rather than trying to interpret every frontend-only workflow representation through the current graph-builder path
  • the current workflow-call queue lifecycle is still implemented through dedicated workflow-call runtime classes rather than a fully generalized parent/child scheduler model

Conclusion:

  • the editor contract is largely in place
  • the parent-side runtime call boundary is in place
  • child execution, argument forwarding, explicit child return capture, suspended parent status, queue-visible child rows, and upward failure cascade now work
  • the remaining major runtime work is to harden and generalize the parent/child scheduler model rather than prove the basic call boundary

Use the architecture that is more likely to be kept long-term:

  • call_saved_workflow is a call boundary.
  • The parent graph does not inline the full child workflow into itself at queue time.
  • Runtime execution pauses at the call node and creates a dependent child workflow execution.
  • The child workflow receives arguments from the parent.
  • The child workflow returns explicit outputs to the parent.
  • The parent resumes once the child returns successfully.

This is preferred over full graph expansion because it:

  • avoids execution-graph blowup
  • preserves workflow boundaries
  • matches the conceptual model of workflow reuse
  • supports explicit return values
  • keeps externally submitted graphs viable as long as they use the same node type and contract

These should not be the first implementation target:

  • full inline graph expansion of called workflows
  • unlimited nested workflow call support
  • automatic exposure of arbitrary internal child workflow state
  • implicit output inference from arbitrary child nodes

The callable interface of a saved workflow is defined by its saved workflow JSON.

Primary source:

  • workflow.form

Fallback source for older workflows:

  • workflow.exposedFields

Only fields exposed by the child workflow form are callable inputs. Internal child inputs that exist in the workflow graph but are not exposed by the form are not part of the public call interface.

CallSavedWorkflowInvocation exposes dynamic inputs in the editor based on the selected workflow’s callable interface.

The saved-workflow picker sends typed search text to the workflow-list endpoint. This keeps large workflow libraries discoverable even when the desired workflow has not already been loaded into the combobox pages.

Each dynamic input must have:

  • a stable external handle name
  • a type
  • a default value if defined by the child workflow
  • a user-facing label and description when available

Current fast-path identity is based on child nodeId + fieldName. That is acceptable short-term in the editor, but a longer-term stable interface ID would be better if child workflows are frequently duplicated or refactored.

At runtime, when the parent reaches call_saved_workflow:

  • the engine resolves workflow_id
  • the engine loads the selected child workflow record
  • the engine reconstructs the callable interface from the saved workflow JSON
  • the engine collects argument values from the parent node’s dynamic inputs
  • the engine starts a dependent child workflow execution using those arguments

Argument values may come from:

  • parent literal field values
  • resolved inbound connections into the call node’s dynamic inputs

For batch-aware child workflows, the parent call boundary should still pass normal exposed form inputs. Batching should emerge from the child workflow’s own internal batch nodes or generators, not from a separate caller-side batch protocol.

The child workflow runs as its own dependent execution context, not as an inlined copy of the parent graph.

Desired semantics:

  • parent execution pauses at the call node
  • child execution runs with inherited context where appropriate
  • child workflow finishes or fails
  • parent resumes only if child execution succeeds

This implies the queue/session/runtime layer needs an explicit parent-child execution relationship.

Current limitation:

  • the temporary workflow_graph_builder.py path still reconstructs only the ordinary invocation subset of child workflows
  • direct batch-special child workflows now bypass that path and use queue batch expansion instead
  • generator-backed batch child workflows now bypass that path too when the batch is fed directly by a supported generator node
  • connected batch child inputs produced by ordinary non-generator upstream nodes are still not supported and should fail early with a clear unsupported-feature error
  • the current queue-visible child execution path still relies on WorkflowCallCoordinator to resume or fail parents directly rather than a more general queue scheduler abstraction
  • the current implementation is still an intermediate architecture step, but it is now materially closer to the intended durable parent/child model than the earlier inline-runner path

The current queue-visible implementation uses the following lifecycle contract:

  • root or parent queue items may enter waiting while suspended on a child workflow call
  • child workflow executions are represented as real queue rows with explicit parent/child relationship metadata
  • child completion resumes the suspended parent and returns control to normal queue execution
  • child failure fails the suspended parent call node and cascades upward through any ancestor chain
  • cancel operations are chain-aware:
    • canceling a waiting parent cancels descendants
    • canceling a child cancels waiting ancestors
    • canceling batched siblings after one child fails includes nested descendants of those siblings
    • bulk “all except current” actions preserve the active queue item and its parent/child chain, not just the single in_progress row; a pending child with a waiting parent is treated as the active chain during processor handoff
  • retry operations are root-aware:
    • retrying a root queue item creates a new root execution
    • retrying a child queue item should be normalized to the root by backend code
    • retry and full-chain delete authorization is checked against the root queue item owner
    • child queue rows should not expose direct retry affordances in the UI
    • retry websocket delivery is owner-scoped; when an admin retries roots owned by multiple users, each non-admin user must receive only the retry item ids for their own roots, while admins can still observe the full retried set
  • workflow live-update sockets join workflow event rooms in both authenticated multiuser mode and unauthenticated single-user mode; the frontend relies on those events to invalidate workflow library data and clear deleted saved workflow selections; in single-user mode, workflow CRUD events emit only to the admin room to avoid duplicate delivery to sockets that are also joined to user:system
  • a public-to-private transition emits a schema-defined workflow_access_revoked event to shared-workflow subscribers; non-owner, non-admin clients clear references to that workflow while owners and admins retain access
  • the saved-workflow node picker queries owned/default workflows and public shared workflows separately, merges them by workflow id, and fetches additional pages as the combobox menu reaches the end
  • queue status events must preserve user isolation:
    • QueueItemStatusChangedEvent.queue_status may keep global aggregate counts
    • embedded current-item identifiers (item_id, session_id, batch_id) must only be present when the current in-progress item belongs to the event owner, or when the status is being built for an admin/global context
    • workflow-call child enqueue events use the same owner-aware redaction as ordinary status transitions, even though they do not pass through _set_queue_item_status

This is now part of the intended user-facing contract, even though the orchestration still lives in WorkflowCallCoordinator.

The current implementation now supports direct batch-special child workflows for:

  • image_batch
  • string_batch
  • integer_batch
  • float_batch

It also supports generator-backed batch child workflows when those batch nodes are fed directly by:

  • integer_generator
  • float_generator
  • string_generator
  • image_generator using image_generator_images_from_board

Current semantics:

  • batch-special nodes are removed from the executable child graph before ordinary graph validation
  • supported generator nodes that feed those batch-special nodes are removed from the executable child graph as well
  • their outgoing edges are converted into queue batch substitutions
  • ungrouped batch nodes expand as a cartesian product
  • grouped batch nodes zip by batch_group_id
  • the workflow call creates one child queue row per expanded batch session
  • supported generator value shapes are resolved into concrete batch items before queue batch expansion
  • declared generator counts are rejected before resolution when they exceed remaining child capacity
  • cartesian expansion size is computed arithmetically before session generation rather than by materializing the product
  • batch outputs may feed a named workflow_return_value.value directly; each expanded child returns one value for that key
  • parent resume waits for all child rows tied to that workflow call
  • parent return aggregation produces values: dict[str, list[Any]], where each key maps to one value per child row
  • all child rows in one batch call must return the same key set; mismatched keys fail the parent call clearly
  • if any child row fails, remaining sibling child rows are canceled and the parent call fails
  • generator-backed image batches must respect board access:
    • the caller may expand images from a board they own
    • admins may expand any board
    • shared/public boards may be expanded by other users
    • inaccessible private boards must fail before image expansion rather than leaking board contents across users

Current generator coverage:

  • integer generators:
    • arithmetic sequence
    • linear distribution
    • parse string
    • seeded uniform random distribution
  • float generators:
    • arithmetic sequence
    • linear distribution
    • parse string
    • seeded uniform random distribution
  • string generators:
    • parse string
    • dynamic prompts combinatorial
    • dynamic prompts random
  • image generators:
    • images from board

Still unsupported:

  • connected batch inputs whose batch values are produced by non-generator upstream nodes

Plain-English summary:

  1. The parent workflow reaches call_saved_workflow.
  2. The parent pauses and enters waiting.
  3. The child workflow is inspected before execution.
  4. If the child contains supported batch inputs, that one call expands into multiple child executions instead of one.
  5. Each expanded child execution becomes its own queue row.
  6. Each child queue row keeps the substituted batch field_values, matching ordinary batch queue rows.
  7. Those child queue rows run independently.
  8. The parent does not resume until all child queue rows for that call have finished.
  9. Each child execution produces its own named workflow_return.values map.
  10. The parent aggregates those maps into values: dict[str, list[Any]].
  11. The call_saved_workflow node completes with that named values map, and the parent workflow continues.

Expansion rules:

  • ungrouped batch inputs expand as a cartesian product
  • batch inputs that share the same batch_group_id zip together by position

Example:

  • ungrouped inputs [1, 2] and [10, 20] produce 4 child executions:
    • (1, 10)
    • (1, 20)
    • (2, 10)
    • (2, 20)
  • grouped inputs [1, 2, 3] and [10, 20, 30] with the same batch_group_id produce 3 child executions:
    • (1, 10)
    • (2, 20)
    • (3, 30)

The following parts of the runtime contract are easy to misread and should stay explicit in both code and tests.

Waiting and resume:

  • a parent queue row in waiting is suspended, not completed
  • a parent resumes only after every child queue row tied to that workflow call has reached a terminal state

Return aggregation:

  • each child queue row returns its own named workflow_return.values
  • for batched calls, the parent call node output is values: dict[str, list[Any]]
  • all child rows in one batched call must return the same key set so each returned list is row-aligned
  • if one key should contain multiple images for a non-batch child, the child must collect those images into a single list value before returning that key

Sibling failure behavior:

  • if one child queue row in a batched workflow call fails, remaining sibling child rows for that same workflow call are canceled
  • if parent return aggregation rejects a completed child row, remaining sibling child rows for that same workflow call are canceled
  • after sibling cancelation, the parent call fails
  • if that parent is itself a child of another workflow call, failure continues upward through the ancestor chain

Cancel behavior:

  • canceling a waiting parent cancels descendant child rows
  • canceling a child row cancels waiting ancestors
  • cancelation should stay cancelation; it should not be rewritten into ordinary failure semantics
  • startup recovery cancels any interrupted in_progress or waiting workflow-call chain, including pending descendants, so a restart cannot leave a suspended parent waiting on a child row that will never report back

Retry behavior:

  • retry is root-oriented
  • child queue rows should not be directly retried from the UI
  • backend retry of a child id should normalize to the root workflow call chain rather than create an isolated child-only rerun

Return values should be explicit.

Implemented model:

  • workflow_return is the explicit return node for callable workflows
  • the child workflow declares named return values through explicit workflow_return_value key/value return members
  • each return member has a stable string key and a connected value
  • when the workflow is run independently, the return node has no caller-visible effect
  • when the workflow is run via call_saved_workflow, the named return map becomes the return value of the call
  • call_saved_workflow exposes that named return map to the parent workflow

Only one workflow return node may exist per workflow. That rule is enforced in both the frontend editor and in Python validation/runtime code.

Do not infer child outputs from arbitrary terminal nodes. That is too ambiguous and too brittle.

Named return contract:

  • the called workflow builds return members with a dedicated key/value node
  • workflow_return accepts one return member directly, or a collected list of return members when the workflow returns multiple named values
  • non-batch execution rejects duplicate return keys
  • if a non-batch workflow needs to return multiple images under one key, the child workflow should collect those images into one list value and return that list under the key
  • the caller extracts a named return value with a companion caller-side extraction node
  • missing keys should fail clearly unless the extraction node explicitly supports and receives a default value

Batch return aggregation:

  • when a called workflow expands into multiple child queue rows, each child row produces its own named return map
  • the parent aggregates those child maps as dict[str, list[Any]]
  • each key maps to child values in child enqueue order, preserving positional correspondence with batch inputs even when child executions complete out of order
  • duplicate keys within a single child return map are still invalid; repeated keys across batch children are the normal aggregation path

If child execution fails:

  • the call node fails
  • the parent workflow fails unless a later design adds explicit error-handling semantics

For the first implementation, failure propagation should be simple and strict.

Runtime must enforce the same access rules used elsewhere for saved workflows.

The caller may execute a child workflow only if it is allowed to access that saved workflow at runtime.

This matters even if the parent workflow was authored in a context where the child was once visible.

Nested and recursive call_saved_workflow execution should be allowed, but bounded.

Initial implementation should enforce:

  • nested workflow calls are allowed
  • recursive workflow calls are allowed
  • maximum workflow call depth is capped at 4 call frames
  • the depth cap is enforced at runtime, based on the active call stack, not by static validation alone

This allows legitimate recursive or conditionally terminating workflow structures while still preventing unbounded call growth.

The goal is to support externally submitted graphs, not only frontend-authored graphs. Therefore the authoritative execution logic must live in Python.

Recommended high-level design:

  • a backend GraphExpander or broader graph-preparation service may still exist as an abstraction point
  • but for this feature, the preferred long-term runtime model is not full graph expansion
  • instead, the runtime needs a call-execution mechanism in the Python execution stack

Relevant existing path:

  • frontend builds and submits a graph and workflow payload
  • backend receives the batch via session queue APIs
  • session queue stores session state
  • runtime executes through GraphExecutionState

Current insertion points already used:

  • DefaultSessionRunner.run_node() detects call_saved_workflow and enters boundary state
  • GraphExecutionState stores the waiting/call-stack state and attached child session
  • WorkflowCallCoordinator currently establishes the call boundary and enqueues child workflow executions as real queue rows
  • WorkflowCallQueueLifecycle currently resumes or fails parents when those child rows complete
  • child queue items already carry stable parent/child identifiers in both runtime objects and durable queue columns

Next runtime work still needed:

  • keep WorkflowCallQueueLifecycle as the bounded workflow-call lifecycle component for this PR
    • the current workflow-call feature is the only caller of parent/child queue semantics
    • replacing it with a generalized queue dependency scheduler now would add regression risk without unlocking a concrete user workflow
    • revisit only if another feature needs dependent queue items, richer retry/cancel policies, or resumable waits
  • if support expands beyond the currently supported direct and generator-backed batch shapes, route those new child workflow execution shapes through machinery that can honor ordinary Invoke batch semantics

Call-specific runtime behavior currently lives in WorkflowCallCoordinator and WorkflowCallQueueLifecycle. Responsibilities:

  • load and validate the selected child workflow record
  • validate runtime access rights
  • extract callable inputs from the child workflow definition
  • build child execution arguments from the parent node state
  • launch dependent child queue rows
  • collect declared returns
  • map returned values back to the parent node outputs

The dedicated child-workflow return nodes are implemented. Responsibilities:

  • define the return interface of the called workflow
  • build named key/value return members with workflow_return_value
  • accept either one named return member or a collected list of return members with workflow_return
  • provide that named values map back to the parent call site when invoked through call_saved_workflow
  • remain inert from a caller perspective when the workflow is run independently
  • guarantee that only one such node exists per workflow
  • behave as a normal node in the editor, with singularity enforced by both frontend and Python validation/runtime code

This should remain the canonical reusable return mechanism for any future subworkflow call behavior.

Session/runtime state records:

  • parent execution waiting on child execution
  • child execution belonging to a parent node call site
  • result propagation back to the parent
  • strict failure propagation rules

The workflow return value should not be persisted back into the saved workflow record and should not be derived from frontend state.

The intended runtime flow is:

  1. The child workflow computes named return members like ordinary node outputs.
  2. The child workflow connects one return member directly to workflow_return.values, or collects multiple return members and connects that list to workflow_return.values.
  3. When the child reaches workflow_return, runtime captures the resolved named return map as the child workflow result.
  4. The child workflow result is stored in child execution state.
  5. That result is handed back to the suspended parent call frame.
  6. The parent call_saved_workflow node is completed with that returned named value map.
  7. The parent graph resumes.

Named returns are implemented for backend invocation behavior, caller-side extraction, runtime propagation, and batch aggregation. Remaining work is limited to incremental frontend UX cleanup and any future expansion of supported batch shapes.

Status: implemented in backend invocation tests.

Goal:

  • establish the named return data model and invocation primitives

Contract:

  • WorkflowReturnValueField stores one key: str and one value: Any
  • workflow_return_value creates a single WorkflowReturnValueField from a key and connected value
  • workflow_return accepts either one WorkflowReturnValueField member or a list of WorkflowReturnValueField members
  • WorkflowReturnOutput exposes values: dict[str, Any]
  • duplicate keys in one non-batch workflow_return execution are invalid and must fail clearly

Tests first:

  • workflow_return_value emits the requested key/value pair
  • workflow_return emits a named value map from one or more return members
  • duplicate keys in one workflow_return execution are rejected
  • empty returns are valid only if that remains an intentional callable-workflow contract

Status: implemented in backend invocation tests.

Goal:

  • let the calling workflow extract a named return value without relying on collection position

Contract:

  • workflow_return_get accepts the named return map and a key
  • workflow_return_get outputs the selected value as Any
  • missing keys fail clearly unless a later version intentionally adds default-value support

Tests first:

  • extracting an existing key returns the stored value
  • extracting a missing key fails with a useful message
  • extracted Any values can feed typed downstream nodes through the existing connection compatibility rules

Status: implemented in backend runtime tests.

Goal:

  • carry named return maps through queue-visible child execution and parent resume

Contract:

  • non-batch child execution returns values: dict[str, Any]
  • call_saved_workflow exposes that map on its output
  • failed child execution behavior is unchanged
  • cancel/retry lifecycle behavior is unchanged

Tests first:

  • a called workflow returning {image: image_value} completes the parent call_saved_workflow output with that key
  • a caller-side extraction node can consume that output after parent resume
  • missing or invalid workflow_return nodes still fail with the existing clear errors

Status: implemented in backend runtime tests.

Goal:

  • define named returns for child workflows that expand into multiple queue rows

Contract:

  • each child queue row produces one named return map
  • the parent aggregates child maps as dict[str, list[Any]]
  • each key maps to values returned by completed child rows for that key
  • all child rows in one batch call must return the same key set
  • repeated keys across child rows are expected
  • duplicate keys within one child row remain invalid
  • if a non-batch workflow wants multiple images under one key, it must collect those images into a single list value before returning that key

Tests first:

  • a batched child returning {image: image_value} from each child row produces {image: [image_1, image_2, ...]}
  • sibling failure still cancels remaining siblings and fails the parent
  • duplicate keys inside one child row are rejected rather than silently aggregated

Status: mostly implemented. Schema/type generation includes the backend nodes and fields; editor connection coverage and localized UI strings are in place for the current node wiring. Future UX cleanup should be driven by concrete user testing rather than added as speculative work.

Goal:

  • make named returns usable and visible in the editor

Contract:

  • generated schema/types include the new return field, return-value node, and extraction node
  • visible UI strings are localized through en.json
  • call_saved_workflow exposes the named return map output
  • users can wire that output to workflow_return_get

Tests first:

  • frontend connection/type tests cover return-value collection wiring
  • frontend connection/type tests cover wiring one workflow_return_value.value directly to workflow_return.values
  • frontend connection/type tests cover call_saved_workflow.values -> workflow_return_get.values
  • docs describe how a called workflow creates named returns and how a caller extracts them

Frontend Responsibilities In The Long-Term Design

Section titled “Frontend Responsibilities In The Long-Term Design”

The frontend remains responsible for editor-time behavior:

  • choosing the saved workflow
  • redrawing dynamic inputs based on the child workflow callable interface
  • persisting those dynamic fields and their values
  • preserving compatible inbound edges when workflow selection changes
  • clearing incompatible edges and invalid selections in a predictable way
  • using backend compatibility metadata so unsupported saved workflows are not presented as callable choices
    • compatibility analysis now tolerates required exposed caller inputs by synthesizing placeholder values for those inputs during backend compatibility evaluation, so workflows that are valid once the caller supplies exposed values are not disabled prematurely

Potential future optimization:

  • add a backend endpoint that returns a normalized callable workflow interface
  • this would let the frontend avoid re-parsing full saved workflow payloads to redraw the node
  • it would also give the frontend a backend-authoritative interface hash for drift detection

Already covered:

  • workflow-call stack and waiting state on GraphExecutionState
  • depth-limit enforcement
  • waiting blocks scheduling
  • parent sessions are not completed while waiting
  • runner boundary entry for call_saved_workflow
  • validation failures and depth-limit failures still follow normal node-error behavior
  • child workflow JSON conversion to backend Graph
  • child graph build failure does not leave the parent in a partial waiting state
  • child GraphExecutionState is attached to the waiting parent session
  • coordinator-owned child execution completes the parent queue item instead of leaving it stuck in in_progress
  • literal and connected dynamic call arguments are applied to the child graph at runtime
  • non-exposed dynamic call arguments are rejected at runtime
  • child workflow_return output is captured and becomes the parent call_saved_workflow output
  • named workflow_return values can be constructed, propagated to the parent, extracted by key, and batch-aggregated as dict[str, list[Any]]
  • child workflows without a workflow_return node fail cleanly when called
  • child execution events now include stable workflow-call relationship metadata on the child SessionQueueItem
  • parent-child resume and failure propagation through queue-visible child rows
  • nested runtime execution with bounded stack depth
  • direct and generator-backed batch-special child workflows through queue child-row expansion
  • compatibility metadata for required exposed inputs, missing/multiple returns, supported named batch-return shapes, and unsupported batch input wiring

Still needed in later increments:

  • focused coverage for any newly supported batch or generator shape when its contract changes
  • possible migration from dedicated workflow-call queue lifecycle handling to a more general scheduler or queue-lifecycle model only if another feature needs reusable dependent queue items

The next incremental step should be:

  • stop adding feature slices unless they close a concrete correctness gap or unlock a realistic user workflow
  • stabilize the current branch with review, targeted test runs, and cleanup of stale design-doc language
  • treat migration from WorkflowCallQueueLifecycle to a generalized parent/child queue lifecycle as a larger architecture slice, not as small follow-on busywork

The current branch is at the point where:

  • parent call-boundary state exists
  • child execution state can be created from the selected saved workflow
  • child execution, argument forwarding, explicit return propagation, suspended parent status, queue-visible child rows, and upward failure cascade work through the current coordinator + queue path
  • but long-term generalized parent/child scheduling semantics are still missing
This site was designed and developed by Aether Fox Studio.