# P2: Owner Tasks, Reminder Engine, Template Library Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** For every pipeline stage, notify the deal owner and create a CRM task, then nag on a fixed cadence until done, with bilingual templates. **Architecture:** Owner-facing reminders are mechanical, so they live in the n8n/CRM layer (deterministic, no LLM). A template renderer turns per-stage template files plus the deal payload into email HTML. A scheduled sweep sends +2 business day, +5 business day, and escalation reminders. Client-facing follow-ups are drafts only. **Tech Stack:** n8n, Python 3 (renderer), HubSpot API, Gmail. Templates are files with `{{placeholders}}`. **Depends on:** P1 (the stage event and `deal_stages.yaml`). **Spec:** `../specs/2026-09-24-sales-pipeline-hermes-design.md` (SP2, sections 5.2, 6, 8, 9). --- ## Prerequisites - P1 merged: stage changes reach Hermes and `workspace/shared/deal_stages.yaml` exists. - Access to `n8n.916.coordenadas.co` via n8n-mcp, plus HubSpot and Gmail credentials already used by the live "Webhook Deal Stage" workflow. - `python3` with `jinja2`. --- ## File Structure | Path | Responsibility | |---|---| | `templates/owner/.html` | Owner email per stage | | `templates/client/.html` | Client email per action | | `templates/manifest.yaml` | Template registry: stage to owner template and CRM task | | `scripts/render_template.py` | Render a template with a context dict | | `scripts/test_render_template.py` | Renderer tests | | `n8n/pipeline-owner-notify.json` | Extends owner notifications to all 7 stages | | `n8n/pipeline-reminder-sweep.json` | Scheduled reminder and escalation | | `n8n/pipeline-client-followup.json` | Client follow-up drafts plus owner tasks | --- ## Task 1: Template manifest and renderer **Files:** - Create: `templates/manifest.yaml` - Create: `scripts/render_template.py` - Test: `scripts/test_render_template.py` - [ ] **Step 1: Write the failing test** Create `scripts/test_render_template.py`: ```python import importlib.util import pathlib HERE = pathlib.Path(__file__).parent ROOT = HERE.parent def load(): spec = importlib.util.spec_from_file_location("rt", HERE / "render_template.py") mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def test_render_substitutes_placeholders(): mod = load() out = mod.render_string("Hi {{ owner_name }}, deal {{ dealname }}.", {"owner_name": "Ana", "dealname": "Acme"}) assert out == "Hi Ana, deal Acme." def test_missing_placeholder_raises(): mod = load() try: mod.render_string("{{ missing }}", {}) assert False, "expected KeyError" except KeyError: pass def test_manifest_covers_all_stages(): mod = load() data = mod.load_manifest() stages = { "appointmentscheduled", "qualifiedtobuy", "presentationscheduled", "decisionmakerboughtin", "contractsent", "closedwon", "closedlost", } assert set(data["owner"].keys()) == stages def test_manifest_templates_exist(): mod = load() data = mod.load_manifest() for stage, entry in data["owner"].items(): assert (ROOT / "templates" / "owner" / entry["template"]).exists(), stage ``` - [ ] **Step 2: Run test to verify it fails** Run: `python3 -m pytest scripts/test_render_template.py -v` Expected: FAIL (module missing). - [ ] **Step 3: Write the renderer** Create `scripts/render_template.py`: ```python """render_template.py — render {{placeholders}} in pipeline email templates.""" import pathlib import re import yaml ROOT = pathlib.Path(__file__).resolve().parent.parent TOKEN = re.compile(r"{{\s*([a-zA-Z0-9_]+)\s*}}") def render_string(text: str, context: dict) -> str: def sub(m): key = m.group(1) if key not in context: raise KeyError(key) return str(context[key]) return TOKEN.sub(sub, text) def render_path(relpath: str, context: dict) -> str: return render_string((ROOT / relpath).read_text(encoding="utf-8"), context) def load_manifest() -> dict: return yaml.safe_load((ROOT / "templates" / "manifest.yaml").read_text(encoding="utf-8")) if __name__ == "__main__": import sys, json rel = sys.argv[1] ctx = json.loads(sys.argv[2]) if len(sys.argv) > 2 else {} print(render_path(rel, ctx)) ``` - [ ] **Step 4: Create the manifest and one template, then run the failing coverage test** Create `templates/manifest.yaml`: ```yaml # manifest.yaml — pipeline notification registry. Owner email + CRM task per stage. owner: appointmentscheduled: template: calificar.html subject: "New deal to qualify: {{ dealname }}" task_subject: "Qualify deal: {{ dealname }}" qualifiedtobuy: template: proyecto_viable.html subject: "{{ dealname }}: complete the Product Details Form" task_subject: "Complete Product Details Form: {{ dealname }}" presentationscheduled: template: presentacion.html subject: "{{ dealname }}: deliver the solution presentation" task_subject: "Deliver solution presentation: {{ dealname }}" decisionmakerboughtin: template: negociacion.html subject: "{{ dealname }}: prepare the quote" task_subject: "Prepare quote: {{ dealname }}" contractsent: template: expectativa_cierre.html subject: "{{ dealname }}: confirm the close" task_subject: "Confirm close date / contract: {{ dealname }}" closedwon: template: ganado.html subject: "{{ dealname }}: complete the Equipment Installation Form" task_subject: "Complete Equipment Installation Form: {{ dealname }}" closedlost: template: perdido.html subject: "{{ dealname }}: record the loss reason" task_subject: "Record loss reason: {{ dealname }}" ``` Create `templates/owner/calificar.html` (repeat this shape for the other six with matching names): ```html

Hi {{ owner_name }},

The deal {{ dealname }} moved to Calificar.

Please confirm industry, need, timeline, and decision authority, then move the deal forward.

Open the deal

``` Create the remaining six owner templates (`proyecto_viable.html`, `presentacion.html`, `negociacion.html`, `expectativa_cierre.html`, `ganado.html`, `perdido.html`) using the same structure and each stage's call to action. Run: `python3 -m pytest scripts/test_render_template.py -v` Expected: 4 passed. - [ ] **Step 5: Commit** ```bash git add templates/ scripts/render_template.py scripts/test_render_template.py git commit -m "feat(pipeline): template renderer and owner notification manifest" ``` --- ## Task 2: Owner notification for all seven stages **Files:** - Create: `n8n/pipeline-owner-notify.json` - Test: `n8n/test_pipeline_owner_notify.py` - [ ] **Step 1: Write the failing test** Create `n8n/test_pipeline_owner_notify.py`: ```python import json import pathlib WF = json.loads((pathlib.Path(__file__).parent / "pipeline-owner-notify.json").read_text()) def names(): return [n["name"] for n in WF["nodes"]] def test_has_renderer_and_gmail_and_task_nodes(): assert any("Render owner email" in n for n in names()) assert any(n.startswith("Gmail") for n in names()) assert any("Create CRM task" in n for n in names()) def test_switch_has_seven_outputs(): switch = next(n for n in WF["nodes"] if n["type"] == "n8n-nodes-base.switch") assert len(switch["parameters"]["rules"]["values"]) == 7 ``` - [ ] **Step 2: Run test to verify it fails** Run: `python3 -m pytest n8n/test_pipeline_owner_notify.py -v` Expected: FAIL (file missing). - [ ] **Step 3: Build the workflow** Create `n8n/pipeline-owner-notify.json` with: 1. **Webhook** `Deal Stage Changed` (same HubSpot subscription as the live workflow, or a shared trigger). 2. **Code** `Normalize Payload`: `{ deal_id, dealname, dealstage, amount, hubspot_owner_id, owner_name, owner_email, deal_url }`. 3. **Switch** with seven rules on `dealstage` (the IDs from `deal_stages.yaml`). 4. Per stage: **Code** `Render owner email ` (loads `templates/owner/` and renders `subject` and `body` via the same placeholder rules as `scripts/render_template.py`), then **Gmail** send to `owner_email` with Jovan copied, then **HubSpot** `Create CRM task` with `task_subject`. For the Code node, embed this reusable function in each render node: ```javascript const tplSub = $json.__subject; const tplBody = $json.__body; const sub = (t) => t.replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (_, k) => { if (!(k in $json)) throw new Error(`missing placeholder ${k}`); return $json[k]; }); return [{ json: { ...$json, subject: sub(tplSub), body: sub(tplBody) } }]; ``` 5. Set `JOVAN_EMAIL` on the n8n host; reuse existing Gmail and HubSpot credentials. - [ ] **Step 4: Validate and run the test** ``` n8n-mcp validate_workflow({ workflow: }) -> errorCount: 0 python3 -m pytest n8n/test_pipeline_owner_notify.py -v ``` Expected: 2 passed. - [ ] **Step 5: Commit** ```bash git add n8n/pipeline-owner-notify.json n8n/test_pipeline_owner_notify.py git commit -m "feat(pipeline): owner notification for all seven stages" ``` --- ## Task 3: Internal reminder sweep and escalation **Files:** - Create: `n8n/pipeline-reminder-sweep.json` - Test: `n8n/test_pipeline_reminder_sweep.py` - [ ] **Step 1: Write the failing test** Create `n8n/test_pipeline_reminder_sweep.py`: ```python import json import pathlib WF = json.loads((pathlib.Path(__file__).parent / "pipeline-reminder-sweep.json").read_text()) NAMES = [n["name"] for n in WF["nodes"]] def test_has_schedule_and_branches(): assert any(n["type"] == "n8n-nodes-base.scheduleTrigger" for n in WF["nodes"]) assert any("+2" in n for n in NAMES) assert any("+5" in n for n in NAMES) assert any("Escalate" in n for n in NAMES) ``` - [ ] **Step 2: Run test to verify it fails** Run: `python3 -m pytest n8n/test_pipeline_reminder_sweep.py -v` Expected: FAIL. - [ ] **Step 3: Build the workflow** Create `n8n/pipeline-reminder-sweep.json`: 1. **Schedule Trigger** `Daily 9:00 business days` (cron `0 9 * * 1-5`, timezone `America/New_York`). 2. **HubSpot** `List open deal tasks`: open tasks created by the owner-notify workflow (use a tag or a custom task type to identify them). For each, compute `age_days` from the task `createdAt`. 3. **Switch** on `age_days`: `>= 2` -> `Reminder +2`; `>= 5` -> `Reminder +5`; `>= 6` -> `Escalate`. Send the matching template (`templates/owner/_reminder.html` with a `{{ reminder_note }}`) to the owner, and on escalation also to `JOVAN_EMAIL`. 4. Guard: skip tasks already `completed` (filter at the HubSpot step). - [ ] **Step 4: Validate and run the test** ``` n8n-mcp validate_workflow({ workflow: }) -> errorCount: 0 python3 -m pytest n8n/test_pipeline_reminder_sweep.py -v ``` Expected: 1 passed. - [ ] **Step 5: Commit** ```bash git add n8n/pipeline-reminder-sweep.json n8n/test_pipeline_reminder_sweep.py git commit -m "feat(pipeline): internal reminder sweep and escalation" ``` --- ## Task 4: Client follow-up cadence (draft only) **Files:** - Create: `templates/client/followup.html` - Create: `n8n/pipeline-client-followup.json` - Test: `n8n/test_pipeline_client_followup.py` - [ ] **Step 1: Write the failing test** Create `n8n/test_pipeline_client_followup.py`: ```python import json import pathlib WF = json.loads((pathlib.Path(__file__).parent / "pipeline-client-followup.json").read_text()) NAMES = [n["name"] for n in WF["nodes"]] def test_creates_draft_not_send(): assert any("Draft" in n for n in NAMES) assert not any(n.startswith("Gmail Send") for n in NAMES) def test_has_full_cadence(): for step in ["72h", "3d", "5d", "15d"]: assert any(step in n for n in NAMES), step ``` - [ ] **Step 2: Run test to verify it fails** Run: `python3 -m pytest n8n/test_pipeline_client_followup.py -v` Expected: FAIL. - [ ] **Step 3: Build the workflow** Create `n8n/pipeline-client-followup.json`: 1. **Schedule Trigger** `Daily 9:00` plus stage entry. 2. **HubSpot** `List silent deals`: deals in Negotiation or Expectativa de Cierre with no activity for the configured interval. 3. **Switch** on `silence_hours`: `72`, `120` (5d), `360` (15d), and a `3d` branch. 4. Per branch: **Gmail** `Create Draft` to the contact using `templates/client/followup.html` (draft only, never send), then **HubSpot** `Create CRM task` for the owner ("follow up with {{ contact_name }}"). - [ ] **Step 4: Validate and run the test** ``` n8n-mcp validate_workflow({ workflow: }) -> errorCount: 0 python3 -m pytest n8n/test_pipeline_client_followup.py -v ``` Expected: 2 passed. - [ ] **Step 5: Commit** ```bash git add templates/client/followup.html n8n/pipeline-client-followup.json n8n/test_pipeline_client_followup.py git commit -m "feat(pipeline): client follow-up cadence as drafts" ``` --- ## Task 5: Docs and verification **Files:** - Modify: `n8n/README.md` - Modify: `AGENTS.md` - [ ] **Step 1: Update n8n/README.md** Add the three new workflows, the reminder cadences, and the draft-only rule for client follow-ups. - [ ] **Step 2: Update AGENTS.md** Note that owner reminders are mechanical and live in n8n/CRM, not in a Hermes agent. - [ ] **Step 3: End-to-end verification** For each of the seven stages, move a test deal into the stage and confirm: one owner email, one CRM task, and (after two business days with the task open) one reminder email. Confirm client follow-ups produce Gmail drafts and never sends. - [ ] **Step 4: Commit** ```bash git add n8n/README.md AGENTS.md git commit -m "docs(pipeline): owner tasks and reminder engine status" ``` --- ## Definition of done - All seven stages send an owner email and create a CRM task (Jovan copied). - Reminder sweep fires at +2 and +5 business days and escalates at +6. - Client follow-up cadence creates drafts only, never sends. - All templates bilingual (EN/ES) and render with no missing placeholders.