You’ll build a classic background job: every morning, remind customers about invoices that are due, and make it robust: it never double-sends, a missed night just runs later, it’s easy to test, and you hear about failures. You’ll use: schedules, flows, mail, alarms.

The pattern: a sweep, not a timer per item

Don’t create one delayed job per invoice. Run a short schedule that finds whatever is due now, handles it, and marks it. A missed run just means the next run finds a bit more; nothing is lost if a server restarts.

1. The table

invoices with: customer_email, amount, due_at (datetime), status (open, paid), and reminded_at (datetime, empty by default). Add an index on (status, due_at) so the sweep is fast as the table grows (Indexes).

2. The template

Mail → Templates: invoice-due from the Invoice design, values invoice.number, invoice.amount, invoice.due_at, pay_url. Publish it.

3. The flow

Flows → New flow invoice-reminders, with a Manual run trigger (the schedule starts it):
  1. SQL query, node id due: find what’s due and not reminded. Using SQL lets you compare dates:
    with params ["{{ input.scheduled_at }}"].
  2. For each over {{ steps.due.output }}. Inside its each branch, the current row is item (and its position index):
    • Send email: To {{ item.customer_email }}, Template invoice-due, Data {"invoice": "{{ item }}", "pay_url": "https://app.example.com/pay/{{ item.id }}"}.
    • Update record: table invoices, id {{ item.id }}, data {"reminded_at": "{{ input.scheduled_at }}"}.
  3. A Log block after the loop’s done exit: Reminded {{ steps.due.output | length }} invoices.
A For each handles items one at a time, at most 10,000 per run. See Control flow.
LIMIT 200 keeps each run well inside the flow’s time limit. If more are due, the next run picks them up. For bigger volumes, run more often (hourly) rather than raising the limit.

4. The schedule

Schedules → New schedule: The flow receives { "scheduled_at": "…" }. Using scheduled_at instead of “now” means a late run still uses the time it was meant for.

5. Test it

  1. Create two invoices in development: one due yesterday, one due next week.
  2. On the schedule, press Run now.
  3. Activity → Flow runs: open the run. The due step should have one row, the loop one iteration.
  4. Mail activity: one invoice-due message.
  5. Run now again: the due step is now empty, because reminded_at is set. That’s the double-send protection working.

6. Make failures loud

  • An alarm on Failed flow runs above 0 over 60 minutes, emailing your team.
  • Activity → Overview flags Schedule invoice-reminders failed on its last run.
If a run fails half-way, the invoices already reminded have reminded_at set and the rest don’t, so re-running is safe: press Run now after fixing the cause.

Variations

Schedules

Every schedule setting.

Background jobs

Retries and replay.