Free lesson
Generate executive discovery reports from structured assessment data
You build a DiscoveryReportGenerator that aggregates outputs from the prior four engines into a Jinja2-rendered markdown report with executive summary, rankings, and provider recommendations.
~25 min read · Free to read — no subscription required.
Report generation
Introduction
Engineers often spend hours after a discovery workshop manually compiling scattered outputs—scored use cases, data readiness assessments, provider comparisons—into a polished client deliverable. Assembling these by hand introduces inconsistencies and delays the transition to scoping. By the end of this lesson, you'll build a DiscoveryReportGenerator backed by a Jinja2 template that assembles all workshop outputs into a structured, five-section discovery report with numbers guaranteed consistent across the executive summary and detailed sections.
Key Terminology
ReportData— A Python dataclass that aggregates all workshop outputs—scored_use_cases,data_readiness,interview_summaries,provider_comparisons, andengagement_metadata—into a single typed container passed to the report generator.- Jinja2 template — A
.j2file (herediscovery_report.md.j2) that defines the report's layout and prose structure using template variables and control flow; editing it changes report formatting without altering any Python logic. FileSystemLoader— A Jinja2 component initialized with atemplate_dirpath that resolves template names to files on disk, enablingenv.get_template("discovery_report.md.j2")to locate the correct file at runtime.- Template rendering — The call to
template.render(use_cases=..., data_profiles=..., ...)that binds named Python values to template variables, producing the final Markdown string from the.j2file. DiscoveryReportGenerator— The class that owns the Jinja2Environment, retrieves the report template viaFileSystemLoader, and exposes thegenerate(report_data)method that drives the full assembly pipeline.- Report validation — A quality-check step built into the generator that verifies cross-section consistency—confirming every
scored_use_caseappears in the rankings and that figures cited in the executive summary match the detailed sections—before the final report is returned.
Concepts
Separating Data from Presentation
The central design decision in this lesson is keeping workshop data entirely separate from the report's prose structure. Without a templating system, a report generator embeds formatting logic—headers, bullet lists, ranking order—directly into Python string concatenation or f-strings. That approach ties layout choices to code: adjusting which use cases appear in the executive summary or reordering sections requires editing Python, re-testing logic, and potentially redeploying. The coupling is unnecessary.
Jinja2 resolves this by moving the document structure into a .j2 template file. Python populates a ReportData dataclass with raw workshop outputs and passes that container to the generator. The template then owns every layout decision: which variables appear where, how rankings are sorted, what prose frames each section. Changing the report layout means editing the template with no Python changes needed (see Code Walkthrough).
The Five-Section Report Structure
A professional discovery report is designed around distinct stakeholder audiences, and its five sections reflect those audiences explicitly. The Executive Summary exists for senior decision-makers who will not read further—it distills use case counts, top scores by weighted_total, and any data blockers such as unresolved PII findings. Use Case Rankings targets strategists comparing opportunities across business impact and data readiness. The Data Readiness Assessment speaks to data engineering teams who must plan remediation before any model is trained. Provider Recommendations gives architecture teams the feasibility analysis and cost projections they need. Next Steps frames the transition to scoping with milestones and team composition.
Structuring the generator around these five sections—rather than a flat data dump—ensures the report is immediately actionable by all stakeholders without manual reorganization after delivery.
The Rendering Pipeline and Whitespace Control
When generate(report_data) is called, three steps execute in sequence. First, the Environment (configured with FileSystemLoader) locates and parses discovery_report.md.j2 from the templates directory. Second, template.render() binds named variables—use_cases, data_profiles, interviews, providers, metadata—to the corresponding ReportData fields, making them available throughout the template. Third, Jinja2 evaluates all template expressions (loops, conditionals, variable substitutions) and returns the fully rendered Markdown string.
The trim_blocks=True and lstrip_blocks=True options on the Environment control whitespace so that Jinja2 control-flow tags ({% for %}, {% if %}) don't leave blank lines in the output—a detail that matters for clean Markdown rendering when the report is shared with client stakeholders.
Validation for Cross-Section Consistency
A generated report can be structurally complete but still misleading if numbers in the Executive Summary don't match the detailed Use Case Rankings. The generator includes a validation step that checks consistency and completeness before returning the report: every scored_use_case must appear in the rankings table, every dataset must appear in the readiness section, and every figure cited in the summary must trace back to the underlying assessment data. This validation runs after rendering and flags discrepancies for review, ensuring the final deliverable meets the professional quality bar expected in a client-facing discovery context.
Code Walkthrough
Now that you understand how ReportData aggregates workshop outputs and how a Jinja2 template owns every layout decision, the DiscoveryReportGenerator class shows how to wire those two pieces together into a working assembly pipeline.
The generator accepts a ReportData dataclass populated with raw workshop outputs and delegates all formatting decisions to a discovery_report.md.j2 template file. Changing the report layout—reordering sections, adjusting the executive summary prose, tweaking ranking logic—requires only editing the template, not the Python class.
Code snippetpython
1from dataclasses import dataclass, field 2from typing import List, Dict, Any 3from jinja2 import Environment, FileSystemLoader 4 5@dataclass 6class ReportData: 7 scored_use_cases: List[Dict[str, Any]] = field(default_factory=list) 8 data_readiness: List[Dict[str, Any]] = field(default_factory=list) 9 interview_summaries: List[Dict[str, Any]] = field(default_factory=list) 10 provider_comparisons: List[Dict[str, Any]] = field(default_factory=list) 11 engagement_metadata: Dict[str, Any] = field(default_factory=dict) 12 13class DiscoveryReportGenerator: 14 """Generates discovery reports from assessment data using Jinja2 templates.""" 15 16 def __init__(self, template_dir: str = "templates"): 17 self.env = Environment( 18 loader=FileSystemLoader(template_dir), 19 trim_blocks=True, 20 lstrip_blocks=True, 21 ) 22 23 def generate(self, report_data: ReportData) -> str: 24 """Render the discovery report template with workshop data.""" 25 template = self.env.get_template("discovery_report.md.j2") 26 return template.render( 27 use_cases=report_data.scored_use_cases, 28 data_profiles=report_data.data_readiness, 29 interviews=report_data.interview_summaries, 30 providers=report_data.provider_comparisons, 31 metadata=report_data.engagement_metadata, 32 )
The FileSystemLoader resolves "discovery_report.md.j2" to a file in the template_dir directory at runtime. Inside that template, Jinja2 control flow drives the executive summary section—ranking the top use cases by weighted_total and surfacing any datasets with unresolved PII findings that would block production pipelines:
Code snippetjinja2
1# {{ metadata.client_name }} AI Discovery Report 2 3## Executive Summary 4 5Identified {{ use_cases | length }} use cases across {{ metadata.departments | length }} departments. 6 7### Top Opportunities 8{% for uc in use_cases | sort(attribute='weighted_total', reverse=True) | list %} 9- **{{ uc.name }}** — Score: {{ uc.weighted_total }} 10{% endfor %} 11 12### Data Readiness Blockers 13{% for profile in data_profiles %} 14{% if profile.pii_findings %} 15- **{{ profile.dataset_name }}**: Unresolved PII findings — production pipeline blocked 16{% endif %} 17{% endfor %}
This template snippet illustrates the separation of concerns the Concepts section describes: the Python class never decides which use cases rank highest or how PII blockers are phrased—the template owns both decisions entirely. Adding a new section to the report means extending the .j2 file; the DiscoveryReportGenerator.generate() method requires no changes.
The full assembly pipeline ties these pieces together: raw workshop outputs populate the ReportData container, generate() loads discovery_report.md.j2 and binds each field to a named variable, and Jinja2 renders the final five-section Markdown deliverable in one pass.
Confirm that instantiating DiscoveryReportGenerator(template_dir="templates") and calling generate() with a populated ReportData—at least two scored use cases and one data_readiness entry whose pii_status is "unresolved"—returns a Markdown string containing the client name in the title, the use cases ranked by weighted_total, and the PII-flagged dataset listed under the blockers heading.
Do's and Don'ts
Having walked through the material above, the following Do's and Don'ts distill it into practice.
Do's
- ✓Do populate all five fields of
ReportData(scored_use_cases,data_readiness,interview_summaries,provider_comparisons,engagement_metadata) before callinggenerate()— the dataclass'sfield(default_factory=list)defaults ensure the Jinja2 template never encountersNonewhen a workshop produces no provider comparisons or interview summaries, preventing silentUndefinedErrorcrashes at render time. - ✓Do set
trim_blocks=Trueandlstrip_blocks=Trueon the Jinja2Environmentwhen loadingdiscovery_report.md.j2— without these flags, every{% for %}and{% if %}block tag leaves a stray blank line in the rendered Markdown, corrupting heading structure and breaking list formatting in the final client deliverable. - ✓Do keep the executive summary's top-three ranking logic inside the
.j2template, sorting onweighted_totalthere rather than pre-slicing in Python — this preserves the separation between data (ReportData) and layout (discovery_report.md.j2), so switching to a top-five summary or adding a tie-break rule is a template edit alone and cannot introduce a mismatch with the Use Case Rankings section.
Don'ts
- ✗Don't embed report formatting in Python f-strings or string concatenation instead of a
.j2template — collapsing layout intoDiscoveryReportGeneratormeans every section reorder (e.g., moving Provider Recommendations before Data Readiness) requires a Python edit, and numbers cited in both the executive summary and the detailed sections can silently diverge when only one occurrence is updated. - ✗Don't bypass
ReportDataby passing raw dicts directly totemplate.render()— the dataclass's typed fields andfield(default_factory=list)defaults are what guarantee the template receives a consistent key shape; raw dicts carry no such contract and will raiseUndefinedErrorthe first time a workshop omits aprovider_comparisonsentry. - ✗Don't filter PII-flagged datasets for the executive summary using a separate Python pass disconnected from the
data_readinesslist passed to the template — if both the executive summary stanza and the Data Readiness Assessment section filter independently, a last-minute correction to a dataset's PII status updates only the section whose filter was touched, sending clients a report with contradictory risk signals across sections.
Keep going with GenAI Solutions & Delivery
Create a free account to track your progress and open this lesson in the full learning view. Subscribe to unlock the entire path — every goal, the hands-on labs, quizzes, and your verifiable skill graph — from . Cancel anytime.