ailiteracynepal 🇳🇵
Text size

Chapter 02 · Section IV · 10 min read

2.4 Event-driven loops: react when something happens

The fourth flavour of trigger — an external event. Webhooks, file drops, inbox arrivals. The loop wakes up because the world did something, not because a clock said so.

Event-driven loops don’t fire on a schedule. They fire when something happens — a customer files a support ticket, a pull request opens on GitHub, an invoice PDF lands in a shared folder, a payment webhook posts to your server. The trigger is the world.

What “an event” actually is

Under the hood, an event-driven loop is almost always one of two shapes:

Push: something else calls you. A webhook fires. A message queue delivers. A subscription pushes an update. Your loop’s endpoint receives the payload and starts the cycle.

Poll: your loop asks something else on a schedule (“any new tickets?”) and only does real work if the answer is “yes.” Technically the trigger is still a schedule, but from the loop’s perspective the meaningful event is the new ticket, not the clock tick.

Push is stricter and cheaper (no work happens when there’s nothing to do) but requires the source to support webhooks. Poll works anywhere but wastes cycles asking “anything yet?” ninety-nine times out of a hundred.

Common event triggers you’ll actually use

  • A support ticket is created. Cycle: triage, tag, propose a draft reply.
  • A pull request opens. Cycle: run the review checklist, comment findings.
  • An email arrives at a specific address. Cycle: extract intent, file, respond or escalate.
  • A file drops in a bucket. Cycle: parse, extract fields, load into the warehouse.
  • A form is submitted. Cycle: validate, enrich, route to the right owner.
  • A payment webhook fires. Cycle: verify, update the record, notify the customer.

Notice the pattern: the event carries the payload the cycle needs. You don’t have to go fetch context; it arrives with the trigger.

Why event-driven is different from scheduled

A scheduled loop runs regardless — even at 6am when there are zero new tickets, the cycle still fires, does nothing, and logs “nothing to do.” That’s fine at daily cadence; it’s ruinous at hourly cadence and worse at faster.

An event-driven loop only fires when there’s work. Ten quiet hours = zero cycles = zero cost. A burst of 200 tickets in five minutes = 200 cycles in five minutes.

The trade: event-driven loops need to scale with the world’s rate, not your schedule. If your ticket volume goes 10× overnight, your loop suddenly has 10× the load. Rate limiting and back-pressure become real design concerns, not theoretical ones.

The de-duplication problem

Every event source you’ll use — every one — will occasionally deliver the same event twice. Webhooks retry when they don’t get a 200. Message queues have “at-least-once” delivery semantics. Poll-based sources will re-see an item if you didn’t mark it as processed.

Two ways to survive:

  • Track what you’ve seen. Every event has (or should have) a stable ID. Keep a set of processed IDs; drop duplicates. This is the same idempotency work from 2.3, now non-optional.
  • Make the action idempotent. Even if the event is processed twice, the effect should be the same. “Set the ticket’s tag to payment_failure” is idempotent; “increment the counter” is not.

Rate limits, cost caps, and back-pressure

Because you can’t predict the rate, you need floors and ceilings:

  • Per-event budget. Any single cycle should have a cap on how much it can spend on model calls before it gives up.
  • Concurrent cycle cap. If 200 events arrive at once, don’t run 200 cycles in parallel. Queue them; process N at a time.
  • Global daily / hourly cap. If cost spikes past a threshold, alert and shed load rather than just paying whatever the day brings.

None of this is glamorous. All of it is what separates an event-driven loop you can leave enabled from one you have to keep an eye on.

The failure mode: the event that stops arriving

The perverse failure mode is when the source breaks silently. Your webhook endpoint is up, your loop is fine, but no events have arrived in three days because the upstream system deprecated the webhook and didn’t tell you. From the loop’s perspective, everything is peaceful. From your perspective, work is silently not getting done.

Two defences:

  • Heartbeat check. Alert if no events have arrived in N hours (where N is well past your normal quiet period).
  • Cross-check. Sample the source directly every so often and compare against what the loop has processed. Any drift is a signal.

Which trigger for which job

Recap of Chapter 2:

  • Live (2.1) — for exploration, drafts, and anything you want to be in the chair for.
  • Run-until-done (2.2) — for bounded tasks with a clean stop condition where you want to reclaim the wait.
  • Scheduled (2.3) — for regular work whose rhythm matches a clock.
  • Event-driven (2.4) — for reactive work whose rhythm matches the world.

The wrong trigger will make the right loop feel like the wrong tool. Match trigger to rhythm.

Check your understanding

Quick check

You are designing a loop that responds to newly-created customer support tickets. Which trigger is the natural fit, and what is the SINGLE most important design decision to make before enabling it?