Call Saved Workflow Architecture
Callable Workflow Contract
Section titled “Callable Workflow Contract”CallSavedWorkflowInvocation is an engine-native workflow call boundary, not a frontend-only dynamic node and not a
compile-time graph inliner.
The callable-workflow contract 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 describes the callable-workflow architecture and execution contract. Saved-workflow calls use a dedicated queue boundary for durable rows and queue transitions, while invocation and graph state use the lifecycle effect/dependency seam to declare, persist, and resume the call.
The callable node, saved-workflow, and queue interaction contract is stable. Internal execution references, tokens,
effects, frames, and scheduler records are persistence-only implementation metadata. Version-2
dump_execution_state() rebuilds execution references and ordinary output tokens, retains frame-scoped activation
tokens and compact dispatch metadata needed for active recovery, and omits terminal saved-workflow lifecycle effects
and completed child dependencies. The active child queue row is the recovery authority: attached child state remains
available in memory but is omitted from the persisted parent snapshot, while terminal child state is omitted entirely.
Ordinary model serialization and public schemas exclude these four internal ledgers. Existing fields,
requiredness, statuses, events, and client behavior remain compatible. Frontend application behavior is outside this
backend architecture. No code under invokeai/frontend/..., including generated openapi.json or schema.ts, is part
of this execution behavior; the existing frontend/backend external interface is stable.
The internal generic boundary is implemented in invokeai.app.services.shared.execution_engine. It provides
frame-scoped gates, ordered streams, continuations, and capability-bound child dependency records. These records are
used as adapters around the existing queue and materialization paths; they do not add frontend handles, queue statuses,
event payloads, or author-time graph fields.
Design Principles
Section titled “Design Principles”Favor the architecturally correct design over the fastest implementation path.
Changes to this area 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
The durable design is preferred over throwaway execution semantics.
Current Behavior
Section titled “Current Behavior”The callable-workflow feature provides:
- 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-sideworkflow_return_get. workflow_returnaccepts one key/value return member directly or a collected list of return members, then emits a namedvalues: dict[str, Any]map.- Only one
workflow_returnnode is allowed per workflow, enforced in both frontend validation and Python validation. - The frontend provides a saved-workflow picker using a reusable
SavedWorkflowFieldUI 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_idexistence and access rights.
The runtime uses an additive execution-effects seam:
- the runner invokes
invoke_internal_with_effects()and applies its result throughGraphExecutionState.apply(); - stable execution references, frames, output tokens, and accepted effects are persisted with the runtime state;
emitandclose_streamare dispatchable by default;- lifecycle recording for
spawn,await, andfailis capability-gated and returns a validated child handle where applicable; queue mutation remains in the queue adapter; - saved workflow calls continue through
WorkflowCallCoordinatorandWorkflowCallQueueLifecycle, with a generic child dependency record validating their relationship and aggregation.
Runtime state:
GraphExecutionStatenow persists workflow-call runtime state:workflow_call_stackworkflow_call_historyworkflow_call_parentwaiting_workflow_callwaiting_workflow_call_executionwaiting_workflow_call_child_sessionmax_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
WorkflowCallExecutionrecord while waiting - completed and failed calls are preserved in
workflow_call_history - child sessions carry a
workflow_call_parentreference back to the parent call relationship
- the parent tracks an active
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()invokescall_saved_workflowthrough the same effect-aware invocation path as other nodes. The invocation records one capability-boundspawn_executioneffect and one matchingawaiteffect; authorization and input collection happen before those effects are recorded.GraphExecutionState.apply()persists the pending lifecycle effects without completing the call node. The queue adapter then consumes the effects to:- build a workflow call frame
- convert the saved workflow JSON into a backend
Graph - validate and apply parent call arguments to the child graph
- create child
GraphExecutionStateinstances and durable child queue rows - attach those child sessions to the waiting parent session
- When the generic child dependency reaches a terminal result, the queue adapter projects the legacy workflow-call
fields, resumes the parent through
GraphExecutionState.apply(), and completes the call node with ordered return values. A capability-gatedfaileffect records a parent failure without creating child rows. - Workflow-call runtime responsibilities are now split:
WorkflowCallCoordinatorhandles call-specific setup:- build the child graph
- apply parent call arguments
- create the child
GraphExecutionState - suspend the parent and enqueue the child queue item
WorkflowCallQueueLifecyclehandles queue-visible parent/child lifecycle:- dispatch child queue items through the normal session runner
- resume waiting parents after child success
- complete the parent call node with the child
workflow_returnvalues - fail suspended parents after child failure and cascade that failure upward through parent call chains
- Child
SessionQueueItemrows now carry explicit relationship metadata:workflow_call_idparent_item_idparent_session_idroot_item_idworkflow_call_depth- this metadata is now used directly by queue-visible child execution and parent resume/failure handling
- The
session_queuetable now has matching durable columns for that relationship metadata:workflow_call_idparent_item_idparent_session_idroot_item_idworkflow_call_depth- child workflow executions are now inserted as their own pending queue rows using those columns
- Child queue rows and the parent’s complete waiting-session projection are published in one durable queue transaction. Concurrent child completions update the latest parent session transactionally, so a sibling cannot overwrite another sibling’s completion.
- Parent queue items now enter a real
waitingstatus 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_inputspayload on the parent node at graph-build time - stale hidden
workflow_inputsvalues 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
- literal dynamic values are serialized into a hidden
- Queue lifecycle semantics now exist for workflow-call chains:
- parent queue items are suspended in
waitingwhile a child queue row runs - child success resumes the suspended parent and completes the parent call node with the child
workflow_returnvalues - 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_currentanddelete_all_except_currentpreserve the active queue item plus its workflow-call ancestors and descendants; this also covers the handoff window where a parent iswaitingand its child is stillpending; 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 rows keep
- 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
- parent queue items are suspended in
Workflow conversion:
workflow_graph_builder.pyconverts saved workflow JSON into an executable backendGraph.- 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 enforces the callable-workflow boundary:
- the selected workflow must contain exactly one
workflow_returnnode - 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_inputcompatibility 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
- the selected workflow must contain exactly one
- 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
- workflow list items and workflow detail responses include
Current callable-workflow limitations:
- 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 queue lifecycle remains implemented by dedicated workflow-call runtime classes, but each waiting call also
registers an internal
ChildDependencyRecordwith an engine-issued capability. The record validates exact parent identity, ordered all-of aggregation, resource limits, and idempotent child terminal events before the existing queue adapter resumes, fails, or cancels the parent
The editor contract, parent call boundary, child execution, argument forwarding, explicit return capture, suspended parent status, queue-visible child rows, and upward failure cascade are all handled by the current runtime.
Architecture
Section titled “Architecture”The implementation uses the following architecture:
call_saved_workflowis 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 keeps execution at the call boundary instead of expanding the full child graph 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
Current Limitations
Section titled “Current Limitations”The current implementation does not provide:
- 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
Execution Contract
Section titled “Execution Contract”1. Callable Interface
Section titled “1. Callable Interface”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.
2. Input Arguments
Section titled “2. Input Arguments”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. A stable interface ID may be useful if child
workflows are frequently duplicated.
3. Input Binding At Runtime
Section titled “3. Input Binding At Runtime”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 passes normal exposed form inputs. Batching emerges from the child workflow’s own internal batch nodes or generators, not from a separate caller-side batch protocol.
4. Child Workflow Execution
Section titled “4. Child Workflow Execution”The child workflow runs as its own dependent execution context, not as an inlined copy of the parent graph.
Execution 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
The queue/session/runtime layer now implements an explicit parent-child execution relationship through runtime state, durable queue metadata, and queue-visible child rows.
Child snapshot semantics are explicit:
- the saved workflow is resolved and converted to a child
Graphwhen the call is created - the child queue row persists that graph snapshot and its inputs; resume/recovery uses that snapshot rather than resolving the saved workflow again
- an active parent retains the child session in memory while it is waiting, but its persisted snapshot keeps only compact lifecycle metadata because the child queue row is authoritative
- queue pruning is the boundary that removes the child row’s postmortem snapshot; retry creates a new execution and therefore resolves the saved workflow again under the retrying user’s authorization
Current limitation:
- the
workflow_graph_builder.pypath currently 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 fail early with a clear unsupported-feature error
- the current queue-visible child execution path still relies on
WorkflowCallCoordinatorandWorkflowCallQueueLifecyclefor durable row creation and parent transitions; generic lifecycle effects andChildDependencyRecordsupply the invocation intent, identity, ordered aggregation, idempotency, and terminal decision without changing the public queue contract
4a. Queue Lifecycle Contract
Section titled “4a. Queue Lifecycle Contract”The current queue-visible implementation uses the following lifecycle contract. The generic child record is an internal validation and aggregation seam; it does not add queue columns, statuses, events, or frontend handles.
- root or parent queue items may enter
waitingwhile 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_progressrow; 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_revokedevent 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_statusmay 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
WorkflowCallCoordinator owns child setup and enqueueing. WorkflowCallQueueLifecycle owns parent resume and terminal
propagation, using the session queue service for durable row transitions.
4b. Batch Child Workflows
Section titled “4b. Batch Child Workflows”The current implementation now supports direct batch-special child workflows for:
image_batchstring_batchinteger_batchfloat_batch
It also supports generator-backed batch child workflows when those batch nodes are fed directly by:
integer_generatorfloat_generatorstring_generatorimage_generatorusingimage_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.valuedirectly; 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:
- The parent workflow reaches
call_saved_workflow. - The parent pauses and enters
waiting. - The child workflow is inspected before execution.
- If the child contains supported batch inputs, that one call expands into multiple child executions instead of one.
- Each expanded child execution becomes its own queue row.
- Each child queue row keeps the substituted batch
field_values, matching ordinary batch queue rows. - Those child queue rows run independently.
- The parent does not resume until all child queue rows for that call have finished.
- Each child execution produces its own named
workflow_return.valuesmap. - The parent aggregates those maps into
values: dict[str, list[Any]]. - The
call_saved_workflownode 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_idzip 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 samebatch_group_idproduce 3 child executions:(1, 10)(2, 20)(3, 30)
4c. Tricky Areas
Section titled “4c. Tricky Areas”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
waitingis 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_progressorwaitingworkflow-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
5. Return Values
Section titled “5. Return Values”Return values should be explicit.
Implemented model:
workflow_returnis the explicit return node for callable workflows- the child workflow declares named return values through explicit
workflow_return_valuekey/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_workflowexposes 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_returnaccepts 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
6. Error Propagation
Section titled “6. Error Propagation”If child execution fails:
- the call node fails
- the parent workflow fails
Failure propagation is simple and strict.
7. Access Control
Section titled “7. Access Control”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.
8. Recursion And Nesting
Section titled “8. Recursion And Nesting”Nested and recursive call_saved_workflow execution is allowed, but bounded.
The runtime enforces:
- 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.
Runtime Ownership
Section titled “Runtime Ownership”Externally submitted graphs and frontend-authored graphs use the same Python execution path. The backend owns the authoritative execution logic.
The runtime design is:
- a backend
GraphExpanderor broader graph-preparation service may still exist as an abstraction point - the runtime uses a call-execution mechanism in the Python execution stack rather than full graph expansion
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()supplies the call capability and inputs, invokes the node through the effect-aware path, persists its pending lifecycle effects throughGraphExecutionState.apply(), then dispatches the queue adapterGraphExecutionStatestores the waiting/call-stack state and attached child sessionWorkflowCallCoordinatorcurrently establishes the call boundary and enqueues child workflow executions as real queue rowsWorkflowCallQueueLifecyclecurrently 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
Queue lifecycle boundary:
WorkflowCallQueueLifecycleremains the bounded workflow-call lifecycle component- 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
Runtime Components
Section titled “Runtime Components”CallSavedWorkflowRuntime
Section titled “CallSavedWorkflowRuntime”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
Workflow Return Node
Section titled “Workflow Return Node”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 is the canonical reusable return mechanism for subworkflow calls.
Execution Relationship Tracking
Section titled “Execution Relationship Tracking”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
Workflow Return Value Flow
Section titled “Workflow Return Value Flow”The workflow return value is not persisted back into the saved workflow record and is not derived from frontend state.
The intended runtime flow is:
- The child workflow computes named return members like ordinary node outputs.
- The child workflow connects one return member directly to
workflow_return.values, or collects multiple return members and connects that list toworkflow_return.values. - When the child reaches
workflow_return, runtime captures the resolved named return map as the child workflow result. - The child workflow result is stored in child execution state.
- That result is handed back to the suspended parent call frame.
- The parent
call_saved_workflownode is completed with that returned named value map. - The parent graph resumes.
Named Return Contract
Section titled “Named Return Contract”Named returns cover backend invocation behavior, caller-side extraction, runtime propagation, and batch aggregation.
Backend Return Contract
Section titled “Backend Return Contract”Current behavior:
- establish the named return data model and invocation primitives
Contract:
WorkflowReturnValueFieldstores onekey: strand onevalue: Anyworkflow_return_valuecreates a singleWorkflowReturnValueFieldfrom a key and connected valueworkflow_returnaccepts either oneWorkflowReturnValueFieldmember or a list ofWorkflowReturnValueFieldmembersWorkflowReturnOutputexposesvalues: dict[str, Any]- duplicate keys in one non-batch
workflow_returnexecution are invalid and must fail clearly
Validation:
workflow_return_valueemits the requested key/value pairworkflow_returnemits a named value map from one or more return members- duplicate keys in one
workflow_returnexecution are rejected - empty returns are valid only if that remains an intentional callable-workflow contract
Caller-Side Extraction Primitive
Section titled “Caller-Side Extraction Primitive”Current behavior:
- let the calling workflow extract a named return value without relying on collection position
Contract:
workflow_return_getaccepts the named return map and a keyworkflow_return_getoutputs the selected value asAny- missing keys fail clearly
Validation:
- extracting an existing key returns the stored value
- extracting a missing key fails with a useful message
- extracted
Anyvalues can feed typed downstream nodes through the existing connection compatibility rules
Runtime Propagation
Section titled “Runtime Propagation”Current behavior:
- carry named return maps through queue-visible child execution and parent resume
Contract:
- non-batch child execution returns
values: dict[str, Any] call_saved_workflowexposes that map on its output- failed child execution behavior is unchanged
- cancel/retry lifecycle behavior is unchanged
Validation:
- a called workflow returning
{image: image_value}completes the parentcall_saved_workflowoutput with that key - a caller-side extraction node can consume that output after parent resume
- missing or invalid
workflow_returnnodes still fail with the existing clear errors
Batch Return Aggregation
Section titled “Batch Return Aggregation”Current behavior:
- 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
Validation:
- 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
Frontend Schema, UI, And Docs
Section titled “Frontend Schema, UI, And Docs”Existing schema/type generation includes the backend nodes and fields; editor connection coverage and localized UI strings are in place for the current node wiring.
Current behavior:
- 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_workflowexposes the named return map output- users can wire that output to
workflow_return_get
Validation:
- frontend connection/type tests cover return-value collection wiring
- frontend connection/type tests cover wiring one
workflow_return_value.valuedirectly toworkflow_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
Section titled “Frontend Responsibilities”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
Validation Coverage
Section titled “Validation Coverage”Coverage includes:
- 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
GraphExecutionStateis 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_returnoutput is captured and becomes the parentcall_saved_workflowoutput - named
workflow_returnvalues can be constructed, propagated to the parent, extracted by key, and batch-aggregated asdict[str, list[Any]] - child workflows without a
workflow_returnnode 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
Supported extension policy:
- focused coverage for any newly supported batch or generator shape when its contract changes
- a more general scheduler or queue-lifecycle model is warranted only if another feature needs reusable dependent queue items; the current queue lifecycle remains authoritative
Operational Guidance
Section titled “Operational Guidance”The queue path owns durable child rows, parent waiting/resume transitions, cancellation, retry, recovery, and events. The generic child record validates identity, ordered aggregation, resource limits, and idempotent terminal events; it does not replace the queue lifecycle.