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, and engagement_metadata—into a single typed container passed to the report generator.
  • Jinja2 template — A .j2 file (here discovery_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 a template_dir path that resolves template names to files on disk, enabling env.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 .j2 file.
  • DiscoveryReportGenerator — The class that owns the Jinja2 Environment, retrieves the report template via FileSystemLoader, and exposes the generate(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_case appears 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.

Loading diagram...

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

  1. Do populate all five fields of ReportData (scored_use_cases, data_readiness, interview_summaries, provider_comparisons, engagement_metadata) before calling generate() — the dataclass's field(default_factory=list) defaults ensure the Jinja2 template never encounters None when a workshop produces no provider comparisons or interview summaries, preventing silent UndefinedError crashes at render time.
  2. Do set trim_blocks=True and lstrip_blocks=True on the Jinja2 Environment when loading discovery_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.
  3. Do keep the executive summary's top-three ranking logic inside the .j2 template, sorting on weighted_total there 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

  1. Don't embed report formatting in Python f-strings or string concatenation instead of a .j2 template — collapsing layout into DiscoveryReportGenerator means 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.
  2. Don't bypass ReportData by passing raw dicts directly to template.render() — the dataclass's typed fields and field(default_factory=list) defaults are what guarantee the template receives a consistent key shape; raw dicts carry no such contract and will raise UndefinedError the first time a workshop omits a provider_comparisons entry.
  3. Don't filter PII-flagged datasets for the executive summary using a separate Python pass disconnected from the data_readiness list 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.