Customizing the Report Template
Set a custom report template
To change the appearance of a flyte-sdk report, assign a pathlib.Path to the active Report.template_path while the task context is active, and enable report flushing on the task:
import pathlib
import flyte
env = flyte.TaskEnvironment(name="report_env")
@env.task(report=True)
async def make_report():
report = flyte.report.current_report()
report.template_path = pathlib.Path("custom_report.html")
flyte.report.log("<p>Report content from the task.</p>")
TaskEnvironment.task() accepts report: bool = False; setting it to True causes the task runner to call flyte.report.flush.aio() after successful task execution. It does not select a template. The assignment to template_path changes presentation, while report=True enables automatic persistence.
The custom_report.html file must be readable by the process that calls Report.get_final_report(). template_path is annotated as a pathlib.Path, and the renderer opens it with self.template_path.open("r") when the report is rendered. The file is not read when Report is constructed.
Keep the template substitution points
Report.get_final_report() collects the HTML for every tab, builds the navigation markup, and applies string.Template.substitute() with exactly two values: NAV_HTML and BODY_HTML. A minimal custom template can therefore be:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Custom Flyte report</title>
<style>
body { font-family: sans-serif; margin: 2rem; }
nav { margin-bottom: 1rem; }
#flyte-frame-tabs { display: flex; gap: 1rem; }
</style>
</head>
<body>
<h1>Task report</h1>
<nav>
<ul id="flyte-frame-tabs">
$NAV_HTML
</ul>
</nav>
<main id="flyte-frame-container">
$BODY_HTML
</main>
</body>
</html>
Both placeholders are required by string.Template.substitute(). If either $NAV_HTML or $BODY_HTML is missing, rendering raises a substitution error instead of silently producing a partial report. Because the template is a string.Template, literal dollar signs in the file must follow string.Template escaping rules.
Preserve tab switching when changing the layout
The default report/_template.html places $NAV_HTML inside #flyte-frame-tabs and $BODY_HTML inside #flyte-frame-container. Its script assigns a matching link_index to each navigation <li> and body child <div>, then handleLinkClick() uses that value to activate the corresponding tab and content. The renderer always generates navigation entries as:
<li onclick="handleLinkClick(this)">TAB_NAME</li>
and body entries as:
<div>TAB_HTML</div>
A custom template that retains the two container elements and the default tab script can change colors, typography, spacing, and surrounding markup without removing the structures used by that script. If the custom template omits the script or substantially changes these containers, the report can still contain the substituted HTML, but the default tab switching behavior will no longer be available.
Add content to tabs before rendering
Report.__post_init__() creates a fresh Tab("main") immediately. The module-level flyte.report.log() helper writes HTML to that main tab through the current task context. Additional tabs can be populated with flyte.report.get_tab():
import pathlib
import flyte
env = flyte.TaskEnvironment(name="multi_tab_report")
@env.task(report=True)
async def make_multi_tab_report():
flyte.report.current_report().template_path = pathlib.Path("custom_report.html")
flyte.report.log("<p>Summary content.</p>")
flyte.report.get_tab("details").log("<p>Details content.</p>")
Report.get_tab(name, create_if_missing=True) creates and stores a missing tab by default. For lookup-only behavior, pass create_if_missing=False; an absent tab then raises ValueError("Tab {name} does not exist."). Tab.get_html() joins the fragments in a tab with newline separators before they are inserted into the template.
Understand rendering and storage
The standard task runner creates flyte.report.Report(name=action.name) and installs it in the new TaskContext before invoking the task. The report starts with the packaged report/_template.html; assigning current_report().template_path in the task changes the path used later by get_final_report().
When report=True, flush() renders the active report and writes the resulting UTF-8 HTML beneath the task output path using the fixed filename report.html. It uploads the data with HTML content-type metadata. Outside a task context, flush() returns without writing a report; it also returns without writing when the context has no report.
In an IPython environment, Report.get_final_report() attempts to return IPython.core.display.HTML(raw_html) rather than a plain string. flush() requires the rendered value to be a str, so the automatic task-runtime flush path is intended for non-IPython task execution. If you call get_final_report() interactively, account for this possible return-type difference.
Treat tab content as HTML
Report.get_final_report() HTML-escapes tab names when creating navigation labels, but it deliberately inserts tab content directly into the generated body <div> elements. Tab.log() and Tab.replace() document their arguments as HTML fragments, not complete HTML documents. Supply valid, trusted or sanitized fragments:
flyte.report.log("<p>Safe, intentionally generated HTML.</p>")
Do not pass untrusted text as an HTML fragment without sanitizing it first. The template controls the outer document, but it does not escape content accumulated in tabs.