Working with Multiple Tabs
Create a report with named tabs
To persist multiple sections from a Flyte task, enable reporting on the task and write the default section through flyte.report.log() while using flyte.report.get_tab() for named sections:
import flyte
import flyte.report
env = flyte.TaskEnvironment(name="tabbed_reports")
@env.task(report=True)
async def build_report():
flyte.report.log("<h2>Summary</h2><p>Run completed.</p>")
metrics = flyte.report.get_tab("metrics")
metrics.log("<h2>Metrics</h2><p>Accuracy: 0.94</p>")
diagnostics = flyte.report.get_tab("diagnostics")
diagnostics.log("<h2>Diagnostics</h2><p>No warnings.</p>")
Report.__post_init__() creates a Tab named main whenever a Report is constructed. The module-level log() helper always resolves that tab, so the first fragment above goes to main. The named calls return the corresponding Tab objects and create metrics and diagnostics the first time they are requested.
Task execution creates Report(name=action.name) and stores it in the TaskContext. The runtime task runner makes that report the current report while it runs the task. Local and hybrid task execution also attach a Report to their task context, so the same flyte.report calls work in those modes.
Add or replace content in a specific tab
Use Tab.log() when a tab should retain all fragments, and Tab.replace() when its complete contents should be replaced:
import flyte
import flyte.report
env = flyte.TaskEnvironment(name="tab_updates")
@env.task(report=True)
async def update_tabs():
progress = flyte.report.get_tab("progress")
progress.log("<p>Started</p>")
progress.log("<p>Loaded inputs</p>")
# The two earlier fragments are discarded.
progress.replace("<h2>Progress</h2><p>Finished</p>")
Tab.log(content) appends the supplied string to the tab’s internal content list. Tab.replace(content) assigns a new one-element list, removing everything previously logged to that tab. Tab.get_html() joins the retained fragments with newline separators.
The module-level flyte.report.replace(content) is the corresponding convenience operation for the implicit main tab. Both flyte.report.log() and flyte.report.replace() accept do_flush=False; passing do_flush=True requests a flush immediately after the update:
import flyte
import flyte.report
env = flyte.TaskEnvironment(name="main_report")
@env.task(report=True)
async def write_main_report():
flyte.report.log("<p>Initial content</p>")
flyte.report.replace("<h2>Final summary</h2><p>Ready.</p>", do_flush=True)
The log, replace, and flush helpers are decorated with syncify, so their synchronous usage is exposed alongside their asynchronous implementation. Tab.log() and Tab.replace() themselves are ordinary methods.
Access existing tabs strictly
get_tab() creates an unknown tab by default. Pass create_if_missing=False when an unknown name should raise an error instead of changing the report:
import flyte
import flyte.report
env = flyte.TaskEnvironment(name="strict_tabs")
@env.task(report=True)
async def use_existing_tab():
flyte.report.get_tab("metrics").log("<p>Metrics are available.</p>")
try:
flyte.report.get_tab("missing", create_if_missing=False)
except ValueError as error:
# The message is: "Tab missing does not exist."
flyte.report.log(f"<p>{error}</p>")
The strict lookup is delegated to Report.get_tab(), which raises ValueError(f"Tab {name} does not exist."). With the default create_if_missing=True, the same lookup would store and return a new Tab under that name.
Render a report directly
For direct report construction or inspection, use the public Report class and obtain tabs through Report.get_tab():
from flyte.report import Report
report = Report(name="evaluation")
report.get_tab("metrics").log("<table><tr><td>Accuracy</td><td>0.94</td></tr></table>")
report.get_tab("notes").replace("<p>Validated on the holdout set.</p>")
rendered = report.get_final_report()
Report.get_final_report() processes tabs in dictionary insertion order. The automatically inserted main tab is therefore first, followed by tabs created later. For each tab, it creates a navigation <li> using the escaped tab name and places the tab HTML inside a <div>. The package template in report/_template.html supplies the navigation and body shell through its $NAV_HTML and $BODY_HTML placeholders; its JavaScript activates the first tab initially and switches the corresponding body when a navigation item is clicked.
Tab content is inserted as HTML without escaping. Supply HTML fragments rather than complete HTML documents: Tab documents that its content is inserted into a div, and get_final_report() performs that wrapping. The source explicitly leaves responsibility for ensuring safe HTML to the renderer/caller. Tab names are escaped for navigation text, but tab bodies are not.
Tab is defined in the private module report._report and is not exported by flyte.report. Normal user code should not import it directly; use flyte.report.get_tab() or Report.get_tab() to obtain a tab. Report and the module-level helpers are exported from flyte.report.
Persist the rendered tabs
The task decorator’s report option defaults to False. The runtime runner still creates a report in the task context, but it calls flyte.report.flush.aio() after successful task execution only when task.report is true. Thus the report=True setting in the examples enables the normal end-of-task persistence path.
flush() returns without uploading outside a task context or when the current context has no report. In a task context it renders the report, obtains the report output path through io.report_path(...), and uploads UTF-8 HTML with text/html content attributes. The fixed output filename is report.html beneath the task context’s output path, and storage initialization plus a valid task output path are required.
You can also request an explicit flush while logging or replacing the main tab:
import flyte
import flyte.report
env = flyte.TaskEnvironment(name="explicit_flush")
@env.task(report=True)
async def flush_after_update():
flyte.report.get_tab("metrics").log("<p>Latest metrics</p>")
flyte.report.log("<p>Report updated.</p>", do_flush=True)
The explicit flush options exist on the module-level log() and replace() functions. They do not exist on Tab.log() or Tab.replace(), so use flyte.report.flush() separately when updating a named tab and flushing immediately.
Troubleshoot tabbed reports
- A tab unexpectedly appears:
get_tab(name)lazily creates missing tabs. Usecreate_if_missing=Falsefor validation. - Earlier tab output disappeared:
replace()removes all fragments previously added withlog()on that tab. - HTML displays as text or produces malformed output: pass an HTML fragment, not a full document. Tab content is embedded raw inside a
<div>and is not escaped. - No
report.htmlis produced: check that the task usesreport=True, that execution is inside a task context, and that task output storage is initialized. The runtime’s automatic flush occurs only after a successful task. - A standalone report does not persist:
current_report()createsReport("dummy")outside task execution. Content written through the module-level helpers in that situation is not attached to a task; construct aReportdirectly when you need an in-memory report. - Rendering behaves differently in a notebook:
get_final_report()returns anIPython.core.display.HTMLobject when IPython is detected and the import succeeds; otherwise it returns an HTML string.flush()requires the rendered value to be a plainstr, so an IPython-detected path can fail its type assertion during flushing.