Task authoring and execution
Flyte tasks are the fundamental building blocks of a workflow. In flytekit, tasks are defined as versioned, independently executable units of logic with strong interfaces. While the underlying Flyte engine treats tasks as containerized execution units, flytekit provides high-level abstractions to define these tasks directly in Python.
Declaring Tasks with the @task Decorator
The most common way to define a task is by using the @task decorator from flytekit.core.task. This decorator transforms a standard Python function into a PythonFunctionTask.
from flytekit import task
@task
def add_one(x: int) -> int:
return x + 1
When you decorate a function, flytekit automatically:
- Detects the Interface: It uses Python type hints to define the task's inputs and outputs.
- Captures Metadata: It records the function name and module for serialization.
- Enables Local Execution: The resulting object remains callable like a normal function for unit testing.
Requirements for Task Functions
Task functions must be accessible at the module level so that the Flyte container can re-import and execute them. The PythonFunctionTask constructor (in flytekit/core/python_function_task.py) enforces this:
- It cannot be a nested or local function (unless in a test module starting with
test_). - If you use custom decorators, you must use
functools.wrapsto preserve the function's identity.
Task Configuration and Metadata
The @task decorator accepts several parameters to control how the task behaves on the Flyte platform. These settings are encapsulated in the TaskMetadata class found in flytekit/core/base_task.py.
Retries and Timeouts
You can protect against transient failures or runaway processes by specifying retries and timeouts:
from datetime import timedelta
@task(retries=3, timeout=timedelta(minutes=5))
def flaky_task(x: int) -> int:
...
Caching
Caching allows Flyte to skip execution if a task is called with the same inputs and cache version.
from flytekit.core.cache import Cache
@task(cache=True, cache_version="1.0")
def expensive_computation(data: list[int]) -> int:
...
Internally, TaskMetadata validates that if cache=True is set, a cache_version must also be provided. You can also use cache_serialize=True to ensure that concurrent executions with identical inputs run serially to avoid redundant work.
Resource Requests
Tasks can request specific hardware resources like CPU, memory, or GPUs using the Resources class:
from flytekit import Resources
@task(requests=Resources(cpu="2", mem="500Mi"), limits=Resources(cpu="4", mem="1Gi"))
def resource_intensive_task():
...
Task Execution Modes
Flytekit supports different execution behaviors via the ExecutionBehavior enum in PythonFunctionTask.
Default Execution
In DEFAULT mode, the task function is executed exactly as written. This is the standard behavior for most tasks.
Dynamic Tasks
Dynamic tasks allow you to generate a workflow structure at runtime based on the task's inputs. You declare these using the @dynamic decorator (which is a specialized version of @task with execution_mode=ExecutionBehavior.DYNAMIC).
from flytekit import dynamic
@dynamic
def my_dynamic_task(n: int) -> list[int]:
return [add_one(x=i) for i in range(n)]
When a dynamic task runs:
- It executes the user code to produce a
DynamicJobSpec. - Flyte Propeller then executes the nodes defined in that spec as a sub-workflow.
Eager Tasks
Eager tasks (or "eager workflows") allow for more flexible, Pythonic control flow that feels like local execution but runs on the Flyte backend. They are implemented via EagerAsyncPythonFunctionTask. In this mode, every task call inside the eager function creates a separate execution on the cluster, and the eager task awaits the result.
Core Abstractions
The task system is built on a hierarchy of classes in flytekit/core/base_task.py:
Task: The base class capturing the Flyte IDLTaskTemplate. It handles the translation between Flyte's type system (Literals) and Python types.PythonTask: A subclass that adds Python-native interfaces, environment variables, and Flyte Decks support.PythonFunctionTask: The implementation for tasks backed by a Python function. It handles thedispatch_executelogic, which is the entry point when the task runs in a container.
The Execution Flow
When a task is executed on the Flyte platform, the container entry point calls dispatch_execute. The internal flow is:
pre_execute: Sets up the execution environment (e.g., initializing Spark sessions).- Input Translation: Converts
LiteralMapinputs from the Flyte engine into Python native types using theTypeEngine. execute: Invokes the actual Python function.post_execute: Performs cleanup or output modification.- Output Translation: Converts Python return values back into a
LiteralMapto be sent back to the Flyte engine.
Task Resolvers
When a task runs in a container, Flyte needs to know how to find and load the task object. This is handled by TaskResolverMixin. The default_task_resolver (in flytekit/core/python_auto_container.py) serializes the task's module and name into the container's command-line arguments.
For example, a task t1 in my_module.py might be invoked with:
pyflyte-execute --resolver flytekit.core.python_auto_container.default_task_resolver -- task-module my_module task-name t1
The resolver uses importlib to load the module and retrieve the task instance.