Applied Module 12 · The Nick E. Playbook

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.

💬This is retention math

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%.

Software pattern: Data merge, rule-based scoring, segmented output

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 generating
personalized outreach letters. Use a structured project layout with SQLite
for 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, chalk
💡The sample data is your validation

The --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.ejs

Set it up

Terminal window
cd renewal-planner
npm install

Try it with sample data first

Terminal window
node src/cli.js --generate-sample -o ./test-output

This creates three sample CSVs in ./test-output/. Now run the full pipeline:

Terminal window
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

ProblemFollow-up prompt
SQLite install fails on Windowsbetter-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 errorsThe 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 sameEvery 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 dataThe 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.

Symptom
Scoring engine crashes on a resident with no payment history
Evidence
Error: Cannot read property 'length' of undefined in rules.js. Resident in Unit 1803 was added mid-lease and has no rows in the payment CSV.
What to ask the AI
"The scoring engine assumes every resident has payment history. Add null checks: if a resident has no payment records, assign 0 points for payment friction (assume on-time until proven otherwise). Same for service requests -- no records means 0 points. Log a warning for residents with missing data so the property manager knows."
Symptom
Cohort calculation puts everyone in the 30-day bucket
Evidence
The action plan shows 50 residents in the 30-day cohort and 0 in the 60-day and 90-day cohorts. Lease end dates are definitely spread across the next 90 days.
What to ask the AI
"The cohort calculation is probably using the wrong date comparison. Days remaining should be calculated as (lease_end_date - reference_date) in days. Make sure both dates are parsed as Date objects. If lease_end is a string like '2026-06-15', parse it with new Date('2026-06-15T00:00:00') to avoid timezone issues. Log the days-remaining calculation for the first 5 residents so I can verify."
Symptom
Offer letters show NaN for pricing
Evidence
The 12-month renewal price shows 'NaN' and the MTM price shows 'NaN'. The current rent column in my CSV uses dollar signs and commas (like '$2,850.00').
What to ask the AI
"The rent parser is not stripping currency formatting. Before calculating pricing, clean the current_rent value: remove '$', remove commas, then parseFloat. Apply this in the lease roster ingestion step so all downstream calculations work with clean numbers."
Symptom
The risk breakdown tooltip is empty
Evidence
The action plan dashboard shows risk scores but when I hover to see the breakdown, the tooltip is blank or shows 'undefined'.
What to ask the AI
"The risk breakdown data is not being passed to the action plan template. The scoring engine should return both the total score AND the per-category breakdown (payment: 16, service: 10, tenure: 8, etc.) for each resident. Pass this breakdown object to the EJS template and format it as a multi-line tooltip showing each category and its point contribution."
Symptom
Sentiment flags are not detecting keywords in service request notes
Evidence
Unit 1507 has a service request with 'Very frustrated with response time' in the notes field, but the sentiment flag score is 0.
What to ask the AI
"The keyword search in the sentiment scoring rule is probably doing an exact match instead of a case-insensitive substring search. Change it to: lowercase the notes field, then check if it includes any of the flag words ('frustrated', 'unacceptable', 'moving', 'leaving', 'unhappy'). Use .toLowerCase().includes() for each keyword."

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.

🔍Why rule-based scoring, not ML

You might wonder why this tool uses hand-tuned point values instead of a machine learning model. Three reasons:

  1. 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.
  2. 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.
  3. 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 lease
roster CSV. Use nodemailer. Send the HTML letter as the email body (not an
attachment). Add a --dry-run flag that logs what WOULD be sent without actually
sending. 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 trend
Save the current run's data as a JSON file in the history directory for future
comparisons. 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 with
Yardi Voyager's batch renewal import. Columns: Unit, ResidentCode, NewLeaseStart,
NewLeaseEnd, NewMonthlyRent, LeaseType (12mo/15mo/MTM). This lets Nick upload
the 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 has
columns: unit, response_date, response (accepted_12mo | accepted_15mo |
accepted_mtm | declined | no_response). Generate an updated action plan that
shows which residents have responded, which offers were accepted, and which
residents still need follow-up. Calculate actual vs. projected renewal rate.

Try it yourself

  1. Open your AI CLI tool in an empty folder.
  2. Paste the main prompt.
  3. Run npm install in the generated renewal-planner/ directory.
  4. Generate sample data with --generate-sample and run the full pipeline.
  5. Open the action plan dashboard and verify the planted risk profiles match expectations.
  6. Open 3-4 offer letters and check that pricing varies by risk tier.
  7. Check the retention forecast — does the “targeted outreach” scenario show meaningful savings over “no intervention”?
  8. 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.

KNOWLEDGE CHECK

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.