AutomationFlowsEmail & Gmail › Barber Log

Barber Log

Barber Log. Uses gmailTrigger, gmail, googleSheets. Event-driven trigger; 4 nodes.

Event trigger★★★★☆ complexity4 nodesGmail TriggerGmailGoogle Sheets
Email & Gmail Trigger: Event Nodes: 4 Complexity: ★★★★☆ Added:

This workflow follows the Gmail → Gmail Trigger 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": "Barber Log",
  "nodes": [
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "filters": {
          "q": "Appointment Request from"
        }
      },
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        0
      ],
      "id": "160d51b3-7fae-4cce-97b6-a5f8afa02819",
      "name": "Gmail Trigger",
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Barber Log: turns one booking notification email into one Google Sheet row.\n//\n// The notification sent by POST /booking carries the row as a JSON object\n// between two fixed sentinels in the email's plain-text part. This node pulls\n// out that block and runs a single JSON.parse on it. There is no label-hunting\n// left to break: previously this split the body on phrases like \"Phone #: \" and\n// \"Preferred Date 1:\", so any wording change silently shifted a field.\n//\n// Because of that, the email's HTML part is now purely presentational. It can be\n// redesigned freely -- new layout, colors, copy, whatever -- without touching\n// this node or the sheet, as long as the sentinel block stays in the plain-text\n// part and the subject keeps starting with \"Appointment Request from\".\n//\n// The twelve keys returned below map one-to-one, in this exact order, onto sheet\n// columns A..L, and empty values stay the string \"N/A\". server/src/Appointments.ts\n// reads that layout back and treats \"N/A\" as empty. Do not reorder, rename, add\n// or remove keys here without changing the sheet and Appointments.ts to match.\n\nconst START = \"---BOOKING_JSON_START---\";\nconst END = \"---BOOKING_JSON_END---\";\n\nconst body = $json.text || $json.textPlain || \"\";\n\nconst start = body.indexOf(START);\nif (start === -1) {\n  throw new Error(\"Booking email is missing \" + START);\n}\n\n// indexOf rather than split: splitting on the end sentinel returns the whole\n// remainder when it is absent, so a truncated email would parse whatever\n// happened to follow instead of failing the execution.\nconst end = body.indexOf(END, start);\nif (end === -1) {\n  throw new Error(\"Booking email is missing \" + END);\n}\n\nconst booking = JSON.parse(body.slice(start + START.length, end).trim());\n\n// The payload carries its own America/Los_Angeles timestamp. These fall back to\n// the email's received date, in the same timezone and format the sheet has\n// always used, so a message from any older sender still lands correctly.\nconst received = new Date($json.date);\nconst fallbackDate = received.toLocaleDateString(\"en-US\", {\n  timeZone: \"America/Los_Angeles\"\n});\nconst fallbackTime = received.toLocaleTimeString(\"en-US\", {\n  timeZone: \"America/Los_Angeles\",\n  hour: \"2-digit\",\n  minute: \"2-digit\"\n});\n\n// Empty becomes \"N/A\", matching what the sheet has always held.\nconst cell = (value) => {\n  const text = value === undefined || value === null ? \"\" : String(value).trim();\n  return text === \"\" ? \"N/A\" : text;\n};\n\n// Columns A..L, in order.\nreturn {\n  name: cell(booking.name),\n  date: cell(booking.date || fallbackDate),\n  time: cell(booking.time || fallbackTime),\n  phone: cell(booking.phone),\n  date1: cell(booking.date1),\n  avail1: cell(booking.avail1),\n  date2: cell(booking.date2),\n  avail2: cell(booking.avail2),\n  date3: cell(booking.date3),\n  avail3: cell(booking.avail3),\n  description: cell(booking.description),\n  email: cell(booking.email)\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        416,
        0
      ],
      "id": "f0cef7e7-bda7-4720-8bc2-47b8d8c0dfec",
      "name": "Code in JavaScript"
    },
    {
      "parameters": {
        "operation": "get",
        "messageId": "={{ $json.id }}",
        "simple": false,
        "options": {}
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        208,
        0
      ],
      "id": "86ac8993-68e8-4809-bf05-f1cafc56ad53",
      "name": "Get a message",
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "18QTVoA7eT5Iga6bnys8X_Q32B2NBgP9epeV8Jul63hU",
          "mode": "list",
          "cachedResultName": "Barber Appointment Log Zapier (n8n)",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/18QTVoA7eT5Iga6bnys8X_Q32B2NBgP9epeV8Jul63hU/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "Sheet1",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/18QTVoA7eT5Iga6bnys8X_Q32B2NBgP9epeV8Jul63hU/edit#gid=0"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Name": "={{ $json.name }}",
            "Date": "={{ $json.date }}",
            "Time": "={{ $json.time }}",
            "Phone #": "={{ $json.phone }}",
            "Preferred Date 1": "={{ $json.date1 }}",
            "Availability for Date 1": "={{ $json.avail1 }}",
            "Preferred Date 2": "={{ $json.date2 }}",
            "Availability for Date 2": "={{ $json.avail2 }}",
            "Preferred Date 3": "={{ $json.date3 }}",
            "Availability for Date 3": "={{ $json.avail3 }}",
            "Description of Haircut / Other Comments": "={{ $json.description }}",
            "Email": "={{ $json.email }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Name",
              "displayName": "Name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Date",
              "displayName": "Date",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Time",
              "displayName": "Time",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Phone #",
              "displayName": "Phone #",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Preferred Date 1",
              "displayName": "Preferred Date 1",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Availability for Date 1",
              "displayName": "Availability for Date 1",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Preferred Date 2",
              "displayName": "Preferred Date 2",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Availability for Date 2",
              "displayName": "Availability for Date 2",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Preferred Date 3",
              "displayName": "Preferred Date 3",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Availability for Date 3",
              "displayName": "Availability for Date 3",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Description of Haircut / Other Comments",
              "displayName": "Description of Haircut / Other Comments",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Email",
              "displayName": "Email",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        624,
        0
      ],
      "id": "2be76b38-df27-4fa2-baa4-d04004fe491a",
      "name": "Append row in sheet",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Gmail Trigger": {
      "main": [
        [
          {
            "node": "Get a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get a message": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Append row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "staticData": {
    "node:Gmail Trigger": {
      "Gmail Trigger": {
        "lastTimeChecked": 1776657798
      }
    }
  },
  "triggerCount": 1,
  "meta": {
    "templateCredsSetupCompleted": true
  }
}

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

Barber Log. Uses gmailTrigger, gmail, googleSheets. Event-driven trigger; 4 nodes.

Source: https://github.com/henry-hai/barber-booking/blob/main/automation/Barber_Log.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

This workflow ingests proof-of-delivery and completion documents from Gmail or a webhook, extracts key fields with an OpenRouter vision model, reconciles them against Google Sheets dispatch data, arch

Gmail Trigger, HTTP Request, Google Sheets +2
Email & Gmail

AICARE Email Blast System. Uses googleDrive, httpRequest, googleSheets, gmail. Event-driven trigger; 39 nodes.

Google Drive, HTTP Request, Google Sheets +2
Email & Gmail

An automated n8n workflow that monitors your Gmail inbox, classifies job application emails using a local AI (Ollama), and logs every application — with company, role, and status — to a Google Sheet i

Gmail, Gmail Trigger, HTTP Request +1
Email & Gmail

Reference Check Parser – Turn Reference Emails into a Comparable Candidate Sheet (powered by easybits). Uses gmailTrigger, @easybits/n8n-nodes-extractor, googleSheets, gmail. Event-driven trigger; 21

Gmail Trigger, @Easybits/N8N Nodes Extractor, Google Sheets +1
Email & Gmail

Googlesheets Gmail. Uses googleDrive, gmailTrigger, gmail, stickyNote. Event-driven trigger; 19 nodes.

Google Drive, Gmail Trigger, Gmail +1