Churn Radar Custom
What it does
A scheduled workflow. A usage agent pulls product signals from the warehouse — login recency, feature adoption, active seats — while a billing agent reads payment health from Stripe: failed charges, downgrades, cards about to expire. A scorer combines both into a churn-risk read for each account with the reasons behind it. The flow branches: at-risk accounts get a specific alert to their success owner with a suggested next play, and healthy accounts are logged so the trend stays visible. Run it weekly.
The cast · 5 agents
Write this run's risk reads to the tracking table so the trend over time stays visible, including the healthy accounts. Report the counts.
Combine usage and billing into a churn-risk read per account. Falling usage plus a failed charge is high risk; strong usage and clean billing is low. Return an object with, per account, an 'atRisk' boolean, the top reasons, and a suggested next play. Set the overall 'atRisk' flag if any account is at risk.
For each at-risk account, post a specific alert to its success owner: the account, the risk reasons, and the suggested play. One message per owner, grouped. No vague nudges.
Query the warehouse for product-usage signals per account in the segment: last login, active seats versus licensed, and adoption of core features. Report the numbers, read-only.
Read billing health per account from Stripe: recent failed charges, downgrades, and cards expiring soon. Report the facts per account.
Flow
Interface
Depends on · 3 MCP servers
io.github.supabase/postgres-mcpnot in the registry yetdb
io.github.slack/slack-mcpnot in the registry yetchat# Generated by @socketcat/compiler for target: langgraph
# blueprint: com.socketcat/churn-radar v1.0.0 schema: socketcat.dev/blueprint/v0
# This code is yours. Edit it freely. The socketcat_runtime helper is optional and can be vendored.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
import socketcat_runtime as _rt
class State(TypedDict, total=False):
segment: object
summary: object
usage: object
billing: object
risk: object
def node_Fork_0(state):
return {}
def node_Join_1(state):
return {}
def node_AgentInvoke_2(state):
return _rt.run_agent("usageAgent", state, {"segment":"segment"}, "usage")
def node_AgentInvoke_3(state):
return _rt.run_agent("billingAgent", state, {"segment":"segment"}, "billing")
def node_AgentInvoke_4(state):
return _rt.run_agent("scorer", state, {"usage":"usage","billing":"billing"}, "risk")
def node_Branch_5(state):
return {}
def node_Join_6(state):
return {}
def node_AgentInvoke_7(state):
return _rt.run_agent("alerter", state, {"risk":"risk"}, "summary")
def node_AgentInvoke_8(state):
return _rt.run_agent("logger", state, {"risk":"risk"}, "summary")
def route_Branch_5(state):
if _rt.cond("risk.atRisk == true", state): return "AgentInvoke_7"
return "AgentInvoke_8"
def build():
b = StateGraph(State)
b.add_node("Fork_0", node_Fork_0)
b.add_node("Join_1", node_Join_1)
b.add_node("AgentInvoke_2", node_AgentInvoke_2)
b.add_node("AgentInvoke_3", node_AgentInvoke_3)
b.add_node("AgentInvoke_4", node_AgentInvoke_4)
b.add_node("Branch_5", node_Branch_5)
b.add_node("Join_6", node_Join_6)
b.add_node("AgentInvoke_7", node_AgentInvoke_7)
b.add_node("AgentInvoke_8", node_AgentInvoke_8)
b.add_edge(START, "Fork_0")
b.add_edge("Fork_0", "AgentInvoke_2")
b.add_edge("Fork_0", "AgentInvoke_3")
b.add_edge("Join_1", "AgentInvoke_4")
b.add_edge("AgentInvoke_2", "Join_1")
b.add_edge("AgentInvoke_3", "Join_1")
b.add_edge("AgentInvoke_4", "Branch_5")
b.add_conditional_edges("Branch_5", route_Branch_5, {"AgentInvoke_7": "AgentInvoke_7", "AgentInvoke_8": "AgentInvoke_8"})
b.add_edge("Join_6", END)
b.add_edge("AgentInvoke_7", "Join_6")
b.add_edge("AgentInvoke_8", "Join_6")
return b.compile()
INPUTS = ["segment"]
OUTPUTS = ["summary"]
if __name__ == "__main__":
_rt.main(build, INPUTS, OUTPUTS)
▸blueprint.json (the portable format)
{
"id": "com.socketcat/churn-radar",
"flow": {
"type": "sequence",
"blocks": [
{
"type": "parallel",
"blocks": [
{
"in": {
"segment": "segment"
},
"out": "usage",
"use": "usageAgent",
"type": "agent"
},
{
"in": {
"segment": "segment"
},
"out": "billing",
"use": "billingAgent",
"type": "agent"
}
]
},
{
"in": {
"usage": "usage",
"billing": "billing"
},
"out": "risk",
"use": "scorer",
"type": "agent"
},
{
"else": [
{
"in": {
"risk": "risk"
},
"out": "summary",
"use": "logger",
"type": "agent"
}
],
"type": "branch",
"cases": [
{
"when": "risk.atRisk == true",
"blocks": [
{
"in": {
"risk": "risk"
},
"out": "summary",
"use": "alerter",
"type": "agent"
}
]
}
]
}
]
},
"tags": [
"customer-success",
"retention",
"scheduled",
"revenue"
],
"title": "Churn Radar",
"agents": {
"logger": {
"model": {
"hint": "fast"
},
"title": "Logger",
"tools": [
"db.query_dsl"
],
"instructions": "Write this run's risk reads to the tracking table so the trend over time stays visible, including the healthy accounts. Report the counts."
},
"scorer": {
"model": {
"hint": "reasoning"
},
"title": "Scorer",
"output": {
"type": "object",
"required": [
"atRisk"
],
"properties": {
"atRisk": {
"type": "boolean"
},
"accounts": {
"type": "array"
}
}
},
"instructions": "Combine usage and billing into a churn-risk read per account. Falling usage plus a failed charge is high risk; strong usage and clean billing is low. Return an object with, per account, an 'atRisk' boolean, the top reasons, and a suggested next play. Set the overall 'atRisk' flag if any account is at risk."
},
"alerter": {
"model": {
"hint": "fast"
},
"title": "Alerter",
"tools": [
"chat.post_message"
],
"instructions": "For each at-risk account, post a specific alert to its success owner: the account, the risk reasons, and the suggested play. One message per owner, grouped. No vague nudges."
},
"usageAgent": {
"model": {
"hint": "fast"
},
"title": "Usage",
"tools": [
"db.query_dsl"
],
"instructions": "Query the warehouse for product-usage signals per account in the segment: last login, active seats versus licensed, and adoption of core features. Report the numbers, read-only."
},
"billingAgent": {
"model": {
"hint": "fast"
},
"title": "Billing",
"tools": [
"pay.list_charges",
"pay.list_subscriptions"
],
"instructions": "Read billing health per account from Stripe: recent failed charges, downgrades, and cards expiring soon. Report the facts per account."
}
},
"estate": {
"risk": {
"type": "object",
"description": "The scorer's per-account risk read, including an 'atRisk' flag."
},
"usage": {
"type": "object",
"description": "Per-account product-usage signals from the warehouse."
},
"billing": {
"type": "object",
"description": "Per-account billing health from Stripe."
}
},
"$schema": "socketcat.dev/blueprint/v0",
"authors": [
{
"url": "https://socketcat.com",
"name": "SocketCat"
}
],
"license": "MIT",
"servers": [
{
"ref": "io.github.supabase/postgres-mcp",
"alias": "db"
},
{
"ref": "com.stripe/payments-mcp",
"alias": "pay"
},
{
"ref": "io.github.slack/slack-mcp",
"alias": "chat"
}
],
"summary": "Combines product-usage and billing signals into a per-account churn-risk read, then alerts the owner on at-risk accounts.",
"targets": [
"langgraph",
"*"
],
"version": "1.0.0",
"interface": {
"inputs": {
"segment": {
"type": "string",
"description": "Which book of accounts to scan, e.g. 'enterprise' or 'all'."
}
},
"outputs": {
"summary": {
"type": "string",
"description": "The at-risk accounts alerted and the healthy count, for this run."
}
}
},
"extensions": {
"dev.langgraph": {
"checkpointer": "memory"
}
},
"description": "A scheduled workflow. A usage agent pulls product signals from the warehouse — login recency, feature adoption, active seats — while a billing agent reads payment health from Stripe: failed charges, downgrades, cards about to expire. A scorer combines both into a churn-risk read for each account with the reasons behind it. The flow branches: at-risk accounts get a specific alert to their success owner with a suggested next play, and healthy accounts are logged so the trend stays visible. Run it weekly."
}More in Sales & marketing
Browse all →Lead Enrichment & Outreach
A new lead is enriched from the web and your CRM at once, scored against your ICP, and — if it fits — gets a tailored first email you approve before send.