---
name: form-flow-pipeline
description: >
  Spin up a full "Form → Flow → Pipeline" capture system on PayMeGPT: a
  custom-coded landing page with a hand-built, AI-generated contact form posts
  straight to a flow's native inbound webhook — no PayMeGPT native form widget
  involved. That one webhook hit triggers a flow that logs the submission to a
  Google Sheet, tags the contact, drops them into a pipeline stage, and texts an
  alert. Use when a custom landing page (optionally deployed to its own domain via
  GitHub Pages) needs a signup/lead form that shows up in a sheet, a pipeline, and
  a phone alert with zero manual work.
---

# Form → Flow → Pipeline

## What this is (and isn't)

Two different things on the platform can sound similar — keep them straight:

- **Custom-coded landing page**: a page built as raw, AI-generated HTML/CSS/JS
  (via `create_landing_page` / `update_landing_page` with `htmlCode`), optionally
  deployed to a custom domain through GitHub Pages
  (`deploy_landing_page_to_github` + `sync_landing_page_to_github`). This skill
  assumes the page already exists (or is being built) this way.
- **The form on that page**: also hand-coded HTML — not PayMeGPT's native form
  builder (`create_form`). Its JavaScript submit handler `fetch()`s a POST
  directly to the flow's webhook URL. It only *feels* "native" because it's
  talking straight to the platform's built-in inbound webhook — there is no
  third-party form tool and no separate native-form widget in the chain.

**The chain, once wired:**

1. Submit form (custom-coded, on the custom-coded page)
2. Triggers Flow (via native inbound webhook)
3. Sheet (row logged)
4. Tag (contact labeled)
5. Add to Stage (pipeline entry)
6. SMS (alert sent)

## MCP Server

PayMeGPT MCP endpoint: `https://paymegpt.com/mcp` (authenticate with Bearer token).

## Placeholder convention

Replace every `<ANGLE_BRACKET>` token with a real value before running.
Leave every `{{double_brace}}` token EXACTLY as written — those are runtime
template variables the platform fills in when the flow executes.

## Inputs (ask the user, or use sensible defaults)

- `<PAGE_ID>` — the landing page this form lives on (existing or newly built)
- `<CUSTOM_DOMAIN>` — the domain it's deployed to, if any (affects step 7)
- `<FORM_FIELDS>` — exact fields the form collects and which are required vs.
  optional (e.g. name required, email required, phone optional)
- `<SHEET_TITLE>` — name for the Google Sheet
- `<SHEET_HEADERS>` — column headers, e.g. `Name, Email, Phone, Source, Date, Time`
  plus any custom fields the form collects
- `<PIPELINE_NAME>` — name for the new pipeline
- `<PIPELINE_STAGES>` — ordered stage names (first stage = where every new
  submission lands); mark exactly one `isWinStage` and one `isLostStage` if the
  funnel has a clear end state
- `<TAG_NAME>` — tag applied to every contact who submits
- `<ALERT_PHONE>` — E.164 format, e.g. `+18435051657`
- `<WIDGET_ID>` — the agent/widget this flow and pipeline should be associated with

## Build order (do these before touching the form's JS)

The backend pieces must exist BEFORE you can wire the form to them, since the
webhook URL, pipeline stage ID, and tag all get baked into the flow the form
calls.

1. **Create the sheet.** `create_google_sheet` with `title=<SHEET_TITLE>` and
   `headers=<SHEET_HEADERS>`. Save the returned **Spreadsheet ID**.

2. **Create the pipeline.** `create_pipeline` with `name=<PIPELINE_NAME>`,
   `stages=<PIPELINE_STAGES>` (array of `{name, color, isWinStage?, isLostStage?}`),
   and `widgetId=<WIDGET_ID>`. Save the returned **pipeline ID**, then call
   `get_pipeline` to capture each **stage ID** — you need the FIRST stage's ID for
   step 4.

3. **Create the tag.** Tags can't be pre-created standalone — either let the
   flow's `add_tag` step create it on first run, or apply it once to any existing
   test contact via `tag_contacts` with `tag=<TAG_NAME>` to create it ahead of
   time and grab its numeric ID via `list_tags`.

4. **Create the flow with all four action nodes**, in this exact order:
   Sheet write → Tag → Add to pipeline stage → SMS. Call `create_flow` with
   `enableWebhook=true`, `widgetId=<WIDGET_ID>`, and a description naming all
   four steps in order (or build the nodes directly with `update_flow` — see
   schema below). Save the returned **flow ID** and **webhook URL**.

5. **Verify the structure.** `get_flow` and confirm:
   - node order is Start → Sheet → Tag → Add to Pipeline → SMS
   - the sheets node has `sheetsOperation="send_data"` and the right
     `sheetsSpreadsheetId`, with every header mapped
   - the tag node references `<TAG_NAME>` (see gotcha below on field names)
   - the pipeline node has the right `pipelineId` and the FIRST stage's `stageId`
   - the SMS node has `smsTo=<ALERT_PHONE>` and a clearly labeled message (spell
     out each field on its own line, e.g. `Name: {{contactName}}` — an unlabeled
     `Name <email>` header-style line reads as "missing" even when present)

## Wire the custom form (now that the webhook exists)

6. **Write the form's submit handler** to POST JSON straight to the webhook URL
   from step 4 — no PayMeGPT form widget, no third-party service:
   ```json
   {
     "contactName": "<from form>",
     "contactEmail": "<from form>",
     "contactPhone": "<from form, OMIT the key entirely if empty/optional>",
     "data": { "source": "<page URL>", "<any custom field>": "<value>" }
   }
   ```
   Built-in contact fields (`contactName`, `contactEmail`, `contactPhone`) go at
   the top level. Everything else — including a custom field like "existing
   customer" — goes inside `data` and is referenced in the flow as
   `{{fieldName}}`.

7. **Publish the page.** Push the updated `htmlCode` via `update_landing_page`.
   If the page is deployed to a custom domain, this ALSO requires
   `sync_landing_page_to_github` (or `sync_all_landing_pages_to_github` for a
   multi-page repo) — the platform preview URL updates instantly, but the custom
   domain only updates after a GitHub sync.

8. **Fire a realistic test** — not a placeholder like `test@test.com`. Use a
   plausible name/email/phone/custom-field combination and POST directly to the
   webhook with `curl` (mirrors exactly what the live form sends — more reliable
   than `trigger_flow`, which does NOT reliably populate `{{contactEmail}}` /
   `{{contactPhone}}` the same way a real webhook POST does). Warn the user
   first — this fires a real SMS.

9. **Verify every step independently — a `"success": true` response does NOT
   mean every node worked.** Check:
   - the sheet row landed (`read_google_sheet` with a lookup filter)
   - the tag was actually applied (`list_tags` — did the contact count go up?)
   - the contact landed in the pipeline's first stage (`get_pipeline`)
   - the user confirms the SMS arrived

10. **Report** the flow ID, webhook URL, sheet link, pipeline dashboard link
    (`https://paymegpt.com/dashboard/pipelines/<pipelineId>`), the live page URL,
    and any test rows/contacts left behind so the user can clear them.

## Node schema reference

```json
[
  { "id": "start", "type": "start", "position": { "x": 50, "y": 200 },
    "data": { "label": "Start", "nodeType": "start" } },

  { "id": "node-1", "type": "action", "position": { "x": 490, "y": 120 },
    "data": {
      "label": "Append Submission to Sheet",
      "nodeType": "action",
      "actions": [{
        "id": "action-1",
        "type": "google_sheets",
        "sheetsOperation": "send_data",
        "sheetsWorksheet": "Sheet1",
        "sheetsSpreadsheetId": "<SPREADSHEET_ID>",
        "sheetsColumnMappings": [
          { "column": "Name",  "value": "{{contactName}}" },
          { "column": "Email", "value": "{{contactEmail}}" },
          { "column": "Phone", "value": "{{contactPhone}}" },
          { "column": "Source","value": "{{source}}" },
          { "column": "Date",  "value": "{{date}}" },
          { "column": "Time",  "value": "{{time}}" }
        ]
      }]
    } },

  { "id": "node-2", "type": "action", "position": { "x": 700, "y": 120 },
    "data": {
      "label": "Tag Contact",
      "nodeType": "action",
      "actions": [{
        "id": "action-1",
        "type": "add_tag",
        "tag": "<TAG_NAME>",
        "tagId": "<TAG_ID>",
        "tagIds": ["<TAG_ID>"],
        "tagName": "<TAG_NAME>",
        "tagNames": ["<TAG_NAME>"]
      }]
    } },

  { "id": "node-3", "type": "action", "position": { "x": 1130, "y": 120 },
    "data": {
      "label": "Add to Pipeline",
      "nodeType": "action",
      "actions": [{
        "id": "action-1",
        "type": "add_to_pipeline",
        "pipelineId": "<PIPELINE_ID>",
        "stageId": "<FIRST_STAGE_ID>",
        "toStageId": "<FIRST_STAGE_ID>"
      }]
    } },

  { "id": "node-4", "type": "action", "position": { "x": 1370, "y": 120 },
    "data": {
      "label": "Send Alert SMS",
      "nodeType": "action",
      "actions": [{
        "id": "action-1",
        "type": "send_sms",
        "smsTo": "<ALERT_PHONE>",
        "smsMessage": "New submission:\nName: {{contactName}}\nEmail: {{contactEmail}}\nPhone: {{contactPhone}}\nSource: {{source}}"
      }]
    } }
]
```

Edges: `start -> node-1`, `node-1 -> node-2`, `node-2 -> node-3`, `node-3 -> node-4`.

## Rules and gotchas

- **Don't confuse the pieces**: the landing page is custom-coded HTML deployed
  through the landing-page tools (optionally to GitHub); the form on it is also
  custom-coded HTML/JS, not `create_form`; the webhook it hits is the flow's
  native inbound webhook, not a third-party integration.
- **Backend before frontend**: create the sheet, pipeline, and flow FIRST — the
  form's JS needs the real webhook URL, and the flow's pipeline node needs a real
  stage ID, so there's nothing to wire until those exist.
- **Node order is fixed**: Sheet → Tag → Pipeline → SMS. Log the data before
  reacting to it.
- **Use individual template variables only** — never a single `{{webhook_data}}`
  blob. `{{date}}` and `{{time}}` auto-stamp the row; don't point them at contact
  data.
- **`add_tag` and `add_to_pipeline` field names are undocumented and
  unreliable — send every plausible field name variant** (`tag`, `tagId`,
  `tagIds`, `tagName`, `tagNames` for tagging; `pipelineId` + `stageId` +
  `toStageId` for pipeline placement). Extra unused fields are harmlessly
  ignored, but guessing wrong and sending only one risks a silent no-op — the
  flow run still reports `"success": true"` even when a step did nothing.
- **`trigger_flow` is not a reliable test of the real webhook path.** It does not
  consistently populate `{{contactEmail}}` / `{{contactPhone}}` the way an
  actual webhook POST does. Always validate the final build with a direct `curl`
  POST to the webhook URL, not `trigger_flow`.
- **Verify each node's output independently**, not just the overall flow-run
  status. A `"success": true"` response only means the flow completed without
  throwing — it does not confirm every action inside it actually did something.
- **SMS messages should label every field explicitly** (`Name: {{contactName}}`),
  not rely on formatting shorthand like `{{contactName}} <{{contactEmail}}>` — it
  reads as a missing field even when the data is present.
- **Phone should stay optional at the JSON level**: omit the `contactPhone` key
  entirely from the payload when the field is empty, rather than sending an
  empty string.
- **A custom-domain landing page needs a GitHub sync after every form/HTML
  change; a flow's internal steps do not.** Editing the tag, the pipeline stage,
  or the SMS wording inside the flow takes effect immediately, server-side — no
  publish step. Editing the page's HTML (including the form) only updates the
  platform preview URL until you sync to GitHub.
- **Always warn before firing a real test** — any webhook test that reaches the
  SMS node sends an actual text.
