Creating Your First Report
What you will build
You will create a flyte.report.Report, write HTML fragments to its automatically created main tab, add a second tab, and render the result with get_final_report(). You will then connect the same authoring API to a Flyte task configured with report=True, which enables the runtime’s report-upload step.
The public report API is exported from report: Report, get_tab, log, replace, flush, and current_report. The implementation lives in report/_report.py.
Prerequisites
You need:
- flyte-sdk installed and importable as
flyteandflyte.report. - A
TaskEnvironmentif you want to execute the report from a task. - An initialized Flyte storage backend and an available task output path if you want
flush()to uploadreport.html. The report package usesflyte.storage.put_streamfor that upload.
The task option is disabled by default: TaskEnvironment.task(..., report=False) declares report as a keyword-only option. Set it to True for a task whose successful execution should flush its report.
Create and render an in-memory report
Start with a direct Report instance. This is useful for seeing the rendering behavior without requiring a task context:
from flyte.report import Report
report = Report(name="first-report")
report.get_tab("main").log("<h1>First report</h1>")
report.get_tab("main").log("<p>This content is on the default tab.</p>")
html = report.get_final_report()
print(type(html).__name__)
print("First report" in str(html))
Report is a dataclass whose required field is name. During __post_init__, it unconditionally creates a Tab named main, so the first get_tab("main") returns an existing tab. Tab.log() appends each fragment, and Tab.get_html() joins appended fragments with newline characters. In a normal Python process, get_final_report() returns a str; when IPython is active and IPython.core.display.HTML can be imported, it returns an IPython HTML object instead. The final print therefore converts the result to text for this check.
Supply HTML fragments, not a complete document
Tab.log() and the module-level log() method expect valid HTML that can be inserted inside a generated <div>. They do not expect a second <!doctype html>, <html>, or <body> document. Report bodies are inserted verbatim:
from flyte.report import Report
report = Report(name="html-fragments")
main = report.get_tab("main")
main.log("<p><strong>Status:</strong> complete</p>")
rendered = report.get_final_report()
assert "<strong>Status:</strong> complete" in str(rendered)
get_final_report() escapes tab names with html.escape, but deliberately does not escape tab content before placing it in <div>{value}</div>. Only pass trusted, valid HTML fragments, or escape untrusted values before constructing a fragment.
Add content to another tab
Report.get_tab() creates a missing tab by default. Add a metrics tab and verify its rendered fragment:
from flyte.report import Report
report = Report(name="tabbed-report")
report.get_tab("main").log("<h1>Run summary</h1>")
metrics = report.get_tab("metrics")
metrics.log("<h2>Metrics</h2>")
metrics.log("<p>Rows: 42</p>")
rendered = str(report.get_final_report())
assert "Run summary" in rendered
assert "Metrics" in rendered
assert "Rows: 42" in rendered
Tabs are kept in the insertion order of Report.tabs. The default main tab is inserted first, followed by lazily created tabs, and the packaged report/_template.html marks the first navigation item and first body container active. The template receives generated navigation through $NAV_HTML and generated bodies through $BODY_HTML; its JavaScript switches matching navigation and body elements by their generated indices.
If you need to reject an unknown tab instead of creating it, pass create_if_missing=False:
from flyte.report import Report
report = Report(name="existing-tab-check")
try:
report.get_tab("missing", create_if_missing=False)
except ValueError as error:
print(error)
The resulting message is Tab missing does not exist.. The default remains True, so omitting the argument creates the tab.
Append versus replace
Use log() to accumulate fragments. Use replace() when the tab should contain only the new fragment:
from flyte.report import Report
report = Report(name="replacement")
main = report.get_tab("main")
main.log("<p>intermediate value</p>")
main.replace("<p>final value</p>")
assert main.get_html() == "<p>final value</p>"
assert "intermediate value" not in str(report.get_final_report())
Tab.replace() assigns a one-element list to content; it is not another append operation.
Write from inside a task
For task code, use the module-level helpers. They resolve the Report stored in the active TaskContext, and log() targets that report’s main tab:
import flyte
env = flyte.TaskEnvironment("report-example")
@env.task(report=True)
async def make_report() -> str:
flyte.report.log("<h1>Task report</h1>")
flyte.report.log("<p>The task completed its report body.</p>")
flyte.report.get_tab("metrics").log("<p>Rows: 42</p>")
return "done"
The report=True setting is the part that enables report generation for the task; its default is False. The runtime still constructs a Report for task execution, but the V2 task runner calls await flyte.report.flush.aio() only after the task succeeds and only when task.report is true. If task execution returns an error, that runner returns the converted error before reaching the flush call.
flyte.report.log() and flyte.report.replace() are decorated with @syncify. The calls above are synchronous, while their asynchronous forms are available as .aio when you need to await them:
import flyte
async def write_async_fragment() -> None:
await flyte.report.log.aio("<p>Written through the async helper.</p>")
You can request an immediate flush by passing do_flush=True to log() or replace(). Those helpers update main first and then await flush.aio().
Run the task and locate the generated artifact
flyte.with_runcontext(...).run(...) is the public execution path. Its run() method accepts a task template and task arguments:
import flyte
env = flyte.TaskEnvironment("report-example")
@env.task(report=True)
async def make_report() -> str:
flyte.report.log("<h1>Task report</h1>")
flyte.report.get_tab("metrics").log("<p>Rows: 42</p>")
return "done"
if __name__ == "__main__":
flyte.with_runcontext(run_base_dir="s3://bucket/metadata/outputs").run(make_report)
The run_base_dir value is required by the hybrid runner when it is not otherwise supplied, and the source uses an object-storage URI in its own error guidance. Configure the matching Flyte storage backend before relying on the uploaded artifact.
During local and hybrid execution, _Runner constructs Report(name=action.name) and places it in a TaskContext before invoking the task. The V2 runtime follows the same model in convert_and_run. flush() renders the active report, computes the path with io.report_path(task_context.output_path), and uploads UTF-8 bytes with HTML content-type attributes. The persisted artifact is therefore:
<task output_path>/report.html
Outside a task context, flush() returns without writing. Likewise, current_report() creates a fresh Report("dummy") when there is no active task report; content written through module-level helpers in that situation is not persisted.
Optional direct customization
For an in-memory report, you can replace the packaged template by passing a pathlib.Path to the dataclass field. A custom template must contain both $NAV_HTML and $BODY_HTML, because get_final_report() uses string.Template.substitute() with those two names:
import pathlib
from flyte.report import Report
report = Report(
name="custom-template",
template_path=pathlib.Path("report-template.html"),
)
report.get_tab("main").log("<p>Custom template content</p>")
html = report.get_final_report()
The default template_path points to the packaged report/_template.html. Report.name is stored on the object, but get_final_report() does not insert it into the standard HTML output.
This checkout contains the report implementation and runtime integration but no report-specific tests, examples, or Markdown documentation. The snippets above use the public API and the exact signatures and behavior in report/_report.py; the runtime behavior comes from _run.py, TaskEnvironment.task, and the V2 task runner.
Complete result and next steps
You now have a report with a default main tab, an optional metrics tab, HTML-fragment content, and a generated full HTML document. For the task form, keep report=True, initialize storage, and run with an appropriate output or run base path so the runtime can upload <output_path>/report.html. From there, add tabs with flyte.report.get_tab(name), append with Tab.log(), replace a tab with Tab.replace(), or render an in-memory Report directly with get_final_report().