Overview of Execution Contexts
Two contexts, two lifecycle phases
A task has different metadata before it runs than it does while it runs. During packaging and task-template generation, flyte-sdk passes a SerializationContext to serialization hooks. Once execution starts, it installs a frozen TaskContext; task code retrieves that runtime object with flyte.ctx().
| Context | Lifecycle role | Update/access model |
|---|---|---|
SerializationContext | Carries version, identity, bundles, paths, and packaging information into task serialization | A regular mutable dataclass; it is passed explicitly to serialization code |
TaskContext | Describes the current action while the task executes | A frozen, keyword-only dataclass installed in the context tree and exposed through flyte.ctx() |
Do not use the serialization object as the task's runtime context. The former is an input to translate_task_to_wire; the latter is installed around execution and contains runtime paths, reports, checkpoints, mode, and per-task data.
Packaging with SerializationContext
When you prepare a task for a run, provide a version even if you have no code bundle. SerializationContext.version has no default:
from flyte.models import SerializationContext
s_ctx = SerializationContext(version="my-task-version")
In the normal run path, _run.py first builds or retrieves the code and image bundles. It chooses the explicitly configured version or CodeBundle.computed_version, raises ValueError("Version is required when running a task") when neither is available, and then serializes the task:
version = self._version or (
code_bundle.computed_version if code_bundle and code_bundle.computed_version else None
)
if not version:
raise ValueError("Version is required when running a task")
s_ctx = SerializationContext(
code_bundle=code_bundle,
version=version,
image_cache=image_cache,
root_dir=cfg.root_dir,
)
task_spec = translate_task_to_wire(obj, s_ctx)
inputs = await convert_from_native_to_inputs(obj.native_interface, *args, **kwargs)
What the serialization fields represent
SerializationContext contains these values:
versionis required and becomes part of the serialized task identity.project,domain, andorgare optional Flyte identity fields. They default toNone.code_bundlecarries the package used to package and inflate task code.image_cachecarries the compiled image-cache information when one is available.input_pathdefaults to"{{.input}}", andoutput_pathdefaults to"{{.outputPrefix}}". These are Flyte template expressions, not local filesystem paths.interpreter_pathdefaults to"/opt/venv/bin/python".root_diris an optionalpathlib.Pathused by packaging and resolver code.
Use get_entrypoint_path() when you need the runtime entrypoint associated with the configured interpreter:
entrypoint = s_ctx.get_entrypoint_path()
custom_entrypoint = s_ctx.get_entrypoint_path("/usr/local/bin/python")
The method does not inspect the filesystem. It selects the argument when supplied, otherwise self.interpreter_path, and returns a sibling runtime.py path by joining that interpreter's directory with runtime.py. Thus the default points to /opt/venv/bin/runtime.py, while the custom interpreter above produces /usr/local/bin/runtime.py.
Deployment and serialization consumers
Deployment creates one serialization context after image and code-bundle preparation, then reuses it for tasks in the deployment plan. _deploy.py obtains identity from configuration and derives the version from the plan or generated bundle:
version = deployment_plan.version
if copy_style == "none" and not version:
raise flyte.errors.DeploymentError("Version must be set when copy_style is none")
else:
code_bundle = await build_code_bundle(from_dir=cfg.root_dir, dryrun=dryrun, copy_style=copy_style)
version = version or code_bundle.computed_version
sc = SerializationContext(
project=cfg.project,
domain=cfg.domain,
org=cfg.org,
code_bundle=code_bundle,
version=version,
image_cache=image_cache,
root_dir=cfg.root_dir,
)
_internal/runtime/task_serde.py consumes this object in get_proto_task(). It copies project, domain, org, the task name, and serialize_context.version into the protobuf task identifier, then passes the same context to container/pod and custom-configuration paths:
task_id = identifier_pb2.Identifier(
resource_type=identifier_pb2.ResourceType.TASK,
project=serialize_context.project,
domain=serialize_context.domain,
org=serialize_context.org,
name=task.name,
version=serialize_context.version,
)
custom = task.custom_config(serialize_context)
TaskTemplate.container_args() in _task.py turns the context into the command-line metadata used by the task container. It uses input_path, output_path, and version, adds raw-data/checkpoint/run/action template expressions, and conditionally adds image-cache and code-bundle arguments. When there is no PKL bundle, it also adds resolver arguments using serialize_context.root_dir.
Runtime execution with TaskContext
At runtime, you need action identity and version alongside locations for inputs, outputs, raw data, checkpoints, and reports. TaskContext requires action, version, raw_data_path, output_path, run_base_dir, and report; its remaining fields provide optional execution metadata. It is declared as @dataclass(frozen=True, kw_only=True), so construct it with keyword arguments and create replacements instead of mutating it.
The remote or hybrid run path in _run.py constructs the context and installs it for the call to run_task:
tctx = TaskContext(
action=action,
checkpoints=checkpoints,
code_bundle=code_bundle,
output_path=output_path,
version=version if version else "na",
raw_data_path=raw_data_path_obj,
compiled_image_cache=image_cache,
run_base_dir=self._run_base_dir,
report=flyte.report.Report(name=action.name),
)
async with ctx.replace_task_context(tctx):
return await run_task(tctx=tctx, controller=controller, task=obj, inputs=inputs)
The local path supplies local metadata paths, uses the sentinel version "na", omits the code and image bundles, and explicitly sets mode="local":
tctx = TaskContext(
action=action,
checkpoints=Checkpoints(
prev_checkpoint_path=internal_ctx().raw_data.path,
checkpoint_path=internal_ctx().raw_data.path,
),
code_bundle=None,
output_path=self._metadata_path,
run_base_dir=self._metadata_path,
version="na",
raw_data_path=internal_ctx().raw_data,
compiled_image_cache=None,
report=Report(name=action.name),
mode="local",
)
with ctx.replace_task_context(tctx):
if obj._call_as_synchronous:
fut = controller.submit_sync(obj, *args, **kwargs)
awaitable = asyncio.wrap_future(fut)
outputs = await awaitable
else:
outputs = await controller.submit(obj, *args, **kwargs)
The runtime entrypoint follows the same pattern in _internal/runtime/taskrunner.py. It builds a TaskContext from execution arguments, inherits the enclosing task context's mode when one exists (otherwise uses "remote"), installs it, loads and converts inputs, runs the task, flushes a report when the task has one, and converts outputs:
ctx = internal_ctx()
tctx = TaskContext(
action=action,
checkpoints=checkpoints,
code_bundle=code_bundle,
input_path=input_path,
output_path=output_path,
run_base_dir=run_base_dir,
version=version,
raw_data_path=raw_data_path,
compiled_image_cache=image_cache,
report=flyte.report.Report(name=action.name),
mode="remote" if not ctx.data.task_context else ctx.data.task_context.mode,
interactive_mode=interactive_mode,
)
with ctx.replace_task_context(tctx):
inputs = await load_inputs(input_path) if input_path else inputs
inputs_kwargs = await convert_inputs_to_native(inputs, task.native_interface)
out, err = await run_task(tctx=tctx, controller=controller, task=task, inputs=inputs_kwargs)
Here compiled_image_cache is the runtime field corresponding to SerializationContext.image_cache when a child task is serialized. The names differ because one describes the compiled cache available during execution and the other is the field consumed during serialization.
Reading and updating the current context
Inside task execution, retrieve the current runtime context through flyte.ctx():
import flyte
tctx = flyte.ctx()
if tctx is not None:
if tctx.mode == "local":
# Local execution-specific behavior
pass
if tctx.is_in_cluster():
# True only for mode == "remote"
pass
flyte.ctx() may return None outside a task run. TaskContext.is_in_cluster() is deliberately narrower than a general “not local” check: it returns True only when mode == "remote", and returns False for both "local" and "hybrid". Inspect mode directly when all three modes matter.
Use data for arbitrary task-scoped values. Indexing the context reads from that dictionary and returns None for a missing key. Since the dataclass is frozen, call replace() to obtain a new context. Supplying a data mapping merges it into a copy of the existing mapping:
updated = tctx.replace(data={"attempt": 2})
attempt = updated["attempt"]
missing = updated["not-present"]
The special handling is implemented directly in TaskContext.replace(): it copies self.data, updates the copy with the supplied mapping, and passes the merged dictionary to dataclasses.replace(). Passing data=None skips that merge and preserves the existing data while applying any other replacement keywords.
The surrounding _context.Context exposes related runtime state. replace_task_context() returns a context containing the supplied task context; get_report() returns its Report or None when no task context is installed. The raw_data property checks the task context's raw_data_path before the base context's raw-data path and raises ValueError("Raw data path has not been set in the context.") if neither exists.
Mode-sensitive behavior and nested execution
Execution mode changes how other flyte-sdk components behave. The blocking map implementation in _map.py treats no context or mode="local" as local execution and invokes each value sequentially. For other modes it proceeds to mapped remote behavior rather than taking that sequential branch:
tctx = flyte.ctx()
if tctx is None or tctx.mode == "local":
logger.warning("Running map in local mode, which will run every task sequentially.")
for v in zip(*args):
yield func(*v)
return
Nested remote submission also bridges the two contexts. _internal/controllers/remote/_controller.py reads the current TaskContext, propagates its action identity and version into a new SerializationContext, and maps compiled_image_cache to image_cache:
new_serialization_context = SerializationContext(
project=current_action_id.project,
domain=current_action_id.domain,
org=current_action_id.org,
code_bundle=code_bundle,
version=tctx.version,
image_cache=tctx.compiled_image_cache,
root_dir=root_dir,
)
task_spec = translate_task_to_wire(_task, new_serialization_context)
For a regular code bundle, the controller propagates the parent bundle. If interactive_mode is enabled or the parent bundle is a PKL bundle, it builds a new PKL bundle before serializing the child. The controller raises RuntimeSystemError("BadContext", "Task context not initialized") when nested submission is attempted without an installed task context.
Execution-mode matrix
TaskContext.mode | is_in_cluster() | Behavior evidenced in flyte-sdk |
|---|---|---|
"local" | False | Local runs install local metadata paths; blocking map executes sequentially |
"remote" | True | The default runtime mode; blocking map takes its non-local path and remote controllers can submit child actions |
"hybrid" | False | It is an accepted mode and is preserved when the runtime entrypoint inherits an enclosing context; code that needs to distinguish it must inspect mode |
Boundaries and gotchas
SerializationContext.versionis mandatory. Run preparation derives it fromCodeBundle.computed_versiononly when an explicit version was not supplied. Deployment rejectscopy_style="none"without an explicit version.- Serialization paths default to Flyte template expressions. They should not be read as already-resolved local paths.
get_entrypoint_path()only computes a sibling path; it does not verify either the interpreter orruntime.py.TaskContextis not guaranteed to exist outside execution. Code usingflyte.ctx()must account forNone; context-level raw-data access can raise when no path is configured.compiled_image_cacheandimage_cacheare the runtime and serialization names for the corresponding cache handoff.- The deployment code contains TODOs for incorporating image-cache and code-bundle digests, environment, and task changes into deployment versioning.
get_proto_task()also retains a TODO for SQL, extra configuration, and custom support, even though it invokestask.custom_config(serialize_context)separately.