BookingFlagWorkflow
BookingFlagWorkflow lives in src/workflows/booking-flag.ts. One
instance per booking. Cloudflare Workflows handles the
sleep-survives-restart semantics — instances can sleep for weeks and
resume cleanly.
Spawn / re-spawn
ensureBookingFlagWorkflow(env, bookingId, opts?) is the entry point.
It accepts an optional { force?: boolean } parameter (default true):
| Caller | force | Why |
|---|---|---|
/webhook/beds24/booking (new/modify/cancel) | true (default) | A push notification means the booking actually changed — refresh state |
/admin/booking/{id}/respawn | true (default) | Manual op, user wants a fresh workflow |
DailyFlagSyncWorkflow (06:00Z cron) | false | Safety-net sweep. If the existing instance is healthy, leave it ticking |
Algorithm:
- Fetch booking from Beds24
- If status is
cancelled: delete theworkflow_instancesrow, email cleaners that the booking was cancelled (BA AA 308252 migration), return - If
force=falseAND an existing instance is healthy (queued/running/paused/waiting/complete): return{ created: false, reason: "already-tracked" }— no churn - If an existing workflow instance is tracked, call
terminate()on it - Create a new workflow instance
- Record
(workflow_name, resource_id, instance_id)inworkflow_instances
The force=false path was added May 2026 to stop daily-sync from
terminating + recreating every active booking’s workflow every morning.
The respawn-on-every-modify path stays force=true so webhook deliveries
always pick up fresh booking data.
The phase pattern
private async runSomePhase( step: WorkflowStep, bookingId: number, booking: Booking,): Promise<void> { const fireAt = computeFireAt(booking); if (fireAt.getTime() > Date.now()) { await step.sleepUntil("until-some-phase", fireAt); } await step.do("some-phase", async () => { const fresh = await readState(this.env, bookingId); if (alreadyDone(fresh)) return; await doTheWork(); await writeStateMarkingDone(); });}step.sleepUntil is what makes Workflows magical — the worker shuts
down and resumes cleanly when the sleep expires.
Phase list (in execution order)
Phases run sequentially in this order. See end-to-end timeline for fire-at times.
| # | Method | Source | What it does |
|---|---|---|---|
| 1 | runCohostNotify | cohost-notify.ts | TP front-desk notification |
| 2 | runAirbnbConfirm + runBookingComPaidFlag | channel-specific.ts | Channel-specific spawn-time work |
| 3 | runCleaningHeadsUp | booking-flag.ts | First cleaner notification |
| 4 | runPreArrivalSingle("T14") | pre-arrival-messages.ts | A-14: welcome email + yellow flag |
| 5 | runPreArrivalSingle("T3") | pre-arrival-messages.ts | A-3: nudge + pink “late” flag |
| 6 | runEarlyCheckinAvailabilityCheck | booking-flag.ts | A-2: M4c capture/decline email |
| 7 | runFlagPhase('arrival') | booking-flag.ts | A-1: arrival flag color |
| 8 | runPreArrivalSingle("T1") | pre-arrival-messages.ts | A-1: final reminder |
| 9 | runArrivalDayBillEmail | booking-flag.ts | A+0: “who’s checking in today” |
| 10 | runPreArrivalSingle("T0") | pre-arrival-messages.ts | A+0: lock-box codes / nudge |
| 11 | runStillDirtyCheck | booking-flag.ts | A+0 3pm: cleaning-incomplete alert |
| 12 | runArrivalPlusOneCheckinChase | booking-flag.ts | A+1: check-in form reminder |
| 13 | runLateCheckoutAvailabilityCheck | booking-flag.ts | D-3: M4d guest picker link |
| 14 | runCleaningT2Reminder | booking-flag.ts | D-2: cleaner ping |
| 15 | runCheckoutEvePhase | booking-flag.ts | D-1: “Checkout tomorrow” guest msg |
| 16 | runCleaningTimeCommit | booking-flag.ts | D-1: cleaner time-pick request |
| 17 | runDepartureDayBillEmail | booking-flag.ts | D+0: “who’s checking out today” |
| 18 | runFlagPhase('departure') | booking-flag.ts | D+0: departure flag color |
The four pre-arrival phases used to be wrapped in a single
runPreArrivalMessages call. May 2026 refactor unbundled them so they
could interleave with M4c, arrival-flag, and T-1 in correct temporal
order — previously M4c (A-2) and M4d (D-3) fired LATE because they
were listed after a bundled call that already slept past their targets.
Why per-booking?
Considered cron-driven sweeps. Rejected because:
- A booking made today for 5 days from now needs the T-3 message tomorrow — cron sweep would miss the window unless it ran hourly
- Workflows let each booking own its own clock — no cross-booking race
- Replay/restart is built in
The exception: account-wide sweeps (delivery summary, watchdog) still use cron because they’re not per-resource.
Shadow mode
booking-flag row in feature_flags has its own shadow_mode. When 1:
- Predictions still get written to
shadow_predictions - Each phase’s
applystep is skipped — no real sends or writes
Currently shadow_mode=0 (live).
Debugging a stuck workflow
npx wrangler workflows instances list booking-flagnpx wrangler workflows instances describe booking-flag <instance-id>
# Force a respawn from admincurl -X POST -H "authorization: Bearer $SECRET" \ https://stayonthesnow-pms.minnetonka.workers.dev/admin/booking/86704187/respawnSee /admin/delivery-status?booking=<id>
for the per-booking communication timeline.