AutomationFlowsEmail & Gmail › Estimate Followup Chase

Estimate Followup Chase

Estimate-Followup-Chase. Uses twilio, gmail. Webhook trigger; 16 nodes.

Webhook trigger★★★★☆ complexity16 nodesTwilioGmail
Email & Gmail Trigger: Webhook Nodes: 16 Complexity: ★★★★☆ Added:

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "nodes": [
    {
      "id": "sticky-d",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1180,
        0
      ],
      "parameters": {
        "content": "## 4. Estimate follow-ups\n- When an estimate goes out, send this URL the customer's name, phone, and email. A JobTread automation or the office can do it\n- Day 2: a check-in text. Day 5: a final email\n- Each touch checks first whether the customer already responded, declined, or signed\n- Add more touches by copying a Wait + check + send block",
        "height": 280,
        "width": 460,
        "color": 6
      }
    },
    {
      "id": "sticky-e",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1180,
        460
      ],
      "parameters": {
        "content": "## 5. Stop switch\n- Anything that hits this URL turns off follow-ups for that customer\n- Point Twilio inbound SMS here so a reply stops the sequence on its own\n- JobTread status changes (declined or signed) can call it too\n- Works once the workflow is active; it remembers who to skip",
        "height": 260,
        "width": 460,
        "color": 3
      }
    },
    {
      "id": "webhook-estimate",
      "name": "Estimate was sent",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -660,
        100
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "stonebriar-estimate-sent",
        "responseMode": "responseNode",
        "options": {}
      }
    },
    {
      "id": "code-tidy-estimate",
      "name": "Tidy up the estimate details",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -440,
        100
      ],
      "parameters": {
        "jsCode": "const raw = $json.body && typeof $json.body === 'object' ? $json.body : $json;\n\nconst pick = (...keys) => {\n  for (const k of keys) {\n    const v = raw[k];\n    if (v !== undefined && v !== null && String(v).trim() !== '') return String(v).trim();\n  }\n  return '';\n};\n\nconst name = pick('name', 'fullName', 'customerName') || 'there';\nconst email = pick('email', 'customerEmail').toLowerCase();\n\nlet phone = pick('phone', 'customerPhone').replace(/[^\\d+]/g, '');\nif (phone && !phone.startsWith('+')) {\n  phone = phone.length === 10 ? `+1${phone}` : `+${phone}`;\n}\n\nreturn {\n  name,\n  firstName: name.split(' ')[0],\n  email,\n  phone,\n};"
      }
    },
    {
      "id": "respond-estimate",
      "name": "Tell JobTread we got it",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        -220,
        100
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "{\n  \"status\": \"received\"\n}",
        "options": {}
      }
    },
    {
      "id": "wait-2d",
      "name": "Wait 2 days",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        0,
        100
      ],
      "parameters": {
        "amount": 2,
        "unit": "days"
      }
    },
    {
      "id": "code-check-1",
      "name": "Did they already respond or sign?",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        100
      ],
      "parameters": {
        "jsCode": "// The stop switch (section 5) writes names into this list. If the customer\n// already replied, declined, or signed, the sequence ends quietly.\nconst stop = $getWorkflowStaticData('global').stopList || {};\nconst phoneKey = ($json.phone || '').replace(/\\D/g, '');\nconst emailKey = ($json.email || '').toLowerCase();\nconst stopped = Boolean((phoneKey && stop[phoneKey]) || (emailKey && stop[emailKey]));\nreturn { ...$json, stopped };"
      }
    },
    {
      "id": "if-still-waiting",
      "name": "Still waiting on them?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        440,
        100
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "not-stopped-1",
              "leftValue": "={{ $json.stopped }}",
              "rightValue": false,
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "twilio-fu1",
      "name": "Text the first follow-up",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        660,
        100
      ],
      "parameters": {
        "resource": "sms",
        "operation": "send",
        "from": "+15550149000",
        "to": "={{ $json.phone }}",
        "message": "=Hi {{ $json.firstName }}, it's Stonebriar Custom Builders. Wanted to make sure the estimate made it to you. Any questions, just text back.",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "wait-3d",
      "name": "Wait 3 more days",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        880,
        100
      ],
      "parameters": {
        "amount": 3,
        "unit": "days"
      }
    },
    {
      "id": "code-check-2",
      "name": "Check again before the last touch",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        100
      ],
      "parameters": {
        "jsCode": "const stop = $getWorkflowStaticData('global').stopList || {};\nconst phoneKey = ($json.phone || '').replace(/\\D/g, '');\nconst emailKey = ($json.email || '').toLowerCase();\nconst stopped = Boolean((phoneKey && stop[phoneKey]) || (emailKey && stop[emailKey]));\nreturn { ...$json, stopped };"
      }
    },
    {
      "id": "if-no-answer",
      "name": "Still no answer?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1320,
        100
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "not-stopped-2",
              "leftValue": "={{ $json.stopped }}",
              "rightValue": false,
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "gmail-fu2",
      "name": "Email the final follow-up",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1540,
        100
      ],
      "parameters": {
        "sendTo": "={{ $json.email }}",
        "subject": "Checking in on your estimate",
        "emailType": "text",
        "message": "=Hi {{ $json.firstName }},\n\nChecking in one more time on the estimate we sent over.\n\nIf the timing isn't right, no problem. Just let us know where you stand and we'll go from there.\n\nThanks,\nStonebriar Custom Builders",
        "options": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "webhook-stop",
      "name": "Customer replied, declined, or signed",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -660,
        560
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "stonebriar-stop-followups",
        "responseMode": "responseNode",
        "options": {}
      }
    },
    {
      "id": "code-stop",
      "name": "Turn off their follow-ups",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -440,
        560
      ],
      "parameters": {
        "jsCode": "// Accepts a plain {phone, email} post, a Twilio inbound SMS (From field),\n// or a JobTread automation call. Whoever it is, their follow-ups stop.\nconst raw = $json.body && typeof $json.body === 'object' ? $json.body : $json;\n\nconst phone = String(raw.phone || raw.From || raw.from || '').replace(/\\D/g, '');\nconst email = String(raw.email || raw.customerEmail || '').toLowerCase();\n\nconst data = $getWorkflowStaticData('global');\ndata.stopList = data.stopList || {};\nif (phone) data.stopList[phone] = true;\nif (email) data.stopList[email] = true;\n\nreturn { status: 'stopped', phone, email };"
      }
    },
    {
      "id": "respond-stop",
      "name": "Confirm it is handled",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        -220,
        560
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "{\n  \"status\": \"stopped\"\n}",
        "options": {}
      }
    }
  ],
  "connections": {
    "Estimate was sent": {
      "main": [
        [
          {
            "node": "Tidy up the estimate details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tidy up the estimate details": {
      "main": [
        [
          {
            "node": "Tell JobTread we got it",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tell JobTread we got it": {
      "main": [
        [
          {
            "node": "Wait 2 days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 2 days": {
      "main": [
        [
          {
            "node": "Did they already respond or sign?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Did they already respond or sign?": {
      "main": [
        [
          {
            "node": "Still waiting on them?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Still waiting on them?": {
      "main": [
        [
          {
            "node": "Text the first follow-up",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Text the first follow-up": {
      "main": [
        [
          {
            "node": "Wait 3 more days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 3 more days": {
      "main": [
        [
          {
            "node": "Check again before the last touch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check again before the last touch": {
      "main": [
        [
          {
            "node": "Still no answer?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Still no answer?": {
      "main": [
        [
          {
            "node": "Email the final follow-up",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Customer replied, declined, or signed": {
      "main": [
        [
          {
            "node": "Turn off their follow-ups",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Turn off their follow-ups": {
      "main": [
        [
          {
            "node": "Confirm it is handled",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Estimate-Followup-Chase. Uses twilio, gmail. Webhook trigger; 16 nodes.

Source: https://github.com/mcruz1799/automation-examples/blob/main/claude-code/02-automation-file-factory/skills/n8n/section-templates/estimate-followup-chase.json — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

Cold Email Automation - Safe Ramp + AI Personalization + Follow-ups. Uses googleSheets, httpRequest, gmail. Scheduled trigger; 34 nodes.

Google Sheets, HTTP Request, Gmail
Email & Gmail

Trigger: When a new meeting is booked in Cal.com.

Cal Trigger, Gmail
Email & Gmail

This workflow runs every weekday morning to find HubSpot deals with stale proposal activity, pulls the associated contact email, enforces opt-out and follow-up limits using Google Sheets, sends a valu

HubSpot, Google Sheets, Slack +1
Email & Gmail

This workflow automates the entire process of managing event participants, from registration and payment to sending reminders and follow-up communications. It's designed for event organizers who want

Typeform Trigger, Google Sheets, Stripe +3
Email & Gmail

Automate clinic appointment booking end to end. Patients submit a request via webhook, the workflow generates a unique Booking ID, routes online consultations to Google Meet and offline ones to the cl

Google Calendar, Gmail, Twilio