{
  "name": "SetterFlow — IG DM Setter",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "setterflow/inbound",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook-inbound",
      "name": "ManyChat Inbound",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -660,
        300
      ],
      "webhookId": "setterflow-inbound"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "api-base",
              "name": "apiBase",
              "value": "http://setterflow-api:8000",
              "type": "string"
            },
            {
              "id": "ghl-base",
              "name": "ghlBase",
              "value": "https://services.leadconnectorhq.com",
              "type": "string"
            },
            {
              "id": "ghl-location",
              "name": "ghlLocationId",
              "value": "REPLACE_WITH_GHL_LOCATION_ID",
              "type": "string"
            },
            {
              "id": "nurture-workflow",
              "name": "ghlNurtureWorkflowId",
              "value": "REPLACE_WITH_GHL_WORKFLOW_ID",
              "type": "string"
            },
            {
              "id": "airtable-base",
              "name": "airtableBaseId",
              "value": "REPLACE_WITH_AIRTABLE_BASE_ID",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "config",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -460,
        300
      ],
      "notes": "Edit these five values; nothing else in the workflow is environment-specific."
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "ig-user-id",
              "name": "ig_user_id",
              "value": "={{ $json.body.ig_id || $json.body.subscriber_id }}",
              "type": "string"
            },
            {
              "id": "subscriber-id",
              "name": "subscriber_id",
              "value": "={{ $json.body.subscriber_id }}",
              "type": "string"
            },
            {
              "id": "text",
              "name": "text",
              "value": "={{ $json.body.last_input_text }}",
              "type": "string"
            },
            {
              "id": "name",
              "name": "name",
              "value": "={{ $json.body.first_name }}",
              "type": "string"
            },
            {
              "id": "traffic-source",
              "name": "traffic_source",
              "value": "={{ $json.body.traffic_source || 'instagram_dm' }}",
              "type": "string"
            },
            {
              "id": "offer",
              "name": "offer",
              "value": "={{ $json.body.offer || '' }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "normalize",
      "name": "Normalize Payload",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -260,
        300
      ],
      "notes": "ManyChat External Request body -> the InboundMessage shape FastAPI expects."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').item.json.apiBase }}/turns",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "run-turn",
      "name": "Run Setter Turn",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -40,
        300
      ],
      "notes": "FastAPI loads Supabase state, runs the LangGraph turn, returns a TurnResult.",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "is-booked",
                    "leftValue": "={{ $json.outcome }}",
                    "rightValue": "booked",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "booked"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "is-disqualified",
                    "leftValue": "={{ $json.outcome }}",
                    "rightValue": "disqualified",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "disqualified"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "is-handoff",
                    "leftValue": "={{ $json.outcome }}",
                    "rightValue": "handoff",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "handoff"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "conversation"
        }
      },
      "id": "route-outcome",
      "name": "Route Outcome",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        200,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').item.json.ghlBase }}/contacts/upsert",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-07-28"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ locationId: $('Config').item.json.ghlLocationId, name: $json.ig_user_id, source: 'instagram_dm', tags: ['setterflow-booked', 'call-booked'], customFields: [{ key: 'instagram_user_id', field_value: $json.ig_user_id }, { key: 'call_time', field_value: $json.booked_slot ? $json.booked_slot.start : '' }] }) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "ghl-booked",
      "name": "GHL — Tag Booked",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        460,
        60
      ],
      "notes": "The appointment itself was already created by FastAPI; this is the CRM tag."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').item.json.ghlBase }}/contacts/upsert",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-07-28"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ locationId: $('Config').item.json.ghlLocationId, name: $json.ig_user_id, source: 'instagram_dm', tags: ['setterflow-disqualified', 'dq-' + ($json.disqualify_reason || 'unknown'), 'nurture-sequence'], customFields: [{ key: 'instagram_user_id', field_value: $json.ig_user_id }, { key: 'dq_reason', field_value: $json.disqualify_reason || '' }] }) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "ghl-disqualified",
      "name": "GHL — Tag + Nurture",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        460,
        240
      ],
      "notes": "The nurture-sequence tag is the trigger on the GHL workflow — no calendar push."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').item.json.ghlBase }}/contacts/upsert",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-07-28"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ locationId: $('Config').item.json.ghlLocationId, name: $json.ig_user_id, source: 'instagram_dm', tags: ['setterflow-handoff', 'handoff-' + ($json.handoff_reason || 'manual'), 'needs-human'], customFields: [{ key: 'instagram_user_id', field_value: $json.ig_user_id }, { key: 'handoff_reason', field_value: $json.handoff_reason || '' }] }) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "ghl-handoff",
      "name": "GHL — Flag For Human",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        460,
        420
      ],
      "notes": "FastAPI has already paused the bot on this thread; this raises it to the team."
    },
    {
      "parameters": {},
      "id": "no-crm-action",
      "name": "No CRM Action",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        460,
        600
      ],
      "notes": "Mid-conversation turns: nothing to sync, just log and reply."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.airtable.com/v0/{{ $('Config').item.json.airtableBaseId }}/Stage%20Events",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ typecast: true, records: ($('Run Setter Turn').item.json.events || []).map(e => ({ fields: { 'Lead ID': e.ig_user_id, 'From Stage': e.from_stage, 'To Stage': e.to_stage, 'Outcome': e.outcome, 'Traffic Source': e.traffic_source, 'Offer': e.offer || '', 'Occurred At': e.created_at } })) }) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "airtable-log",
      "name": "Airtable — Log Stage Events",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        720,
        300
      ],
      "notes": "One row per transition. Booking rate / qualification rate / drop-off are group-bys over this table.",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($('Run Setter Turn').item.json.reply ? { version: 'v2', content: { type: 'instagram', messages: [{ type: 'text', text: $('Run Setter Turn').item.json.reply }] } } : { version: 'v2', content: { messages: [] } }) }}",
        "options": {}
      },
      "id": "respond",
      "name": "Reply In DM",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        960,
        300
      ],
      "notes": "ManyChat renders this straight into the thread — no booking link, no channel switch."
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ version: 'v2', content: { messages: [] } }) }}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "respond-error",
      "name": "Stay Silent On Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        200,
        620
      ],
      "notes": "If the setter service is down, say nothing rather than something wrong. The lead's message is still in ManyChat."
    }
  ],
  "connections": {
    "ManyChat Inbound": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Normalize Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Payload": {
      "main": [
        [
          {
            "node": "Run Setter Turn",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Setter Turn": {
      "main": [
        [
          {
            "node": "Route Outcome",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stay Silent On Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Outcome": {
      "main": [
        [
          {
            "node": "GHL — Tag Booked",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "GHL — Tag + Nurture",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "GHL — Flag For Human",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No CRM Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GHL — Tag Booked": {
      "main": [
        [
          {
            "node": "Airtable — Log Stage Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GHL — Tag + Nurture": {
      "main": [
        [
          {
            "node": "Airtable — Log Stage Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GHL — Flag For Human": {
      "main": [
        [
          {
            "node": "Airtable — Log Stage Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No CRM Action": {
      "main": [
        [
          {
            "node": "Airtable — Log Stage Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable — Log Stage Events": {
      "main": [
        [
          {
            "node": "Reply In DM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  }
}