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-pipelinemove_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, andmark_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_ITEMSmatches after #1814). - DND. A send is allowed unless the CRM says DND is on. Block when
dnd === true, ordndSettings[channel].statusisactiveorpermanent, or the contact has no address for that channel. Missing DND data is not a block. Same rule for email and SMS. Implemented inlib/onboardingEngineLive.ts(messageAllowed; #1807). Members only waiting for permission reportwaiting, 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 usehttps://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 OperationsaltIdandaltType=location, Versionv3. - Account-link/Identity contact, field, pipeline and opportunity paths use Version
2021-07-28; opportunity search useslocation_id/contact_id. - Calendar listing (
/calendars/?locationId=…) uses Version2021-07-28. Appointment synchronization/management and phone APIs use Versionv3. 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 useslocationId/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 version2026-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 hassource: 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=trueintercepts 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)
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.lastSeenAtvia login bootstrap →lib/operatorLastSeen.ts→internal.ghlInstallations.touchInstallationLastSeen.lastCreditPullAt— newestcreditReportRequests.createdAtper Location (by_location_createdAt,take(1)).
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 enablingONBOARDING_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 packagerun/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 tof2d5d98e7e99d209.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-readinesssupported 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.
Recovery and verification for the next agent
- Fetch current
origin/main, read this map and the runbook, and check open PR/deployed revisions. Work in an isolated current branch. - Inspect flags, Labs, source calendar config and the selected Location’s mappings without printing tokens or invoking write-capable refresh actions unintentionally.
- Resolve ambiguous linking at the source; do not overwrite another Location’s reservation, choose by display name, create a duplicate opportunity, or fake completion.
- 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. - 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.
- 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.

