Skip to content

Commit 6db16a9

Browse files
Merge pull request #6 from zoho/patch_recovery_agent
Recovery agent
2 parents 548b4aa + c7555e2 commit 6db16a9

8 files changed

Lines changed: 866 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "recovery-agent",
3+
"version": "1.0.0",
4+
"description": "Signup recovery workflow for Zoho Billing. Fetches the last 7 days of lost opportunities and abandoned carts, ranks them by lost value, applies a tunable YAML decision matrix, and produces a propose-only Markdown recovery action report for human review.",
5+
"author": {
6+
"name": "Vishwa Ganesan",
7+
"email": "vishwa.ganesan@zohocorp.com"
8+
},
9+
"keywords": [
10+
"zoho-billing",
11+
"recovery",
12+
"abandoned-cart",
13+
"lost-opportunities",
14+
"signup-recovery",
15+
"revenue-recovery"
16+
]
17+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Recovery Agent
2+
3+
> **Signup and cart recovery workflow for Zoho Billing.** Fetches the last 7 days of lost opportunities and abandoned carts, ranks them by lost value, applies a tunable YAML decision matrix, and produces a propose-only Markdown recovery action report for human review.
4+
5+
The agent is **propose-only**: it never sends emails, applies discounts, or modifies records in Zoho. Every output is a Markdown proposal file for a human to act on.
6+
7+
---
8+
9+
## Skills in this plugin
10+
11+
| Skill | What it does |
12+
|---|---|
13+
| `recovery-orchestrator` | End-to-end run — fetches cohort, applies strategy, writes the proposal report. Main entry point. |
14+
| `recovery-cohort-builder` | Fetches and normalizes the last 7 days of lost opportunities + abandoned carts, sorted by lost value. |
15+
| `recovery-strategy` | Applies the YAML decision matrix and recommends a per-record action (human outreach, email with discount, email reminder, email sequence, or accept). |
16+
17+
---
18+
19+
## How it works
20+
21+
```
22+
Lost Opportunities (7d) ─┐
23+
├─→ Normalized cohort ─→ Decision matrix ─→ proposal.md
24+
Abandoned Carts (7d) ─┘ (sorted by $) (recovery-matrix.yaml)
25+
```
26+
27+
**Priority order in the report:**
28+
1. Lost Opportunities — sorted by lost value DESC
29+
2. Abandoned Carts — sorted by lost value DESC
30+
31+
Abandoned carts appear second in the report because they represent the highest purchase intent (the visitor chose a plan and started checkout). The Sales team should action `urgent` rows from both sections immediately; marketing queues the rest.
32+
33+
---
34+
35+
## Installation
36+
37+
**Cowork** — Settings → Plugins → Add plugin → paste this repo URL, then select `recovery-agent`.
38+
39+
**Claude Code:**
40+
```bash
41+
claude plugin install recovery-agent@zoho/zoho-billing-ai
42+
```
43+
44+
---
45+
46+
## Quickstart
47+
48+
Once the Zoho Billing MCP is connected, ask:
49+
50+
> *"Run today's recovery review."*
51+
52+
or
53+
54+
> *"What leads and carts did we lose this week?"*
55+
56+
The orchestrator fetches the last 7 days, applies the recovery matrix, and saves a proposal to `recovery-runs/<date>/proposal.md`.
57+
58+
Other useful phrases:
59+
- *"Build the recovery cohort for this week."* → runs `recovery-cohort-builder` standalone
60+
- *"Apply the recovery strategy to the cohort."* → runs `recovery-strategy` standalone
61+
- *"Show me what abandoned carts we have."* → triggers the cohort builder, filtered mentally to abandoned carts
62+
63+
---
64+
65+
## Tuning the decision matrix
66+
67+
Edit `config/recovery-matrix.yaml` to change:
68+
69+
- **`value_tiers`** — adjust the ACV thresholds for `high` / `medium` / `low`
70+
- **`recency_tiers`** — adjust how many days counts as `fresh` vs `recent` vs `cold`
71+
- **`lost_opportunity_matrix`** — change what action to take for each value × recency combination
72+
- **`abandoned_cart_matrix`** — same for abandoned carts
73+
74+
No code changes needed — the strategy script reads the YAML on every run.
75+
76+
---
77+
78+
## Output files
79+
80+
Each run saves three files under `recovery-runs/<DATE>/`:
81+
82+
| File | Contents |
83+
|------|----------|
84+
| `cohort-raw.json` | Normalized cohort (lost opps + carts), sorted by lost value |
85+
| `cohort-recommendations.json` | Cohort with per-record recommendation appended |
86+
| `proposal.md` | Human-readable action proposal |
87+
88+
---
89+
90+
## What the plugin doesn't do
91+
92+
- **No outbound communication.** It never sends emails, posts Cliq messages, or modifies subscriptions.
93+
- **No credential storage.** OAuth tokens stay in your environment variables.
94+
- **No cross-tenant data.** Each run uses the org the `ZOHO_BILLING_ORG_ID` env var points to.
95+
96+
---
97+
98+
## License
99+
100+
Apache 2.0 — see the [repo root](../../LICENSE).
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# ============================================================
2+
# Recovery Strategy Matrix
3+
# ============================================================
4+
# This file defines the rules the recovery agent applies to each
5+
# lost opportunity and abandoned cart. EDIT THIS FILE to tune the
6+
# agent's behavior — no skill code changes needed.
7+
#
8+
# Sections:
9+
# 1. value_tiers — lost value thresholds that bucket leads/carts
10+
# 2. recency_tiers — days-since-event thresholds (freshness)
11+
# 3. lost_opportunity_matrix — rules for leads who never completed signup
12+
# 4. abandoned_cart_matrix — rules for visitors who started checkout but left
13+
#
14+
# Rule evaluation:
15+
# - Rules in each matrix are evaluated TOP-TO-BOTTOM.
16+
# - The FIRST rule whose `match` clause is satisfied wins.
17+
# - Always end each matrix with a catch-all rule (match: {}) so
18+
# every record gets a recommendation.
19+
# ============================================================
20+
21+
# ----------------------------------------------------------------
22+
# 1. Value tiers — potential/cart value buckets
23+
# ----------------------------------------------------------------
24+
# Bucket is determined by the `lost_value` field (annual or monthly
25+
# contract value, whichever is available). Currency: USD (or your
26+
# org's base currency — keep consistent).
27+
value_tiers:
28+
high: { min: 1000 } # ≥ $1,000 ACV
29+
medium: { min: 200, max: 1000 } # $200 – $999 ACV
30+
low: { max: 200 } # < $200 ACV
31+
32+
# ----------------------------------------------------------------
33+
# 2. Recency tiers — how fresh the event is
34+
# ----------------------------------------------------------------
35+
# Fresh leads respond much better. The sooner we act the higher the
36+
# recovery rate — recency is therefore the primary sort key.
37+
recency_tiers:
38+
fresh: { max_days: 2 } # ≤ 2 days ago — highest urgency
39+
recent: { max_days: 7 } # 3–7 days ago — still warm
40+
cold: { min_days: 7 } # > 7 days ago — harder to recover
41+
42+
# ----------------------------------------------------------------
43+
# 3. Lost Opportunity matrix
44+
# ----------------------------------------------------------------
45+
# Rules for prospects who expressed intent (e.g., started a trial,
46+
# reached the pricing page, or began onboarding) but never converted.
47+
# These leads have lower intent than abandoned-cart but still
48+
# warrant proactive outreach.
49+
#
50+
# Each rule has:
51+
# match: — conditions that must ALL be true (AND semantics).
52+
# Fields: value (tier name), recency (tier name).
53+
# Omit a field to mean "any value".
54+
# action: — one of: human_outreach, email_with_discount,
55+
# email_reminder, email_sequence, accept
56+
# params: — action-specific parameters
57+
# rationale: — shown in the proposal so reviewers understand why
58+
lost_opportunity_matrix:
59+
60+
# -- High value, very fresh — personal touch wins --
61+
- match: { value: high, recency: fresh }
62+
action: human_outreach
63+
params: { route_to: "Sales", priority: urgent, contact_within_hours: 4 }
64+
rationale: "High-value lead lost <2 days ago — Sales should reach out personally within 4 hours while intent is highest"
65+
66+
# -- High value, still warm — strong incentive email --
67+
- match: { value: high, recency: recent }
68+
action: email_with_discount
69+
params: { discount_percent: 20, coupon_duration_months: 3, expiry_hours: 48 }
70+
rationale: "High-value lead cooling off — 20% welcome-back discount with a 48-hour urgency window"
71+
72+
# -- Medium value, fresh — move fast with a mid-tier offer --
73+
- match: { value: medium, recency: fresh }
74+
action: email_with_discount
75+
params: { discount_percent: 15, coupon_duration_months: 2, expiry_hours: 72 }
76+
rationale: "Mid-value fresh lead — 15% discount email before they forget"
77+
78+
# -- Medium value, recent — nurture sequence --
79+
- match: { value: medium, recency: recent }
80+
action: email_sequence
81+
params: { sequence: "lost_opportunity_nurture", steps: 3, cadence_days: [0, 3, 7] }
82+
rationale: "Mid-value lead gone quiet — 3-step nurture sequence (day 0, 3, 7) with value messaging"
83+
84+
# -- Low value, any recency — lightweight automated flow --
85+
- match: { value: low }
86+
action: email_sequence
87+
params: { sequence: "standard_recovery", steps: 2, cadence_days: [0, 5] }
88+
rationale: "Low-value lead — standard 2-step automated recovery sequence"
89+
90+
# -- Catch-all --
91+
- match: {}
92+
action: email_sequence
93+
params: { sequence: "standard_recovery", steps: 2, cadence_days: [0, 5] }
94+
rationale: "Default recovery: standard 2-step email sequence"
95+
96+
# ----------------------------------------------------------------
97+
# 4. Abandoned Cart matrix
98+
# ----------------------------------------------------------------
99+
# Rules for visitors who reached checkout, added a plan to their cart,
100+
# and left without completing payment. These are the highest-intent
101+
# prospects — a small nudge often converts them.
102+
#
103+
# Prioritize these above lost opportunities of equivalent value because
104+
# the barrier to recovery is lower (they know the product, they chose
105+
# a plan — they just didn't click Pay).
106+
abandoned_cart_matrix:
107+
108+
# -- High value, very fresh — call them now --
109+
- match: { value: high, recency: fresh }
110+
action: human_outreach
111+
params: { route_to: "Sales", priority: urgent, contact_within_hours: 2 }
112+
rationale: "High-value cart abandoned <2 days — immediate personal outreach; they were seconds from buying"
113+
114+
# -- High value, still warm — urgency discount --
115+
- match: { value: high, recency: recent }
116+
action: email_with_discount
117+
params: { discount_percent: 15, expiry_hours: 48, include_cart_contents: true }
118+
rationale: "High-value cart cooling — 15% off with 48-hour timer and cart contents reminder"
119+
120+
# -- Medium value, fresh — strong reminder, no discount needed --
121+
- match: { value: medium, recency: fresh }
122+
action: email_reminder
123+
params: { include_cart_contents: true, urgency: high, expiry_hours: 24 }
124+
rationale: "Mid-value fresh abandon — cart reminder with urgency; try without discount first"
125+
126+
# -- Medium value, recent — add a small incentive --
127+
- match: { value: medium, recency: recent }
128+
action: email_with_discount
129+
params: { discount_percent: 10, expiry_hours: 72, include_cart_contents: true }
130+
rationale: "Mid-value cart going cold — 10% nudge with cart contents and time pressure"
131+
132+
# -- Low value, any recency — simple reminder --
133+
- match: { value: low }
134+
action: email_reminder
135+
params: { include_cart_contents: true, urgency: normal }
136+
rationale: "Low-value abandoned cart — standard cart reminder email"
137+
138+
# -- Catch-all --
139+
- match: {}
140+
action: email_reminder
141+
params: { include_cart_contents: true, urgency: normal }
142+
rationale: "Default: cart contents reminder email"
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
name: recovery-cohort-builder
3+
description: Build the recovery cohort by fetching the last 7 days of lost opportunities and abandoned carts from Zoho Billing. Normalizes both sources into a unified JSON array sorted by lost_value DESC — lost opportunities first (higher intent), then abandoned carts. Use this skill whenever you need to pull fresh data before a recovery run, or any time someone asks "what leads or carts did we lose this week?", "show me what we need to recover", or "build the recovery list".
4+
---
5+
6+
# Recovery Cohort Builder
7+
8+
Fetches two Zoho Billing reports — **Lost Opportunities** and **Abandoned Carts** — for the last 7 days, normalizes them into a unified schema, and sorts by `lost_value` DESC. Lost opportunities are fetched first (earlier in the output) because they represent higher-funnel intent worth investigating separately; abandoned carts follow.
9+
10+
The output is a single JSON file ready for the `recovery-strategy` skill to process.
11+
12+
## Output schema (one object per lead/cart)
13+
14+
```json
15+
{
16+
"source": "lost_opportunity | abandoned_cart",
17+
"record_id": "API-provided ID",
18+
"customer_name": "string | null",
19+
"email": "string | null",
20+
"plan_name": "string | null",
21+
"lost_value": 123.45,
22+
"currency": "USD",
23+
"days_since_event": 3,
24+
"event_date": "YYYY-MM-DD",
25+
"stage": "string | null",
26+
"raw": { ...original API fields }
27+
}
28+
```
29+
30+
`lost_value` is the annual or monthly contract value of what was not recovered. Use whatever monetary field the API provides (`amount`, `total`, `plan_price`, etc.) — prefer annual value when both are present.
31+
32+
## Workflow
33+
34+
### Step 1 — Fetch reports in parallel
35+
36+
Call both tools with a 7-day date range. Use today's date for `to_date` and 7 days ago for `from_date` (format: `YYYY-MM-DD`).
37+
38+
```
39+
ZohoBilling_get_lost_opportunities_report — from_date, to_date
40+
ZohoBilling_get_abandoned_carts_report — from_date, to_date
41+
```
42+
43+
If either call returns an empty result set, note it but continue — the cohort may still have data from the other source.
44+
45+
### Step 2 — Normalize
46+
47+
Map API response fields to the unified schema above. Common field mappings to try:
48+
49+
| Unified field | Lost Opp candidates | Abandoned Cart candidates |
50+
|------------------|----------------------------------------|----------------------------------------|
51+
| `record_id` | `opportunity_id`, `id` | `cart_id`, `id` |
52+
| `customer_name` | `customer_name`, `contact_name` | `customer_name`, `visitor_name` |
53+
| `email` | `email`, `contact_email` | `email`, `customer_email` |
54+
| `plan_name` | `plan_name`, `product_name` | `plan_name`, `product` |
55+
| `lost_value` | `amount`, `total`, `plan_price` | `cart_total`, `amount`, `plan_price` |
56+
| `event_date` | `lost_date`, `closed_date`, `date` | `abandoned_date`, `created_time` |
57+
| `stage` | `stage`, `lost_stage` | `checkout_step`, null |
58+
59+
If a field is missing or null in the API response, set it to `null` in the output — don't drop the row.
60+
61+
`days_since_event` = today's date − `event_date` in calendar days.
62+
63+
### Step 3 — Sort
64+
65+
Sort the full combined array:
66+
1. **Source order**: all `lost_opportunity` records first, then `abandoned_cart` records.
67+
2. **Within each source**: sort by `lost_value` DESC (highest value first).
68+
69+
This ordering ensures the proposal report always leads with the highest-priority recoveries.
70+
71+
### Step 4 — Write output
72+
73+
Save to:
74+
```
75+
<workspace>/recovery-runs/<DATE>/cohort-raw.json
76+
```
77+
78+
Where `<DATE>` is today in `YYYY-MM-DD` format. Create the directory if it doesn't exist.
79+
80+
Print a summary to the user:
81+
82+
```
83+
Recovery cohort built — <DATE>
84+
Lost Opportunities : N records (total lost value: $X,XXX)
85+
Abandoned Carts : N records (total lost value: $X,XXX)
86+
Combined total : N records (total lost value: $X,XXX)
87+
Saved to: recovery-runs/<DATE>/cohort-raw.json
88+
```
89+
90+
## Edge cases
91+
92+
- **API returns no data for a source** → include that source with 0 records in the summary; don't error.
93+
- **`lost_value` is null or non-numeric** → default to `0` for sorting; do not drop the record.
94+
- **Duplicate records** (same `record_id` appearing in both reports) → keep both, add a `duplicate_flag: true` field for downstream review.
95+
- **Date parsing fails** → set `event_date` to `null` and `days_since_event` to `null`; flag the row.

0 commit comments

Comments
 (0)