MDX Limo
Email Scheduling Audit

title: Email Scheduling Audit doc_type: reference status: active owner: Product Engineering last_verified: 2026-03-15 review_cycle_days: 30 ai_assisted: true source_of_truth: code

Email Scheduling Audit

This is a codebase-only structural audit of the checked-in email scheduling system as of 2026-03-15. It covers email scheduling only, not iMessage scheduling, and it does not assume anything about live Supabase, Inngest, or deployed cron state.

1. Executive summary

  • The active email scheduling path starts in the messaging gateway's routing pipeline, not in the Gmail webhook processor. The gateway builds email context, resolves route bindings, and either starts a new workflow for a CC scheduling scenario or resumes an existing suspended workflow for the thread (apps/messaging/src/router.ts:1-12, apps/messaging/src/router.ts:87-100, apps/messaging/src/pipeline/stages/resolve-route.ts:1-17, apps/messaging/src/pipeline/bindings/registry.ts:39-78).
  • The active service interface is the agents service trio of /scheduling/start, /scheduling/resume, and /scheduling/check, backed by scheduling/email.started and scheduling/email.replied events and an Inngest durable workflow called emailSchedulingFunction (apps/agents/src/mastra/routes/scheduling-workflow-route.ts:53-356, apps/agents/src/mastra/lib/inngest.ts:77-136, apps/agents/src/mastra/index.ts:278-313, apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:109-321).
  • The intended active persistence layer is scheduling_workflows, with user_scheduling_settings as the user-configurable settings store. Legacy scheduling_requests, email_actions, and approval-oriented scheduling code are still present in the repo and still read in some places (supabase/migrations/20260116064301_scheduling_workflows.sql:8-126, supabase/migrations/00000000000000_baseline.sql:258-400, apps/messaging/src/pipeline/stages/build-context.ts:153-332).
  • Outbound email is split between two mechanisms. Initial offers, booking confirmations, and executive notifications are driven by the scheduling agent plus its tools, while proactive follow-up reminders are hard-coded in the Inngest workflow helper (apps/agents/src/mastra/agents/scheduling/scheduling-agent.ts:23-249, apps/agents/src/mastra/tools/scheduling/send-email.ts:65-237, apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:665-721, apps/agents/src/mastra/workflows/inngest/email-scheduling/emails.ts:296-332).
  • The highest-confidence drift findings are:
    • The repo's active gateway binding only starts workflows for CC scenarios, while direct-to-EA routing exists in determineEmailRouting with no in-repo caller found during this audit (apps/messaging/src/pipeline/bindings/intercepts/cc-scheduling.ts:14-80, apps/agents/src/mastra/services/scheduling-workflow-handler.ts:307-467).
    • The active Inngest loop hard-codes 48h -> 4d -> 7d waits and MAX_FOLLOW_UPS = 2, even though user_scheduling_settings stores max_follow_ups and follow_up_interval_hours (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:28-32, apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:665-721, apps/web/app/api/scheduling/settings/route.ts:98-121, supabase/migrations/00000000000000_baseline.sql:382-400).
    • Checked-in scheduling_workflows DDL does not fully match runtime expectations. Runtime code reads or writes agentmail_thread_id, thread_history, last_offered_slots, follow_ups_sent, and RPCs such as mark_message_processed, but those definitions are not fully present in the checked-in migrations (apps/agents/src/mastra/lib/safe-scheduling-query.ts:17-50, apps/agents/src/mastra/workflows/inngest/email-scheduling/state.ts:74-97, apps/agents/src/mastra/services/scheduling-workflow-handler.ts:617-633, supabase/migrations/20260116064301_scheduling_workflows.sql:8-55, supabase/migrations/20260116042024_scheduling_idempotency.sql:8-20, supabase/migrations/20260116100001_drop_duplicate_scheduling_rpc.sql:1-31).
  • Test coverage is narrow. Checked-in tests directly cover resume-state handling, resume route error mapping, and onboarding prerequisite logic. No direct tests were found for /scheduling/start, /scheduling/check, the full Inngest email loop, thread merge, booking completion, or the send-email tool (apps/agents/src/mastra/__tests__/unit/services/scheduling-workflow-resume.test.ts:111-228, apps/agents/src/mastra/__tests__/unit/routes/scheduling-workflow-route.test.ts:53-160, apps/web/lib/onboarding/reconciler.test.ts:81-204).

2. System boundaries and prerequisites

Scope boundary

  • In scope: inbound email detection, workflow start and resume, persistence, follow-ups, booking, outbound email behavior, settings, and tests for email scheduling.
  • Out of scope: iMessage meeting scheduling, reminder workflows, and live infrastructure verification.

Runtime boundary

  • The active entrypoint is the messaging gateway pipeline: CapabilityResolver -> EmailDeduplicator -> ProfileEnricher -> ContextBuilder -> RouteResolver -> AgentCaller .... Scheduling intercepts happen inside RouteResolver, which replaced the older WorkflowInterceptor stage (apps/messaging/src/router.ts:6-12, apps/messaging/src/router.ts:87-100, apps/messaging/src/pipeline/stages/index.ts:9-13).
  • Gmail webhooks are explicitly triage-only. The checked-in webhook processor says scheduling workflows are handled exclusively by AgentMail webhooks for the EA inbox (apps/agents/src/mastra/services/webhook-processor.ts:366-380).
  • The agents service registers the three scheduling control routes and the Inngest email scheduling functions in the same service process (apps/agents/src/mastra/index.ts:278-313).
  • External dependencies in the active path are:
    • Supabase for workflow and settings state.
    • Inngest for durable wait/resume execution.
    • Google Calendar for free/busy lookup and event creation.
    • Gmail, optionally, to reconstruct conversation history using RFC822 headers or subject fallback.
    • The messaging gateway /messages/send endpoint for outbound email (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:171-248, apps/agents/src/mastra/tools/scheduling/get-scheduling-context.ts:132-160, apps/agents/src/mastra/tools/scheduling/book-meeting.ts:59-104, apps/agents/src/mastra/tools/scheduling/send-email.ts:175-229).

Prerequisites and gating

  • Active route start requires email context with a thread ID and a CC scenario in the gateway binding. The binding does not start workflows for generic direct-to-EA mail on its own (apps/messaging/src/pipeline/bindings/intercepts/cc-scheduling.ts:14-80).
  • startSchedulingWorkflow requires an active EA identity and blocks workflow start if user_scheduling_settings.scheduling_enabled is false or meeting_windows is empty (apps/agents/src/mastra/services/scheduling-workflow-handler.ts:482-512).
  • The broader feature gate used by onboarding requires all of: active subscription, calendar connection, and active EA identity (apps/web/lib/onboarding/reconciler-pure.ts:36-70, apps/web/lib/onboarding/reconciler.ts:37-76).
  • The Skills page explicitly fetches hasEAIdentity, hasCalendar, and the stored scheduling settings, and the scheduling page UI uses those signals to gate setup (apps/web/app/(authenticated)/skills/_data/fetch-skills-data.ts:313-400, apps/web/app/(authenticated)/skills/scheduling/_components/scheduling-content.tsx:114-176, apps/web/app/(authenticated)/skills/scheduling/_components/scheduling-content.tsx:354-360).
  • Calendar connectivity is also enforced at tool execution time. getSchedulingContext, getAvailableSlots, and bookMeeting all throw if a Google Calendar token cannot be resolved (apps/agents/src/mastra/tools/scheduling/get-scheduling-context.ts:329-342, apps/agents/src/mastra/tools/scheduling/get-available-slots.ts:176-184, apps/agents/src/mastra/tools/scheduling/book-meeting.ts:44-52).
  • Gmail connectivity is not a hard start prerequisite in the active path. The Inngest workflow uses it opportunistically for thread reconstruction and falls back to AgentMail-only history or the original message if Gmail fetch fails (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:184-248).

Component map

ComponentRole in email schedulingTriggerStorage touchedChecked-in tests
Gateway ContextBuilder + RouteResolverDerive requester/participant context and intercept CC starts or suspended-thread resumesInbound AgentMail emailReads legacy scheduling_requests during context build; no active writes hereNo direct scheduling-specific tests found (apps/messaging/src/pipeline/stages/build-context.ts:153-332, apps/messaging/src/pipeline/bindings/registry.ts:39-78)
Scheduling control routesExpose /scheduling/start, /scheduling/resume, /scheduling/check to the gatewayGateway HTTP callsReads and writes scheduling_workflows through handlers/helpersResume route only (apps/agents/src/mastra/__tests__/unit/routes/scheduling-workflow-route.test.ts:53-160)
scheduling-workflow-handlerValidate prerequisites, extract attendees, start atomically, resume idempotentlyRoute invocationuser_scheduling_settings, scheduling_workflows, RPCsResume service only (apps/agents/src/mastra/__tests__/unit/services/scheduling-workflow-resume.test.ts:111-228)
Inngest emailSchedulingFunctionDurable orchestration, thread reconstruction, wait/resume, follow-ups, analyticsscheduling/email.startedscheduling_workflows checkpoints and completion stateNo direct tests found (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:109-321)
schedulingAgent + toolsDecide what to send, fetch availability, book, and send emailsCalled from the Inngest loopReads user_scheduling_settings, Google Calendar; sends via gatewayNo direct tests found (apps/agents/src/mastra/agents/scheduling/scheduling-agent.ts:23-249)
Scheduling settings API + UIPersist scheduling preferences and surface prerequisitesWeb settings screenuser_scheduling_settingsNo direct settings-route or UI tests found; onboarding gating is tested (apps/web/app/api/scheduling/settings/route.ts:76-239, apps/web/lib/onboarding/reconciler.test.ts:81-204)

3. Active control-flow sequence for email scheduling

3.1 Active start path

  1. The messaging gateway always runs the pipeline through ContextBuilder and RouteResolver; the older WorkflowInterceptor is exported as deprecated and is not the primary active path (apps/messaging/src/router.ts:87-100, apps/messaging/src/pipeline/stages/index.ts:9-13).
  2. ContextBuilder derives requestContext.identities and requestContext.inboundEmail, including requesterEmail, requesterName, otherParticipants, and isCCScenario (apps/messaging/src/pipeline/stages/build-context.ts:214-309).
  3. The route binding registry gives priority 80 to CCScheduling. matchCCScheduling requires email, inboundEmail.isCCScenario, a requester, a thread ID, and no existing scheduling reply context; interceptCCScheduling then calls /scheduling/start via the scheduling API helper (apps/messaging/src/pipeline/bindings/registry.ts:67-78, apps/messaging/src/pipeline/bindings/intercepts/cc-scheduling.ts:14-80, apps/messaging/src/lib/scheduling-api.ts:111-183).
  4. /scheduling/start reconstructs email metadata so the handler can treat the executive as the sender and the attendees as recipients, then calls startSchedulingWorkflow (apps/agents/src/mastra/routes/scheduling-workflow-route.ts:53-158).
  5. startSchedulingWorkflow performs early validation against user_scheduling_settings, extracts all attendees from TO plus CC, falls back to FROM for direct-style metadata, then calls the atomic RPC start_scheduling_workflow_atomic and emits scheduling/email.started (apps/agents/src/mastra/services/scheduling-workflow-handler.ts:488-699).
  6. The agents service has the route and the Inngest functions registered together, so the start event is immediately consumable by emailSchedulingFunction (apps/agents/src/mastra/index.ts:278-313, apps/agents/src/mastra/workflows/inngest/index.ts:26-29).

3.2 Active resume path

  1. The highest-priority route binding is SuspendedSchedulingWorkflow. It only runs on email messages with an inbound thread ID and no existing schedulingReply marker from context build (apps/messaging/src/pipeline/bindings/registry.ts:41-52, apps/messaging/src/pipeline/bindings/intercepts/scheduling-workflow.ts:14-28).
  2. The intercept calls /scheduling/check. That route looks up a suspended workflow by either thread_id or agentmail_thread_id, intentionally without sender-email fallback, and expires stale workflows older than WORKFLOW_EXPIRATION_DAYS (apps/messaging/src/lib/scheduling-api.ts:15-55, apps/agents/src/mastra/routes/scheduling-workflow-route.ts:255-356, apps/agents/src/mastra/lib/safe-scheduling-query.ts:17-50, apps/agents/src/mastra/constants/scheduling.ts:54-58).
  3. If a suspended workflow exists, the intercept calls /scheduling/resume, passing the current message ID, thread ID, sender email, and reply body (apps/messaging/src/pipeline/bindings/intercepts/scheduling-workflow.ts:35-70, apps/messaging/src/lib/scheduling-api.ts:57-109).
  4. resumeSchedulingWorkflow verifies that the workflow exists, that its status is exactly suspended, and that it has not expired. It then calls the mark_message_processed RPC for atomic dedupe and emits scheduling/email.replied only if the message was newly marked (apps/agents/src/mastra/services/scheduling-workflow-handler.ts:714-824).

3.3 Active durable execution and completion path

  1. emailSchedulingFunction is keyed by threadId, listens for scheduling/email.started, emits analytics start, and then runs a loop that alternates between agent calls and wait/resume handling (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:109-321).
  2. Initialization loads user display data, fetches AgentMail thread messages plus RFC822 headers, optionally fetches the Gmail thread by references or subject fallback, and merges both sources into a chronological thread history (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:168-274, apps/agents/src/mastra/services/agentmail-thread-service.ts:191-260, apps/agents/src/mastra/services/gmail-thread-fetcher.ts:251-320, apps/agents/src/mastra/workflows/inngest/email-scheduling/thread-merge.ts:24-122).
  3. Each loop iteration calls the scheduling agent. If the agent completes, the workflow maps the reason to a terminal outcome and updates scheduling_workflows; if the agent suspends, the workflow checkpoints state and waits for a reply or follow-up timeout (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:328-449).
  4. The workflow persists completion outcomes and emits analytics/scheduling.completed with outcome, round count, duration, follow-ups, and group-scheduling flag (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:296-321, apps/agents/src/mastra/workflows/inngest/email-scheduling/state.ts:121-147).
  5. The start analytics event currently hard-codes triggerType: "cc", even though the service handler still contains direct-routing logic. That means analytics are currently modeled as CC-triggered only in the active Inngest flow (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:276-288).

4. State model and persistence

Active persistence contracts

  • scheduling_workflows is the intended active workflow-state table. Checked-in DDL defines workflow_run_id, status, thread_id, inbox_id, requester and executive identity fields, meeting details, negotiation_round, offered_slots, rejected_constraints, calendar_event_id, booked_slot, error_message, and timestamps (supabase/migrations/20260116064301_scheduling_workflows.sql:8-55).
  • processed_message_ids is added later for idempotent resume handling (supabase/migrations/20260116042024_scheduling_idempotency.sql:8-20).
  • user_scheduling_settings is the active settings table and stores scheduling_enabled, num_slots_to_offer, meeting_windows, default_duration_minutes, minimum_notice_minutes, buffer_between_meetings_minutes, max_follow_ups, follow_up_interval_hours, approval flags, and the optional scheduling link (supabase/migrations/00000000000000_baseline.sql:382-400).

How the active code writes state

  • Workflow creation is intended to happen atomically through start_scheduling_workflow_atomic, with the handler passing userId, workflowRunId, threadId, inboxId, requester/executive identity, original subject, an optional agentmail_thread_id, and serialized attendee emails (apps/agents/src/mastra/services/scheduling-workflow-handler.ts:597-699).
  • Runtime checkpoint sync marks a workflow suspended and writes negotiation_round, thread_history, offered_slots, last_offered_slots, rejected_constraints, follow_ups_sent, and updated_at (apps/agents/src/mastra/workflows/inngest/email-scheduling/state.ts:74-97).
  • Runtime completion writes the terminal status, booked slot, calendar event ID, optional error message, completed_at, and updated_at (apps/agents/src/mastra/workflows/inngest/email-scheduling/state.ts:121-147).
  • Resume dedupe uses the mark_message_processed RPC and the processed_message_ids concept to skip duplicate webhook deliveries for the same email message (apps/agents/src/mastra/services/scheduling-workflow-handler.ts:770-797, supabase/migrations/20260116042024_scheduling_idempotency.sql:8-20).

User settings that actively influence behavior

  • The start handler only checks two settings before allowing a workflow to begin: scheduling_enabled and non-empty meeting_windows (apps/agents/src/mastra/services/scheduling-workflow-handler.ts:488-512).
  • The agent tools use the rest of the settings model:
    • getSchedulingContext loads meeting_windows, default_duration_minutes, buffer_between_meetings_minutes, minimum_notice_minutes, num_slots_to_offer, and timezone (apps/agents/src/mastra/tools/scheduling/get-scheduling-context.ts:76-127, apps/agents/src/mastra/tools/scheduling/get-scheduling-context.ts:251-342).
    • getAvailableSlots uses meeting windows, duration, timezone, buffer, and prior offered slots for later and earlier requests (apps/agents/src/mastra/tools/scheduling/get-available-slots.ts:47-118, apps/agents/src/mastra/tools/scheduling/get-available-slots.ts:135-317).
    • availability-finder progressively relaxes timeframe, buffer, notice, and optionally duration, but never expands outside the user's meeting windows (apps/agents/src/mastra/services/availability-finder.ts:96-225).

Unresolved state-model drift

  • The checked-in scheduling_workflows migration does not define agentmail_thread_id, thread_history, last_offered_slots, follow_ups_sent, or attendee_emails, but active code reads or writes those names (apps/agents/src/mastra/lib/safe-scheduling-query.ts:17-50, apps/agents/src/mastra/routes/scheduling-workflow-route.ts:303-324, apps/agents/src/mastra/workflows/inngest/email-scheduling/state.ts:74-97, apps/agents/src/mastra/services/scheduling-workflow-handler.ts:617-633, supabase/migrations/20260116064301_scheduling_workflows.sql:8-55).
  • The repo contains a migration documenting the surviving 9-parameter start_scheduling_workflow_atomic RPC, but it does not include the checked-in RPC body. The same is true for mark_message_processed: runtime code depends on it, but its checked-in SQL definition was not found in this audit (supabase/migrations/20260116100001_drop_duplicate_scheduling_rpc.sql:1-31, apps/agents/src/mastra/services/scheduling-workflow-handler.ts:617-633, apps/agents/src/mastra/services/scheduling-workflow-handler.ts:775-781).
  • Because this audit is codebase-only, the safest conclusion is that the repo reflects an active runtime dependency on out-of-band or uncommitted database objects.

5. Email transport and threading behavior

Active email send path

  • The active tool for agent-driven scheduling mail is sendSchedulingEmail. It:
    • strips markdown,
    • normalizes duplicate Re: prefixes,
    • de-duplicates to and cc,
    • folds extra to recipients into cc,
    • resolves the inbox from explicit input or requestContext.inboundEmail,
    • passes replyToThreadId and replyToMessageId to the messaging gateway for in-thread replies (apps/agents/src/mastra/tools/scheduling/send-email.ts:15-63, apps/agents/src/mastra/tools/scheduling/send-email.ts:111-229).
  • The tool sends through the messaging gateway's /messages/send endpoint using channel: "email" and the gateway secret (apps/agents/src/mastra/tools/scheduling/send-email.ts:175-195).

Follow-up emails

  • Proactive follow-up reminders do not go through the scheduling agent. The Inngest loop calls sendFollowUpEmail, which composes a plain-text reminder and sends it directly through the same gateway send endpoint, replying in-thread and CC'ing the executive (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:683-705, apps/agents/src/mastra/workflows/inngest/email-scheduling/emails.ts:17-55, apps/agents/src/mastra/workflows/inngest/email-scheduling/emails.ts:296-332).

Booking and the two-email completion behavior

  • bookMeeting creates a Google Calendar event against the primary calendar and uses sendUpdates=all, so calendar invitations are sent by Google as part of the booking call (apps/agents/src/mastra/tools/scheduling/book-meeting.ts:14-104).
  • The active code expects two outbound emails after booking, but that behavior is instruction-driven rather than code-enforced:
    • the scheduling agent instructions require a confirmation reply to all attendees in the original thread and a new-thread notification to the executive,
    • the bookMeeting tool description repeats the same requirement,
    • the workflow itself does not call a hard-coded post-booking mail helper (apps/agents/src/mastra/agents/scheduling/scheduling-agent.ts:76-82, apps/agents/src/mastra/agents/scheduling/scheduling-agent.ts:138-165, apps/agents/src/mastra/tools/scheduling/book-meeting.ts:16-24).
  • emails.ts contains sendConfirmationEmails, sendOptionsEmail, sendCancellationAck, and sendNoAvailabilityEmail, but during this audit no in-repo caller was found for those helpers. Only sendFollowUpEmail is actively imported by the Inngest workflow (apps/agents/src/mastra/workflows/inngest/email-scheduling/emails.ts:147-355, apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:14, apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:686-704).

Subject and recipient rules

  • The agent instructions say replies should use all attendee emails in to, always CC the executive in the scheduling thread, and omit threadId and messageId for the executive notification new thread (apps/agents/src/mastra/agents/scheduling/scheduling-agent.ts:145-165).
  • The send-email tool implements recipient dedupe and subject normalization, which means the final sent mail can differ slightly from the LLM-authored draft if duplicate addresses or repeated Re: prefixes were supplied (apps/agents/src/mastra/tools/scheduling/send-email.ts:33-63, apps/agents/src/mastra/tools/scheduling/send-email.ts:151-173).

6. Context assembly and LLM injection

Gateway-side context

  • ContextBuilder still performs legacy scheduling lookups against scheduling_requests, but it also computes the active email identity block and the inbound email block that the route bindings use: sender, recipients, subject, body, thread ID, inbox ID, requester, other participants, and CC-scenario classification (apps/messaging/src/pipeline/stages/build-context.ts:153-332).
  • The generic email AgentCaller fetches AgentMail conversation history and injects it into requestContext.emailConversationHistory for the normal email agent path (apps/messaging/src/pipeline/stages/call-agent.ts:35-50).
  • In the active intercepted scheduling path, that generic AgentCaller context is bypassed because the pipeline short-circuits before agent selection and hands control to the dedicated scheduling routes (apps/messaging/src/pipeline/stages/resolve-route.ts:52-74, apps/messaging/src/pipeline/bindings/registry.ts:141-175).

Workflow-side context reconstruction

  • emailSchedulingFunction does its own thread reconstruction:
    • AgentMail thread fetch with RFC822 headers (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:184-203, apps/agents/src/mastra/services/agentmail-thread-service.ts:191-260).
    • Gmail lookup by RFC822 References first, then subject plus sender fallback (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:205-227, apps/agents/src/mastra/services/gmail-thread-fetcher.ts:251-320).
    • merge and dedupe by content hash plus chronological ordering (apps/agents/src/mastra/workflows/inngest/email-scheduling/thread-merge.ts:24-122).
  • Sender classification in the merged thread is role-based: requester, executive, or ea, with unknown senders defaulting to requester (apps/agents/src/mastra/workflows/inngest/email-scheduling/thread-merge.ts:124-159).

How context reaches the LLM

  • callSchedulingAgent creates a RequestContext, sets userId, inboundEmail.threadId, inboundEmail.messageId, inboundEmail.inboxId, and carries offeredSlots forward when prior options exist (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:485-520).
  • buildAgentContext injects:
    • current date and time in the user's timezone,
    • phase and negotiation round,
    • participants and email threading IDs,
    • last offered slots in both human and JSON forms,
    • thread history,
    • learned constraints,
    • the required JSON response contract (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:556-647).
  • The scheduling agent instructions then tell the model how to interpret the thread, which tools to call, when to book, how to avoid reusing offered slots, and what emails to send (apps/agents/src/mastra/agents/scheduling/scheduling-agent.ts:29-249).
  • The active email scheduling workflow does not rely on the generic DateTimeInjector used by consulAgent; it injects time directly inside buildAgentContext. This keeps the email scheduling flow self-contained (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:560-647, apps/agents/src/mastra/processors/datetime-injector.ts:1-67, apps/agents/src/mastra/agents/consul-agent.ts:184-185).

7. Follow-ups, cleanup, and failure handling

Active follow-up timing

  • The Inngest workflow checkpoints state and then waits:
    • first reply wait: 48h,
    • first follow-up,
    • second reply wait: 4d,
    • second follow-up,
    • final reply wait: 7d,
    • then no_response completion (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:426-449, apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:665-721).
  • The hard-coded MAX_FOLLOW_UPS = 2 in the workflow matches the default settings table value, but it is not dynamically read from user_scheduling_settings (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:28-32, supabase/migrations/00000000000000_baseline.sql:392-393).

Active cleanup and expiry behavior

  • Route-side and handler-side resume logic expire workflows after WORKFLOW_EXPIRATION_DAYS = 14 days of inactivity and mark them error (apps/agents/src/mastra/constants/scheduling.ts:54-58, apps/agents/src/mastra/routes/scheduling-workflow-route.ts:326-347, apps/agents/src/mastra/services/scheduling-workflow-handler.ts:745-768).
  • A checked-in SQL function cleanup_expired_workflows also marks suspended workflows older than 14 days as expired/error, with a comment saying it is meant to be called daily by cron (supabase/migrations/20260116100003_cleanup_expired_workflows.sql:1-70).
  • The Inngest function itself has:
    • cancelOn for scheduling/email.cancelled,
    • an inngest/function.cancelled cleanup handler,
    • an onFailure handler that marks the workflow error and emits failure analytics (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:77-103, apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:109-147).
  • No in-repo producer for scheduling/email.cancelled was found during this audit, so cancellation is wired on the consumer side but unresolved on the producer side (apps/agents/src/mastra/lib/inngest.ts:102-108, apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:115).

Failure handling shape

  • If the scheduling agent returns error, the workflow returns an error outcome without a second attempt in the loop (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:406-413).
  • If callSchedulingAgent throws, the helper catches it and converts it into a structured "action": "error" payload (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:516-549).
  • If no reply is received after all waits, the workflow explicitly marks the run complete with outcome: "no_response" and errorMessage: "No response after follow-ups" (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:436-448).

8. Settings/UI dependencies and onboarding hooks

Scheduling settings API

  • GET /api/scheduling/settings returns defaults if no row exists, including scheduling_enabled, meeting_windows, default_duration_minutes, minimum_notice_minutes, buffer_between_meetings_minutes, max_follow_ups, follow_up_interval_hours, approval controls, and scheduling_link (apps/web/app/api/scheduling/settings/route.ts:72-140).
  • PUT /api/scheduling/settings upserts the same fields and normalizes meeting-window times to HH:MM (apps/web/app/api/scheduling/settings/route.ts:142-239).

Scheduling settings UI behavior

  • The Skills page server loader fetches the settings row plus hasEAIdentity, hasCalendar, and subscriber status for the scheduling screen (apps/web/app/(authenticated)/skills/_data/fetch-skills-data.ts:313-400).
  • The scheduling screen auto-saves only:
    • scheduling_enabled,
    • default_duration_minutes,
    • minimum_notice_minutes,
    • max_follow_ups,
    • meeting_windows (apps/web/app/(authenticated)/skills/scheduling/_components/scheduling-content.tsx:125-219).
  • The same component has a TODO noting that emailProposing and emailAccepting are not persisted yet (apps/web/app/(authenticated)/skills/scheduling/_components/scheduling-content.tsx:136-142).
  • The loader and API both support more fields than the current page actually edits. During this audit, no current scheduling page writer was found for num_slots_to_offer, buffer_between_meetings_minutes, follow_up_interval_hours, require_approval_for_counter_proposals, approval_channel, include_link_in_drafts, or scheduling_link (apps/web/app/(authenticated)/skills/_data/fetch-skills-data.ts:354-389, apps/web/app/api/scheduling/settings/route.ts:162-216, apps/web/app/(authenticated)/skills/scheduling/_components/scheduling-content.tsx:187-198).

Onboarding and identity hooks

  • Effective scheduling enablement in onboarding requires desiredConfig.scheduling.enabled plus hasCalendar, hasEaIdentity, and an active subscription (apps/web/lib/onboarding/reconciler-pure.ts:36-70).
  • Applying onboarding activation upserts user_scheduling_settings with scheduling_enabled, default_duration_minutes, minimum_notice_minutes, and meeting_windows (apps/web/lib/onboarding/reconciler.ts:385-414).
  • Creating an EA identity auto-enables scheduling only for legacy non-v2 onboarding users (apps/web/app/api/ea/identities/route.ts:119-149).

9. Active vs legacy findings

Main findings

  • The active start path is CC-driven and gateway-intercepted.
    • The active binding that starts workflows is CCScheduling in the gateway (apps/messaging/src/pipeline/bindings/registry.ts:67-78, apps/messaging/src/pipeline/bindings/intercepts/cc-scheduling.ts:14-80).
    • The Gmail webhook processor explicitly excludes scheduling and only triggers triage (apps/agents/src/mastra/services/webhook-processor.ts:366-380).
  • Direct-to-EA scheduling logic exists but is not part of the active gateway path.
    • determineEmailRouting can detect direct-to-EA scheduling requests and alternate-address CC scenarios, but no in-repo caller was found for that helper during this audit (apps/agents/src/mastra/services/scheduling-workflow-handler.ts:307-467).
  • Legacy scheduling_requests code is still read and still wired, but it is no longer the main active state machine.
    • ContextBuilder still looks up scheduling_requests to populate schedulingReply and rescheduleRequest (apps/messaging/src/pipeline/stages/build-context.ts:153-332).
    • process-scheduling still queries scheduling_requests for follow-ups and expirations and attempts to call a scheduling-followup-workflow endpoint (supabase/functions/process-scheduling/index.ts:47-130).
    • scheduling-approval-handler still reads email_actions, updates scheduling_requests, and tries to start a scheduling-booking-workflow (apps/agents/src/mastra/services/scheduling-approval-handler.ts:34-124).
  • Some scheduling helpers look orphaned in the current repo snapshot.
    • sendOptionsEmail, sendConfirmationEmails, sendCancellationAck, and sendNoAvailabilityEmail are defined but not invoked by the active Inngest workflow or current gateway path (apps/agents/src/mastra/workflows/inngest/email-scheduling/emails.ts:147-355).
    • scheduling/email.cancelled is declared and consumed, but no producer was found in the repo (apps/agents/src/mastra/lib/inngest.ts:102-108, apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:115).

Required validation scenarios

  • A user CCs their EA on a scheduling email and a workflow starts: implemented in the active path. ContextBuilder marks the inbound email as a CC scenario, matchCCScheduling accepts it, interceptCCScheduling calls /scheduling/start, and the handler emits scheduling/email.started after atomic creation (apps/messaging/src/pipeline/stages/build-context.ts:214-309, apps/messaging/src/pipeline/bindings/intercepts/cc-scheduling.ts:14-80, apps/agents/src/mastra/services/scheduling-workflow-handler.ts:477-699).
  • A reply arrives on an already suspended thread and resumes the workflow exactly once: implemented, assuming the mark_message_processed RPC exists at runtime. The gateway checks /scheduling/check, resume validates status === "suspended", and duplicate message IDs are skipped after atomic marking (apps/messaging/src/pipeline/bindings/intercepts/scheduling-workflow.ts:35-70, apps/agents/src/mastra/routes/scheduling-workflow-route.ts:261-356, apps/agents/src/mastra/services/scheduling-workflow-handler.ts:714-824).
  • No one replies and the system sends follow-ups, then times out: implemented in the active Inngest loop with 48h -> follow-up -> 4d -> follow-up -> 7d -> no_response (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:665-721).
  • A slot is accepted and booking plus both outbound email behaviors are documented: partially code-enforced and partially instruction-enforced. bookMeeting always creates the calendar event and invites attendees; the two post-booking emails are required by the scheduling agent instructions and tool description, while the hard-coded sendConfirmationEmails helper is currently unused (apps/agents/src/mastra/tools/scheduling/book-meeting.ts:14-104, apps/agents/src/mastra/agents/scheduling/scheduling-agent.ts:76-82, apps/agents/src/mastra/agents/scheduling/scheduling-agent.ts:138-165, apps/agents/src/mastra/workflows/inngest/email-scheduling/emails.ts:207-289).
  • A missing prerequisite blocks scheduling before workflow start: partially implemented. The start handler blocks if scheduling is disabled or if meeting windows are missing; onboarding and UI also gate on active subscription, calendar, and EA identity; calendar-dependent tools fail if no calendar token is available (apps/agents/src/mastra/services/scheduling-workflow-handler.ts:488-512, apps/web/lib/onboarding/reconciler-pure.ts:36-70, apps/agents/src/mastra/tools/scheduling/get-scheduling-context.ts:329-342).
  • Legacy scheduling_requests behavior is either proven active, proven dead, or called out as drift: the safest codebase-only classification is drift. The active gateway/route/Inngest path uses scheduling_workflows, but build-context, process-scheduling, and scheduling-approval-handler still read or mutate scheduling_requests. No active writer for new scheduling_requests records was found in the current email scheduling entry path during this audit (apps/messaging/src/pipeline/stages/build-context.ts:153-332, supabase/functions/process-scheduling/index.ts:47-130, apps/agents/src/mastra/services/scheduling-approval-handler.ts:79-124, apps/agents/src/mastra/services/scheduling-workflow-handler.ts:597-699).

10. Test coverage and gaps

What is directly covered

  • Resume state handling:
    • emits scheduling/email.replied for valid suspended workflows,
    • rejects invalid states,
    • rejects missing workflow run IDs,
    • marks expired workflows as error,
    • preserves duplicate-message dedupe behavior (apps/agents/src/mastra/__tests__/unit/services/scheduling-workflow-resume.test.ts:111-228).
  • Resume route mapping:
    • success 200,
    • workflow_not_found -> 404,
    • workflow_invalid_state -> 409,
    • unexpected 500,
    • validation 400 (apps/agents/src/mastra/__tests__/unit/routes/scheduling-workflow-route.test.ts:58-160).
  • Feature gating logic:
    • scheduling enabled with full prerequisites,
    • disabled without subscription,
    • disabled without calendar,
    • enabled without Gmail,
    • disabled when user preference says so (apps/web/lib/onboarding/reconciler.test.ts:81-204).

What is not directly covered in checked-in tests

  • /scheduling/start route behavior and start-handler happy-path logic.
  • /scheduling/check route behavior, including dual thread lookup and expiry.
  • The Inngest email scheduling happy path, follow-up path, timeout path, cancellation path, and failure analytics.
  • The thread merge path across AgentMail and Gmail.
  • The sendSchedulingEmail tool's recipient de-duplication, subject normalization, and threading behavior.
  • Booking completion, including the expected two post-booking emails.
  • Scheduling settings API behavior and scheduling screen persistence.
  • Legacy compatibility or migration tests between scheduling_requests and scheduling_workflows.

Gap assessment

  • Highest-risk gap: the active durable workflow has no direct checked-in tests despite owning follow-up timing, timeout behavior, and final completion state.
  • Second-risk gap: the post-booking email behavior is mostly prompt-contract driven rather than explicit orchestration code, and there is no test proving the tool sequence occurs consistently.
  • Third-risk gap: the checked-in schema drift around scheduling_workflows means unit tests can pass while runtime database contracts remain unresolved.
  • General testing strategy docs say high-risk workflows should have real integration coverage, but no email-scheduling-specific real integration coverage was found in the checked-in repo (docs/testing/strategy.md:20-34).

11. Source index appendix

11.1 Source index

AreaPrimary filesNotes
Gateway routingapps/messaging/src/router.ts, apps/messaging/src/pipeline/stages/resolve-route.ts, apps/messaging/src/pipeline/bindings/registry.ts, apps/messaging/src/pipeline/bindings/intercepts/cc-scheduling.ts, apps/messaging/src/pipeline/bindings/intercepts/scheduling-workflow.tsActive start/resume entry path
Gateway context buildapps/messaging/src/pipeline/stages/build-context.ts, apps/messaging/src/pipeline/stages/call-agent.tsIdentity derivation, requester detection, legacy scheduling context reads
Scheduling control APIapps/messaging/src/lib/scheduling-api.ts, apps/agents/src/mastra/routes/scheduling-workflow-route.ts, apps/agents/src/mastra/services/scheduling-workflow-handler.tsHTTP contract between gateway and agents service
Durable workflowapps/agents/src/mastra/lib/inngest.ts, apps/agents/src/mastra/index.ts, apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts, apps/agents/src/mastra/workflows/inngest/email-scheduling/state.ts, apps/agents/src/mastra/workflows/inngest/email-scheduling/types.tsActive event contract, orchestration, and state sync
Thread reconstructionapps/agents/src/mastra/services/agentmail-thread-service.ts, apps/agents/src/mastra/services/gmail-thread-fetcher.ts, apps/agents/src/mastra/workflows/inngest/email-scheduling/thread-merge.tsContext assembly before LLM call
Scheduling agent and toolsapps/agents/src/mastra/agents/scheduling/scheduling-agent.ts, apps/agents/src/mastra/tools/scheduling/index.ts, apps/agents/src/mastra/tools/scheduling/get-scheduling-context.ts, apps/agents/src/mastra/tools/scheduling/get-available-slots.ts, apps/agents/src/mastra/tools/scheduling/book-meeting.ts, apps/agents/src/mastra/tools/scheduling/send-email.ts, apps/agents/src/mastra/services/availability-finder.tsActive reasoning and execution layer
Settings and onboardingapps/web/app/api/scheduling/settings/route.ts, apps/web/app/(authenticated)/skills/_data/fetch-skills-data.ts, apps/web/app/(authenticated)/skills/scheduling/_components/scheduling-content.tsx, apps/web/app/api/ea/identities/route.ts, apps/web/lib/onboarding/reconciler.ts, apps/web/lib/onboarding/reconciler-pure.tsFeature gating, stored settings, UI drift
Legacy scheduling surfacessupabase/functions/process-scheduling/index.ts, apps/agents/src/mastra/services/scheduling-approval-handler.ts, apps/agents/src/mastra/types/scheduling-types.ts, apps/messaging/src/pipeline/stages/intercept-workflow.tsLegacy or deprecated code still in repo
Schemasupabase/migrations/00000000000000_baseline.sql, supabase/migrations/20260116064301_scheduling_workflows.sql, supabase/migrations/20260116042024_scheduling_idempotency.sql, supabase/migrations/20260116100001_drop_duplicate_scheduling_rpc.sql, supabase/migrations/20260116100003_cleanup_expired_workflows.sqlActive and legacy table definitions plus workflow cleanup

11.2 Test matrix

LayerWhat existsEvidenceGaps
Route testsResume route onlyapps/agents/src/mastra/__tests__/unit/routes/scheduling-workflow-route.test.ts:53-160No start-route or check-route tests
Service testsResume handler onlyapps/agents/src/mastra/__tests__/unit/services/scheduling-workflow-resume.test.ts:111-228No start-handler tests or direct-thread-routing tests
Workflow testsNone found for active email scheduling loopNo checked-in direct test file found in this auditNo happy path, timeout, follow-up, cancellation, or failure coverage
Tool testsNone found for active scheduling toolsNo checked-in direct test file found in this auditNo send-email, get-context, slot-finding, or book-meeting coverage
Settings and onboarding testsOnboarding prerequisite logicapps/web/lib/onboarding/reconciler.test.ts:81-204No scheduling settings API or scheduling UI tests
Manual validation docsLaunch checklist scenarios for email CC schedulingdocs/operations/launch-checklist.md:953-958Checklist exists, but it is not automated and is currently unchecked in the doc snapshot

11.3 Active vs legacy classification table

SurfaceClassificationEvidenceAudit note
Gateway RouteResolver bindings for SuspendedSchedulingWorkflow and CCSchedulingactiveapps/messaging/src/pipeline/bindings/registry.ts:39-78Current start/resume entrypoint
/scheduling/start, /scheduling/resume, /scheduling/check routesactiveapps/agents/src/mastra/routes/scheduling-workflow-route.ts:53-356Current service contract
scheduling_workflows plus findSchedulingWorkflowactivesupabase/migrations/20260116064301_scheduling_workflows.sql:8-126, apps/agents/src/mastra/lib/safe-scheduling-query.ts:17-50Active state store, but with schema drift
Inngest emailSchedulingFunctionactiveapps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:109-321Active durable orchestrator
Scheduling agent plus getSchedulingContext, getAvailableSlots, bookMeeting, sendSchedulingEmailactiveapps/agents/src/mastra/agents/scheduling/scheduling-agent.ts:23-249Active reasoning and execution layer
build-context reads from scheduling_requestslegacy-but-still-readapps/messaging/src/pipeline/stages/build-context.ts:153-332Legacy reply/reschedule context still leaks into active gateway context build
scheduling_requests table and SchedulingRequest typeslegacy-but-still-readsupabase/migrations/00000000000000_baseline.sql:344-400, apps/agents/src/mastra/types/scheduling-types.ts:18-62Still modeled and read, not the active workflow store
supabase/functions/process-schedulinglegacy-but-still-triggeredsupabase/functions/process-scheduling/index.ts:1-180Code says cron-driven and still targets scheduling_requests
Deprecated WorkflowInterceptor stagelegacy-but-still-triggeredapps/messaging/src/pipeline/stages/index.ts:9-13, apps/messaging/src/pipeline/stages/intercept-workflow.ts:1-232Deprecated replacement path still exists in repo
determineEmailRouting helper with direct-to-EA logicorphaned referenceapps/agents/src/mastra/services/scheduling-workflow-handler.ts:307-467No in-repo caller found during this audit
scheduling-approval-handler and scheduling-booking-workflow handofforphaned referenceapps/agents/src/mastra/services/scheduling-approval-handler.ts:90-124References legacy scheduling_requests and a workflow ID not found in current registration
emails.ts helpers other than sendFollowUpEmailorphaned referenceapps/agents/src/mastra/workflows/inngest/email-scheduling/emails.ts:147-355Defined but not called in the active path
scheduling/email.cancelled producer sideorphaned referenceapps/agents/src/mastra/lib/inngest.ts:102-108, apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:115Consumer exists; producer not found in repo
Email Scheduling Audit | MDX Limo