Reminds the rep, then a manager, when a routed lead sits unclaimed, and posts a nurture message whenever a not-yet-ready lead is due one.
Uses
Claude
Gmail
Google Sheets
Slack
Chase leads nobody has picked up and nurture the not-yet-ready
Every 15 minutes, the 'Ledger' tab that 'Route every new sales enquiry to the right rep' writes is checked for leads nobody has claimed. Once a routed lead passes its reply deadline, its rep is reminded in Slack; 30 minutes later a manager is told, and 30 minutes after that it is offered to anyone who wants it, which is the last reminder. Every reminder says how to claim the lead: type the time in its 'Claimed At' cell. Leads parked for nurture because they were not ready get up to five nurture messages, 0, 4, 8, 15 and 22 days after they enquired, each written by AI from what they originally said and posted in Slack for a person to send.
Each step is written to the lead's Ledger row before its message goes out, so no lead hears the same step twice, and after the last step it is left alone; a filled 'Claimed At' cell stops everything for that lead. Only the cells it changes are written, so nothing a rep types is overwritten. If a row cannot be updated, the AI fails or Slack cannot be reached, the run is marked failed and names the leads.
You need the routing workflow and its spreadsheet in place, Google Sheets, Slack and Anthropic connected (and Gmail if nurture messages should also be saved as drafts), and the spreadsheet link and Slack channel filled in. A manager's Slack member ID is optional.
spreadsheetUrl to blank.Set spreadsheetUrl to blank.slackChannelId to blank.Set slackChannelId to blank.salesManagerSlackId to blank.Set salesManagerSlackId to blank.minutesBetweenEscalations to 30.Set minutesBetweenEscalations to 30.nurtureTouchDaysAfterEnquiry to 0,4,8,15,22.Set nurtureTouchDaysAfterEnquiry to 0,4,8,15,22.replyMode to slack.Set replyMode to slack.ledger to {status:,headers:[],rows:[],count:0}.Set ledger to {status:,headers:[],rows:[],count:0}.ledgerErrorIf a step below fails, save the error as ledgerErrorspreadsheetUrl. Save it as ledger.Read the rows of spreadsheetUrl. Save it as ledger.ledgerError.message.Stop and report a problem: Could not read the 'Ledger' tab. This workflow reads the spreadsheet that 'Route every new sales enquiry to the right rep' writes - check that the link on the step called 'Create spreadsheetUrl' is the same one and that it has a tab named exactly 'Ledger'. The error was: ledgerError.message.ledger.rows isn't empty and ledger.headers doesn't contain Submission ID or…If ledger.rows isn't empty and ledger.headers doesn't contain Submission ID or…nurtureTouchDaysAfterEnquiry by , and save it as touchDayList.Split nurtureTouchDaysAfterEnquiry by , and save it as touchDayList.managerMention to A manager.Set managerMention to A manager.salesManagerSlackId isn't blankIf salesManagerSlackId isn't blankmanagerMention to <@@salesManagerSlackId>.Change managerMention to <@@salesManagerSlackId>.escalationsThisRun to 0.Set escalationsThisRun to 0.nurtureTouchesThisRun to 0.Set nurtureTouchesThisRun to 0.ledger.rows where item["Submission ID"] isn't blank and item["Claimed At"] is blank and… and save it as openLeads.Keep each item in ledger.rows where item["Submission ID"] isn't blank and item["Claimed At"] is blank and… and save it as openLeads.chaseProblems to an empty list.Set chaseProblems to an empty list.row in openLeads:For each row in openLeads:sweepErrorIf a step below fails, save the error as sweepErrorrow["Submission ID"] and save it as submissionId.Trim row["Submission ID"] and save it as submissionId.submissionId is emptyIf submissionId is emptyrow.Decision lowercase and save it as decision.Make row.Decision lowercase and save it as decision.row["Claimed At"] and save it as claimedAt.Trim row["Claimed At"] and save it as claimedAt.rowNumber to row.row_number.Set rowNumber to row.row_number.row.Email and save it as leadEmail.Trim row.Email and save it as leadEmail.row.Company and save it as leadCompany.Trim row.Company and save it as leadCompany.row["Routed To"] and save it as routedTo.Trim row["Routed To"] and save it as routedTo.row["Routed To Slack ID"] and save it as routedToSlackId.Trim row["Routed To Slack ID"] and save it as routedToSlackId.routedMention to routedTo.Set routedMention to routedTo.routedToSlackId isn't emptyIf routedToSlackId isn't emptyroutedMention to <@@routedToSlackId>.Change routedMention to <@@routedToSlackId>.row["Processed At"] and save it as processedAt.Trim row["Processed At"] and save it as processedAt.row["Next Touch At"] and save it as nextTouchAt.Trim row["Next Touch At"] and save it as nextTouchAt.touchesSent to 0.Set touchesSent to 0.row["Touches Sent"] isn't blankIf row["Touches Sent"] isn't blanktouchesSent to row["Touches Sent"] minus 0.Change touchesSent to row["Touches Sent"] minus 0.claimedAt isn't emptyIf claimedAt isn't emptyisDue to true.Set isDue to true.nextTouchAt isn't emptyIf nextTouchAt isn't emptyisDue to the difference between nextTouchAt and the current date and time in minutes is greater than or equal to 0.Change isDue to the difference between nextTouchAt and the current date and time in minutes is greater than or equal to 0.isDue is noIf isDue is nodecision is routeIf decision is routerow["SLA Due"] and save it as replyDeadline.Trim row["SLA Due"] and save it as replyDeadline.minutesOverdue to 0.Set minutesOverdue to 0.replyDeadline isn't emptyIf replyDeadline isn't emptyreplyDeadline to the current date and time and save it as minutesOverdue.Count the minutes from replyDeadline to the current date and time and save it as minutesOverdue.minutesOverdue is greater than 0If minutesOverdue is greater than 0escalationNote to blank.Set escalationNote to blank.newReason to QUEUE_UNACCEPTED.Set newReason to QUEUE_UNACCEPTED.minutesBetweenEscalations minutes and save it as nextCheckAt.Move the current date and time by minutesBetweenEscalations minutes and save it as nextCheckAt.nextCheckAt readable as an ISO 8601 timestamp and save it as nextCheckText.Make nextCheckAt readable as an ISO 8601 timestamp and save it as nextCheckText.touchesSent is 0If touchesSent is 0escalationNote to :hourglass_flowing_sand: routedMention this lead has been waiting minutesOverdue minutes and nobody has picked it up yet.Change escalationNote to :hourglass_flowing_sand: routedMention this lead has been waiting minutesOverdue minutes and nobody has picked it up yet.
leadEmail at leadCompany.escalationNote to :rotating_light: managerMention escalating: leadEmail at leadCompany plus has now been unclaimed for minutesOverdue plus minutes. It was routed to routedTo.Change escalationNote to :rotating_light: managerMention escalating: leadEmail at leadCompany plus has now been unclaimed for minutesOverdue plus minutes. It was routed to routedTo.escalationNote to :arrows_counterclockwise: Returning to the pool - leadEmail at leadCompany went unclaimed for minutesOverdue minutes. Anyone who wants it, take it - this is the last reminder about it.Change escalationNote to :arrows_counterclockwise: Returning to the pool - leadEmail at leadCompany went unclaimed for minutesOverdue minutes. Anyone who wants it, take it - this is the last reminder about it.newReason to RETURNED_TO_POOL.Change newReason to RETURNED_TO_POOL.nextCheckText to blank.Change nextCheckText to blank.touchesSent.Add 1 to touchesSent.newReason; Next Touch At: nextCheckText; Touches Sent: touchesSent on row rowNumber of 'Ledger' in spreadsheetUrl.Set Reason Code: newReason; Next Touch At: nextCheckText; Touches Sent: touchesSent on row rowNumber of 'Ledger' in spreadsheetUrl.postErrorIf a step below fails, save the error as postErrorslackChannelId.Send a message to slackChannelId.escalationNote
To claim it, type the time (or your name) in its Claimed At cell: row rowNumber of the <spreadsheetUrl|Ledger tab>.leadEmail (Ledger row rowNumber): the reminder was recorded on the Ledger but could not be posted to Slack, so nobody was told (postError.message). to the end of chaseProblems and save it as chaseProblems.Add leadEmail (Ledger row rowNumber): the reminder was recorded on the Ledger but could not be posted to Slack, so nobody was told (postError.message). to the end of chaseProblems and save it as chaseProblems.escalationsThisRun.Add 1 to escalationsThisRun.decision is nurtureIf decision is nurturetouchesSent is greater than or equal to the length of touchDayListIf touchesSent is greater than or equal to the length of touchDayListrowNumber of 'Ledger' in spreadsheetUrl.Set Reason Code: NURTURE_EXHAUSTED; Next Touch At: on row rowNumber of 'Ledger' in spreadsheetUrl.touchNumber to touchesSent plus 1.Set touchNumber to touchesSent plus 1.nurturePrompt to Write nurture touch number touchNumber of the length of touchDayList plus for an inbound lead who was a good fit but had no urgency when they first got in touch.Set nurturePrompt to Write nurture touch number touchNumber of the length of touchDayList plus for an inbound lead who was a good fit but had no urgency when they first got in touch.
They are leadEmail at leadCompany.
What they originally said: row.Evidence
Why we thought they were worth keeping: row.Reasoning
Write under 80 words. Send one useful thing - a relevant example, a number, a question worth answering - not a check-in. Never say 'just circling back', 'touching base' or 'following up'. Later touches in the sequence should be shorter and less frequent in tone, not more insistent. Output only the message body.nurturePrompt. Save it as nurtureDraft.Ask Claude: nurturePrompt. Save it as nurtureDraft.
Maximum response length: 400touchesSent.Add 1 to touchesSent.nextOffsetDays to 0.Set nextOffsetDays to 0.touchesSent is less than the length of touchDayListIf touchesSent is less than the length of touchDayListnextOffsetDays to touchDayList[@touchesSent] minus 0.Change nextOffsetDays to touchDayList[@touchesSent] minus 0.anchorDate to processedAt.Set anchorDate to processedAt.anchorDate is emptyIf anchorDate is emptyanchorDate.Make the current date and time readable as an ISO 8601 timestamp and save it as anchorDate.anchorDate by nextOffsetDays days and save it as nextTouchDue.Move anchorDate by nextOffsetDays days and save it as nextTouchDue.nextTouchDue readable as an ISO 8601 timestamp and save it as nextTouchText.Make nextTouchDue readable as an ISO 8601 timestamp and save it as nextTouchText.nextTouchText; Touches Sent: touchesSent on row rowNumber of 'Ledger' in spreadsheetUrl.Set Reason Code: NURTURING; Next Touch At: nextTouchText; Touches Sent: touchesSent on row rowNumber of 'Ledger' in spreadsheetUrl.postErrorIf a step below fails, save the error as postErrorslackChannelId.Send a message to slackChannelId.:seedling: *Nurture touch touchNumber of the length of touchDayList due* - leadEmail at leadCompany plus
nurtureDraft.response_textleadEmail (Ledger row rowNumber): the nurture touch was recorded on the Ledger but could not be posted to Slack, so nobody was told (postError.message). to the end of chaseProblems and save it as chaseProblems.Add leadEmail (Ledger row rowNumber): the nurture touch was recorded on the Ledger but could not be posted to Slack, so nobody was told (postError.message). to the end of chaseProblems and save it as chaseProblems.replyMode in lowercase is draft and leadEmail isn't emptyIf replyMode in lowercase is draft and leadEmail isn't emptydraftErrorIf a step below fails, save the error as draftErrorleadEmail.Draft an email to leadEmail.
Subject: A thought on your enquirynurtureDraft.response_textleadEmail (Ledger row rowNumber): nurture touch touchNumber was posted to Slack but the Gmail draft could not be saved, so send it from the Slack post (draftError.message). to the end of chaseProblems and save it as chaseProblems.Add leadEmail (Ledger row rowNumber): nurture touch touchNumber was posted to Slack but the Gmail draft could not be saved, so send it from the Slack post (draftError.message). to the end of chaseProblems and save it as chaseProblems.nurtureTouchesThisRun.Add 1 to nurtureTouchesThisRun.row.Email (Ledger row row.row_number) was not updated and will be tried again on the next check (sweepError.message). to the end of chaseProblems and save it as chaseProblems.Add row.Email (Ledger row row.row_number) was not updated and will be tried again on the next check (sweepError.message). to the end of chaseProblems and save it as chaseProblems.chaseProblems isn't emptyIf chaseProblems isn't emptychaseProblems with a new line and save it as problemList.Join chaseProblems with a new line and save it as problemList.problemList.escalationsThisRun plus nurtureTouchesThisRun is greater than 0If escalationsThisRun plus nurtureTouchesThisRun is greater than 0escalationsThisRun reminder(s) and nurtureTouchesThisRun nurture touch(es) posted to Slack.Show in the output: Checked the Ledger: escalationsThisRun reminder(s) and nurtureTouchesThisRun nurture touch(es) posted to Slack.