Thirty Emails, or One

The day the automation worked perfectly and it was awful

A host joined Airflow, connected his mailbox and his accounting, and started forwarding things in. Reviews first, then a batch of bookings. Everything Airflow was supposed to do, it did.

In eight minutes he received roughly thirty emails.

Fifteen items went in. Each one produced an “Airflow received and queued your message”. Each one then produced a second email announcing the outcome — stored successfully, or not a booking, or needs more detail. A batch of reviews the previous day had turned seven forwarded emails into fourteen replies in four minutes. On top of that came two automated warnings about his credit balance running down.

Not one of those emails was wrong. Each was individually defensible: you told us to tell you when things happen, and things happened. Collectively they were an inbox flood on someone’s first real day, and they buried the only message that actually mattered.

Because something had gone wrong, and it was silent

While thirty emails were going out about things that succeeded, four draft invoices were failing.

They were failing for a completely fixable reason: the account’s accounting currency had never been set, and the code fell back to a default currency the connected accounting organisation wasn’t subscribed to. Every attempt was rejected. The failure was recorded internally and raised as an issue for us — and the host was never told. He had four bookings with no draft invoice and no reason to suspect it.

Worse, the retry made it expensive. A job runs every fifteen minutes and picks up every uninvoiced booking. Once his credits ran out, each denial wrote a row to his activity log. Eighty rows an hour. After seventeen hours there were over 1,400 of them, and his activity feed was a wall of the same failure repeating, because that feed had no way to filter by status.

So: thirty emails about success, zero about the failure, and a log drowning in noise. That’s not an automation problem. It’s a reporting problem, and it had exactly one sensible fix.

One principle, and everything follows from it

An action writes one ledger row. Hosts are told from the ledger, not from the action. Retries update the row. The digest reads the rows. A failure the host can fix must reach the host.

That’s the whole design, and it’s worth unpacking the one clause that does most of the work: retries update the row.

Under the old model, each of those 80 hourly denials was an independent event that could have emailed you. Under the new one, they upsert into a single record. The fifteen-minute retry storm becomes one line:

Draft invoices deferred for 11 bookings — no credits (since 18 Sept, 12:56)

Not eleven lines. Not 1,400. One line that tells you the situation, the scale and when it started.

What you actually receive

Every host-facing notification is now a row waiting to be reported rather than an email trying to leave. A digest goes out once the oldest waiting item is about ten minutes old, then at most hourly while things keep happening — or on a daily schedule at an hour you choose, if you’d rather batch it up properly.

One email, in two halves:

  • Done — what went through. Bookings stored, invoices raised, reviews received.
  • Needs attention — what didn’t, and what to do about it.

Plus where your credits stand, and one link into your activity page.

The second half is the part that didn’t exist before at all. A failed draft invoice is now something you’re told about, with the reason and a link to the fix, rather than something that happens quietly in a log you’ll never read.

Not everything waits

Some things genuinely shouldn’t sit in a queue for an hour, so a short list still goes out immediately:

  • A booking that arrived missing information you need to supply
  • A reply to an email you personally sent
  • Credits depleted, an overage cap reached, or credits topped up
  • Your accounting connection dropping
  • A failed payment
  • A booking waiting on your confirmation

Everything else digests by default — message queued, booking parsed, not a booking, review received, invoice created or updated, invoice deferred, booking modified, duplicate message.

The dividing line is not importance. It’s whether you can do something about it right now. A booking stored successfully is good news that can wait an hour. A booking that can’t be stored until you supply the guest’s name is a task, and a task an hour late is an hour of a guest waiting.

And you decide

Our split won’t suit everyone, so it isn’t fixed. Preferences now offer three settings per event type — Immediately, In the digest, or Off — across ten grouped controls covering fourteen kinds of event, plus the choice between the rolling digest and a daily one at an hour you pick.

A host running one property who likes knowing things might set bookings to immediate. A management company handling sixty properties probably wants the daily summary and nothing else. Neither is wrong, and neither should have to be the default for the other.

The three fixes that came first

Before any of the digest work, three things had to be fixed, because a beautifully-reported disaster is still a disaster.

The retry loop was stopped at the source. With no credits and overages off, the job now skips the account entirely instead of re-attempting every booking and writing a denial for each. One deferred record, updated, instead of eighty rows an hour.

The charge order was reversed. A credit was being consumed before the attempt to create the draft invoice, with no refund when the attempt failed. Four failures cost four credits and produced nothing. Now the credit is consumed after the invoice is actually created, or refunded if it isn’t. Left alone, the retry cap would have allowed 55 credits of pure failure on a plan that includes 20.

The currency guess was removed. Rather than defaulting to a currency and hoping the accounting system accepts it, Airflow now reads the base currency from your accounting organisation when you connect it — and if it still doesn’t know, it refuses and tells you, with a link to set it, instead of failing four times in silence.

That third one is the whole philosophy in miniature. The old behaviour was a guess that failed invisibly. The new behaviour is a refusal that reaches you.

Still going direct, on purpose

Two paths deliberately stayed immediate rather than moving into the digest: replies to emails you sent yourself, and the message telling you a booking arrived without enough detail. Both are conversations rather than notifications, and a conversation in a daily summary isn’t a conversation.

What we took from it

The host in this story hit all three defects on his first real batch of work — the flood, the silence and the retry loop. Nothing here was found by a test suite or an audit. It was found by someone using the product properly for an afternoon.

That’s an uncomfortable way to learn something and a very reliable one. We’ve written about the ambition behind all this in Admin That Handles Itself — and this is the correction to it. Admin that handles itself must also report on itself honestly: quietly when it worked, clearly when it didn’t, and never thirty times in eight minutes.

Related: The 20-Minute Problem on what manual handling costs, and Stop Sending Guest Emails Manually on the outbound side. Or see how Airflow works.