AutomationFlowsEmail & Gmail › Permit Cash Printer V1

Permit Cash Printer V1

Permit Cash Printer v1. Uses httpRequest, csvFile, gmail. Scheduled trigger; 15 nodes.

Cron / scheduled trigger★★★★☆ complexity15 nodesHTTP RequestCsv FileGmail
Email & Gmail Trigger: Cron / scheduled Nodes: 15 Complexity: ★★★★☆ Added:

This workflow follows the Gmail → HTTP Request recipe pattern — see all workflows that pair these two integrations.

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
{
  "name": "Permit Cash Printer v1",
  "nodes": [
    {
      "parameters": {
        "cronExpression": "*/30 * * * *"
      },
      "id": "Cron",
      "name": "Cron - every 30 min",
      "type": "n8n-nodes-base.cron",
      "typeVersion": 2,
      "position": [
        200,
        300
      ]
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "CITY_1_PERMITS_URL",
              "value": "https://data.city1.gov/permits?status=issued"
            },
            {
              "name": "CITY_2_PERMITS_URL",
              "value": "https://data.city2.gov/permits?status=issued"
            },
            {
              "name": "TRADE_KEYWORDS",
              "value": "striping|sealcoat|paving|glass|hvac|clean|tenant improvement|parking"
            },
            {
              "name": "MARKET_NAME",
              "value": "OKC Metro"
            },
            {
              "name": "SAMPLE_LEADS_TO_SEND",
              "value": "3"
            },
            {
              "name": "PRICE_PER_METRO",
              "value": "149"
            },
            {
              "name": "STRIPE_PAYMENT_LINK",
              "value": "https://buy.stripe.com/test_XXXXXXXXXXXX"
            },
            {
              "name": "OUTBOUND_LIST_CSV",
              "value": "gsheet://YourSpreadsheetName/LeadsOut"
            },
            {
              "name": "FULL_DATA_CSV",
              "value": "gsheet://YourSpreadsheetName/LeadsFull"
            }
          ]
        },
        "options": {}
      },
      "id": "EnvVars",
      "name": "Config Vars",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3,
      "position": [
        400,
        300
      ]
    },
    {
      "parameters": {
        "functionCode": "\n// Combine multiple city endpoints; allows JSON or HTML feed URLs.\nconst urls = [\n  $json['CITY_1_PERMITS_URL'],\n  $json['CITY_2_PERMITS_URL']\n].filter(Boolean);\nreturn urls.map(u => ({json: {url: u}}));\n"
      },
      "id": "PrepURLs",
      "name": "Prep URLs",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [
        600,
        300
      ]
    },
    {
      "parameters": {
        "url": "={{$json[\"url\"]}}",
        "responseFormat": "string",
        "options": {
          "followRedirect": true
        }
      },
      "id": "Fetch",
      "name": "Fetch Permit Page",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [
        800,
        300
      ]
    },
    {
      "parameters": {
        "functionCode": "\n/**\n * Attempts to parse either JSON (CKAN/Socrata style) or HTML tables into a normalized list of permits.\n * For HTML, looks for rows with columns like Address, Description, IssueDate.\n */\nconst input = items[0].json;\nconst body = input.body || input; // http node returns body in .json.body (string)\nlet permits = [];\ntry {\n  const data = typeof body === 'string' ? JSON.parse(body) : body;\n  // Try common Socrata format\n  if (Array.isArray(data)) {\n    // Assume array of permit objects\n    permits = data;\n  } else if (data && data.data && Array.isArray(data.data)) {\n    permits = data.data;\n  }\n} catch(e){\n  // Fallback: naive HTML parse via regex (works for simple <tr><td>)\n  const html = String(body);\n  const rows = html.split(/<tr[^>]*>/i).slice(1);\n  for (const r of rows) {\n    const tds = r.split(/<td[^>]*>/i).slice(1).map(x => x.replace(/<[^>]+>/g,'').trim());\n    if (tds.length >= 3) {\n      permits.push({\n        address: tds[0],\n        description: tds[1],\n        issued: tds[2],\n      });\n    }\n  }\n}\n\nreturn permits.map(p => ({json: {raw: p}}));\n"
      },
      "id": "Parse",
      "name": "Parse to Permits",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [
        1000,
        300
      ]
    },
    {
      "parameters": {
        "functionCode": "\n/**\n * Normalize fields to: address, project, issued_at, city, contact_url\n */\nfunction pick(obj, keys){\n  const out={};\n  for (const k of keys){\n    if (obj[k]!==undefined) out[k]=obj[k];\n  }\n  return out;\n}\nconst tradeRegex = new RegExp($item(0).$node[\"Config Vars\"].json[\"TRADE_KEYWORDS\"], 'i');\nconst city = $item(0).$node[\"Config Vars\"].json[\"MARKET_NAME\"];\nconst normalized = [];\n\nfor (const item of items){\n  const r = item.json.raw;\n  const rec = {\n    address: r.address || r.Address || r.site_address || r.location || r['Site Address'] || '',\n    project: r.description || r.Description || r.work_description || r['Work Description'] || '',\n    issued_at: r.issued || r.issue_date || r['Issue Date'] || r.permit_issued_date || '',\n    permit_no: r.permit || r.permit_number || r['Permit Number'] || '',\n    city\n  };\n  // Filter to trades of interest\n  if ((rec.project||'').match(tradeRegex)){\n    normalized.push({json: rec});\n  }\n}\nreturn normalized;\n"
      },
      "id": "NormalizeFilter",
      "name": "Normalize + Filter by Trade",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [
        1200,
        300
      ]
    },
    {
      "parameters": {
        "functionCode": "\n// De-dupe by (permit_no || address+project+issued_at) using a simple in-memory cache\n// For production, use Redis node or a Database node.\nconst seen = (global.get('seenPermits') || new Set());\nconst out = [];\nfor (const item of items){\n  const j = item.json;\n  const key = (j.permit_no || (j.address+'|'+j.project+'|'+j.issued_at)).toLowerCase();\n  if (!seen.has(key)){\n    seen.add(key);\n    out.push(item);\n  }\n}\nglobal.set('seenPermits', seen);\nreturn out;\n"
      },
      "id": "DeDupe",
      "name": "De-dup",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [
        1400,
        300
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentFormat": "raw",
        "url": "={{$item(0).$node[\"Config Vars\"].json[\"FULL_DATA_CSV\"]}}",
        "options": {}
      },
      "id": "WriteFull",
      "name": "Write \u2192 Google Sheet (Full)",
      "type": "n8n-nodes-base.csvFile",
      "typeVersion": 1,
      "position": [
        1600,
        300
      ]
    },
    {
      "parameters": {
        "functionCode": "\n// Prepare a teaser list (first N) for outbound email/DM.\nconst N = parseInt($item(0).$node[\"Config Vars\"].json[\"SAMPLE_LEADS_TO_SEND\"]) || 3;\nreturn items.slice(0, N);\n"
      },
      "id": "TeaserSlice",
      "name": "Slice Teaser N",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [
        1800,
        260
      ]
    },
    {
      "parameters": {
        "fromEmail": "",
        "toEmail": "=",
        "subject": "={{$item(0).$node[\"Config Vars\"].json[\"MARKET_NAME\"]}} Fresh {{new Date().toLocaleDateString()}} Permit Leads ({{$item(0).$node[\"Config Vars\"].json[\"SAMPLE_LEADS_TO_SEND\"]}} free sample)",
        "text": "={{`Hey there,\n\nHere are ${$item(0).$node[\"Config Vars\"].json[\"SAMPLE_LEADS_TO_SEND\"]} fresh ` + $item(0).$node[\"Config Vars\"].json[\"MARKET_NAME\"] + ` permit-based opportunities matching your trade:\n\n`}}",
        "additionalFields": {
          "text2": "={{$json.address + ' \u2014 ' + $json.project + ' \u2014 Issued: ' + $json.issued_at}}"
        }
      },
      "id": "EmailTeaser",
      "name": "Email Teaser (Gmail)",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 3,
      "position": [
        2000,
        240
      ]
    },
    {
      "parameters": {
        "functionCode": "\n// Build CTA with Stripe payment link\nconst price = $item(0).$node[\"Config Vars\"].json[\"PRICE_PER_METRO\"];\nconst link = $item(0).$node[\"Config Vars\"].json[\"STRIPE_PAYMENT_LINK\"];\nreturn [{\n  json: {\n    message: `Unlock full weekly sheet for ${$item(0).$node[\"Config Vars\"].json[\"MARKET_NAME\"]}: $${price}/mo \u2192 ${link}`\n  }\n}];\n"
      },
      "id": "CTA",
      "name": "Build CTA",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [
        1800,
        360
      ]
    },
    {
      "parameters": {
        "fromEmail": "",
        "toEmail": "=",
        "subject": "={{$item(0).$node[\"Config Vars\"].json[\"MARKET_NAME\"]}} \u2013 Get the Full Leads Sheet (Stripe Link Inside)",
        "text": "={{$json.message}}"
      },
      "id": "EmailCTA",
      "name": "Email CTA (Gmail)",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 3,
      "position": [
        2000,
        360
      ]
    },
    {
      "parameters": {
        "path": "stripe/payment-success",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "WebhookStripe",
      "name": "Webhook: Stripe Success",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        1600,
        540
      ]
    },
    {
      "parameters": {
        "operation": "lookup",
        "documentFormat": "raw",
        "url": "={{$item(0).$node[\"Config Vars\"].json[\"FULL_DATA_CSV\"]}}"
      },
      "id": "ReadFull",
      "name": "Read Full Sheet",
      "type": "n8n-nodes-base.csvFile",
      "typeVersion": 1,
      "position": [
        1800,
        540
      ]
    },
    {
      "parameters": {
        "fromEmail": "",
        "toEmail": "={{$json.query.email || 'buyer@example.com'}}",
        "subject": "Full Permit Leads Sheet \u2013 Access Granted",
        "text": "={{'Thanks for your purchase! Here is your full leads sheet for ' + $item(0).$node[\"Config Vars\"].json[\"MARKET_NAME\"] + '.\\n\\n' + 'Google Sheet: ' + $item(0).$node[\"Config Vars\"].json[\"FULL_DATA_CSV\"] + '\\n\\nStay winning.'}}"
      },
      "id": "DeliverFull",
      "name": "Deliver Full Access (Gmail)",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 3,
      "position": [
        2000,
        540
      ]
    }
  ],
  "connections": {
    "Cron - every 30 min": {
      "main": [
        [
          {
            "node": "Config Vars",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config Vars": {
      "main": [
        [
          {
            "node": "Prep URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prep URLs": {
      "main": [
        [
          {
            "node": "Fetch Permit Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Permit Page": {
      "main": [
        [
          {
            "node": "Parse to Permits",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse to Permits": {
      "main": [
        [
          {
            "node": "Normalize + Filter by Trade",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize + Filter by Trade": {
      "main": [
        [
          {
            "node": "De-dup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "De-dup": {
      "main": [
        [
          {
            "node": "Write \u2192 Google Sheet (Full)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slice Teaser N",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build CTA",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slice Teaser N": {
      "main": [
        [
          {
            "node": "Email Teaser (Gmail)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build CTA": {
      "main": [
        [
          {
            "node": "Email CTA (Gmail)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook: Stripe Success": {
      "main": [
        [
          {
            "node": "Read Full Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Full Sheet": {
      "main": [
        [
          {
            "node": "Deliver Full Access (Gmail)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "meta": {
    "version": "1.0",
    "notes": "Import into n8n. Configure Gmail creds, Stripe payment link, and Google Sheets names. Replace CITY_* URLs with your local permit feeds."
  }
}
Pro

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

About this workflow

Permit Cash Printer v1. Uses httpRequest, csvFile, gmail. Scheduled trigger; 15 nodes.

Source: https://github.com/405naturaldesign-tech/puraestate/blob/main/n8n-workflows/permit_cash_printer_v1.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

Relatórios aos Laboratórios. Uses httpRequest, gmail, formTrigger. Scheduled trigger; 54 nodes.

HTTP Request, Gmail, Form Trigger
Email & Gmail

YOUR_ID 4. Uses gmail, googleDrive, googleSheets, httpRequest. Scheduled trigger; 53 nodes.

Gmail, Google Drive, Google Sheets +1
Email & Gmail

14310 Send Overdue Invoice Payment Reminders With Ifirma Gmail Postgrid And Slack. Uses httpRequest, stopAndError, slack, gmail. Scheduled trigger; 53 nodes.

HTTP Request, Stop And Error, Slack +1
Email & Gmail

Addendo — Blog Automatico Don Jacinto Nahual. Uses httpRequest, redis, github, gmail. Scheduled trigger; 51 nodes.

HTTP Request, Redis, GitHub +1
Email & Gmail

Addendo — Blog Automatico Don Jacinto Nahual. Uses httpRequest, redis, github, gmail. Scheduled trigger; 51 nodes.

HTTP Request, Redis, GitHub +1