Renewal Risk & Outreach Planner
What you'll learn
~40 min- Merge lease, payment, and service-history CSVs into a unified renewal model
- Build transparent rule-based risk scoring (not black-box AI)
- Segment residents into 30/60/90-day renewal cohorts with risk tiers
- Batch-generate personalized outreach letters with staggered pricing options
What you’re building
Lease renewals are the single biggest lever for occupancy at The Concord Crystal City. Every unit that turns over costs money — vacancy loss, make-ready expenses, marketing spend to fill the unit, concessions for the new resident. A 1% occupancy drop across 413 units means roughly 4 empty apartments. At an average rent of $2,500/month, that is about $120,000 in lost annual revenue. And that is before you count the $3,000-5,000 it costs to turn and re-lease each unit.
Right now, renewal outreach at most communities is a calendar reminder: “Lease expires in 90 days, send the renewal letter.” Every resident gets the same letter, the same pricing, the same timing. The resident who pays on the first of every month, never submits a complaint, and has lived there for three years gets the same treatment as the resident who has been late four times, submitted six maintenance requests in two months, and is on their first lease.
You are going to build a tool that scores renewal risk for every resident, groups them into 30/60/90-day outreach cohorts by risk tier, and generates personalized HTML offer letters with 12-month, 15-month, and month-to-month pricing options. One command produces a weekly action plan your leasing team can execute immediately.
Industry average renewal rate for Class A multifamily is around 55-60%. If The Concord renews 60% of expiring leases instead of 55%, that is roughly 9 fewer turnovers per year. At $4,000 per turn (vacancy + make-ready + concession), that is $36,000 saved annually — from a 5-point improvement. Targeted outreach based on risk scoring is how professional operators push renewal rates above 65%.
Ingest multiple data sources with different schemas, join on a shared key, compute a composite score using transparent rules, segment the results into action tiers, and generate batch output per segment. This pattern is used in credit scoring, sales lead prioritization, patient risk stratification — anywhere you need to triage a population for targeted action.
🔍Property management primer: Renewals, occupancy, and MTM
If you are new to property management, here are the concepts this lesson builds on:
- Lease renewal: When a resident’s lease term (usually 12 months) is about to expire, the property offers a new lease at updated pricing. If the resident accepts, they “renew.” If they decline, they either move out or convert to month-to-month.
- Occupancy rate: The percentage of units that are leased and generating rent. A 413-unit community at 95% occupancy has about 393 occupied units and 20 vacant. Every percentage point matters — dropping from 96% to 95% means 4 more empty units.
- Renewal cohorts: Grouping residents by how far out their lease expires. The 90-day cohort gets the first outreach (early touch), 60-day gets the formal offer, and 30-day gets the urgency push. Staggering outreach prevents a flood of renewals hitting your leasing team all at once.
- Month-to-month (MTM): When a lease expires without renewal, the resident typically converts to a month-to-month arrangement at a premium rate (often 10-20% above the lease rate). MTM is bad for the property because the resident can leave with 30 days notice, making vacancy unpredictable. MTM is bad for the resident because the rent is higher. Both sides prefer a signed lease.
- Make-ready cost: The expense of preparing a vacated unit for a new resident — painting, cleaning, carpet, appliance repair, general wear-and-tear fixes. At a luxury community like The Concord, make-ready runs $2,000-5,000 per unit depending on condition.
- Concession: A discount offered to attract a new resident (e.g., “first month free” or “$500 off move-in”). Concessions cost the property real revenue and are only necessary when a unit turns over. Retaining the current resident avoids concession spend entirely.
Who this is for
Nick manages leasing operations at The Concord Crystal City — 413 units, 18 stories, Bozzuto-managed. He does not write code. He uses Yardi for property management, Excel for ad-hoc analysis, and Outlook for resident communication. His leasing team sends renewal letters manually, one at a time, using a Word template. They check the lease expiration report in Yardi every Monday, eyeball who is coming up, and start outreach. There is no scoring, no segmentation, no personalization beyond swapping the resident’s name.
Nick wants to know: which residents are likely to leave, which are safe bets, and what pricing will keep the ones in the middle? He wants his leasing coordinators to open a report on Monday morning and know exactly who to call, what to offer, and in what order.
The showcase
When finished, your tool will:
- Ingest three CSVs: lease roster (unit, resident, lease dates, current rent), payment history (on-time/late/NSF per month), and service request log (dates, categories, resolution times).
- Join on unit number: merge all three sources into a single resident profile.
- Score renewal risk (0-100) using transparent, weighted rules:
- Payment friction (late payments, NSF checks): 0-30 points
- Service request volume and recency: 0-20 points
- Lease age (first lease vs. multi-year): 0-15 points
- Sentiment flags (noise complaints filed against them, multiple roommate changes): 0-15 points
- Time remaining on lease: 0-10 points
- Rent-to-market gap (current rent vs. market rate): 0-10 points
- Segment into cohorts:
- 90-day cohort (leases expiring in 61-90 days) — early touch
- 60-day cohort (31-60 days) — formal offer
- 30-day cohort (0-30 days) — urgency push
- Each cohort split into Low Risk (0-30), Medium Risk (31-60), High Risk (61-100)
- Generate HTML offer letters per resident with:
- Personalized greeting and tenure acknowledgment
- Three pricing options: 12-month, 15-month, and MTM
- Staggered increases based on risk tier (low risk = smaller increase, high risk = market rate)
- Response deadline tied to cohort timing
- Export a weekly action plan as an HTML dashboard:
- Priority-sorted resident list with risk score, cohort, and recommended action
- Retention impact forecast: projected renewals, projected turnovers, estimated vacancy cost
- Summary stats: total expiring, by cohort, by risk tier
The prompt
Open your AI CLI tool in an empty directory and paste this prompt:
Build a Node.js CLI tool for scoring lease renewal risk and generatingpersonalized outreach letters. Use a structured project layout with SQLitefor data joining and EJS for HTML templates.
PROJECT STRUCTURE:renewal-planner/ package.json src/ cli.js (entry point, argument parsing) ingest/ lease-roster.js (parse lease CSV: unit, resident_name, lease_start, lease_end, current_rent, bedroom_count, floor) payment-history.js (parse payment CSV: unit, month, status [on-time, late, nsf], days_late) service-requests.js (parse service request CSV: unit, date, category, priority, resolution_days, notes) scoring/ risk-engine.js (rule-based composite scoring) rules.js (individual scoring rules with weights) segmentation/ cohort-builder.js (30/60/90 day cohorts with risk tiers) output/ letter-generator.js (EJS-based HTML letter generation) action-plan.js (weekly action plan HTML dashboard) forecast.js (retention impact projections) db/ schema.js (SQLite table creation) queries.js (join queries across tables) sample-data.js (generate realistic sample CSVs) templates/ offer-letter.ejs (personalized renewal offer letter) action-plan.ejs (weekly action plan dashboard)
REQUIREMENTS:
1. CLI INTERFACE (src/cli.js) - Usage: node src/cli.js [options] - --leases or -l: path to lease roster CSV (required) - --payments or -p: path to payment history CSV (required) - --services or -s: path to service request CSV (required) - --output or -o: output directory (default: ./renewal-output) - --date or -d: reference date for cohort calculation (default: today) - --market-rate: average market rate per bedroom for gap calculation (format: "1br:2400,2br:3100,3br:3800") - --generate-sample: generate sample CSVs instead of processing
2. DATA INGESTION (src/ingest/) - Parse each CSV and insert into SQLite (in-memory database) - Lease roster columns: unit, resident_name, email, phone, lease_start, lease_end, current_rent, bedroom_count, floor, lease_number (1 = first lease, 2+ = renewal) - Payment history columns: unit, payment_month (YYYY-MM), status (on_time | late_3 | late_10 | late_30 | nsf), days_late - Service request columns: unit, request_date, category (maintenance | noise_complaint | pest | plumbing | hvac | appliance | general), priority (low | medium | high | emergency), resolution_days, notes - Validate CSV headers on load; print clear error if columns don't match
3. RISK SCORING (src/scoring/) Composite score 0-100 (higher = higher risk of non-renewal):
Payment Friction (0-30 points): - 0 late payments in last 12 months: 0 points - 1-2 late payments: 8 points - 3-4 late payments: 16 points - 5+ late payments: 24 points - Any NSF (bounced check): +6 points - Average days late > 15: +6 bonus points
Service Request Load (0-20 points): - 0-1 requests in last 6 months: 0 points - 2-3 requests: 5 points - 4-5 requests: 10 points - 6+ requests: 15 points - Any request with resolution > 7 days: +5 points (property dropped the ball — resident may be frustrated)
Lease Tenure (0-15 points): - 3+ renewals (long-term resident): 0 points - 2nd renewal: 3 points - 1st renewal: 8 points - First lease, no renewal history: 15 points
Sentiment Flags (0-15 points): - Noise complaint filed AGAINST the unit: +5 per complaint (max 15) - Service request notes containing "frustrated", "unacceptable", "moving", "leaving", "unhappy": +5 per flag (max 15) - Cap at 15 total for this category
Time Remaining (0-10 points): - > 90 days: 0 points - 61-90 days: 2 points - 31-60 days: 5 points - 0-30 days: 10 points
Rent Gap (0-10 points): - Current rent >= market rate: 0 points (already at market) - Current rent 1-5% below market: 3 points - Current rent 6-10% below market: 6 points - Current rent > 10% below market: 10 points (big increase needed)
Risk Tiers: - Low Risk: 0-30 (likely to renew, standard increase OK) - Medium Risk: 31-60 (could go either way, moderate approach) - High Risk: 61-100 (at risk of leaving, retention pricing needed)
IMPORTANT: Log the breakdown for every resident. The property manager needs to see WHY a score is what it is, not just the number.
4. COHORT SEGMENTATION (src/segmentation/) - Calculate days until lease expiration from --date - 90-day cohort: 61-90 days remaining - 60-day cohort: 31-60 days remaining - 30-day cohort: 0-30 days remaining - Already expired / MTM: negative days (flag separately) - Within each cohort, sort by risk score descending (highest risk first)
5. OFFER LETTER GENERATION (src/output/letter-generator.js) - Generate one HTML file per resident using EJS template - Letter includes: - Concord Crystal City header/branding - Personalized greeting: "Dear [Name]" - Tenure acknowledgment: "Thank you for [X] years as a valued resident of The Concord Crystal City" - Three pricing options in a styled table: * 12-month renewal: rent amount * 15-month renewal: slightly lower rate (reward for longer commitment) * Month-to-month: premium rate (15-20% above 12-month) - Pricing logic based on risk tier: * Low Risk: 3-4% increase from current rent for 12-month * Medium Risk: 1-2% increase (retention-friendly) * High Risk: 0% increase or slight decrease (keep them) - Response deadline: 14 days from letter date for 90-day cohort, 10 days for 60-day, 7 days for 30-day - Contact information for the leasing office - Dark theme: background #0f172a, cards #1e293b, text #e2e8f0, accent #10b981 (green for The Concord branding) - Save as: letters/[unit]-[resident-last-name].html
6. ACTION PLAN DASHBOARD (src/output/action-plan.js) - Generate a single HTML dashboard for the leasing team - Sections: a. Summary cards: total expiring leases, by cohort (30/60/90), by risk tier (low/medium/high), current MTM count b. Priority action list: table sorted by urgency (30-day high-risk first, then 30-day medium, then 60-day high, etc.) Columns: Unit, Resident, Floor, Bedrooms, Risk Score, Risk Breakdown (hover tooltip), Cohort, Current Rent, Recommended Action, Letter Status c. This week's calls: filtered list of residents the leasing team should contact THIS WEEK based on cohort timing and risk - Dark theme matching the offer letters
7. RETENTION FORECAST (src/output/forecast.js) - Based on risk scores, project: - Expected renewals (low risk residents assumed 80% renewal, medium 50%, high 25%) - Expected turnovers with estimated cost per turn ($4,000) - Projected vacancy days (average 30 days to re-lease) - Revenue impact: lost rent during vacancy + make-ready cost - Compare "no intervention" vs "targeted outreach" scenarios (assume targeted outreach improves renewal probability by 15% for medium and high risk) - Include this forecast in the action plan dashboard
8. SAMPLE DATA GENERATOR (src/sample-data.js) - When --generate-sample is passed, create 3 CSV files: - sample-leases.csv: 50 units (subset of 413) with realistic data Mix of lease numbers (1-5), bedroom types (1br/2br/3br), lease end dates spread across next 90 days, rents $2,100-3,800 - sample-payments.csv: 12 months of payment history per unit Most on-time, some with late patterns, 2-3 with NSF - sample-services.csv: 80-120 service requests across the 50 units Mix of categories, some units with many requests, some with none - Plant known patterns: - Unit 1205: perfect payment, no requests, 3rd renewal (low risk) - Unit 0812: 4 late payments, 6 service requests, 1st lease (high risk) - Unit 1507: 1 late payment, 2 requests with "frustrated" in notes, rent 12% below market (medium-high risk) - Unit 0304: on MTM already, 2 NSF payments (critical)
DEPENDENCIES: better-sqlite3, csv-parser, ejs, commander, chalkThe --generate-sample flag creates CSVs with residents whose risk profiles you know in advance. When you run the scoring engine on sample data, check: Does Unit 1205 score Low Risk? Does Unit 0812 score High Risk? Does Unit 0304 get flagged as MTM-critical? If the planted patterns match the expected tiers, your scoring engine is working.
What you get
After your AI CLI tool finishes, you will have a project folder:
renewal-planner/ package.json src/ cli.js ingest/ lease-roster.js payment-history.js service-requests.js scoring/ risk-engine.js rules.js segmentation/ cohort-builder.js output/ letter-generator.js action-plan.js forecast.js db/ schema.js queries.js sample-data.js templates/ offer-letter.ejs action-plan.ejsSet it up
cd renewal-plannernpm installTry it with sample data first
node src/cli.js --generate-sample -o ./test-outputThis creates three sample CSVs in ./test-output/. Now run the full pipeline:
node src/cli.js \ -l ./test-output/sample-leases.csv \ -p ./test-output/sample-payments.csv \ -s ./test-output/sample-services.csv \ -o ./test-output \ --market-rate "1br:2400,2br:3100,3br:3800"Open ./test-output/action-plan.html in your browser. You should see:
- Summary cards showing total expiring leases broken down by cohort and risk tier
- Priority action list with Unit 0812 and Unit 0304 near the top (highest risk, soonest expiry)
- This week’s calls filtered to the most urgent outreach targets
- Retention forecast showing projected renewals vs. turnovers and the dollar impact
Then check ./test-output/letters/ — there should be one HTML offer letter per resident. Open a few and verify:
- The low-risk resident (Unit 1205) gets a standard 3-4% increase
- The high-risk resident (Unit 0812) gets a 0% increase or slight discount to retain them
- The medium-risk resident (Unit 1507) gets a modest 1-2% increase
- Each letter shows all three pricing options (12-month, 15-month, MTM)
Common issues and fixes
| Problem | Follow-up prompt |
|---|---|
| SQLite install fails on Windows | better-sqlite3 requires a C++ compiler for native bindings. If it fails, replace better-sqlite3 with sql.js (a pure JavaScript SQLite implementation that works everywhere). Update the import and initialization in db/schema.js. |
| CSV parsing fails with encoding errors | The CSV parser is failing on special characters in resident names. Add encoding detection: read the file buffer first, check for BOM bytes, and pass the correct encoding option to csv-parser. |
| Risk scores are all the same | Every resident is getting the same risk score. The scoring rules are probably not pulling the right columns from SQLite. Log the raw data for each resident before scoring so I can see what values the rules are working with. |
| Letters have placeholder text instead of real data | The EJS template is rendering variable names instead of values. Make sure the template uses <%%= variable %> syntax and that the render call passes the resident data object with the correct property names matching the template. |
When things go wrong
The most common issues involve data joining (CSV columns that do not match), scoring edge cases (residents with no payment history), and template rendering.
When Things Go Wrong
Use the Symptom → Evidence → Request pattern: describe what you see, paste the error, then ask for a fix.
How it works
The pipeline follows a clear data flow:
1. Ingest and join. Each CSV is parsed and loaded into an in-memory SQLite database. The lease roster is the primary table. Payment history and service requests join on unit number. SQLite handles the joins efficiently even for the full 413-unit roster.
2. Score. The risk engine iterates through every resident and applies each scoring rule independently. Rules are pure functions: they take the resident’s data and return a point value with an explanation string. The composite score is the sum. Because every rule is transparent and logged, Nick can look at any resident’s score and understand exactly why it is what it is. No black box.
3. Segment. The cohort builder calculates days remaining until lease expiration and sorts residents into 30/60/90-day buckets. Within each bucket, residents are sorted by risk score (highest first). This creates the priority order for outreach.
4. Generate. The letter generator applies pricing logic based on risk tier and renders one HTML file per resident using EJS templates. The action plan generator aggregates everything into a single dashboard. The forecast module projects financial impact.
You might wonder why this tool uses hand-tuned point values instead of a machine learning model. Three reasons:
- Explainability. When Nick asks “Why is Unit 0812 rated High Risk?”, you need to say “4 late payments (16 points) + 6 service requests (15 points) + first lease (15 points) = 46 points.” A random forest model cannot answer that question clearly.
- Sample size. A 413-unit community does not generate enough renewal outcomes to train a reliable model. You need thousands of labeled examples for ML to outperform hand-tuned rules in a domain this narrow.
- Trust. Property managers will not act on scores they do not understand. If the tool says “high risk” and cannot explain why, it gets ignored. Transparent rules earn trust from the operations team.
Rule-based scoring is the right tool for this problem. If you later manage a portfolio of 10,000+ units with years of renewal outcome data, then a trained model might add value — but you would still want the rule-based baseline for comparison.
Customize it
Add email integration
Add a --send flag that, when combined with --smtp-host and --smtp-from options,emails each offer letter directly to the resident's email address from the leaseroster CSV. Use nodemailer. Send the HTML letter as the email body (not anattachment). Add a --dry-run flag that logs what WOULD be sent without actuallysending. Include a 2-second delay between emails to avoid triggering rate limits.Log every send with unit, email, and timestamp.Add historical trend tracking
Add a --history flag that accepts a directory of previous action plan JSON exports.When provided, add a "Trends" section to the action plan dashboard showing:- Renewal rate trend over the last 6 months (line chart)- Average risk score trend- Turnover cost trendSave the current run's data as a JSON file in the history directory for futurecomparisons. Use Chart.js for the trend charts.Add Yardi export format
Add a --yardi flag that exports the renewal offers in a CSV format compatible withYardi Voyager's batch renewal import. Columns: Unit, ResidentCode, NewLeaseStart,NewLeaseEnd, NewMonthlyRent, LeaseType (12mo/15mo/MTM). This lets Nick uploadthe offers directly into Yardi instead of manually entering each one.Add renewal response tracker
Add a --track-responses flag and a responses CSV input. The responses CSV hascolumns: unit, response_date, response (accepted_12mo | accepted_15mo |accepted_mtm | declined | no_response). Generate an updated action plan thatshows which residents have responded, which offers were accepted, and whichresidents still need follow-up. Calculate actual vs. projected renewal rate.Try it yourself
- Open your AI CLI tool in an empty folder.
- Paste the main prompt.
- Run
npm installin the generatedrenewal-planner/directory. - Generate sample data with
--generate-sampleand run the full pipeline. - Open the action plan dashboard and verify the planted risk profiles match expectations.
- Open 3-4 offer letters and check that pricing varies by risk tier.
- Check the retention forecast — does the “targeted outreach” scenario show meaningful savings over “no intervention”?
- Pick one customization from the list above and add it with a follow-up prompt.
Key takeaways
- Renewal retention is the highest-ROI activity in property management. Preventing one turnover saves $4,000+ in make-ready, vacancy, and concession costs. A tool that improves renewal rate by even 5 percentage points pays for itself many times over.
- Transparent scoring beats black-box predictions. Property managers will not act on a score they cannot explain. Rule-based scoring with logged breakdowns builds trust and lets the team challenge individual scores when they have context the data does not capture.
- Multi-source data joining is the hard part. Lease data, payment history, and service requests live in different systems with different formats. The SQLite join layer normalizes everything so the scoring engine sees one unified profile per resident.
- Segmentation drives action. A flat list of 413 residents is overwhelming. Cohorts (30/60/90 days) combined with risk tiers (low/medium/high) create a priority matrix that tells the leasing team exactly who to call, in what order, with what offer.
- Batch personalization scales. Generating 50 personalized offer letters by hand takes a day. Generating them from templates and data takes 10 seconds. The letters look better because the pricing logic is consistent, and the leasing team can focus on conversations instead of paperwork.
Your renewal risk report shows that Unit 0812 has a composite score of 67 (High Risk). The breakdown shows: Payment Friction 24, Service Requests 15, Lease Tenure 15, Sentiment 5, Time Remaining 5, Rent Gap 3. Which single factor would you address first to improve this resident's renewal probability?
What’s next
In the next lesson, you will build the Vendor & Make-Ready Orchestrator — a tool that models unit turn workflows as dependency chains, auto-schedules vendor tasks from move-out dates, detects bottlenecks and SLA breaches, and exports ICS calendar files for vendor coordination. It is the most operationally complex tool in the property management track.