Generating Rich HTML Reports
Enable report persistence on the task
Set report=True on TaskEnvironment.task, then write HTML fragments through flyte.report. The flag enables the runtime to persist the report; it does not add content by itself.
import flyte
import flyte.report
env = flyte.TaskEnvironment(name="reporting")
@env.task(report=True)
async def summarize() -> int:
flyte.report.log("<h2>Summary</h2><p>The task completed its summary step.</p>")
metrics = flyte.report.get_tab("metrics")
metrics.log("<h2>Metrics</h2><table><tr><th>Name</th><th>Value</th></tr><tr><td>rows</td><td>42</td></tr></table>")
diagnostics = flyte.report.get_tab("diagnostics")
diagnostics.replace("<h2>Diagnostics</h2><pre>no errors</pre>")
return 42
TaskEnvironment.task declares report as a Boolean keyword whose default is False. The value is passed into the task template as report. During runtime execution, Flyte creates a Report in the task context and, after a successful task, calls await flyte.report.flush.aio() when task.report is true.
Write the main and named tabs
The public flyte.report package re-exports Report, current_report, flush, get_tab, log, and replace. The module-level helpers operate on the report associated with the active task context:
flyte.report.log(content, do_flush=False)appends to themaintab.flyte.report.replace(content, do_flush=False)replaces the contents of themaintab.flyte.report.get_tab(name, create_if_missing=True)retrieves or creates a namedTab.flyte.report.current_report()returns the activeReport.
A Report always creates an empty main tab in __post_init__. Named tabs are inserted into the report’s dictionary when first requested, so the order in which tabs are first created becomes their rendering order.
Use log when several fragments should accumulate, and replace when the tab should contain only the latest fragment:
import flyte.report
flyte.report.log("<p>Started processing.</p>")
flyte.report.log("<p>Finished processing.</p>")
status = flyte.report.get_tab("status")
status.log("<p>initializing</p>")
status.replace("<p>ready</p>")
Tab.log appends the supplied string to content. Tab.replace sets content to a one-element list containing the supplied string. Tab.get_html() joins the stored fragments with newline characters. The module-level replace helper only targets main; to replace a secondary tab, call replace on the Tab returned by get_tab.
The report API expects each value to be an HTML fragment, not a complete HTML document, because the fragment is inserted into a generated div:
import flyte.report
report = flyte.report.current_report()
report.get_tab("details").log("<h3>Details</h3><pre>step=load\nstate=complete</pre>")
Because Tab.log and Tab.replace do not escape or sanitize their input, supply HTML that is safe to render. Do not pass untrusted text directly as markup. If text must be displayed, escape it before placing it in the fragment.
Render a Report directly
For code that needs to construct and inspect a report explicitly, instantiate Report from the public flyte.report package and call get_final_report():
import flyte.report
report = flyte.report.Report("batch-summary")
report.get_tab("metrics").log("<h2>Metrics</h2><p>accuracy: 0.97</p>")
report.get_tab("logs").log("<pre>worker started\nworker finished</pre>")
html = report.get_final_report()
Report itself has no log method; content is added through a Tab. The main tab is available immediately, so a complete explicit construction can write to it through get_tab:
import flyte.report
report = flyte.report.Report("batch-summary")
report.get_tab("main").log("<h2>Summary</h2><p>accuracy: 0.97</p>")
report.get_tab("metrics").log("<p>rows: 1000</p>")
html = report.get_final_report()
Report uses the package-local report/_template.html by default. get_final_report() computes each tab’s HTML, substitutes navigation and body markup into the template, and returns the resulting HTML string in ordinary execution. When ipython_check() detects an IPython environment and IPython.core.display.HTML can be imported, it returns an IPython HTML object instead.
A custom template can be selected with the template_path dataclass field. The template must provide the $NAV_HTML and $BODY_HTML substitutions used by string.Template:
from pathlib import Path
import flyte.report
report = flyte.report.Report("custom")
report.template_path = Path("report-template.html")
report.get_tab("main").log("<p>Rendered with the selected template.</p>")
html = report.get_final_report()
The default template supplies the navigation, tab containers, CSS, and JavaScript that switches the active tab. Report tab names are escaped with html.escape when navigation labels are generated. Tab bodies are deliberately inserted as raw HTML, so the renderer—not Report or Tab—must ensure that body content is safe.
Flush the report to task output
Call flush when an intermediate upload is needed, or let the runtime perform the final upload automatically for a task declared with report=True:
import flyte
import flyte.report
env = flyte.TaskEnvironment(name="streaming-report")
@env.task(report=True)
async def process() -> None:
flyte.report.replace("<h2>Running</h2><p>Stage one is complete.</p>")
await flyte.report.flush.aio()
flyte.report.log("<p>Stage two is complete.</p>")
log, replace, and flush are syncified asynchronous functions. Synchronous callers invoke the syncified callable directly; asynchronous task code can use its .aio() form. Both log and replace accept do_flush=True, which updates the selected main-tab content and then awaits flush.aio() internally:
import flyte.report
flyte.report.log("<p>Checkpoint reached.</p>", do_flush=True)
flyte.report.replace("<p>Current status: complete.</p>", do_flush=True)
flush obtains the active report from internal_ctx(), renders it, maps the task context’s output_path to the fixed report filename report.html through io.report_path, and uploads UTF-8 HTML with text/html content-type attributes through flyte.storage.put_stream. It is a no-op when there is no active task context or that context has no report.
The generic runtime creates the context report as flyte.report.Report(name=action.name), installs it with ctx.replace_task_context, runs the task, and flushes only after the task has produced no error and task.report is true. The local and hybrid execution paths also create a Report and attach it with replace_task_context, so report helpers are available during those task executions as well.
Troubleshoot missing or unexpected report content
The report is empty
report=True controls runtime persistence but does not generate content. Add content with flyte.report.log, flyte.report.replace, or a named tab obtained from flyte.report.get_tab.
Updates made outside a task disappear
Outside a task context, current_report() creates and returns a fresh Report("dummy"). Module-level mutations affect that temporary report only, and flush() returns without uploading. Run the code inside a Flyte task context to associate it with TaskContext.report.
A tab lookup raises ValueError
The default get_tab(name) behavior creates a missing tab. To validate a name instead, use create_if_missing=False:
import flyte.report
tab = flyte.report.get_tab("metrics", create_if_missing=False)
This raises ValueError("Tab metrics does not exist.") when the tab has not already been created. The default creation behavior can otherwise conceal a spelling mistake.
A secondary tab does not get replaced
The module-level flyte.report.replace always replaces main. Retrieve the secondary tab and call its method directly:
import flyte.report
flyte.report.get_tab("metrics").replace("<p>latest metrics only</p>")
Markup appears unsafe or malformed
Tab content is emitted without escaping. Its docstring requires valid HTML that is a fragment rather than a complete document. Tab names are escaped, but bodies are not. Also note that the default template aligns navigation items and body containers by insertion order; creating tabs in the intended order avoids mismatched tab positions.
Flush fails in an interactive Python session
Report.get_final_report() can return IPython.core.display.HTML when IPython is detected. flush() asserts that the rendered value is a str before encoding it for upload. Consequently, explicitly flushing from an IPython context can hit that assertion; the notebook return behavior and the runtime upload path should be treated separately. In task execution, ensure a task output path and a configured Flyte storage backend are available, since the final report is uploaded below that path as report.html.