How to automate coworking reservations without sending anything twice
Automation in a booking system is easy to start and hard to trust. The whole problem is one question: how does a rule know it has already run?
Two kinds of trigger, and they are not interchangeable
Rules in a booking system fire for two structurally different reasons, and conflating them is the source of most automation bugs.
An event trigger fires because something just happened: a booking was created, modified or cancelled; a check-in or check-out completed; an invoice was issued; a payment arrived. There is a precise moment, it happens once, and the rule runs inside that moment.
A scheduled trigger fires because a condition has become true over time: a booking starts soon, a booking has finished, an invoice is overdue, a membership is expiring. Nothing "happened" — a job woke up, looked at the data, and found candidates.
The difference matters because scheduled triggers are evaluated repeatedly. An invoice that is overdue today is still overdue tomorrow. If the evaluator runs every five minutes, "invoice is overdue" is true 288 times a day. Without protection, that is 288 emails.
The idempotency key is the whole design
The fix is to give every potential execution a stable identity, computed from the things that make it the same execution: the rule, the trigger type, the specific entity, and the date the event belongs to. Boris hashes exactly those four into a dedupe key and records it when the action runs.
Before executing, the rule checks whether that key has already been recorded. If it has, nothing happens. Re-running the evaluator for the same rule, entity and event date does not duplicate the action — which means the job is safe to run every five minutes, safe to run twice by accident, and safe to catch up after an outage.
Note what is in the key and what is not. The entity is in it, so two different members each get their reminder. The event date is in it, so a membership expiring next month and the same membership expiring next year are separate executions. The current time is not in it, which is what makes repeated evaluation harmless.
Conditions are filters, not triggers
A useful rule is rarely "on every booking". It is "on every booking in the meeting rooms", or "on every invoice over 200 euros". Those are conditions evaluated against the candidate once the trigger has already selected it.
Keeping conditions separate from triggers is what stops the trigger list from exploding. You do not need a booking.created.in_meeting_room trigger; you need booking.created plus a condition on the resource.

Rules should drive the services, not reimplement them
The tempting shortcut is to let an automation action do the work itself: write the invoice row, poke the door API. It is the wrong move, and it shows up later as two subtly different behaviours for the same operation — one when a human does it, one when a rule does.
In Boris an automation action calls the service that already owns the operation. Issuing an invoice from a rule runs the same invoicing code as issuing it by hand. The immediate benefit is consistency; the more valuable one is that a rule's failure lands in the same execution dossier as a built-in failure, so you have one place to look rather than two.
Rules worth writing first
The rules that pay for themselves in a coworking space are unglamorous:
- Booking starts soon — the access details, sent before arrival rather than after a phone call.
- Booking finished — the cleaning task for the meeting room, created without anyone remembering.
- Invoice overdue — the reminder nobody enjoys sending, which is exactly why it does not get sent.
- Membership expiring — the renewal conversation, started while there is still time to have it.
Notice that three of the four are scheduled triggers. That is not a coincidence: the work that actually gets forgotten in a space is the work with no moment attached to it. Nobody forgets to react to a new booking. Everybody forgets the invoice that quietly went past due three weeks ago.
Before you turn a rule on
- Know whether the trigger is an event or a schedule, and expect the schedule to be evaluated constantly.
- Make sure executions are keyed by rule, entity and event date, not by timestamp.
- Put the narrowing logic in conditions, not in new trigger types.
- Have the action call the real service, so automated and manual paths cannot drift.
- Check that a failed rule surfaces somewhere you actually look.
