Skip to main content

New Lead marketplace workflow action (FM-2606)

Internal runbook. Operator-facing steps live in docs/operators/getting-started/new-lead-workflow-action.mdx.

What it is

The marketplace app’s existing draft workflow action create_fm_applicant (“Create Funding Machine Applicant”, templateId 01KM6NZ3ERBWMSCMT231E6VTWG) is repointed so its execution URL is POST /api/marketplace/new-lead. That draft pre-dated FM-2606 with a custom payload mirroring the webhook body and no execution URL; it was never published. Reusing it avoids a second near-duplicate action. It is the drop-in replacement for the workflow “Custom Webhook” step that posts to POST /api/webhooks/new-lead. Both routes are thin adapters over the ONE intake + applicant-gate + auto-credit-pull implementation in lib/webhooks/new-lead-intake.ts (processNewLeadIntake). They differ only in transport: Execution payload the CRM sends:
Response (same shape as the webhook route): { "success": true, "leadEventId": "...", "duplicate": false }. leadEventId and duplicate are exposed to later workflow steps as custom variables.

Why dual execution cannot double pull or double charge

Idempotency is not in either adapter; it is in the shared implementation and the Convex mutations it calls, so it holds regardless of which ingress fires:
  • leadEvents.logLeadEvent collapses a second delivery inside its dedupe window, or while an applicant-opportunity claim is unresolved, into the same lead event.
  • leadEvents.claimLeadEventOpportunity lets exactly one caller create the applicant opportunity for a lead event.
  • autoCreditPullEvents.reserveTrigger lets exactly one caller reserve the auto pull per (locationId, contactId), and any prior decision short-circuits before the wallet or bureau is touched. Outside the lead-event window a second ingress still gets its own lead event and application (one application per submission is the existing policy; the CRM gives the action no submission id to key on, and inventing one would change the webhook’s semantics too) but it cannot pull. Operator docs therefore say: back to back is fine, never separate the two steps with a Wait.
  • /api/credit-report/new keeps its own wallet/charge checks unchanged.
tests/newLeadApplicantPerSubmission.test.ts (“FM-2606” block) drives the webhook route and the action route against the same in-memory Convex state and asserts one application and one bureau POST in both the inside-window and outside-window cases.

Where it is in the developer portal

Workflow actions are app-scoped and are not in the app version dropdown (v7.0.1 / v6.0.0). In the developer portal open App Settings for the app, then modules/workflows → Actions. Direct link to the draft config (Brock, 2026-09-17): https://marketplace.gohighlevel.com/app-settings/697a568ce94ccaee772a84b0/version/6a74dfe1a56f7909daa13f72/modules/workflows/actions/69bdcaa344f5e92d32354e65/config?version=1.0&tempId=01KM6NZ3ERBWMSCMT231E6VTWG The action is “Create Funding Machine Applicant” (id 01KM6NZ3ERBWMSCMT231E6VTWG). The portal’s “Test” button and ghl app actions test both execute the real endpoint; do not use them against production until GHL_NEW_LEAD_ACTION_TOKEN is set and a test sub-account and contact have been agreed, because a successful run writes a lead event and can trigger a billable pull.

Environment

  • GHL_NEW_LEAD_ACTION_TOKEN: static bearer the action sends. Generated with openssl rand -hex 32. Set on Vercel (project ghl-marketplace-app-fundingmachine, team My Figures) on Production, Preview, and Development as a Sensitive variable on 2026-09-17 with Brock’s approval; the portal draft header carries the same value. Existing deployments do not see a new variable until they redeploy; merging this PR deploys production and picks it up.
  • The proxy exempts the exact path /api/marketplace/new-lead (see lib/proxy/isExemptPath.ts); the route verifies the bearer itself.

Marketplace configuration with the official CLI

Use the official @gohighlevel/marketplace-cli in isolation (not ~/.local/bin/ghl). Run everything from a scratch directory outside the repo and pin the version.
Rules while doing this:
  • Only the ghl app actions ... sub-commands. Never ghl app push: it owns the app profile, OAuth, and the shared marketplace webhook (src/webhooks/ghl-webhooks.json), none of which this task touches.
  • ghl app actions publish is HELD. Publishing makes the action visible to every installed sub-account and needs its own approval. Publish only after this PR has merged and deployed, otherwise executions hit a production proxy that has no route.
  • If workflows.readonly is missing from the app’s scopes, stop. Adding a scope is Sensitive Scope (auth/scopes) and forces a major version bump on the next app publish; it needs separate approval.
  • The header value ${env:GHL_NEW_LEAD_ACTION_AUTHORIZATION} is resolved at push time from the shell and never written to disk or to git. After a pull the portal value is exported as ${remote}.

Status from the 2026-09-17 run

  • Logged in as the Funding Machine developer account (isolated GHL_CONFIG_DIR).
  • App identified: “My Funding Machine”, appId 697a568ce94ccaee772a84b0, live version 7.0.1 (versionId 6a74dfe1a56f7909daa13f72). workflows.readonly is already in oauth.allowedScopes; no scope change needed.
  • Existing remote actions before the change: create_fm_applicant (draft 1.0, custom payload mirroring the webhook body, no execution URL, created 2026-03-20) and fm_admin_processing_form_submitted (draft 1.0, untouched).
  • ghl app actions push applied exactly one operation, update:create_fm_applicant@1.0, and the post-push ghl app actions diff is empty. The action is still a DRAFT (isActive: false); nothing is visible to sub-accounts until publish, which is held.
  • The previous draft definition is preserved at /Users/fm-mini/tmp/cos-launch/fm-2606-ghl-cli/create-fm-applicant.PREVIOUS-DRAFT.json.
  • The bearer was generated with openssl rand -hex 32 into the 0600 file /Users/fm-mini/tmp/cos-launch/fm-2606-ghl-cli/secrets/GHL_NEW_LEAD_ACTION_TOKEN and injected at push time, then set as GHL_NEW_LEAD_ACTION_TOKEN on Vercel (see Environment above). It was never printed or committed.
  • ghl app pull for the full app stops at billing validation because the portal’s existing usage meters fail the CLI’s local rules (pricingPageUrl, minPricePerUnit). App, webhook, and action files still pull. Not this task’s problem; do not “fix” the meters from here.
  • Not run: ghl app push (app profile / shared webhook untouched), ghl app actions publish, ghl app actions test (would execute a real intake against production).

CLI verification on 2026-10-06

  • The pinned official CLI 1.0.0 successfully pulled the full app with --with-types after the billing configuration repair documented in marketplace-meters.md. The September 17 billing-validation blocker is resolved.
  • create_fm_applicant@1.0 remains a draft. Local action validation, including --publishable --action create_fm_applicant --version 1.0, passes. Its pulled definition matches the repository definition apart from the generated $schema reference and the redacted ${remote} authorization placeholder.
  • Schema and declaration generation succeeds. An action-only pull and diff showed no drift or conflicts. Local validation does not establish runtime correctness or authorize publication.
  • No action was executed, pushed, or published during this verification. Continue using action-scoped commands for action changes; a successful full pull does not authorize a whole-app push.