Skip to main content

SOP: Batch Agency Migrations

Use /admin/migrations when several sub-accounts need the same source-agency to MFM migration flow. Do not start batch transfers from the Admin Locations tab. The Locations migration card is for one-off visibility and recovery. This workflow is live operations tooling. It calls CRM agency-transfer and SaaS V2 relink endpoints after an admin confirms the batch.
  • Batch board: /admin/migrations
  • Single-location tracker: /admin/locations, then the location detail card
  • Source UI: app/admin/migrations/page.tsx
  • API route: app/api/admin/migrations/batch/route.ts
  • Convex runner: convex/admin/agencyMigrationBatch.ts

What the batch board does

The workflow has three parts.
1

Pre-flight validation

Search for sub-accounts or paste location IDs in Advanced. The API validates at most 200 candidates per request. It confirms the app is installed, reads the source subscription status, resolves the source plan price, and classifies each row before anything runs.
2

Queue live work

Click Migrate selected only after reviewing the runnable rows. The request enqueues a Convex runner chain. The HTTP request does not run the migration synchronously.
3

Monitor the status board

The board polls recent agencyMigrations rows. Each row persists its step state, last error, relink pause state, trial override audit fields, and timestamps.

Candidate classes

Held rows remain visible with reasons and are excluded from selection. The enqueue route re-validates the submitted rows server-side, so a stale browser state cannot force a held row into the runner.

Plan mapping

The relink must mirror the customer’s existing tier. V2 SaaS plan amounts are whole dollars. Odd monthly prices, missing amounts, and stale price IDs hold for manual review. Manual plan confirm accepts only $1,300 or $1,500, plus an explicit trial end more than 24 hours in the future. It does not bypass the active-account gate.

Migration steps

Each migration persists these steps in order:
  1. unpause reactivates the source sub-account if needed.
  2. cancel_addon cancels the Ad Publishing add-on when one exists. If no add-on subscription is found, the step records a skip and continues.
  3. create_transfer requests the agency transfer from the source agency to MFM.
  4. accept_transfer accepts the transfer into MFM. The batch runner can run this step automatically. The single-location card also has Mark accepted for the manual CRM UI fallback.
  5. relink_v2 enables SaaS V2 on the MFM side with the resolved target plan and price.
The batch runner advances up to 3 accounts per tick and reschedules itself every 2 seconds while runnable work remains. It is scoped to the explicit migration IDs from the batch. It never scans the full migration table for work to run.

Status glossary

propagating is controlled by DEFAULT_TRANSFER_PROPAGATION_DELAY_MS in code, with an optional MFM_TRANSFER_PROPAGATION_DELAY_MS Convex env override. A 0 value makes relink immediate. Use Pause relink when the transfer is accepted but the account should not relink yet. Pausing blocks:
  • the scheduled relink job,
  • the stuck-propagation reconciler,
  • manual Relink now actions.
Use Resume relink when it is safe to continue. If the wait is already over, the reconciler or a fresh runner tick can recover the row. Use Relink now only when an admin has verified the transfer is visible in MFM and the propagation wait is the only blocker. It clears the scheduled wait and runs relink_v2 immediately. The migration does not charge canceled customers during setup-pending. When the customer later adds a payment method, Stripe trial adjustment uses one of these paths:
  • monthly source plans use the normal win-back floor,
  • annual source plans preserve the prepaid annual term when the source dates are available,
  • manual overrides use the admin-confirmed trial end,
  • missing annual anchors hold for manual review rather than charging on the 1-day floor.
The webhook and reconciler both require the migration to be accepted or relinked before applying trial changes.

Common problems

A row says “App not installed”

The batch route checks ghlInstallations before classifying the row. Reinstall or reauthorize the marketplace app for that sub-account, then validate again.

A row is held for price mismatch

Confirm the current source plan with the customer record and the CRM subscription. If it should migrate, choose one of the two approved MFM V2 plans and an explicit trial end through the manual override prompt.

A row failed during a live step

Read the row error and the step request/response preview. Fix credentials, source subscription state, or plan data first. Then retry the failed step from the single-location migration card or requeue the same location in the batch board. Failed rows do not auto-loop.

A row stays in propagating

Check whether relink is paused. If it is paused, resume only after the transfer is safe to relink. If it is not paused and the wait has elapsed, the reconciler should retry. Use Relink now only after confirming the sub-account is available under MFM.

Source of truth

When behavior is unclear, check these files first:
  • convex/admin/agencyMigrationPlan.ts for classification, plan mapping, request builders, and propagation delay.
  • convex/admin/agencyMigrationBatch.ts for enqueue, runner concurrency, manual override, and board reads.
  • convex/admin/agencyMigrations.ts for status derivation, pause/resume, retry guards, and trial matching.
  • convex/admin/agencyMigrationActions.ts for live step execution and scheduled relink behavior.
  • app/api/admin/migrations/batch/route.ts for request validation and defense-in-depth checks.
  • components/admin/AgencyMigrationBatchBoard.tsx for admin-facing labels and actions.
  • components/admin/AgencyMigrationCard.tsx for single-location retry, manual acceptance, and relink recovery actions.