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: Renderspyarrow.Tableobjects as HTML usingdf.to_string().PythonDependencyRenderer: Inspects installed pip packages in the environment (pip list --format jsonandpip 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>"