> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiveku.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Workflow Recipe: Subscription Renewal Reminders

> Automatically remind customers before their subscription renews or expires

Subscription businesses lose customers to involuntary churn — expired cards, forgotten renewals, surprise charges that trigger refund requests. A tiered reminder sequence gives users time to update payment info, adjust their plan, or confirm they want to stay. Done well, it also becomes your highest-opened email of the month.

<Info>
  Before you start: you'll need a `subscriptions` or `customers` table with a `renewal_date` or `current_period_end` column, and an email service configured. See [Send Emails](/how-tos/send-emails).
</Info>

## The Flow at a Glance

<CardGroup cols={4}>
  <Card title="7 days out" icon="calendar">
    Gentle heads-up
  </Card>

  <Card title="3 days out" icon="clock">
    Upgrade/downgrade options
  </Card>

  <Card title="1 day out" icon="bell">
    Last chance to change
  </Card>

  <Card title="Day of" icon="check">
    Receipt or payment retry
  </Card>
</CardGroup>

## Step 1: Create the Workflow

<Steps>
  <Step title="Open Workflows">
    In your project, go to **Workflows > New Workflow**. Name it `Subscription Renewal Reminders`.
  </Step>

  <Step title="Add a Schedule trigger">
    Click **Add Trigger > Schedule**. Set it to run **daily at 8 AM UTC** (or whatever matches your audience's morning).

    <Tip>
      Pick a time when customers are likely to open email but before support staff clock in — gives them a chance to handle issues themselves first.
    </Tip>
  </Step>
</Steps>

## Step 2: Query Upcoming Renewals

Each tier uses a separate query so you can send different messaging. Here's the 7-day query as a template:

```sql theme={null}
SELECT id, user_email, plan_name, renewal_date, amount
FROM subscriptions
WHERE renewal_date BETWEEN now() + interval '6 days 23 hours'
                       AND now() + interval '7 days'
  AND renewal_reminder_7d_sent IS NULL
  AND status = 'active'
```

Add three more blocks with adjusted date intervals and sent-flag columns for the 3-day, 1-day, and day-of tiers.

<Warning>
  Always check a `renewal_reminder_*_sent` flag in the query. Without it, a single user can get the same reminder multiple times if the workflow runs more than once a day or is re-enabled mid-window.
</Warning>

## Step 3: Send Tiered Reminder Emails

Different days, different tone:

* **7 days out:** "Your plan renews next week" — casual, reassuring. Include the plan name and renewal amount. One soft CTA: "Manage your subscription."
* **3 days out:** "Renewing in 3 days" — introduce upgrade/downgrade options. Highlight what they get on a higher tier.
* **1 day out:** "Your subscription renews tomorrow" — last chance to change anything. Include a link to update payment method prominently.
* **Day of:** Either a "Thanks for renewing" receipt, or a "Renewal failed — update payment" notice depending on charge result.

<Steps>
  <Step title="Add a Send Email action per tier">
    After each query, add a **Send Email** action scoped to that query's results.
  </Step>

  <Step title="Personalize per plan">
    Monthly customers get different messaging than annual. High-tier customers deserve a more personal touch. Use conditionals in your email template or split into multiple workflows.
  </Step>

  <Step title="Add a database update action">
    After each send, update the sent flag:

    ```sql theme={null}
    UPDATE subscriptions
    SET renewal_reminder_7d_sent = now()
    WHERE id = {{row.id}}
    ```
  </Step>
</Steps>

## Step 4: Handle Failed Renewals

Failed payments are recoverable — most are card expirations, not intentional cancellations. Build a second workflow for this:

<Steps>
  <Step title="Create a new workflow">
    Name it `Renewal Recovery`.
  </Step>

  <Step title="Add a Stripe webhook trigger">
    Listen for `invoice.payment_failed`. See [Webhook Patterns](/how-tos/webhook-patterns).
  </Step>

  <Step title="Send a recovery email">
    Subject: "Your payment didn't go through." Body: plain and calm. Explain what happened, link to update payment method, mention what happens if they don't.
  </Step>

  <Step title="Schedule retry reminders">
    Add delays for day 2, day 3, and day 5. Each sends another reminder with increasing urgency. After the final retry, cancel the subscription (or pause it if your product supports that).
  </Step>
</Steps>

<Tip>
  Renewal emails are some of the most-opened emails you'll send. Use the opportunity to share what's new since their last billing cycle, highlight features they haven't tried, or tease an upcoming release. Don't waste the attention.
</Tip>

## Loyalty Incentives

Tenure is a retention lever. Pull it in your renewal emails:

* "You've been with us 1 year — here's 10% off next month"
* "Celebrating 6 months — unlocked: priority support"
* "Year 2 with Acme — we upgraded you to the new Pro features at no extra cost"

Calculate tenure from `created_at` in your subscription record and branch the email template based on thresholds.

## Personalize by Plan

One-size-fits-all reminders get unsubscribes. Branch your messaging:

* **Monthly vs annual:** annual customers need less hand-holding but bigger-deal framing ("your annual plan renews in 30 days")
* **Free trial converting to paid:** emphasize what they'll lose if they cancel vs what they've already set up
* **High-tier:** consider a personal email from a human, not a templated one
* **At-risk (low usage):** combine renewal reminder with a "need help getting started?" offer

## Verify It Worked

<Steps>
  <Step title="Create a test subscription">
    Insert a test row with `renewal_date = now() + interval '7 days'`.
  </Step>

  <Step title="Trigger the workflow manually">
    Go to **Workflows > Runs > Run Now**.
  </Step>

  <Step title="Confirm the email arrives">
    Check the test email inbox. Verify the subject, tone, and CTAs render correctly. Confirm the `renewal_reminder_7d_sent` flag was set.
  </Step>

  <Step title="Reset and test other tiers">
    Update the test row to `now() + interval '3 days'`, clear the flag, run again.
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="No reminder went out">
    Check the workflow is **Enabled** (toggle in top right). Then check the `renewal_reminder_*_sent` flag on the target row — if it's already set, the query filters the row out. To re-test, clear the flag and re-run.
  </Accordion>

  <Accordion title="Wrong tier fired (7-day instead of 3-day)">
    Date math is tricky. Make sure your `BETWEEN` ranges don't overlap — the 7-day window should end exactly where the 3-day window begins. Also check your database timezone matches the workflow's schedule timezone.
  </Accordion>

  <Accordion title="Customer says 'my card was charged without warning'">
    The 7-day email is the most commonly overlooked — spam folder, busy week, address change. Always send a day-of confirmation email too, even for successful renewals. That way they have at least one touchpoint they can search their inbox for.
  </Accordion>

  <Accordion title="Renewals failing after email sends fine">
    If the email workflow runs but Stripe payments aren't going through, the issue is at the payment layer, not the email layer. Decouple email sending from payment retry — each should succeed or fail independently. Check your Stripe API logs and webhook configuration.
  </Accordion>

  <Accordion title="Duplicate emails on the same day">
    Usually a sent-flag race condition. Make sure the flag update happens before the next workflow tick — or use a uniqueness constraint like `UNIQUE(subscription_id, reminder_tier, date)` on a reminders log table.
  </Accordion>
</AccordionGroup>

## What's Next?

<CardGroup cols={2}>
  <Card title="Add Stripe" icon="credit-card" href="/how-tos/add-stripe">
    Wire up subscriptions and payment processing
  </Card>

  <Card title="Webhook Patterns" icon="webhook" href="/how-tos/webhook-patterns">
    Handle Stripe and other third-party webhooks reliably
  </Card>
</CardGroup>
