A small, explainable customer-lifecycle automation engine in pure Python.
Feed it a customer's event history and it will:
- Score health & churn risk (0–100 / 0.0–1.0) — every point is explained, so a CSM or a customer can always ask "why?"
- Classify the lifecycle stage —
new → onboarding → activated → engaged → at_risk → churned - Decide the right lifecycle action via a declarative rule set — onboarding sequence, activation nudge, engagement tip, review request, re-engagement, CSM hand-off for high-risk accounts, win-back
- Personalise the copy — offline templates by default, or drop in an LLM (Claude / OpenAI) provider in production without touching the engine
No runtime dependencies — 27 unit tests across 5 modules covering the core paths.
Built as a focused demonstration of customer-success / lifecycle engineering: turning raw product signals into the right, timely, personalised touch — automatically.
git clone https://github.com/itsviseph/customer-lifecycle-engine.git
cd customer-lifecycle-engine
python examples/demo.pyExample output:
Cara (c3) · stage=engaged health=100/100 churn_risk=0.0
-> request_review [email] — happy customer — ask for a review
preview: Hi Cara, so glad it's working for you! Would you share a quick review?
Dev (c4) · stage=at_risk health=35/100 churn_risk=0.65
-> send_reengagement [email] — health is dropping
preview: Hi Dev, we miss you — here's what's new since you last logged in.
from datetime import datetime, timedelta
from customer_lifecycle import Customer, Event, EventType, LifecycleEngine
now = datetime(2026, 6, 1)
customer = Customer(
id="c1", name="Asha", email="asha@acme.com",
signed_up_at=now - timedelta(days=2),
events=[Event(EventType.SIGNUP, now - timedelta(days=2))],
)
result = LifecycleEngine().process(customer, now=now)
print(result.stage, result.health.score, [a.type.value for a in result.actions])
# LifecycleStage.NEW 35 ['send_onboarding_sequence']The defaults are side-effect free. In production you subclass two things:
from customer_lifecycle import ActionDispatcher, LifecycleEngine, personalize
class EmailDispatcher(ActionDispatcher):
def dispatch(self, action):
# send via your ESP / CRM (customer.io, Resend, Salesforce, ...)
...
class ClaudeProvider: # satisfies the MessageProvider protocol
def generate(self, system, prompt):
# call Claude / OpenAI and return the message
...
engine = LifecycleEngine(dispatcher=EmailDispatcher())
# personalize(action, customer, provider=ClaudeProvider()) -> LLM-written copy| Module | Responsibility |
|---|---|
models.py |
Customer, Event, and the EventType / LifecycleStage enums |
health.py |
transparent, weighted health & churn-risk scoring |
stages.py |
maps a customer + health onto a lifecycle stage |
rules.py |
declarative (stage, health) -> action rules (bring your own) |
actions.py |
action requests + a pluggable dispatcher |
personalize.py |
offline templates by default; LLM provider in prod |
engine.py |
orchestrates ingest → score → stage → rules → dispatch |
The scoring weights and stage thresholds are deliberately simple constants at the top of each module, so they're easy to read, tune, and test.
pip install -e ".[dev]"
pytestCovers health scoring, stage transitions (including churn and at-risk edges), rule firing (incl. the high-churn CSM escalation), personalisation, and the end-to-end engine.
Built with AI assistance (Claude), reviewed and tested by me. The scoring weights and stage thresholds are an illustrative starter heuristic — the constants are exposed at the top of each module so they're easy to tune.
The engine above is intentionally tool-agnostic — it's the portable decision logic. To show it works in the kind of platform a lifecycle / customer-success team actually runs, I stood the same model up in a customer.io workspace (free trial, test mode — fictional people, nothing is sent to anyone real).
What's live in the workspace today
-
The five demo customers as People. The exact customers from
examples/demo.py— Asha, Ben, Cara, Dev, Eli — exist as People, each carrying the attributes the engine reasons about:plan,activated,lifecycle_stage. -
Four data-driven segments, one per lifecycle stage, defined by conditions on those attributes:
Segment Condition Engine stage Trial – not activated plan = trialANDactivated = falseonboarding Engaged customers lifecycle_stage = engagedengaged At-risk accounts lifecycle_stage = at_riskat_risk Churned – win-back lifecycle_stage = churnedchurned -
Four working journeys — one per lifecycle stage. Each is a segment-triggered automation: an email, a 2-day wait, a True/False branch on
lifecycle_stage(continue only if the person is still in the stage), then a second, more personal touch — all Liquid-personalised ({{customer.first_name}},{{customer.plan}}). For example, "Trial onboarding & activation" fires when someone enters Trial – not activated:- Send a welcome email
- Wait 2 days
- Branch on
activated: still not activated → send an activation nudge; activated → exit.
That's the engine's
new → onboarding → activatedpath expressed as a real automation — segment-driven entry, a delay, a conditional branch, and personalised copy.
The full lifecycle, mapped engine → segment → journey
Every segment triggers its own journey, exactly as the engine's rule set decides:
| Engine stage | Segment | Engine action | customer.io journey |
|---|---|---|---|
| onboarding | Trial – not activated | onboarding + activation nudge | Trial onboarding & activation |
| engaged | Engaged customers | request_review | Advocacy: reviews & referrals |
| at_risk | At-risk accounts | re-engagement + CSM hand-off | At-risk re-engagement & CSM hand-off |
| churned | Churned – win-back | win-back | Win-back |
Honest scope: this is a learning / portfolio build in a trial workspace with test data, not a production deployment — nothing has been sent to a real person. The repo is the reusable logic; the customer.io workspace shows I can operationalise it — segment by behaviour, branch on state, and personalise the message.
MIT