Cloudflare Worker · Python Runtime · fetch() handler
validate_bearer() Bearer Token Check
Authorization header extracted and compared to INTAKE_TOKEN env secret.
<1ms
→ 401 if bad
request.json() Body Parsing
JSON body decoded. Validates phone_number and message_text fields present.
<2ms
→ 400 if missing fields
normalizePhone() Phone Normalization
Strips formatting from Shortcut-provided phone. Converts +1 (725) 231-0373 → +17252310373 (E.164).
<1ms
→ E.164 string
⚠️
Phone arrives from iOS as +1 (725) 231-0373 format. Normalization must happen before this value is used anywhere — Durable Object key, SMS destination, or D1 record — or the phone is unusable as an identifier.
parseGuestMessage() Workers AI Call #1
Sends message text to Workers AI (LLM). Extracts structured fields: name, party size, gender split, VIP status. Runs fully inside the sync path — Shortcut waits.
5–15s
Blocking
generateWarmHandoff() Workers AI Call #2
Second AI call. Generates a warm reply message for Chase to send manually to the guest. Also fully blocking.
5–15s
Blocking
⚠️
iOS Shortcut timeout risk: AI calls #1 and #2 run sequentially in the sync path. Combined worst case: 30s. iOS Shortcuts timeout at ~30s. If both AI calls are slow the Shortcut dies before the response arrives — meaning Chase's phone shows an error even if the intake actually succeeded.
insertSubmission() D1 Insert
Writes initial submission record to GUEST_DB (D1). Status set to received. Returns row submission_id.
<100ms
→ submission_id
{
"action": "add_guest",
"warm_handoff": "Hey! I got Chase's list, you're on for Sat…",
"reply_text": "You're on the list for Saturday night! ✓",
"submission_id":1042,
"status": "pending_sms"
}
CONVERSATION.idFromName(phone) Durable Object
Gets (or creates) a Durable Object instance keyed on the normalized phone number. One DO per guest phone — persists conversation state across SMS exchanges.
<10ms
→ DO stub
stub.seedFromIntake() DO RPC Call
RPC into the Durable Object. DO constructor runs before the method body executes.
50–200ms
→ state seeded
Inside Durable Object constructor (runs on EVERY instantiation)
PRAGMA table_info — checks if state table exists. This is a migration check, not a read. Runs every single time the DO is instantiated.
ALTER TABLE — column existence check for any new columns. Also runs every instantiation. The executeTransaction entries you see in CF logs are these migration checks — not the actual seedFromIntake data writes.
INSERT into state table — stores parsed guest data and conversation context.
INSERT into messages table — logs initial intake message.
Set 24h alarm — schedules automatic follow-up if guest doesn't respond within 24 hours.
⚠️
When debugging from wrangler tail, the executeTransaction RPC calls in logs are the schema migration checks (PRAGMA + ALTER TABLE), which run on every DO instantiation. The actual seedFromIntake data writes are separate and come after. Don't mistake migration noise for data activity.
getMissingFields() + buildFollowUp()
Pure logic — no I/O. Inspects parsed guest data for missing required fields (e.g. party size, table preference). Builds a templated follow-up SMS string.
<1ms
→ SMS text
sendSms() SignalWire REST API
HTTP POST to SignalWire API to send follow-up SMS to the guest. Parses response JSON for message_sid. Entire background block is wrapped in try/catch — failure here is swallowed silently.
200–800ms
silent on fail
⚠️
SignalWire trial account restrictions: inbound message bodies are masked (content hidden on trial plan). Outbound SMS to unverified numbers is blocked. Guests who haven't verified their number with SignalWire trial will never receive the follow-up SMS, and this failure is silent.
updateSubmission() D1 Update
Updates the D1 record from step 6 (sync path). Sets status to pending_sms and stores message_sid from SignalWire response.
<100ms
→ D1 updated
⚠️
Silent failure zone: the entire background block (steps above) is wrapped in a single try/catch that only calls console.error(). Any failure after the response is sent — DO errors, SignalWire failures, D1 update failures — produces no visible signal to the user or to the iOS Shortcut. The only trace is in Worker logs, and wrangler tail may not capture errors from waitUntil tasks since the main request already completed.
Timing Breakdown — Sync Path (Shortcut is waiting)
TOTAL (worst case)
~30s ⚠️
iOS Shortcut limit
~30s max
Background Path (after response)
DO seed + migrations
50–200ms
💡
Optimization opportunity: Moving AI calls #1 and #2 into waitUntil() and returning an immediate 202 Accepted would eliminate the timeout risk entirely. The parsed data would arrive asynchronously; the DO could handle the handoff reply once parsing completes. The current sequential blocking design is the primary reliability bottleneck.