Task authoring and execution
Flyte tasks are the fundamental building blocks of a workflow. In flytekit, a task is a versioned, independently executable unit of logic with a strongly typed interface. While the underlying system uses the Task base class to map to Flyte IDL specifications, most developers interact with tasks through the @task decorator and the PythonFunctionTask class.
Declaring Tasks
You define a task by decorating a Python function with @task. flytekit uses the function's type hints to automatically generate the task's interface, including input and output names and types.
from flytekit import task
import typing
@task
def greet(name: str) -> str:
return f"Hello, {name}!"
@task
def add_numbers(a: int, b: int) -> int:
return a + b
When you decorate a function, flytekit creates an instance of PythonFunctionTask (defined in python_function_task.py). This class captures the function body, the detected interface, and any configuration provided to the decorator.
Task Metadata and Configuration
The @task decorator accepts various parameters to control execution behavior, which are stored in the TaskMetadata class (found in base_task.py). Common configurations include:
- Retries: The number of times to retry the task on failure.
- Caching: Enable caching by setting
cache=Trueand providing acache_version. - Timeout: A
datetime.timedeltaor integer (seconds) representing the maximum duration for a single execution. - Interruptible: A boolean indicating if the task can run on lower-cost, pre-emptible nodes.
from flytekit import task
from datetime import timedelta
@task(
retries=3,
cache=True,
cache_version="1.0",
timeout=timedelta(minutes=5),
interruptible=True
)
def heavy_computation(data: list[int]) -> int:
return sum(data)
Internally, TaskMetadata validates these settings. For example, it ensures that if cache=True, a cache_version is also provided, and that cache_serialize is only enabled if caching is active.
Task Execution Flow
Flytekit handles task execution differently depending on whether it is running locally or on a remote Flyte cluster.
Local Execution
When you call a task function directly in a Python script, flytekit invokes Task.local_execute. This method:
- Translates Python native inputs into Flyte literals using
translate_inputs_to_literals. - Checks the local cache if caching is enabled via
LocalTaskCache.get. - Executes the task logic.
- Wraps the results back into
Promiseobjects or native Python types.
Dispatch and Sandbox Execution
During remote execution or local sandbox testing, the system calls dispatch_execute. This method is responsible for the full lifecycle of a task run:
- Pre-execution:
pre_executeis called to set up the environment (e.g., initializing a Spark session). - Input Translation: Input literals are converted to Python native types using
_literal_map_to_python_input. - User Code Execution: The actual
executemethod (which wraps your decorated function) is invoked. - Post-execution:
post_executeallows for cleanup or output modification. - Output Translation: Results are converted back to a
LiteralMapvia_output_to_literal_map.
Specialized Task Types
Flytekit provides specialized task abstractions for complex execution patterns.
Dynamic Tasks
A dynamic task, declared with @dynamic, is a task that generates a workflow at runtime based on its inputs. This is implemented by PythonFunctionTask with an execution_mode of ExecutionBehavior.DYNAMIC.
from flytekit import task, dynamic
@task
def process_item(item: int) -> int:
return item * 2
@dynamic
def process_list(items: list[int]) -> list[int]:
return [process_item(item=i) for i in items]
When a dynamic task executes, it calls compile_into_workflow, which produces a DynamicJobSpec containing the generated nodes and task templates.
Map Tasks
Map tasks allow you to run a single task across a list of inputs in parallel. You use map_task to wrap an existing task.
from flytekit import task, workflow, map_task
@task
def square(val: int) -> int:
return val * val
@workflow
def my_map_workflow(inputs: list[int]) -> list[int]:
return map_task(square)(val=inputs)
Eager Tasks
Eager tasks (or eager workflows), declared with @eager, allow for fully dynamic Pythonic execution where each task call results in a separate execution on the Flyte cluster. These are implemented by EagerAsyncPythonFunctionTask. Unlike dynamic tasks, eager tasks are not compiled into a static spec but use asyncio to manage remote executions via a Controller.
Task Resolvers
When a task runs in a container on a Flyte cluster, the system needs to know how to find and load the original Python task object. This is handled by TaskResolverMixin. The default_task_resolver captures the module and function name of the task. At runtime, pyflyte-execute uses these arguments to import the module and retrieve the task instance for execution.