Skip to main content

Onboarding integrations and agent handoff

Start here for onboarding work, after reading VISION.md, AGENTS.md, and CODEBASE.md. This is the current integration map. For pilot enablement, rollout order, and recovery steps see the onboarding runbook. Engine cutover lives in services/onboarding-engine/docs/RUNTIME.md — do not duplicate it here. Open code-review findings and the recommended fix order are tracked in the October 2 code review (which supersedes the September 29 review); check it before changing onboarding code. Last audited: September 29, 2026. Source baseline: origin/main at d61ceac8. Production evidence below came from read-only Convex configuration/table queries and provider GETs. No settings, customer records, sessions, appointments, or messages were changed. Recheck deployment and configuration before a rollout; a merged feature, an enabled flag, and a completed customer journey are different evidence.

Decided boundaries (2026-09-29)

  • Cloud sender only. The Convex cloud runner in this repo is the only communications sender. The Mac mini onboarding-engine job retires (its Slack support gateway stays). The cutover procedure is RUNTIME.md (cloud cutover runbook; do not duplicate it here).
  • Stage ownership. reconcilePage (convex/crons.ts → convex/onboardingAdmin.ts) is the only automated mover of onboarding stages. The live runner never executes an onboarding-pipeline move_stage, even when a cohort allows that kind (lib/onboardingEngineLive.ts, merged in #1807). The engine owns messages, tasks, field stamps, retention-card creation, retention-pipeline moves, and mark_won. Native CRM workflows must stop moving onboarding stages. A card moved by hand is never moved back automatically. After #1814, W5 still sends E2 while the card is in Compliance or Setup In Progress, and W8 still creates the Final QA task while it is in Setup In Progress or Final QA — so those handoffs survive when progression moves the card first.
  • Compliance checklist (progression + engine). Shared items are Payment Verified, Slack Joined, and Skool Joined. Slack Channel Created is retired from the shared checklist (staff UI already omits it; engine COMPLIANCE_CHECKLIST_ITEMS matches after #1814).
  • DND. A send is allowed unless the CRM says DND is on. Block when dnd === true, or dndSettings[channel].status is active or permanent, or the contact has no address for that channel. Missing DND data is not a block. Same rule for email and SMS. Implemented in lib/onboardingEngineLive.ts (messageAllowed; #1807). Members only waiting for permission report waiting, not a failed run.
  • Custom values (after #1815). The live runner reads location custom values from the CRM on every pass and fails closed on a bad read; the imported snapshot’s copy of those values is never used for sends. CRM IDs still come from the configured snapshot.
  • FM1 / FM2 calendars. Ops assigns FM2 a separate FM Follow-up calendar in Admin → Settings → Onboarding. Never reuse one appointment for both slots.
  • Help-center links. Operator-page help-center URLs are kept by decision.

Current state and next work

This document is not merge or production-write authorization. The earlier audit below described manual kickoff channel creation. The replacement uses a dedicated support workspace, automatic private channels and free members; it is not live until its code and workspace configuration are approved. Missing Slack in the earlier audit was expected before then; do not create a channel or start identity verification on the customer’s behalf. Agreement and card ownership review remain manual compliance duties. The optional staff introductions section is deferred.

Locations, environments, and entry points

These IDs are routing identifiers, not credentials. Never substitute the admin entry Location for the Operations source or the customer’s product Location. The four enabled Labs Locations (not an automatic-progression or communications allowlist): Pilot Operator B’s intended Operations contact is KvUhx6Luui3Vw3POkUuY and Onboarding opportunity is C2gg7sYKYkpgym20UrZA. An older opportunity in another pipeline is not the onboarding destination. Revalidate these bindings before any repair; never choose a similarly named record or infer identity solely from email/name.

Data ownership and API map

The relationship is product Location → agency SaaS subscription → Operations contact/payment subscription → Stripe subscription/customer → existing Onboarding opportunity → Location-scoped setup and Identity record. Each edge is checked; an ambiguous or mismatched edge blocks linking. CRM supplies business facts, Convex stores scoped state/audit/recovery, and Stripe supplies identity verification evidence. Tokens come from ghlInstallations keyed by Location; agency installs use agency:{companyId}. Same-agency checks do not remove Location isolation. Provider refresh helpers can persist rotated tokens, so invoking a read-like action is not necessarily a read-only audit. Use known queries or authenticated provider GETs with valid existing credentials when investigating production without write approval.

Provider routes and version contracts

CRM APIs use https://services.leadconnectorhq.com. Do not normalize all clients to one version or one pagination/search shape:
  • SaaS lookup: /saas-api/public-api/get-saas-subscription/{locationId}?companyId=… using agency credentials. CRM payment subscription: /payments/subscriptions/{id} with Operations altId and altType=location, Version v3.
  • Account-link/Identity contact, field, pipeline and opportunity paths use Version 2021-07-28; opportunity search uses location_id / contact_id.
  • Calendar listing (/calendars/?locationId=…) uses Version 2021-07-28. Appointment synchronization/management and phone APIs use Version v3. Appointment routes include /calendars/events/appointments/{id}, /calendars/events, and /calendars/{id}/free-slots; phone routes start /phone-system/numbers/location/{id}.
  • Slack’s Operations opportunity lookup and general admin CRM writes use Version v3; Slack search uses locationId / contactId. Keep each reader’s completeness checks; partial/paginated results cannot establish a unique destination.
  • Business settings and form discovery use Version 2021-07-28.
  • Stripe uses https://api.stripe.com/v1, server Bearer authentication and version 2026-06-24.dahlia. Source modules remain authoritative for request bodies and retries.

Linking, payments, and KYC

Automatic linking requires its flag, Location Labs, same agency, exactly one eligible payment subscription and a unique open unassigned Onboarding → New - Paid opportunity. Existing valid mappings survive later stages. It fills Product Location ID, reads it back, then prepares contact/Identity state. It never creates an opportunity or moves a stage. Reservations protect concurrent/uncertain writes; refresh is cached for one minute and an in-flight attempt has a five-minute lease. A saved card on an eligible trial can prepare KYC. That is not proof of a collected payment. Missing cardholder names are allowed; compliance confirms card ownership and the agreement manually. Automatic evidence has source: saas_subscription and never claims a human attestation. The displayed KYC name is the Operations contact name. There is no new onboarding payment-success/failure writer in this integration. CRM payment workflows and the manual Payment Verified requirement remain distinct from subscription evidence and KYC. Other app domains have Stripe payment webhooks; do not repurpose them or mark Payment Verified from active/trialing alone. Identity needs matching account, mode, approved flow, session generation, successful document/selfie report, and normalized contact name. Completion additionally requires successful CRM KYC Verified readback. The return page is navigation only. Standalone one-off verification links created by compliance are not automatically app-bound. Automatic linking does not import an existing CRM Verified value as a new KYC pass. Stripe’s verified first name and full surname must match the Operations contact and saved identity evidence after case, Unicode and whitespace normalization. Middle names may differ or be absent. Staff can explicitly override a name mismatch after reviewing the current names and supplying a reason. reviewNameMismatch requests the comparison; callers that omit it retain the existing mismatch refusal. A SHA-256 review key binds confirmation to the Location, contact, CRM target, identity generation, saved evidence, Stripe session/report and names; it is a stale-review guard, not an access credential. Every request still validates staff access and provider evidence. Missing names cannot be overridden. Automatic matching is unchanged. identity_name_override_confirmed persists reviewer/time/session/report/reason before the CRM write; identity_linked_name_override records completed linking. Only staff see linked.nameOverrideReason. Reopening removes the current link but retains the audit. Verified names are returned for staff review, never persisted. Staff can explicitly accept an external verified document session with no selfie report and no selfie requirement under earlier requirements. Confirmation is bound to the resolved session ID. The linked record stores legacyDocumentOnly: true, the reviewer and timestamp; identity_linked_legacy_document_only records the acceptance in the audit log. Failed checks and app-owned flows cannot use this exception. The live webhook is enabled at https://brainy-crocodile-746.convex.site/onboarding/identity-webhook, destination we_1UKip1JIA23cYEIpI5PcTsT0, for identity.verification_session. events: created, processing, requires_input, verified, canceled, redacted. It validates raw-body signatures, ignores unrelated sessions and retrieves current Stripe state. Configuration/read access does not prove delivery or CRM completion. See staff account linking.

Identity recovery (still true)

  • Session IDs and progress are stored per Location. Hosted session URLs and Stripe client secrets are never persisted or logged. Document images and selfies are never downloaded.
  • A crashed action’s lock expires after 35 minutes. Writes recheck generation, lock, Labs, agency, and contact mapping. Initial creation uses a stable idempotency key.
  • After 23 hours an unresolved creation is held for staff to recover the existing vs_ session; never start a replacement because a response was lost.
  • Reopen records the staff request, clears local completion, cancels an unfinished Stripe session, writes Incomplete with readback, and replaces the generation. Old-session webhooks cannot complete the new attempt.
  • Synthetic preview: ONBOARDING_PREVIEW_IDENTITY=true intercepts providers (no real Stripe sessions or CRM writes). CRM shape fixture: tests/fixtures/onboardingIdentityCrm.json.

Calendars and appointment semantics

Calendar configuration belongs to the Operations source Location, not each customer’s calendar inventory. Intended IDs below were inspected September 22 and were not saved in production as of the September 29 audit; recheck availability and obtain approval for the exact configuration write before saving. Setup reads contact appointments and bounded calendar windows, retains pinned IDs, and fetches exact remembered appointments to detect moves/cancellations. One event cannot satisfy both FM slots. Broad discovery is bounded and rejects saturated or incomplete results. Review lib/onboardingCalendar.ts before changing matching rules. Display uses the customer Location’s business timezone, explicitly falling back to UTC. CRM milestone date serialization is UTC. Elapsed time never proves attendance: an ended booking awaits team confirmation; Showed establishes completion. Kickoff updates Date Compliance Call Completed, FM 2 updates Date Onboarding Call Completed; FM 1 does not complete FM 2. FM 2 “Not needed” is a dated staff waiver, not attendance and not an appointment cancellation. Before enabling live edits, validate provider scopes, host/duration preservation, availability, notification behavior, and readback on approved test appointments. Synthetic previews do not establish any of those live properties.

Website, phone, Slack, and staff ownership

  • Website and timezone come from the product Location’s business settings. There is no Operator website-entry step. Intake form discovery uses exactly 1. MAIN Apply For Funding Form; do not substitute the processing application. Staff’s application-funnel verification is still a separate outcome.
  • Phone-provider identity is separate from Stripe KYC. Inventory errors are not evidence that an existing number disappeared. Purchasing is billable and needs the exact approved target; never purchase to validate documentation.
  • An account admin can mark phone not needed (Complete · Phone not needed); that does not cancel a number or change billing, and simply deferring a number leaves the step incomplete.
  • Slack workspace setup uses the exact Operations contact and Product Location ID, a private non-Connect channel and the approved email-matched Slack user. The returned channel ID is saved to the exact opportunity field and read back before the local link is published. The provisioning ledger records creation and access attempts before external writes.
  • The dedicated signed receiver is POST /slack/onboarding-events; it validates the app and workspace IDs. Membership events re-read actual access. Missed events and interrupted attempts are checked by a bounded recovery job. A removed member or uncertain invitation needs staff approval before another invitation.
  • Admin client selection, expansion and task disclosures read saved setup state only. Explicit connection refresh retains provisioning behavior; it is not triggered by navigation.
  • Configure the dedicated app and gates per Slack workspace setup. These changes are implementation requirements, not evidence of live activation. Internal staff Slack keeps its existing app and credentials.
  • Staff own checks, blockers, ownership, holds, notes, attendance and final QA in Admin → Onboarding. The Operator sees the shared readiness and next action. “Ready for QA” does not automatically move a client Live.

Operator Pulse (retention signals)

Response 200: { "locationId": string, "lastLoginAt": number|null, "lastCreditPullAt": number|null } (401 when the token is missing/wrong or unset — fail closed; 400 on missing locationId.)
  • lastLoginAt — ghlInstallations.lastSeenAt via login bootstrap → lib/operatorLastSeen.ts → internal.ghlInstallations.touchInstallationLastSeen.
  • lastCreditPullAt — newest creditReportRequests.createdAt per Location (by_location_createdAt, take(1)).
Implementation: convex/agentPulse.ts + convex/http.ts. The marketplace MCP get_last_login tool reads the same data through internal.agentPulse.getAgencyOperatorPulse. These timestamps do not populate the admin queue’s verified Last Active column by themselves. Opportunity fields the engine may stamp from pulse/mapping (Operations Location):

Configuration inventory (no secret values)

Convex owns the server integration configuration below. Store credentials in deployment secret settings, never source, screenshots, logs, public docs, or browser variables.

New customer enrollment activation

Before enabling ONBOARDING_NEW_LOCATION_ENROLLMENT_ENABLED, refresh the full agency inventory and review customer Locations created at or after 2026-10-05T06:00:00.000Z. For approved catch-up targets installed before activation, invoke the internal action onboardingEnrollment:afterProvisioning with { "locationId": "<verified Location ID>" }. It takes a Location ID directly; no provisioningJobs row is needed. Use the same action to retry enrollment after fixing provider evidence. Never use installation time as the cutoff or bulk-set the agency Labs default. The new enrollment flag must be enabled; the action still checks current ownership, CRM creation date, subscription binding and manual opt-outs. Production configuration and each catch-up target require approval. Successful enrollment schedules account linking atomically with a fresh retry window; its independent flag and billing/identity guards still apply.

Communications

The October 2 local payment-welcome implementation and its uncompleted production cutover are documented in Payment welcome cutover. It preserves the existing Operations survey and scheduling funnel. This is not evidence that the production settings recorded below have changed. The cloud shadow runner computes proposals without external writes. Cloud live has a separate cohort and durable execution ledger. Both ten-minute cron entries exist but neither is active in production. A Labs pilot does not authorize messages. Cutover (cohort review, final ledger/snapshot import for historical-receipt dedupe, single-writer token revoke, native stage-move retirement, enablement, and rollback) lives only in RUNTIME.md. Never run two writers. Do not treat the retired Mac mini job or a local package run/bootstrap as the current sender. Zoom supplies the onboarding-call summary; E3 and its send-recap owner task are retired. E5 Monday and day-three training digests are removed for now; legacy template settings do not trigger sends. E5a remains the personal week-one check-in. W10’s day-14 action is a human Slack-touch task anchored to retention-card creation. The new workspace flow has no Slack Pro upgrade reminder (see the runbook).

Temporary preview infrastructure

The following preview resources were prepared earlier, not freshly inspected in this audit. Inventory exact current owners/dependencies before any cleanup:
  • onboarding-preview.myfundingmachine.com: DNS-only CNAME to f2d5d98e7e99d209.vercel-dns-016.com; assigned to a sandbox Vercel Preview with login protection retained. Do not assume the original deployment ID is still current.
  • Restored branch codex/onboarding-shared-readiness supported branch-only development secret configuration. That historical preview branch is not the starting point for new work.
  • Admin-only FM Onboarding — Sandbox Test menu in internal/test Location oE9ILHco0XXZUu79wAA0, plus preview/development backend and sandbox Stripe settings.
These resources are not the live Identity webhook. Keep them until the sandbox acceptance work is finished or explicitly retired. Cleanup needs exact-target approval; preserve the live app domain, Zoho wildcard, production secrets and unrelated services.

Recovery and verification for the next agent

  1. Fetch current origin/main, read this map and the runbook, and check open PR/deployed revisions. Work in an isolated current branch.
  2. Inspect flags, Labs, source calendar config and the selected Location’s mappings without printing tokens or invoking write-capable refresh actions unintentionally.
  3. Resolve ambiguous linking at the source; do not overwrite another Location’s reservation, choose by display name, create a duplicate opportunity, or fake completion.
  4. For uncertain KYC creation, recover the existing vs_ session rather than creating a one-off link. Respect lock/retry guidance; reopening must clear CRM completion and protect against old-session webhooks. For uncertain admin/appointment writes, read back the exact destination before retrying. Preserve pending recovery records.
  5. Complete approved acceptance separately (see runbook): calendar mappings and real event matching; customer KYC → signed event → CRM readback; post-kickoff Slack; authorized staff writes. Record dated outcomes, not merely green feature flags.
  6. For source changes run the affected tests; CI runs the full suite. Never deploy Convex production or perform live provider writes just to run tests.
Update this document’s state table and evidence date when configuration changes. Keep the staff connection guide, staff workspace guide, and Operator guide aligned with actual behavior.

Engine audience rollout

The engine audience is stored per customer and processed in ten-member pages. Deployment preserves existing pilot state until staff reconfigure it. Automatic new-customer admission requires a separate reviewed enrollment policy and verified original CRM creation evidence. Pre-message-cutover customers join in compute mode. See Engine rollout for configuration, recovery, review, pause/resume and the production cutover checklist.