> For the complete documentation index, see [llms.txt](https://docs.karini.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.karini.ai/agentic-ai-workshops/automotive-and-manufacturing/predictive-diagnostic-assistant.md).

# FaultLens: Build a Predictive Diagnostic Assistant

Build FaultLens, a webhook-triggered workflow where a router screens anomalies, one agent gathers evidence, and another diagnoses the fault and emails a cited report.

**Lab time:** about 60 minutes, including a 10-minute live simulation

## The Problem

A sensor flags abnormal vibration on a pump. Now a maintenance engineer has to pull the machine logs, check what work was done recently, search past incidents for the same pattern, look up the right procedure, and decide what's actually wrong, often while the pump keeps running toward failure. The evidence exists across several systems. Assembling it takes time the plant doesn't have.

## What You Will Build

FaultLens, a workflow that runs automatically the moment an anomaly is detected:

1. **Event\_Classifier router** checks that the request is a real anomaly and ignores everything else
2. **Collect Evidence agent** gathers machine logs, maintenance history, past incidents, and procedures
3. **Diagnostic Reporter agent** ranks the likely causes with citations, saves the report, and emails the maintenance team

You'll finish by connecting FaultLens to a live pump simulator. When a fault develops, your workflow runs on its own and the diagnostic report lands in your inbox.

## What You Will Learn

* Trigger an agent workflow from an external system using a webhook
* Split a task between agents: one gathers evidence, one reasons over it
* Use a router to filter requests before any agent runs
* Match model size to the job: a small model for routing, a larger one for diagnosis
* Keep agents on safe rails with predefined database calls and human-readable citations

## Build Order

Build the two agents first, then the router, and the recipe last. A recipe can only use prompts, tools, and skills that already exist.

| Step | You create                 | Contains                                         |
| ---- | -------------------------- | ------------------------------------------------ |
| 1    | Agent: Collect Evidence    | 1 database tool, 2 knowledge base tools, 1 skill |
| 2    | Agent: Diagnostic Reporter | 1 database tool, 1 email tool, 2 skills          |
| 3    | Router: Event\_Classifier  | Prompt only, no tools                            |
| 4    | Recipe: faultlens-workflow | Webhook, Start, Router, 2 Agents, End            |
| 5    | End-to-end test            | One live run from the PDA web app                |

## Before You Start

Your instructor has prepared the following so you can focus on building the agents:

* Login to Karini AI at app.karini.ai
* The skills **evidences-retrieval**, **faults-diagnosis**, and **reports-notification**, available in the Skills Library
* The prompt templates **Collect Evidence**, **Diagnostic Reporter**, and **Event\_Classifier**, available in Prompt Playground
* The datasets **PDA incidents** and **PDA procedures**, available in Karini datasets
* PostgreSQL credentials for the PDA database: host, user name, and password
* The PDA web app at [diagnostic-agent.karini.ai](https://diagnostic-agent.karini.ai/) and a workshop access code

Use every tool name exactly as written in this guide. The prompts call the tools by these names, so a renamed tool breaks the agent.

## Words You Will See

* **Prompt Playground:** where you create agents and prompts, attach tools and skills, and test them
* **Prompt template:** a ready-made prompt stored in Karini. You load it with Use template instead of typing it.
* **Tool:** something an agent can call, such as a database, a knowledge base, or email
* **Skill:** a file of detailed instructions that an agent reads when it needs it
* **Metadata:** structured details passed along with a request, such as the event ID and machine ID. The agents read the event ID from it.
* **Router:** a node that decides which path a request takes. Here it sends real anomalies to the agents and everything else straight to End.
* **Webhook:** a URL that lets an outside system start your recipe. The pump simulator calls it when it detects an anomaly.
* **Recipe:** the workflow that connects everything on a canvas
* **Publish:** saves a version that recipes can use. Saved but unpublished changes are not used.
* **Deploy:** makes a published recipe version live, uses it. Click **Actions → Deploy** after you publish.&#x20;

## Step 1: Create the Collect Evidence Agent

### 1.1 Create the prompt

1. From the left menu, open Prompt Playground.
2. Click Add new in the top right corner.
3. Click Prompt templates, Select **Name** from the dropdown next to the search field, search for **Collect Evidence**, select it, and click Use template. The template sets the prompt name to Collect Evidence.
4. Check that Task is **Agent** and set Max State Updates to **30**.
5. Click Save.

Save before you move to the Tools or Skills tab, or your changes are lost.

### 1.2 Add tool 1: PDA database

1. Open the Collect Evidence prompt.
2. Open the Tools tab, click Add new, and select Agent tools.
3. Fill in:

| Field                          | Value                          |
| ------------------------------ | ------------------------------ |
| Name                           | PDA database                   |
| Description                    | \<Copy the text below exactly> |
| Type                           | Database                       |
| Connector type                 | PostgreSQL                     |
| Host, User name, User password | From your instructor           |
| Database                       | postgres                       |
| Port                           | 5432                           |

Description text:

{% code overflow="wrap" %}

```
Predictive maintenance database for the pump fleet. Use it to read an anomaly event, the recent machine logs, the maintenance history and similar past incidents, and to save each agent's output, the diagnostic report and the email status. The login can only run the pda functions listed in your skill, one statement per call, and cannot read or change tables directly. Use exactly the calls in your skill and never write any other SQL.
```

{% endcode %}

3. Click Test and wait for PostgreSQL connection test passed.
4. Click Save.

### 1.3 Add tool 2: PDA incidents KB

1. In the Tools tab, click Add new and select Agent tools.
2. Fill in:

| Field                | Value                          |
| -------------------- | ------------------------------ |
| Name                 | PDA incidents KB               |
| Description          | \<Copy the text below exactly> |
| Type                 | Dataset                        |
| Dataset              | PDA incidents                  |
| Prompt context       | Use embedding chunks           |
| Top-K                | 5                              |
| Enable hybrid search | Enable Checkbox                |

Description text:

{% code overflow="wrap" %}

```
Past pump incident reports with symptoms, root cause and the fix applied. Use it to find earlier incidents with the same alarm codes or the same vibration and temperature pattern as the current event.
```

{% endcode %}

3. Click Save.

### 1.4 Add tool 3: PDA procedures KB

1. In the Tools tab, click Add new and select Agent tools.
2. Fill in:

| Field                | Value                          |
| -------------------- | ------------------------------ |
| Name                 | PDA procedures KB              |
| Description          | \<Copy the text below exactly> |
| Type                 | Dataset                        |
| Dataset              | PDA procedures                 |
| Prompt context       | Use embedding chunks           |
| Top-K                | 5                              |
| Enable hybrid search | Enable Checkbox                |

Description text:

{% code overflow="wrap" %}

```
Standard maintenance and troubleshooting procedures (SOPs) for the pumps. Use it to find the inspection steps and corrective actions that match the suspected fault.
```

{% endcode %}

3. Click Save.

### 1.5 Add the skill

1. Open the Skills tab, click Add new, and select Custom.
2. Search for **evidences-retrieval** and click Add.

### 1.6 Test & Compare

1. In Edit prompt, set the metadata variable to:

{% code overflow="wrap" %}

```json
{"trigger": "anomaly", "event_id": "e5000000-0000-4000-8000-000000000001", "machine_id": "PUMP-01"}
```

{% endcode %}

If your instructor assigns you a different event ID, use it here instead. If the metadata variable is empty, the agent replies that it has no event\_id and stops.

2. Open the Test & Compare tab.
3. Select two models to compare: Claude Sonnet 5.5 and Claude Haiku 5.5.
4. Click Test.
5. Compare the outputs. Each should be an evidence result in JSON, like the sample in [Appendix A:](#appendix-a-sample-evidence-result) telemetry deviations, warning logs, recent maintenance, knowledge base references, and an evidence status.
6. This is a practice event. The database returns sample data and nothing is saved. if Tool results show “sandbox”: true; that is expected.
7. Click Select as best answer on the best output to set the Primary Model.
8. Optional: click Select as best answer on the other output to set the Fallback Model.
9. Click Save prompt run in the right corner.
10. In Edit prompt Clear the metadata variable and leave it empty, otherwise the agent always uses this fixed event\_id.
11. Click Save, Reopen the Collect Evidence prompt and then Publish.

## Step 2: Create the Diagnostic Reporter Agent

### 2.1 Create the prompt

1. In Prompt Playground, click Add new.
2. Click Prompt templates, Select **Name** from the dropdown next to the search field, search for **Diagnostic Reporter**, select it, and click Use template. The template sets the prompt name to Diagnostic Reporter.
3. Check that Task is **Agent** and set Max State Updates to **30**.
4. The prompt has one variable, **html\_template**. If it is empty, paste the email template from [Appendix B.](#appendix-b-email-template-html_template)
5. In Agent input, paste the sample evidence result from [Appendix A](#appendix-a-sample-evidence-result). This stands in for the output of Collect Evidence Agent.
6. Click Save.

### 2.2 Add tool 1: PDA database

Open Diagnostic Reporter and add the PDA database tool with the same name, description, and settings as in [1.2,](#id-1.2-add-tool-1-pda-database) then click Save.

### 2.3 Add tool 2: Maintenance Email

1. In the Tools tab, click Add new and select Agent tools.
2. Fill in:

| Field                 | Value                                                  |
| --------------------- | ------------------------------------------------------ |
| Name                  | Maintenance Email                                      |
| Description           | Copy the text below exactly                            |
| Type                  | Messaging                                              |
| Messaging Type        | Email                                                  |
| Provider              | Gmail                                                  |
| Overwrite credentials | Off: the workshop sender account is already configured |

Description text:

{% code overflow="wrap" %}

```
Sends the diagnostic report as one HTML email to the maintenance team. Inputs: recipient_email (a list of addresses), subject, and email_message (the complete HTML report built from the saved report). Send the saved values unchanged. A success response means the email was submitted, not that it was delivered.
```

{% endcode %}

3. In Recipient email, enter your own email address and press Tab, add a subject and body, and click Test.
4. When Test passed appears, click Save.

### 2.4 Add the skills

1. Open the Skills tab, click Add new, and select Custom.
2. Search for **faults-diagnosis** and click Add.
3. In the same way, search for **reports-notification** and click Add.

### 2.5 Test & Compare

1. Open the Test & Compare tab.
2. Select model to compare: Claude Sonnet 5.5.
3. In Edit prompt, set test\_email to your own email address.
4. Click Test.
5. Expected result: The report is checked but not saved, and a test email with the subject \[PRACTICE] Diagnostic report PUMP-01 arrives in your inbox.
6. Validate the output. It should rank likely causes with citations to the logs and knowledge base, with misalignment ranked first for the sample, The practice event is checked but nothing is saved and no email is sent. You receive real emails from the Maintenance Email tool’s Test button and from your live run in Step 5.
7. Click Select as best answer on the output to set the Primary Model.
8. Click Save prompt run in the right corner.
9. Clear test\_email and keep it empty, then Save and Publish.This works the same way as the metadata note in [Step 1.6.](#id-1.6-test-and-compare)
10. Click Save, reopen the Diagnostic Reporter prompt then Publish.

Diagnosis is where reasoning depth matters, so compare carefully here. If the smaller model misses the coupling work or misreads the 2x vibration peak, that's the case for the larger one.

## Step 3: Create the Event\_Classifier Router

### 3.1 Create the prompt

1. In Prompt Playground, click Add new.
2. Click Prompt templates, Select **Name** from the dropdown next to the search field, search for **Event\_Classifier**, select it, and click Use template. The template sets the prompt name to Event\_Classifier.
3. Check that Task is **Summarization**. Select it here even though this prompt classifies requests rather than summarizing them.
4. Click Save.

### 3.2 Test & Compare

Run two tests: one request the router should accept, and one it should turn away.

1. Set the metadata variable to an anomaly in Edit Prompt:

```json
{"trigger": "anomaly", "event_id": "e5000000-0000-4000-8000-000000000001"}
```

2. Open the Test & Compare tab and select Claude Sonnet 5.5 and Claude Haiku 5.5.
3. Click Test. Both should classify the request as an anomaly to diagnose so it will pass anomaly to next agent as {"next": "Collect Evidence"}.
4. Change the metadata variable to a request that is not an anomaly in Edit prompt:

```json
{"trigger": "status_check", "event_id": "e5000000-0000-4000-8000-000000000001"}
```

5. Click Test again. Both should classify it as not an anomaly, so router will pass that request to                    {"next": "End"} the recipe will skip the agents.
6. Click Select as best answer on the best output to set the Primary Model.
7. Optional: click Select as best answer on the other output to set the Fallback Model.
8. Click Save prompt run in the right corner.
9. Remove metadata from the variable keep it empty (Otherwise agent will pass fixed metadata even in production recipe).
10. Click Save, Reopen the Event\_Classifier prompt then Publish.

The router makes a simple yes-or-no decision on every request, so a smaller, faster, cheaper model like Haiku usually does it as well as a larger one. Save the larger model for the diagnosis, where reasoning depth pays off.

## Step 4: Create the Karini Recipe with Bedrock AgentCore Runtime

### 4.1 Create the recipe

1. From the left menu, open Recipes and click Add new in the top corner.
2. Configure the recipe:

| Field   | Value              |
| ------- | ------------------ |
| Name    | faultlens-workflow |
| Type    | Workflow           |
| Runtime | AgentCore          |

### 4.2 Add the nodes

Drag each element from Elements onto the canvas, then click it to fill in the right panel.

| # | Element | Label               | Settings in the right panel                                                                                                                                                                                     |
| - | ------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Webhook | Webhook             | **Keep the defaults**                                                                                                                                                                                           |
| 2 | Start   | Start               | **None**                                                                                                                                                                                                        |
| 3 | Router  | Event\_Classifier   | <p><strong>Node method</strong> Prompt, <br><strong>Prompt</strong> = Event\_Classifier. <br><strong>State settings:</strong> <strong>Messages</strong> on (Last message),<br> <strong>Metadata</strong> on</p> |
| 4 | Agent   | Collect Evidence    | <p><strong>Prompt</strong> = Collect Evidence. <br><strong>State settings: Messages</strong> on (Last message), <br><strong>Metadata</strong> on</p>                                                            |
| 5 | Agent   | Diagnostic Reporter | <p><strong>Prompt</strong> = Diagnostic Reporter. <br><strong>State settings:</strong> <strong>Messages</strong> on (Last message). <br> <strong>Metadata</strong> can stay off</p>                             |
| 6 | End     | End                 | **None**                                                                                                                                                                                                        |

Collect Evidence must have Metadata on. Without it, the agent never gets the event\_id and a live run saves nothing. Also the Agent Labels should be as stated because the router chooses the next node by its label.

4.3 Connect the nodes

| From                | To                  |
| ------------------- | ------------------- |
| Webhook             | Start               |
| Start               | Event\_Classifier   |
| Event\_Classifier   | Collect Evidence    |
| Collect Evidence    | Diagnostic Reporter |
| Diagnostic Reporter | End                 |
| Event\_Classifier   | End                 |

Diagnostic Reporter must come straight after Collect Evidence, because Last message is the output of the node before it. Put anything in between and the reporter receives the wrong input. The second connection from Event\_Classifier to End is the path for requests that are not anomalies.

### 4.4 Recipe settings, test, and deploy

1. Click an empty area of the canvas and set Number of state updates to **100**.
2. Click Save, reopen faultlens-workflow, then click Publish.
3. Click **Action → Deploy** after you publish.
4. Click the Webhook node and copy the **Webhook URL** and **Webhook token**. Click Generate new token if the token is empty. Your recipe ID is the last part of the webhook URL.

Treat the webhook token like a password: anyone with it can trigger your recipe. Don't share it or paste it into chat.

{% embed url="<https://www.loom.com/share/729b433c23ca46a39e5dac2b540cfed7>" %}

## Step 5: Run FaultLens Live

1. Open the PDA web app at [diagnostic-agent.karini.ai](https://diagnostic-agent.karini.ai/) and click **Start run** in the top right corner.
2. Enter the details:

| Field                                        | Value                             |
| -------------------------------------------- | --------------------------------- |
| Email                                        | Your email address                |
| Workshop access code                         | The code from your instructor     |
| Run label                                    | Optional                          |
| Your Karini recipe → Recipe ID               | The last part of your webhook URL |
| Your Karini recipe → API token (x-api-token) | Your webhook token                |

3. Click **Start**. The simulator runs the pump for up to 10 minutes and calls your recipe when it confirms an anomaly.
4. Optional: click **Inject fault now** in the web app to trigger an anomaly right away.
5. While you wait, open the traces: in Karini, open **Recipes**, then for faultlens-workflow click **Actions → Webhook history**, find the latest request and click **View Traces**. You can watch the router decision, each Collect Evidence tool call and the Diagnostic Reporter’s report.
6. Check your inbox for the diagnostic report, and check the run under **Your recent runs** in the web app.

This is the moment that shows the value: no one asked FaultLens anything. The pump developed a fault, the workflow triggered itself, and a cited diagnosis reached the maintenance inbox.

## Appendix A: Sample Evidence Result

Paste this into the Diagnostic Reporter's Agent input in Step 2.1. It is also what a good Collect Evidence output looks like in Step 1.6.

{% code overflow="wrap" %}

```json
{
  "status": "ready",
  "event_id": "e5000000-0000-4000-8000-000000000001",
  "machine_id": "PUMP-01",
  "event_summary": "PUMP-01 score 3.32 >= 2.5; vibration, bearing temperature and motor current above normal after recent coupling work.",
  "prediction": {"anomaly_score": 3.32, "threshold": 2.5, "predicted_fault": "misalignment", "inference_mode": "synthetic"},
  "telemetry_deviations": [
    {"sensor": "vibration_mm_s", "value": 6.1, "normal_range": "1.6-3.3", "direction": "high"},
    {"sensor": "temperature_c", "value": 69.8, "normal_range": "57-68", "direction": "high"},
    {"sensor": "motor_current_a", "value": 45.2, "normal_range": "39-44.5", "direction": "high"}
  ],
  "logs": [
    {"log_id": 901, "ts": "2026-01-01T00:00:12Z", "level": "WARN", "event_code": "VIB-205", "message": "Axial vibration component high, 2x running-speed peak dominant (4.27 mm/s)"},
    {"log_id": 900, "ts": "2026-01-01T00:00:02Z", "level": "WARN", "event_code": "VIB-201", "message": "Overall vibration 6.1 mm/s above 4.5 mm/s alert limit"}
  ],
  "maintenance_history": [
    {"log_id": 850, "ts": "2025-12-30T10:14:00Z", "event_code": "MNT-610", "message": "Coupling element replaced and shaft re-aligned (work order WO-46482)"}
  ],
  "recurring_warnings": [
    {"event_code": "VIB-201", "level": "WARN", "occurrences": 3, "last_seen": "2025-12-31T22:10:00Z"}
  ],
  "kb_refs": [
    {"doc_id": "SOP-PUMP-105", "type": "procedure", "title": "Shaft alignment troubleshooting", "excerpt": "Axial 2x vibration after coupling work indicates possible misalignment; take handheld axial and radial readings.", "relevance": "Matches VIB-205 and the recent MNT-610 coupling work."},
    {"doc_id": "INC-2026-016", "type": "incident", "title": "Shaft misalignment on PUMP-01", "excerpt": "Same VIB-205/VIB-201 pattern; first misdiagnosed as bearing wear.", "relevance": "Same axial-vibration pattern after coupling work."}
  ],
  "evidence_status": "sufficient",
  "evidence_gaps": ["No vibration spectrum available to confirm the 2x component independently"],
  "tool_errors": [],
  "untrusted_content_flags": [],
  "summary": "2 warning logs, 2 knowledge-base documents"
}
```

{% endcode %}

## Appendix B: Email Template (html\_template)

Paste this into the Diagnostic Reporter's html\_template variable in Step 2.1.

{% code overflow="wrap" %}

```html
<!doctype html>
<html>
  <body style="margin:0;padding:0;background:#f4f5f7;">
    <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f4f5f7;padding:24px 0;">
      <tr>
        <td align="center">
          <table role="presentation" width="640" cellpadding="0" cellspacing="0" style="width:640px;max-width:640px;background:#ffffff;border:1px solid #e2e5ea;border-radius:8px;font-family:Arial,Helvetica,sans-serif;color:#1f2430;">
            <tr>
              <td style="background:#1f2a44;color:#ffffff;padding:18px 24px;border-radius:8px 8px 0 0;">
                <div style="font-size:13px;letter-spacing:.5px;opacity:.8;">PREDICTIVE DIAGNOSTIC ASSISTANT &middot; workshop run {{run_label}}</div>
                <div style="font-size:20px;font-weight:bold;margin-top:4px;">Diagnostic report: {{machine_id}}</div>
                <div style="font-size:13px;opacity:.8;margin-top:2px;">{{detected_at_utc}}</div>
              </td>
            </tr>
            <tr>
              <td style="padding:16px 24px 0 24px;">
                <div style="background:#fff8e1;border:1px solid #f0d58a;border-radius:6px;padding:10px 12px;font-size:13px;color:#6b5300;">
                  Decision support only. Hypotheses are not confirmed root causes. Follow SAFE-001 before any intervention.
                </div>
              </td>
            </tr>
            <tr>
              <td style="padding:16px 24px 0 24px;">
                <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:14px;">
                  <tr><td style="padding:6px 10px;border:1px solid #e2e5ea;background:#f7f8fa;width:200px;">Run</td><td style="padding:6px 10px;border:1px solid #e2e5ea;">{{run_id}}</td></tr>
                  <tr><td style="padding:6px 10px;border:1px solid #e2e5ea;background:#f7f8fa;">Event</td><td style="padding:6px 10px;border:1px solid #e2e5ea;">{{event_id}}</td></tr>
                  <tr><td style="padding:6px 10px;border:1px solid #e2e5ea;background:#f7f8fa;">Anomaly score / threshold</td><td style="padding:6px 10px;border:1px solid #e2e5ea;">{{anomaly_score}} / {{threshold}}</td></tr>
                  <tr><td style="padding:6px 10px;border:1px solid #e2e5ea;background:#f7f8fa;">Model fault class</td><td style="padding:6px 10px;border:1px solid #e2e5ea;">{{predicted_fault}} ({{inference_mode}})</td></tr>
                  <tr><td style="padding:6px 10px;border:1px solid #e2e5ea;background:#f7f8fa;">Severity / confidence</td><td style="padding:6px 10px;border:1px solid #e2e5ea;">{{severity}} / {{overall_confidence}}</td></tr>
                </table>
              </td>
            </tr>
            <tr>
              <td style="padding:18px 24px 0 24px;">
                <div style="font-size:16px;font-weight:bold;border-bottom:2px solid #1f2a44;padding-bottom:4px;">Summary</div>
                <div style="font-size:14px;line-height:1.5;margin-top:8px;">{{summary}}</div>
              </td>
            </tr>
            <tr>
              <td style="padding:18px 24px 0 24px;">
                <div style="font-size:16px;font-weight:bold;border-bottom:2px solid #1f2a44;padding-bottom:4px;">Telemetry at detection</div>
                <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:14px;margin-top:8px;">
                  <tr><th style="padding:6px 10px;border:1px solid #e2e5ea;background:#f7f8fa;text-align:left;">Sensor</th><th style="padding:6px 10px;border:1px solid #e2e5ea;background:#f7f8fa;text-align:left;">Value</th></tr>
                  {{telemetry_rows}}
                </table>
              </td>
            </tr>
            <tr>
              <td style="padding:18px 24px 0 24px;">
                <div style="font-size:16px;font-weight:bold;border-bottom:2px solid #1f2a44;padding-bottom:4px;">Observed facts</div>
                <ul style="font-size:14px;line-height:1.5;margin:8px 0 0 0;padding-left:20px;">
                  {{observed_facts_items}}
                </ul>
              </td>
            </tr>
            <tr>
              <td style="padding:18px 24px 0 24px;">
                <div style="font-size:16px;font-weight:bold;border-bottom:2px solid #1f2a44;padding-bottom:4px;">Ranked hypotheses</div>
                {{hypotheses_cards}}
              </td>
            </tr>
            <tr>
              <td style="padding:18px 24px 0 24px;">
                <div style="font-size:16px;font-weight:bold;border-bottom:2px solid #1f2a44;padding-bottom:4px;">Recommended safe checks (inspection only)</div>
                <ul style="font-size:14px;line-height:1.5;margin:8px 0 0 0;padding-left:20px;">
                  {{safe_checks_items}}
                </ul>
              </td>
            </tr>
            <tr>
              <td style="padding:18px 24px 0 24px;">
                <div style="font-size:16px;font-weight:bold;border-bottom:2px solid #1f2a44;padding-bottom:4px;">Evidence gaps and uncertainties</div>
                <ul style="font-size:14px;line-height:1.5;margin:8px 0 0 0;padding-left:20px;">
                  {{evidence_gaps_items}}
                </ul>
              </td>
            </tr>
            <tr>
              <td style="padding:18px 24px 24px 24px;">
                <div style="font-size:12px;color:#9099a6;border-top:1px solid #e2e5ea;padding-top:12px;">
                  This email was generated by an AI diagnostic workflow on synthetic workshop data.
                </div>
              </td>
            </tr>
          </table>
        </td>
      </tr>
    </table>
  </body>
</html>
```

{% endcode %}
