Playbook

The contract renewal tracker.

90, 60, 30, 14-day renewal alerts with conditional escalation. CSM gets context. Sales gets time. Nobody slips.

8 min read

The pain

Why renewals slip

$200k contract auto-cancels at midnight. Your CSM finds out the next morning. The customer was happy, just forgot to confirm. Now you're chasing them through procurement to re-up.

The four ways renewal tracking breaks:

  • Calendar reminders only. CSM sets a Google Calendar reminder. CSM leaves the company. Reminder fires to empty inbox.
  • One-shot notifications. Single email at 30 days. Nobody reads it. Renewal slips.
  • No context. Notification says "Acme renews in 30 days." CSM has to dig for last QBR notes, usage metrics, recent tickets.
  • No escalation. CSM ignores the 30-day notification. Nothing happens. Day 0 hits, contract expires.

The architecture

What we're building

A daily cron that scans HubSpot deals nearing renewal and triggers per-customer notification workflows at 90, 60, 30, and 14 days. Each notification includes context. No-acknowledgement escalates to the CSM's manager.

workflow({
  id: "renewal-scanner",
  trigger: { schedule: "0 9 * * *" },  // every morning at 9am
  concurrency: { limit: 1 },

  steps: async ({ step }) => {
    const today = new Date();
    const milestones = [90, 60, 30, 14];

    for (const days of milestones) {
      const renewals = await step.run(`find-${days}-day-renewals`, () =>
        hubspot.deals.search({
          filterGroups: [{ filters: [
            { propertyName: "deal_type", operator: "EQ", value: "renewal" },
            { propertyName: "renewal_date", operator: "EQ", value: addDays(today, days).toISOString() },
          ]}],
        })
      );

      for (const deal of renewals) {
        await step.invoke(`notify-${deal.id}-d${days}`, {
          workflow: notifyRenewal,
          data: { dealId: deal.id, daysRemaining: days },
        });
      }
    }
  },
});

The notification flow

With context and escalation

workflow({
  id: "notify-renewal",
  trigger: { event: "renewal.notify" },

  steps: async ({ event, step }) => {
    const { dealId, daysRemaining } = event.data;

    // Gather context
    const context = await step.run("gather-context", async () => {
      const [deal, account, lastQbr, recentTickets, usage] = await Promise.all([
        hubspot.deals.get(dealId),
        hubspot.companies.getByDealId(dealId),
        gainsight.qbrs.latest(account.id),
        zendesk.tickets.recent(account.id, { days: 90 }),
        product.usage.summary(account.id, { days: 30 }),
      ]);
      return { deal, account, lastQbr, recentTickets, usage };
    });

    // Notify CSM with full context
    const ackId = `ack-${dealId}-d${daysRemaining}`;
    await step.run("notify-csm", () =>
      slack.dm(context.account.csm_slack_id, formatRenewalNotification(context, ackId))
    );

    // Escalation: 14-day notifications must be acknowledged in 24h
    if (daysRemaining <= 14) {
      const ack = await step.waitForEvent("wait-csm-ack", {
        event: "renewal.acked",
        match: "data.ackId",
        timeout: "24h",
      });

      if (!ack) {
        await step.run("escalate-to-manager", () =>
          slack.dm(context.account.csm_manager_slack_id,
            `No CSM ack on ${context.account.name} renewal (${daysRemaining} days). Escalating.`)
        );
      }
    }

    // Audit
    await step.run("log-notification", () =>
      db.renewalNotifications.insert({
        dealId,
        daysRemaining,
        notifiedAt: new Date(),
      })
    );
  },
});

The 14-day notification has the escalation flow. step.waitForEvent suspends for up to 24 hours waiting for the CSM to ack. If they do, workflow exits. If not, manager gets pinged.

Edge cases

What goes wrong, and how to handle it

Multiple notifications for the same deal. The same deal can hit the 90, 60, 30, 14 milestones across the calendar. The audit table prevents duplicate-day notifications. Add a check at the top.

Renewal date moved. Customer pushed renewal by two weeks. Re-trigger: the cron picks them up at the new milestone. Old notifications stay in audit.

CSM is on PTO. Look up the OOO calendar in the context-gather step. If the CSM is OOO, route to the backup immediately. Don't wait for the 24h ack timeout.

Auto-renewal contracts. Skip notifications if auto_renew = true. Send a confirmation email instead, 7 days before the auto-renewal fires.

The math

What this costs on Rotor

A team with 200 active customers averaging 4 notifications per customer per year (one at each milestone): 800 notifications/yr = ~70/month. Each notification = ~10 step-runs.

~700 step-runs/month. Fits Rotor Hobby ($9/mo). The savings on one prevented churn pays for years of Rotor at any tier.

Fork this playbook on Rotor.

$9 to start. 30-day money back. Hard caps protect you from runaway bills.

Start shipping