Skip to main content

Inspecting Run Inputs, Outputs, and Logs

Retrieve a run and inspect its state

To inspect a remote execution, retrieve a Run after configuring Flyte’s remote client, then use the run handle for identity and lifecycle operations:

import flyte.remote as remote

run = remote.Run.get(name="my_run")
print(run.name)
print(run.phase)
print(run.done())
print(run.url)

Run.get() is the synchronous wrapper generated for Run.get.aio(). It first obtains RunDetails, then reconstructs a Run from the returned action identifier, metadata, and status. Remote calls require an initialized client; RunDetails.get() builds the run identifier from the configured organization, project, and domain and the supplied name.

Run is the lightweight execution handle backed by a run_definition_pb2.Run. Its __post_init__ requires that the protobuf contain an action field and raises RuntimeError("Run does not have an action") otherwise. The handle exposes the action’s name and phase, whether the action is done, and a UI URL. raw_phase is available when the generated run_definition_pb2.Phase value is needed instead of the string returned by phase.

Wait before reading completed outputs

In asynchronous code, use the .aio member of the syncified lifecycle methods:

import flyte.remote as remote

async def inspect_run() -> None:
run = remote.Run.get.aio(name="my_run")
await run.wait.aio(quiet=True)
details = await run.details.aio()
print(details.name)

Run.wait() delegates to the root Action.wait(). Run.details() fetches a RunDetails with RunDetails.get_details() the first time it is called and caches that object on the run. A later call returns the cached details; it does not refresh the object. Run.sync() currently returns the same Run object and does not refresh it.

The detailed object bridges run-level inspection to action-level inspection through RunDetails.action_details. It exposes name, task_name, action_id, and done(), and its inputs() and outputs() methods delegate to that root ActionDetails.

A production use in RemoteImageBuilder waits for completion, checks the raw phase, and only then consumes outputs:

await run.wait.aio(quiet=True)
run_details = await run.details.aio()

if run_details.action_details.raw_phase == run_definition_pb2.PHASE_SUCCEEDED:
outputs = await run_details.outputs()

The source uses this same sequence when building a remote image. A non-success phase causes the builder to raise flyte.errors.ImageBuildError before it reads the outputs.

Read inputs and outputs

Fetch the detailed run and await both accessors. The accessors are asynchronous because they can issue a remote data request:

import flyte.remote as remote

async def read_io() -> None:
details = await remote.RunDetails.get.aio(name="my_run")

inputs = await details.inputs()
print(inputs.data)

outputs = await details.outputs()
print(outputs[0])

RunDetails.get() is also available synchronously:

import flyte.remote as remote

run_details = remote.RunDetails.get(name="my_run")

Use await run_details.inputs() and await run_details.outputs() from an async caller, or run those coroutines from a synchronous application. The methods delegate to ActionDetails.inputs() and ActionDetails.outputs(), which share the action’s lazy _cache_data() path. That path calls run_service.GetActionData, then uses the task or trace interface to convert Flyte literals to native Python values.

Work with native values and raw protobufs

ActionInputs is a UserDict containing native values in data and the original run_definition_pb2.Inputs message in pb2. It therefore supports dictionary-style access when the input name is known:

inputs = await details.inputs()
value = inputs["input_name"]
raw_inputs = inputs.pb2

ActionOutputs is an immutable tuple. Its tuple contents are the converted native output values, while .pb2 holds the original run_definition_pb2.Outputs message:

outputs = await details.outputs()
first_value = outputs[0]
raw_outputs = outputs.pb2
first_literal = raw_outputs.literals[0]

The image-builder path uses the protobuf form when it interprets the returned output, rather than relying on tuple indexing. If no recognizable task or trace interface is available, the conversion path produces {} for native inputs and () for native outputs even when protobuf data is present; inspect .pb2 when protobuf-level data is required. ActionInputs.to_dict() and to_json() come from ToJSONMixin and serialize the protobuf representation, not necessarily the native data mapping.

Outputs should be requested after the action reaches a terminal state. If the action is not done, ActionDetails._cache_data() can return without fetching again when inputs are already cached. ActionDetails.outputs() then raises a RuntimeError when data is not available. The CLI deliberately handles this case while preserving the inputs:

inputs = await details.inputs()
outputs = None
try:
outputs = await details.outputs()
except Exception:
outputs = "[red]not yet available[/red]"

View logs for a run or action

For the root action, use Run.show_logs():

await run.show_logs.aio(max_lines=30, show_ts=True, raw=False)

The method delegates to the root action’s show_logs(). Action.show_logs() obtains action details, waits for the action to reach the log-ready state when necessary, and selects the latest attempt when attempt is omitted or false. Pass a positive, one-based attempt number to select a particular attempt:

await run.show_logs.aio(
attempt=2,
max_lines=50,
show_ts=True,
raw=True,
filter_system=True,
)

For a named action rather than the root action, retrieve an Action and use the same method. The CLI follows this selection rule:

if action_name:
obj = remote.Action.get(run_name=run_name, name=action_name)
else:
obj = remote.Run.get(run_name)

await obj.show_logs.aio(
max_lines=lines,
show_ts=show_ts,
raw=not pretty,
attempt=attempt,
filter_system=filter_system,
)

raw=False creates and runs an AsyncLogViewer. The viewer formats each payload_pb2.LogLine, updates a Rich Live display for each received line, and retains a bounded visible history using deque(maxlen=max_lines + 1). max_lines limits the displayed history; it does not stop the stream or limit the number counted in total_lines. Set panel=True only when using the lower-level viewer creation path to wrap the display in a Rich Panel.

raw=True instead formats each line and prints it through a Rich Console. show_ts adds timestamps when formatting lines. filter_system removes system-originated lines and most messages containing [flyte]; messages containing both [flyte] and flyte.errors remain visible.

After submitting a run, the Flyte CLI’s follow behavior uses the live viewer and timestamps:

await r.show_logs.aio(max_lines=30, show_ts=True, raw=False)

The CLI prints that it will wait for the task to start and for the log stream to become available before invoking this call.

Stream log lines directly

remote._logs contains the lower-level Logs namespace and AsyncLogViewer. Logs.tail() calls the initialized client’s logs_service.TailLogs, flattens returned log sets into individual payload_pb2.LogLine values, and yields them from an async generator:

from flyte.remote._logs import Logs

async for line in Logs.tail.aio(action_id=run.action.action_id, attempt=1):
print(line)

This module is private; public callers should prefer Run.show_logs() or Action.show_logs(). Direct use is useful when an application needs the raw streamed LogLine objects or wants to construct its own AsyncLogViewer:

from flyte.remote._logs import AsyncLogViewer, Logs

viewer = AsyncLogViewer(
log_source=Logs.tail.aio(action_id=run.action.action_id, attempt=1),
max_lines=30,
show_ts=True,
name=f"{run.name} (1)",
filter_system=True,
panel=True,
)
await viewer.run()

Logs.tail() treats attempts as one-based in its implementation (attempt=1 by default). It returns cleanly on cancellation, keyboard interruption, or stream termination. gRPC errors are retried with a two-second delay; after the retry threshold, a NOT_FOUND response raises LogsNotYetAvailableError identifying the action and run. Logs.create_viewer() rejects an attempt below 1. In IPython, if widgets are unavailable it logs a warning and switches to raw console output; otherwise the non-raw path uses the live viewer.

Use the CLI inspection commands

The CLI initializes the configured project and domain before retrieving remote objects. Run-level metadata is available with:

flyte get run my_run
flyte get run

The implementation calls RunDetails.get(name=name) for a named run and Run.listall(limit=limit) when no name is supplied. For I/O, omit the action name for the root action or provide it for a named action:

flyte get io my_run
flyte get io my_run my_action
flyte get io my_run --inputs-only
flyte get io my_run --outputs-only

The CLI rejects --inputs-only and --outputs-only together. It catches output retrieval failures in the combined view and prints not yet available, allowing inputs to remain visible.

For logs, the same run-versus-action choice applies:

flyte get logs my_run
flyte get logs my_run my_action
flyte get logs my_run my_action --pretty --lines 50

The command passes max_lines, show_ts, attempt, and filter_system to show_logs(). It maps pretty to raw=not pretty: the raw path prints formatted lines, while the pretty path uses the live scrolling viewer.

Troubleshoot unavailable data and logs

  • Initialization errors: Run, details, I/O, and log retrieval use the globally initialized client. Configure Flyte with a valid remote endpoint or API key before calling these APIs; the underlying ensure_client()/get_client() calls raise an initialization error otherwise. The configured organization, project, and domain are also used to build run identifiers.
  • Outputs are not ready: Wait with await run.wait.aio(quiet=True) before calling details.outputs(). A run can have readable inputs while outputs are still unavailable, and the CLI explicitly reports that state.
  • Details look stale: run.details() caches the first RunDetails. Fetch a new RunDetails with RunDetails.get(name=...) or RunDetails.get_details(...) when a fresh server response is required; Run.sync() is currently a no-op.
  • Native values are empty: Native conversion uses the task or trace interface. If the action has no recognized interface, use inputs.pb2 or outputs.pb2 rather than assuming the native containers represent the protobuf data.
  • The log stream is unavailable: Use a positive attempt number and allow the built-in retries to run. After repeated NOT_FOUND responses, Logs.tail() reports LogsNotYetAvailableError; AsyncLogViewer.run() catches it and prints an error. An omitted attempt on show_logs() selects the latest attempt reported by action details.