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 byscheduling/email.startedandscheduling/email.repliedevents and an Inngest durable workflow calledemailSchedulingFunction(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, withuser_scheduling_settingsas the user-configurable settings store. Legacyscheduling_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
determineEmailRoutingwith 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 -> 7dwaits andMAX_FOLLOW_UPS = 2, even thoughuser_scheduling_settingsstoresmax_follow_upsandfollow_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_workflowsDDL does not fully match runtime expectations. Runtime code reads or writesagentmail_thread_id,thread_history,last_offered_slots,follow_ups_sent, and RPCs such asmark_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).
- The repo's active gateway binding only starts workflows for CC scenarios, while direct-to-EA routing exists in
- 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 insideRouteResolver, which replaced the olderWorkflowInterceptorstage (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/sendendpoint 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). startSchedulingWorkflowrequires an active EA identity and blocks workflow start ifuser_scheduling_settings.scheduling_enabledisfalseormeeting_windowsis 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, andbookMeetingall 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
| Component | Role in email scheduling | Trigger | Storage touched | Checked-in tests |
|---|---|---|---|---|
Gateway ContextBuilder + RouteResolver | Derive requester/participant context and intercept CC starts or suspended-thread resumes | Inbound AgentMail email | Reads legacy scheduling_requests during context build; no active writes here | No 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 routes | Expose /scheduling/start, /scheduling/resume, /scheduling/check to the gateway | Gateway HTTP calls | Reads and writes scheduling_workflows through handlers/helpers | Resume route only (apps/agents/src/mastra/__tests__/unit/routes/scheduling-workflow-route.test.ts:53-160) |
scheduling-workflow-handler | Validate prerequisites, extract attendees, start atomically, resume idempotently | Route invocation | user_scheduling_settings, scheduling_workflows, RPCs | Resume service only (apps/agents/src/mastra/__tests__/unit/services/scheduling-workflow-resume.test.ts:111-228) |
Inngest emailSchedulingFunction | Durable orchestration, thread reconstruction, wait/resume, follow-ups, analytics | scheduling/email.started | scheduling_workflows checkpoints and completion state | No direct tests found (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:109-321) |
schedulingAgent + tools | Decide what to send, fetch availability, book, and send emails | Called from the Inngest loop | Reads user_scheduling_settings, Google Calendar; sends via gateway | No direct tests found (apps/agents/src/mastra/agents/scheduling/scheduling-agent.ts:23-249) |
| Scheduling settings API + UI | Persist scheduling preferences and surface prerequisites | Web settings screen | user_scheduling_settings | No 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
- The messaging gateway always runs the pipeline through
ContextBuilderandRouteResolver; the olderWorkflowInterceptoris 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). ContextBuilderderivesrequestContext.identitiesandrequestContext.inboundEmail, includingrequesterEmail,requesterName,otherParticipants, andisCCScenario(apps/messaging/src/pipeline/stages/build-context.ts:214-309).- The route binding registry gives priority
80toCCScheduling.matchCCSchedulingrequiresemail,inboundEmail.isCCScenario, a requester, a thread ID, and no existing scheduling reply context;interceptCCSchedulingthen calls/scheduling/startvia 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). /scheduling/startreconstructs email metadata so the handler can treat the executive as the sender and the attendees as recipients, then callsstartSchedulingWorkflow(apps/agents/src/mastra/routes/scheduling-workflow-route.ts:53-158).startSchedulingWorkflowperforms early validation againstuser_scheduling_settings, extracts all attendees fromTOplusCC, falls back toFROMfor direct-style metadata, then calls the atomic RPCstart_scheduling_workflow_atomicand emitsscheduling/email.started(apps/agents/src/mastra/services/scheduling-workflow-handler.ts:488-699).- 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
- The highest-priority route binding is
SuspendedSchedulingWorkflow. It only runs on email messages with an inbound thread ID and no existingschedulingReplymarker from context build (apps/messaging/src/pipeline/bindings/registry.ts:41-52,apps/messaging/src/pipeline/bindings/intercepts/scheduling-workflow.ts:14-28). - The intercept calls
/scheduling/check. That route looks up a suspended workflow by eitherthread_idoragentmail_thread_id, intentionally without sender-email fallback, and expires stale workflows older thanWORKFLOW_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). - 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). resumeSchedulingWorkflowverifies that the workflow exists, that its status is exactlysuspended, and that it has not expired. It then calls themark_message_processedRPC for atomic dedupe and emitsscheduling/email.repliedonly 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
emailSchedulingFunctionis keyed bythreadId, listens forscheduling/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).- 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). - 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). - The workflow persists completion outcomes and emits
analytics/scheduling.completedwith 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). - 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_workflowsis the intended active workflow-state table. Checked-in DDL definesworkflow_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_idsis added later for idempotent resume handling (supabase/migrations/20260116042024_scheduling_idempotency.sql:8-20).user_scheduling_settingsis the active settings table and storesscheduling_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 passinguserId,workflowRunId,threadId,inboxId, requester/executive identity, original subject, an optionalagentmail_thread_id, and serialized attendee emails (apps/agents/src/mastra/services/scheduling-workflow-handler.ts:597-699). - Runtime checkpoint sync marks a workflow
suspendedand writesnegotiation_round,thread_history,offered_slots,last_offered_slots,rejected_constraints,follow_ups_sent, andupdated_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, andupdated_at(apps/agents/src/mastra/workflows/inngest/email-scheduling/state.ts:121-147). - Resume dedupe uses the
mark_message_processedRPC and theprocessed_message_idsconcept 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_enabledand non-emptymeeting_windows(apps/agents/src/mastra/services/scheduling-workflow-handler.ts:488-512). - The agent tools use the rest of the settings model:
getSchedulingContextloadsmeeting_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).getAvailableSlotsuses meeting windows, duration, timezone, buffer, and prior offered slots forlaterandearlierrequests (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-finderprogressively 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_workflowsmigration does not defineagentmail_thread_id,thread_history,last_offered_slots,follow_ups_sent, orattendee_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_atomicRPC, but it does not include the checked-in RPC body. The same is true formark_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
toandcc, - folds extra
torecipients intocc, - resolves the inbox from explicit input or
requestContext.inboundEmail, - passes
replyToThreadIdandreplyToMessageIdto 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/sendendpoint usingchannel: "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
bookMeetingcreates a Google Calendar event against the primary calendar and usessendUpdates=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
bookMeetingtool 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.tscontainssendConfirmationEmails,sendOptionsEmail,sendCancellationAck, andsendNoAvailabilityEmail, but during this audit no in-repo caller was found for those helpers. OnlysendFollowUpEmailis 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 omitthreadIdandmessageIdfor 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
ContextBuilderstill performs legacy scheduling lookups againstscheduling_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
AgentCallerfetches AgentMail conversation history and injects it intorequestContext.emailConversationHistoryfor the normal email agent path (apps/messaging/src/pipeline/stages/call-agent.ts:35-50). - In the active intercepted scheduling path, that generic
AgentCallercontext 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
emailSchedulingFunctiondoes 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
Referencesfirst, 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).
- AgentMail thread fetch with RFC822 headers (
- Sender classification in the merged thread is role-based:
requester,executive, orea, with unknown senders defaulting torequester(apps/agents/src/mastra/workflows/inngest/email-scheduling/thread-merge.ts:124-159).
How context reaches the LLM
callSchedulingAgentcreates aRequestContext, setsuserId,inboundEmail.threadId,inboundEmail.messageId,inboundEmail.inboxId, and carriesofferedSlotsforward when prior options exist (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:485-520).buildAgentContextinjects:- 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
DateTimeInjectorused byconsulAgent; it injects time directly insidebuildAgentContext. 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_responsecompletion (apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:426-449,apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:665-721).
- first reply wait:
- The hard-coded
MAX_FOLLOW_UPS = 2in the workflow matches the default settings table value, but it is not dynamically read fromuser_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 = 14days of inactivity and mark themerror(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_workflowsalso 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:
cancelOnforscheduling/email.cancelled,- an
inngest/function.cancelledcleanup handler, - an
onFailurehandler that marks the workflowerrorand 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.cancelledwas 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
callSchedulingAgentthrows, 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"anderrorMessage: "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/settingsreturns defaults if no row exists, includingscheduling_enabled,meeting_windows,default_duration_minutes,minimum_notice_minutes,buffer_between_meetings_minutes,max_follow_ups,follow_up_interval_hours, approval controls, andscheduling_link(apps/web/app/api/scheduling/settings/route.ts:72-140).PUT /api/scheduling/settingsupserts the same fields and normalizes meeting-window times toHH: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
emailProposingandemailAcceptingare 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, orscheduling_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.enabledplushasCalendar,hasEaIdentity, and an active subscription (apps/web/lib/onboarding/reconciler-pure.ts:36-70). - Applying onboarding activation upserts
user_scheduling_settingswithscheduling_enabled,default_duration_minutes,minimum_notice_minutes, andmeeting_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
CCSchedulingin 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).
- The active binding that starts workflows is
- Direct-to-EA scheduling logic exists but is not part of the active gateway path.
determineEmailRoutingcan 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_requestscode is still read and still wired, but it is no longer the main active state machine.ContextBuilderstill looks upscheduling_requeststo populateschedulingReplyandrescheduleRequest(apps/messaging/src/pipeline/stages/build-context.ts:153-332).process-schedulingstill queriesscheduling_requestsfor follow-ups and expirations and attempts to call ascheduling-followup-workflowendpoint (supabase/functions/process-scheduling/index.ts:47-130).scheduling-approval-handlerstill readsemail_actions, updatesscheduling_requests, and tries to start ascheduling-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, andsendNoAvailabilityEmailare 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.cancelledis 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.
ContextBuildermarks the inbound email as a CC scenario,matchCCSchedulingaccepts it,interceptCCSchedulingcalls/scheduling/start, and the handler emitsscheduling/email.startedafter 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_processedRPC exists at runtime. The gateway checks/scheduling/check, resume validatesstatus === "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.
bookMeetingalways 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-codedsendConfirmationEmailshelper 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_requestsbehavior is either proven active, proven dead, or called out as drift: the safest codebase-only classification is drift. The active gateway/route/Inngest path usesscheduling_workflows, butbuild-context,process-scheduling, andscheduling-approval-handlerstill read or mutatescheduling_requests. No active writer for newscheduling_requestsrecords 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.repliedfor 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).
- emits
- 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).
- success
- 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/startroute behavior and start-handler happy-path logic./scheduling/checkroute 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
sendSchedulingEmailtool'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_requestsandscheduling_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_workflowsmeans 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
| Area | Primary files | Notes |
|---|---|---|
| Gateway routing | apps/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.ts | Active start/resume entry path |
| Gateway context build | apps/messaging/src/pipeline/stages/build-context.ts, apps/messaging/src/pipeline/stages/call-agent.ts | Identity derivation, requester detection, legacy scheduling context reads |
| Scheduling control API | apps/messaging/src/lib/scheduling-api.ts, apps/agents/src/mastra/routes/scheduling-workflow-route.ts, apps/agents/src/mastra/services/scheduling-workflow-handler.ts | HTTP contract between gateway and agents service |
| Durable workflow | apps/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.ts | Active event contract, orchestration, and state sync |
| Thread reconstruction | apps/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.ts | Context assembly before LLM call |
| Scheduling agent and tools | apps/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.ts | Active reasoning and execution layer |
| Settings and onboarding | apps/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.ts | Feature gating, stored settings, UI drift |
| Legacy scheduling surfaces | supabase/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.ts | Legacy or deprecated code still in repo |
| Schema | supabase/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.sql | Active and legacy table definitions plus workflow cleanup |
11.2 Test matrix
| Layer | What exists | Evidence | Gaps |
|---|---|---|---|
| Route tests | Resume route only | apps/agents/src/mastra/__tests__/unit/routes/scheduling-workflow-route.test.ts:53-160 | No start-route or check-route tests |
| Service tests | Resume handler only | apps/agents/src/mastra/__tests__/unit/services/scheduling-workflow-resume.test.ts:111-228 | No start-handler tests or direct-thread-routing tests |
| Workflow tests | None found for active email scheduling loop | No checked-in direct test file found in this audit | No happy path, timeout, follow-up, cancellation, or failure coverage |
| Tool tests | None found for active scheduling tools | No checked-in direct test file found in this audit | No send-email, get-context, slot-finding, or book-meeting coverage |
| Settings and onboarding tests | Onboarding prerequisite logic | apps/web/lib/onboarding/reconciler.test.ts:81-204 | No scheduling settings API or scheduling UI tests |
| Manual validation docs | Launch checklist scenarios for email CC scheduling | docs/operations/launch-checklist.md:953-958 | Checklist exists, but it is not automated and is currently unchecked in the doc snapshot |
11.3 Active vs legacy classification table
| Surface | Classification | Evidence | Audit note |
|---|---|---|---|
Gateway RouteResolver bindings for SuspendedSchedulingWorkflow and CCScheduling | active | apps/messaging/src/pipeline/bindings/registry.ts:39-78 | Current start/resume entrypoint |
/scheduling/start, /scheduling/resume, /scheduling/check routes | active | apps/agents/src/mastra/routes/scheduling-workflow-route.ts:53-356 | Current service contract |
scheduling_workflows plus findSchedulingWorkflow | active | supabase/migrations/20260116064301_scheduling_workflows.sql:8-126, apps/agents/src/mastra/lib/safe-scheduling-query.ts:17-50 | Active state store, but with schema drift |
Inngest emailSchedulingFunction | active | apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:109-321 | Active durable orchestrator |
Scheduling agent plus getSchedulingContext, getAvailableSlots, bookMeeting, sendSchedulingEmail | active | apps/agents/src/mastra/agents/scheduling/scheduling-agent.ts:23-249 | Active reasoning and execution layer |
build-context reads from scheduling_requests | legacy-but-still-read | apps/messaging/src/pipeline/stages/build-context.ts:153-332 | Legacy reply/reschedule context still leaks into active gateway context build |
scheduling_requests table and SchedulingRequest types | legacy-but-still-read | supabase/migrations/00000000000000_baseline.sql:344-400, apps/agents/src/mastra/types/scheduling-types.ts:18-62 | Still modeled and read, not the active workflow store |
supabase/functions/process-scheduling | legacy-but-still-triggered | supabase/functions/process-scheduling/index.ts:1-180 | Code says cron-driven and still targets scheduling_requests |
Deprecated WorkflowInterceptor stage | legacy-but-still-triggered | apps/messaging/src/pipeline/stages/index.ts:9-13, apps/messaging/src/pipeline/stages/intercept-workflow.ts:1-232 | Deprecated replacement path still exists in repo |
determineEmailRouting helper with direct-to-EA logic | orphaned reference | apps/agents/src/mastra/services/scheduling-workflow-handler.ts:307-467 | No in-repo caller found during this audit |
scheduling-approval-handler and scheduling-booking-workflow handoff | orphaned reference | apps/agents/src/mastra/services/scheduling-approval-handler.ts:90-124 | References legacy scheduling_requests and a workflow ID not found in current registration |
emails.ts helpers other than sendFollowUpEmail | orphaned reference | apps/agents/src/mastra/workflows/inngest/email-scheduling/emails.ts:147-355 | Defined but not called in the active path |
scheduling/email.cancelled producer side | orphaned reference | apps/agents/src/mastra/lib/inngest.ts:102-108, apps/agents/src/mastra/workflows/inngest/email-scheduling/function.ts:115 | Consumer exists; producer not found in repo |