Workflow composition, failure handlers, and nodes
Flytekit workflows are defined by decorating Python functions with @workflow. While these functions look like standard Python, they serve a dual purpose: they are executed locally for testing and compiled into a Directed Acyclic Graph (DAG) of Node objects for remote execution on Flyte.
Workflow Composition and Dataflow
In flytekit, workflow composition is achieved by passing the outputs of one task or subworkflow as inputs to another. During compilation, these outputs are represented by Promise objects.
Task Outputs and Promises
When you call a task inside a workflow, it returns a Promise (or a collection of Promises). A Promise is a placeholder for a value that will be computed at runtime.
@workflow
def my_wf(a: int) -> int:
# x is a Promise object during compilation
x = add_5(a=a)
# Passing the promise x to another task creates a dependency edge
return add_5(a=x)
If a task returns multiple values, flytekit wraps them in a NamedTuple of Promises. You can access individual outputs using attribute notation or indexing:
@workflow
def multi_output_wf(a: int) -> int:
# Assume t1 returns (int, str)
o = t1(a=a)
# Access by attribute or index
t2(x=o.o0)
t3(y=o[1])
return o.o0
Manual Node Creation
While standard task calls are sufficient for most dataflows, create_node allows for explicit control over node construction. This is useful for tasks with side effects where no data is returned, or when you need to manually manage dependencies.
Unlike ordinary task calls that return Promises, create_node returns a Node object. You must access its outputs via the .outputs attribute or shorthand properties like .o0.
from flytekit import workflow, create_node
@workflow
def manual_node_wf(a: int):
# create_node returns a Node object, not a Promise
t1_node = create_node(t1, a=a)
t2_node = create_node(t2)
# Use >> to define execution order without dataflow
t1_node >> t2_node
# Access outputs from the Node object
t3(x=t1_node.o0)
t4(y=t1_node.outputs["o1"])
Note: The .outputs property is only available on Node objects created via create_node. Calling .outputs on a standard Node (internal to flytekit) will raise an AssertionError.
Per-Node Overrides
You can customize the execution behavior of specific nodes using .with_overrides(). This method is available on both Promise objects and Node objects. Overrides are static and cannot be determined by other task outputs (Promises).
from flytekit import Resources, workflow
@workflow
def override_wf(a: int) -> int:
return add_5(a=a).with_overrides(
requests=Resources(cpu="500m", mem="1Gi"),
retries=3,
interruptible=True,
node_name="custom-add-node"
)
Supported Overrides
The Node.with_overrides method supports several parameters:
- Resources: Set
requestsandlimitsusingflytekit.Resources. Note thatrequestsare clamped to originallimitsif limits are not also overridden. - Retries: An integer specifying the number of retry attempts.
- Timeout: A
datetime.timedeltaor integer seconds. - Cache: A boolean or
Cacheobject. If using aCacheobject, you must specify aversion. - Metadata:
interruptiblestatus and customnode_name.
Failure Handlers
Flytekit allows you to define a cleanup task or workflow that runs if any node in the workflow fails. This is configured via the on_failure parameter in the @workflow decorator.
Defining a Failure Handler
A valid failure handler must:
- Accept all inputs defined in the parent workflow.
- Optionally accept an additional parameter for the error, which must be typed as
typing.Optional[FlyteError].
from typing import Optional
from flytekit import workflow, task, FlyteError
@task
def clean_up(name: str, err: Optional[FlyteError] = None):
if err:
print(f"Workflow failed at node {err.failed_node_id} with error: {err.message}")
print(f"Cleaning up resources for {name}")
@workflow(on_failure=clean_up)
def my_wf(name: str):
t1(name=name)
t2()
Internal Behavior
When a failure occurs:
- Flytekit catches the exception.
- It injects a
FlyteErrorobject into the handler'serr(orerror) input. This object contains themessageand thefailed_node_id. - The handler is executed.
- After the handler completes, the original exception is re-raised to ensure the workflow is marked as failed.
Imperative Workflows
For dynamic DAG construction, ImperativeWorkflow provides a programmatic interface to add inputs, nodes, and outputs.
from flytekit.core.workflow import ImperativeWorkflow
wb = ImperativeWorkflow(name="my_imperative_wf")
wb.add_workflow_input("in1", int)
# add_entity returns a Node
node = wb.add_entity(add_5, a=wb.inputs["in1"])
# Bind workflow output to a node output
wb.add_workflow_output("out1", node.outputs["o0"])
# Add a failure handler
wb.add_on_failure_handler(clean_up)
In imperative workflows, add_entity internally uses create_node. The resulting Node objects are used to wire dependencies and define workflow outputs via node.outputs.
Common Pitfalls
- Truth Value Testing: You cannot use
if promise:orbool(promise)inside a workflow function because Promises are not resolved during compilation. Use theconditionalconstruct instead. - Logical Operators: Use
&and|for logical operations on Promises. Standard Pythonandandorwill raise aValueError. - Iteration: You cannot iterate over a Promise (e.g.,
for i in promise_list:). Workflows must usemap_taskor dynamic tasks for variable-length iteration. - VoidPromise: Tasks that return
Nonereturn aVoidPromise. These cannot be used as inputs to other tasks but can be used for sequencing with>>. - Positional Arguments:
create_nodeonly accepts keyword arguments for task inputs. Passing positional arguments will result in aFlyteAssertion.