Skip to main content

How to Customize UI Rendering

When tasks produce complex data structures like custom summaries, metrics, or formatted tables, Flyte UI displays basic string representations by default unless custom HTML rendering is specified. flyte-sdk provides the Renderable protocol and TypeTransformer.to_html extension points to generate custom HTML views for execution inputs, outputs, and reports.

from typing import Annotated
from flyte import task
from flyte.types._renderer import Renderable, MarkdownRenderer

class MetricSummaryRenderer(Renderable):
def to_html(self, python_value: dict) -> str:
rows = "".join(f"<tr><td><b>{k}</b></td><td>{v}</td></tr>" for k, v in python_value.items())
return f"<table border='1' style='border-collapse: collapse;'>{rows}</table>"

@task
def generate_summary() -> Annotated[dict, MetricSummaryRenderer()]:
return {"accuracy": 0.985, "loss": 0.015, "epoch": 50}

@task
def generate_notes() -> Annotated[str, MarkdownRenderer()]:
return "# Model Run Summary\n\n- Accuracy achieved: **98.5%**\n- Converged at epoch 50."

The Renderable Protocol and TypeEngine Resolution

The core protocol for UI rendering in flyte-sdk is flyte.types.Renderable (defined in flyte.types._renderer). It is a @runtime_checkable Protocol with a single required method:

@runtime_checkable
class Renderable(Protocol):
def to_html(self, python_value: Any) -> str:
"""Convert an object(markdown, pandas.dataframe) to HTML and return HTML as a unicode string.
Returns: An HTML document as a string.
"""
raise NotImplementedError

When flyte-sdk resolves the HTML representation of a value, TypeEngine.to_html checks whether the type annotation is wrapped with typing.Annotated. If any argument passed to Annotated is an instance of Renderable, TypeEngine.to_html delegates the rendering to arg.to_html(python_val):

@classmethod
def to_html(cls, python_val: typing.Any, expected_python_type: Type[typing.Any]) -> str:
transformer = cls.get_transformer(expected_python_type)
if is_annotated(expected_python_type):
expected_python_type, *annotate_args = get_args(expected_python_type)
from flyte.types._renderer import Renderable

for arg in annotate_args:
if isinstance(arg, Renderable):
return arg.to_html(python_val)
return transformer.to_html(python_val, expected_python_type)

Using Built-in Renderers

flyte-sdk provides pre-built renderers in flyte.types._renderer for common data types.

TopFrameRenderer

TopFrameRenderer renders pandas DataFrames as HTML tables using pandas.DataFrame.to_html(). You can configure table boundaries via max_rows (default: 10) and max_cols (default: 100).

from typing import Annotated
import pandas as pd
from flyte import task
from flyte.types._renderer import TopFrameRenderer

@task
def preview_dataset() -> Annotated[pd.DataFrame, TopFrameRenderer(max_rows=25, max_cols=10)]:
return pd.DataFrame({
"id": list(range(100)),
"score": [i * 1.5 for i in range(100)],
"category": ["A" if i % 2 == 0 else "B" for i in range(100)],
})

MarkdownRenderer

MarkdownRenderer converts Markdown text into HTML using markdown_it.MarkdownIt.

from typing import Annotated
from flyte import task
from flyte.types._renderer import MarkdownRenderer

@task
def generate_documentation() -> Annotated[str, MarkdownRenderer()]:
return """
## Pipeline Output
* Validation: Passed
* Total rows processed: 100,000
"""

SourceCodeRenderer

SourceCodeRenderer converts Python source code strings to syntax-highlighted HTML using Pygments (HtmlFormatter(style="colorful") and PythonLexer).

from typing import Annotated
from flyte import task
from flyte.types._renderer import SourceCodeRenderer

@task
def inspect_task_code() -> Annotated[str, SourceCodeRenderer(title="Training Logic")]:
return "def train():\n model.fit(X, y)\n return model"

ArrowRenderer and PythonDependencyRenderer

  • ArrowRenderer: Renders pyarrow.Table objects as HTML using df.to_string().
  • PythonDependencyRenderer: Inspects installed pip packages in the environment (pip list --format json and pip freeze) and renders an interactive HTML table with a copy button.

Method 1: Annotating Types with Custom Renderable Classes

To customize rendering for a specific task output without altering global type behavior, create a class implementing Renderable and instantiate it inside typing.Annotated.

from typing import Annotated, Dict, Any
from flyte import task

class JsonKeyCardRenderer:
def __init__(self, border_color: str = "#4CAF50"):
self.border_color = border_color

def to_html(self, python_value: Dict[str, Any]) -> str:
items = "".join(f"<li><b>{k}:</b> {v}</li>" for k, v in python_value.items())
return f"<div style='border: 2px solid {self.border_color}; padding: 10px;'><ul>{items}</ul></div>"

@task
def evaluate_metrics() -> Annotated[dict, JsonKeyCardRenderer(border_color="#2196F3")]:
return {"precision": 0.94, "recall": 0.89, "f1": 0.91}

Method 2: Overriding to_html on a Custom TypeTransformer

When implementing a custom native domain type with a TypeTransformer, override TypeTransformer.to_html to provide default HTML rendering across all task inputs and outputs of that type.

from typing import Type, Optional
from dataclasses import dataclass
from flyte.types import TypeTransformer, TypeEngine
from flyte.models.core.types_pb2 import LiteralType, SimpleType
from flyte.models.literals_pb2 import Literal, Scalar, Primitive

@dataclass
class CustomReport:
title: str
score: float

class CustomReportTransformer(TypeTransformer[CustomReport]):
def __init__(self):
super().__init__("CustomReport-Transformer", CustomReport)

def get_literal_type(self, t: Type[CustomReport]) -> LiteralType:
return LiteralType(simple=SimpleType.STRING)

async def to_literal(
self, python_val: CustomReport, python_type: Type[CustomReport], expected: LiteralType
) -> Literal:
return Literal(scalar=Scalar(primitive=Primitive(string_value=f"{python_val.title}:{python_val.score}")))

async def to_python_value(
self, lv: Literal, expected_python_type: Type[CustomReport]
) -> Optional[CustomReport]:
title_str, score_str = lv.scalar.primitive.string_value.split(":")
return CustomReport(title=title_str, score=float(score_str))

def to_html(self, python_val: CustomReport, expected_python_type: Type[CustomReport]) -> str:
return (
f"<div style='background-color: #f0f0f0; padding: 8px; border-radius: 4px;'>"
f"<h4>Report: {python_val.title}</h4>"
f"<p>Score: <b>{python_val.score:.2f}</b></p>"
f"</div>"
)

# Register transformer with the engine
TypeEngine.register(CustomReportTransformer())

When TypeEngine.to_html evaluates CustomReport values without an explicit Annotated[..., Renderable()] metadata override, it delegates to CustomReportTransformer.to_html.


Method 3: Registering Custom DataFrame Renderers

For dataframe structures, flyte.io._dataframe.DataFrameTransformerEngine maintains a renderer registry mapping types to Renderable instances:

import pandas as pd
from flyte.io._dataframe import DataFrameTransformerEngine
from flyte.types._renderer import TopFrameRenderer

# Register custom default limits for pandas DataFrame rendering
DataFrameTransformerEngine.register_renderer(
pd.DataFrame,
TopFrameRenderer(max_rows=50, max_cols=20)
)

DataFrameTransformer.to_html looks up self.Renderers[type(df)] and invokes to_html(df) to generate table previews in execution reports.


Troubleshooting and Best Practices

Pass Class Instances to Annotated, Not Classes

TypeEngine.to_html evaluates isinstance(arg, Renderable). If you pass the uninstantiated class Annotated[pd.DataFrame, TopFrameRenderer], the check evaluates to False because the class object is not an instance of Renderable. Always pass an instance:

# Incorrect: will not invoke renderer
Annotated[pd.DataFrame, TopFrameRenderer]

# Correct: pass instantiated object
Annotated[pd.DataFrame, TopFrameRenderer(max_rows=20)]

TopFrameRenderer Type Assertion

TopFrameRenderer.to_html enforces assert isinstance(df, pandas.DataFrame). Attempting to use TopFrameRenderer on non-pandas objects (such as pyarrow.Table or polars.DataFrame) will raise an AssertionError. Use ArrowRenderer or implement a dedicated renderer class for non-pandas tabular structures.

HTML Escaping and Sanitization

flyte-sdk does not escape the raw HTML string returned by to_html() so that custom HTML markup, tables, and styles are rendered directly in the UI. When rendering user-supplied strings or untrusted metadata within to_html(), escape text using html.escape() from the standard library:

import html

class SafeRenderer(Renderable):
def to_html(self, python_value: str) -> str:
safe_content = html.escape(str(python_value))
return f"<pre><code>{safe_content}</code></pre>"