8
n8n 中文网amn8n.com

面试安排器

高级

这是一个HR, AI领域的自动化工作流,包含 25 个节点。主要使用 If, Set, Code, Merge, GoogleCalendar 等节点,结合人工智能技术实现智能自动化。 使用GPT-4o和Google Calendar聊天机器人自动安排面试

前置要求
  • OpenAI API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "bh3H2b654RSYgIm9",
  "meta": {
    "instanceId": "efb474b59b0341d7791932605bd9ff04a6c7ed9941fdd53dc4a2e4b99a6f9439",
    "templateCredsSetupCompleted": true
  },
  "name": "面试安排器",
  "tags": [],
  "nodes": [
    {
      "id": "cd5664f9-0b6b-491a-a0a0-1d8b3b2f2461",
      "name": "OpenAI 聊天模型2",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        320,
        1480
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "id": "ghJTvay8CvwXDsXz",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "e8ca4a14-ee58-4be0-838b-5cbf8a802b6e",
      "name": "窗口缓冲记忆2",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        520,
        1480
      ],
      "parameters": {
        "sessionKey": "={{ $json.sessionId }}",
        "sessionIdType": "customKey",
        "contextWindowLength": 10
      },
      "typeVersion": 1.3
    },
    {
      "id": "d2957530-acd1-4875-a75b-69b890f08065",
      "name": "OpenAI 聊天模型",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1220,
        1440
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "id": "ghJTvay8CvwXDsXz",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "897c8189-aaa9-45c7-99c6-95378a7a13f2",
      "name": "运行获取可用性",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "position": [
        720,
        1520
      ],
      "parameters": {
        "name": "get_availability",
        "source": "parameter",
        "description": "调用此工具以获取我的可用时间",
        "workflowJson": "{\n  \"nodes\": [\n    {\n      \"parameters\": {\n        \"operation\": \"getAll\",\n        \"calendar\": {\n          \"__rl\": true,\n          \"value\": \"rbreen.ynteractive@gmail.com\",\n          \"mode\": \"list\",\n          \"cachedResultName\": \"rbreen.ynteractive@gmail.com\"\n        },\n        \"returnAll\": true,\n        \"options\": {\n          \"fields\": \"\"\n        }\n      },\n      \"type\": \"n8n-nodes-base.googleCalendar\",\n      \"typeVersion\": 1.3,\n      \"position\": [\n        -500,\n        220\n      ],\n      \"id\": \"a1017705-8866-469f-83e0-9f5d5f37af53\",\n      \"name\": \"Check My Calendar\",\n      \"credentials\": {\n        \"googleCalendarOAuth2Api\": {\n          \"id\": \"nc5M45R7LyFadByw\",\n          \"name\": \"Google Calendar account\"\n        }\n      }\n    },\n    {\n      \"parameters\": {\n        \"jsCode\": \"const events = items.map(item => item.json);\\nconst intervalMinutes = 30;\\nconst timeZone = 'America/New_York';\\n\\nfunction formatToEastern(date) {\\n  const tzDate = new Intl.DateTimeFormat('en-US', {\\n    timeZone,\\n    year: 'numeric',\\n    month: '2-digit',\\n    day: '2-digit',\\n    hour: '2-digit',\\n    minute: '2-digit',\\n    second: '2-digit',\\n    hour12: false\\n  }).formatToParts(date).reduce((acc, part) => {\\n    if (part.type !== 'literal') acc[part.type] = part.value;\\n    return acc;\\n  }, {});\\n\\n  const offset = getEasternOffset(date);\\n  return `${tzDate.year}-${tzDate.month}-${tzDate.day}T${tzDate.hour}:${tzDate.minute}:${tzDate.second}${offset}`;\\n}\\n\\nfunction getEasternOffset(date) {\\n  const options = { timeZone, timeZoneName: 'short' };\\n  const parts = new Intl.DateTimeFormat('en-US', options).formatToParts(date);\\n  const tzName = parts.find(p => p.type === 'timeZoneName').value;\\n  return tzName.includes('EDT') ? '-04:00' : '-05:00';\\n}\\n\\nfunction alignToPreviousSlot(date) {\\n  const aligned = new Date(date);\\n  const minutes = aligned.getMinutes();\\n  aligned.setMinutes(minutes < 30 ? 0 : 30, 0, 0);\\n  return aligned;\\n}\\n\\nfunction alignToNextSlot(date) {\\n  const aligned = new Date(date);\\n  const minutes = aligned.getMinutes();\\n  if (minutes > 0 && minutes <= 30) {\\n    aligned.setMinutes(30, 0, 0);\\n  } else if (minutes > 30) {\\n    aligned.setHours(aligned.getHours() + 1);\\n    aligned.setMinutes(0, 0, 0);\\n  } else {\\n    aligned.setMinutes(0, 0, 0);\\n  }\\n  return aligned;\\n}\\n\\nconst splitEventIntoETBlocks = (event) => {\\n  const blocks = [];\\n\\n  let current = alignToPreviousSlot(new Date(event.start.dateTime));\\n  const eventEnd = alignToNextSlot(new Date(event.end.dateTime));\\n\\n  while (current < eventEnd) {\\n    const blockEnd = new Date(current);\\n    blockEnd.setMinutes(current.getMinutes() + intervalMinutes);\\n\\n    blocks.push({\\n      start: formatToEastern(current),\\n      end: formatToEastern(blockEnd)\\n    });\\n\\n    current = blockEnd;\\n  }\\n\\n  return blocks;\\n};\\n\\nlet allBlocks = [];\\nfor (const event of events) {\\n  if (event.start?.dateTime && event.end?.dateTime) {\\n    const blocks = splitEventIntoETBlocks(event);\\n    allBlocks = allBlocks.concat(blocks);\\n  }\\n}\\n\\nreturn allBlocks.map(block => ({ json: block }));\\n\"\n      },\n      \"type\": \"n8n-nodes-base.code\",\n      \"typeVersion\": 2,\n      \"position\": [\n        -280,\n        240\n      ],\n      \"id\": \"fb9063c2-de6b-4513-8901-d12625f5d772\",\n      \"name\": \"Split Events into 30 min blocks\"\n    },\n    {\n      \"parameters\": {\n        \"assignments\": {\n          \"assignments\": [\n            {\n              \"id\": \"f1270be8-1d11-4086-8bc0-ae53c99507c1\",\n              \"name\": \"start\",\n              \"value\": \"={{ $json.start }}\",\n              \"type\": \"string\"\n            },\n            {\n              \"id\": \"1a5f24ff-7d0c-436d-bb0b-015fc0c85cb7\",\n              \"name\": \"end\",\n              \"value\": \"={{ $json.end }}\",\n              \"type\": \"string\"\n            },\n            {\n              \"id\": \"befe6645-c0c1-40eb-9ba6-eccf2a762247\",\n              \"name\": \"Blocked\",\n              \"value\": \"Blocked\",\n              \"type\": \"string\"\n            }\n          ]\n        },\n        \"options\": {}\n      },\n      \"type\": \"n8n-nodes-base.set\",\n      \"typeVersion\": 3.4,\n      \"position\": [\n        -80,\n        240\n      ],\n      \"id\": \"23d8ed50-131f-49ea-9ce8-72a0067fe828\",\n      \"name\": \"Add Blocked Field\"\n    },\n    {\n      \"parameters\": {\n        \"jsCode\": \"const slots = [];\\nconst slotMinutes = 30;\\nconst timeZone = 'America/New_York';\\nconst businessStartHour = 9;\\nconst businessEndHour = 17;\\n\\n// Get offset like -04:00 or -05:00\\nfunction getEasternOffset(date) {\\n  const options = { timeZone, timeZoneName: 'short' };\\n  const parts = new Intl.DateTimeFormat('en-US', options).formatToParts(date);\\n  const tz = parts.find(p => p.type === 'timeZoneName')?.value || 'EST';\\n  return tz.includes('EDT') ? '-04:00' : '-05:00';\\n}\\n\\n// Format Date as ISO with Eastern offset\\nfunction formatToEasternISO(date) {\\n  const formatter = new Intl.DateTimeFormat('en-CA', {\\n    timeZone,\\n    year: 'numeric',\\n    month: '2-digit',\\n    day: '2-digit',\\n    hour: '2-digit',\\n    minute: '2-digit',\\n    second: '2-digit',\\n    hour12: false,\\n  });\\n\\n  const parts = formatter.formatToParts(date).reduce((acc, part) => {\\n    if (part.type !== 'literal') acc[part.type] = part.value;\\n    return acc;\\n  }, {});\\n\\n  const offset = getEasternOffset(date);\\n  return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}${offset}`;\\n}\\n\\n// Convert a Date to the hour/minute of its Eastern time\\nfunction getEasternTimeParts(date) {\\n  const formatter = new Intl.DateTimeFormat('en-US', {\\n    timeZone,\\n    hour: '2-digit',\\n    minute: '2-digit',\\n    hour12: false,\\n  });\\n  const [hourStr, minStr] = formatter.format(date).split(':');\\n  return { hour: parseInt(hourStr), minute: parseInt(minStr) };\\n}\\n\\nconst now = new Date();\\nconst endDate = new Date(now);\\nendDate.setDate(now.getDate() + 7);\\n\\n// Set current time to 24 hours in the future\\nconst current = new Date(now);\\ncurrent.setHours(current.getHours() + 24);\\n\\n// Round to the next 30-minute block in Eastern time\\nconst { minute } = getEasternTimeParts(current);\\nif (minute < 30) {\\n  current.setMinutes(30, 0, 0);\\n} else {\\n  current.setHours(current.getHours() + 1);\\n  current.setMinutes(0, 0, 0);\\n}\\n\\n// Generate 30-minute blocks only during business hours & weekdays\\nwhile (current < endDate) {\\n  const dayOfWeek = current.getDay(); // 0 = Sunday, 6 = Saturday\\n\\n  // Skip weekends\\n  if (dayOfWeek !== 0 && dayOfWeek !== 6) {\\n    const { hour } = getEasternTimeParts(current);\\n\\n    if (hour >= businessStartHour && hour < businessEndHour) {\\n      const start = new Date(current);\\n      const end = new Date(start);\\n      end.setMinutes(start.getMinutes() + slotMinutes);\\n\\n      slots.push({\\n        start: formatToEasternISO(start),\\n        end: formatToEasternISO(end),\\n      });\\n    }\\n  }\\n\\n  current.setMinutes(current.getMinutes() + slotMinutes);\\n}\\n\\nreturn slots.map(slot => ({ json: slot }));\\n\"\n      },\n      \"type\": \"n8n-nodes-base.code\",\n      \"typeVersion\": 2,\n      \"position\": [\n        -400,\n        460\n      ],\n      \"id\": \"01597a94-d94b-47e7-9488-adea3abb741c\",\n      \"name\": \"Generate 30 Minute Timeslots\"\n    },\n    {\n      \"parameters\": {\n        \"mode\": \"combine\",\n        \"fieldsToMatchString\": \"start, end\",\n        \"joinMode\": \"enrichInput2\",\n        \"options\": {}\n      },\n      \"type\": \"n8n-nodes-base.merge\",\n      \"typeVersion\": 3,\n      \"position\": [\n        180,\n        300\n      ],\n      \"id\": \"2d9f98a1-02ac-4332-a288-635a48ea3ee8\",\n      \"name\": \"Combine My Calendar with All Slots\"\n    },\n    {\n      \"parameters\": {\n        \"conditions\": {\n          \"options\": {\n            \"caseSensitive\": true,\n            \"leftValue\": \"\",\n            \"typeValidation\": \"strict\",\n            \"version\": 2\n          },\n          \"conditions\": [\n            {\n              \"id\": \"af65c6c8-31c7-4f27-a073-cf7f72079882\",\n              \"leftValue\": \"={{ $json.Blocked }}\",\n              \"rightValue\": \"Blocked\",\n              \"operator\": {\n                \"type\": \"string\",\n                \"operation\": \"notEquals\"\n              }\n            }\n          ],\n          \"combinator\": \"and\"\n        },\n        \"options\": {}\n      },\n      \"type\": \"n8n-nodes-base.if\",\n      \"typeVersion\": 2.2,\n      \"position\": [\n        420,\n        280\n      ],\n      \"id\": \"0438b5be-b3c4-4645-9604-303ace7bfead\",\n      \"name\": \"Check if Calendar Blocked\"\n    },\n    {\n      \"parameters\": {\n        \"jsCode\": \"const formatted = items.map(item => {\\n  const start = item.json.start;\\n  const end = item.json.end;\\n  return `${start} - ${end}`;\\n});\\n\\nconst combined = formatted.join(', ');\\n\\nreturn [\\n  {\\n    json: {\\n      availableSlots: combined\\n    }\\n  }\\n];\\n\"\n      },\n      \"type\": \"n8n-nodes-base.code\",\n      \"typeVersion\": 2,\n      \"position\": [\n        660,\n        300\n      ],\n      \"id\": \"4a6bfde4-7d9f-4837-bc6c-66bf968e782a\",\n      \"name\": \"Return string of all available times\"\n    },\n    {\n      \"parameters\": {\n        \"inputSource\": \"passthrough\"\n      },\n      \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n      \"typeVersion\": 1.1,\n      \"position\": [\n        -760,\n        340\n      ],\n      \"id\": \"8bde95cb-7239-4b7d-aca1-0adacf2ea257\",\n      \"name\": \"Get Availability\"\n    }\n  ],\n  \"connections\": {\n    \"Check My Calendar\": {\n      \"main\": [\n        [\n          {\n            \"node\": \"Split Events into 30 min blocks\",\n            \"type\": \"main\",\n            \"index\": 0\n          }\n        ]\n      ]\n    },\n    \"Split Events into 30 min blocks\": {\n      \"main\": [\n        [\n          {\n            \"node\": \"Add Blocked Field\",\n            \"type\": \"main\",\n            \"index\": 0\n          }\n        ]\n      ]\n    },\n    \"Add Blocked Field\": {\n      \"main\": [\n        [\n          {\n            \"node\": \"Combine My Calendar with All Slots\",\n            \"type\": \"main\",\n            \"index\": 0\n          }\n        ]\n      ]\n    },\n    \"Generate 30 Minute Timeslots\": {\n      \"main\": [\n        [\n          {\n            \"node\": \"Combine My Calendar with All Slots\",\n            \"type\": \"main\",\n            \"index\": 1\n          }\n        ]\n      ]\n    },\n    \"Combine My Calendar with All Slots\": {\n      \"main\": [\n        [\n          {\n            \"node\": \"Check if Calendar Blocked\",\n            \"type\": \"main\",\n            \"index\": 0\n          }\n        ]\n      ]\n    },\n    \"Check if Calendar Blocked\": {\n      \"main\": [\n        [\n          {\n            \"node\": \"Return string of all available times\",\n            \"type\": \"main\",\n            \"index\": 0\n          }\n        ]\n      ]\n    },\n    \"Get Availability\": {\n      \"main\": [\n        [\n          {\n            \"node\": \"Check My Calendar\",\n            \"type\": \"main\",\n            \"index\": 0\n          },\n          {\n            \"node\": \"Generate 30 Minute Timeslots\",\n            \"type\": \"main\",\n            \"index\": 0\n          }\n        ]\n      ]\n    }\n  },\n  \"pinData\": {},\n  \"meta\": {\n    \"instanceId\": \"efb474b59b0341d7791932605bd9ff04a6c7ed9941fdd53dc4a2e4b99a6f9439\"\n  }\n}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "8892f883-aaae-4616-bb50-bbe0f9dacb23",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1440,
        1660
      ],
      "parameters": {
        "color": 3,
        "width": 520,
        "height": 480,
        "content": "检查日期名称工具"
      },
      "typeVersion": 1
    },
    {
      "id": "234b89da-9003-43d5-842a-4ecf92522b51",
      "name": "检查日期名称",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "position": [
        880,
        1480
      ],
      "parameters": {
        "name": "check_days",
        "source": "parameter",
        "workflowJson": "{\n  \"nodes\": [\n    {\n      \"parameters\": {\n        \"inputSource\": \"passthrough\"\n      },\n      \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n      \"typeVersion\": 1.1,\n      \"position\": [\n        -400,\n        -120\n      ],\n      \"id\": \"dec37e15-3695-4911-91a6-1f97018ab982\",\n      \"name\": \"When Executed by Another Workflow\"\n    },\n    {\n      \"parameters\": {\n        \"jsCode\": \"function getWeekdaysNextTwoWeeks() {\\n  const items = [];\\n  const longDayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\\n\\n  const today = new Date();\\n  const endDate = new Date();\\n  endDate.setDate(today.getDate() + 14); // 2 weeks ahead\\n\\n  const current = new Date(today);\\n\\n  while (current <= endDate) {\\n    const dayOfWeek = current.getDay(); // 0 = Sunday, 6 = Saturday\\n\\n    // Only weekdays (Mon–Fri)\\n    if (dayOfWeek >= 1 && dayOfWeek <= 5) {\\n      const dateStr = current.toISOString().split('T')[0]; // YYYY-MM-DD\\n      const output = `${longDayNames[dayOfWeek]} - ${dateStr}`;\\n\\n      items.push({\\n        json: {\\n          day: output\\n        }\\n      });\\n    }\\n\\n    current.setDate(current.getDate() + 1); // Go to next day\\n  }\\n\\n  return items;\\n}\\n\\n// Example usage:\\nreturn getWeekdaysNextTwoWeeks();\\n\"\n      },\n      \"type\": \"n8n-nodes-base.code\",\n      \"typeVersion\": 2,\n      \"position\": [\n        -180,\n        -120\n      ],\n      \"id\": \"cbbe4248-d1cc-48e3-9ea8-67a844f3de29\",\n      \"name\": \"Check Day Names\"\n    }\n  ],\n  \"connections\": {\n    \"When Executed by Another Workflow\": {\n      \"main\": [\n        [\n          {\n            \"node\": \"Check Day Names\",\n            \"type\": \"main\",\n            \"index\": 0\n          }\n        ]\n      ]\n    }\n  },\n  \"pinData\": {},\n  \"meta\": {\n    \"instanceId\": \"efb474b59b0341d7791932605bd9ff04a6c7ed9941fdd53dc4a2e4b99a6f9439\"\n  }\n}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "c052c7e4-1587-4c7e-9a8e-043c8571338d",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        180,
        1660
      ],
      "parameters": {
        "width": 1200,
        "height": 500,
        "content": "获取可用性执行。"
      },
      "typeVersion": 1
    },
    {
      "id": "b7c71153-fbd1-45ac-8dbf-d4beb241daaf",
      "name": "将输出转换为 JSON",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1240,
        1260
      ],
      "parameters": {
        "text": "={{ $json.output }}",
        "options": {
          "systemMessage": "=take in this message and output json"
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.7
    },
    {
      "id": "1f902158-5885-46d6-8d7e-26ccf116ed0a",
      "name": "面试安排器",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        520,
        1220
      ],
      "parameters": {
        "text": "={{ $json.chatInput }}",
        "options": {
          "systemMessage": "=You are a friendly AI chatbot helping users schedule meetings. Ask for Phone, email, preferred date, and time. Confirm details before booking. Time zone: Eastern.\n\nToday's date is {{ $now }}\n\n1. Use the get_availability tool to find when I am available. it will return comma separated timeslots the interviewer can meet. check the proposed time against the results. Times are in 24 hour clock times in this format.  2025-03-31T09:00:00-04:00\n3. If I am not available, look at get_availability tool again and propose a similar time where I am available\n2. use the check_days tool if the user mentions something like next tuesday so you know what date they are talking about\n3. Once a time is aggreed upon, output json in this format \n2025-03-28T13:00:00-04:00. \n4. once you have the email, phone start and end time, output only the json and nothing else\n\n{\n  \"interview\": {\n    \"email\": \"applicant@example.com\",\n    \"phone\": \"814-882-1293\",\n    \"start_datetime\": \"2025-03-28T10:00:00\",\n    \"end_datetime\": \"2025-03-28T11:00:00\"\n  }\n}\n\n## Rules\n- If the calendar is not available at the time requested, do not double book. Send a new time.\n- Interviews are all 30 minutes long\n- Do not book over another meeting\n- do not give details about what is on the interviewers calendar\n- do not converse with the user about anything else",
          "returnIntermediateSteps": true
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "ba0fb82e-a280-4392-833e-04f00a47170c",
      "name": "如果是最终输出",
      "type": "n8n-nodes-base.if",
      "position": [
        960,
        1160
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "e75b6a50-680f-4f5b-8dd3-fc93be1bc7f1",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.output }}",
              "rightValue": "start_datetime"
            },
            {
              "id": "cadd4bff-8d53-446c-8ad0-14b3fb9ab335",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.output }}",
              "rightValue": "end_datetime"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "c56bcba9-ac39-474b-a186-ceb67fa4008d",
      "name": "请求更多信息",
      "type": "n8n-nodes-base.noOp",
      "position": [
        1040,
        1400
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "efd03308-0da1-4797-b899-3d4446eba722",
      "name": "解析为 JSON",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1400,
        1500
      ],
      "parameters": {
        "jsonSchemaExample": "{\n  \"interview\": {\n    \"email\": \"applicant@example.com\",\n    \"phone\": \"814-882-1293\",\n    \"start_datetime\": \"2025-03-28T10:00:00\",\n    \"end_datetime\": \"2025-03-28T11:00:00\"\n  }\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "11abd142-d509-4459-bdf5-861dcf4263bf",
      "name": "使用 Google 设置会议",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        1640,
        1280
      ],
      "parameters": {
        "end": "={{ $json.output.interview.end_datetime }}",
        "start": "={{ $json.output.interview.start_datetime }}",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "rbreen.ynteractive@gmail.com",
          "cachedResultName": "rbreen.ynteractive@gmail.com"
        },
        "additionalFields": {
          "summary": "Interview",
          "attendees": [
            "={{ $json.output.interview.email }}"
          ],
          "description": "=I will call you at  {{ $json.output.interview.phone }}"
        }
      },
      "credentials": {
        "googleCalendarOAuth2Api": {
          "id": "nc5M45R7LyFadByw",
          "name": "Google Calendar account"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "fef5ba53-4386-4e88-9f28-8a9b5d9c928f",
      "name": "给用户的最终回复",
      "type": "n8n-nodes-base.code",
      "position": [
        1640,
        1500
      ],
      "parameters": {
        "jsCode": "const email = $('Convert Output to JSON').first().json.output.interview.email;\nconst phone = $('Convert Output to JSON').first().json.output.interview.phone;\nconst start_datetime = $('Convert Output to JSON').first().json.output.interview.start_datetime;\nconst end_datetime = $('Convert Output to JSON').first().json.output.interview.end_datetime;\n\nlet text = `✅ Interview Confirmed!\\n\\n📧 Email: ${email}\\n📞 Phone: ${phone}\\n🕒 Start: ${start_datetime}\\n🕕 End: ${end_datetime}`;\n\nreturn { text };\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a06664e2-d5d2-40a7-98a5-a3de2d775b7c",
      "name": "生成面试时间",
      "type": "n8n-nodes-base.code",
      "position": [
        1620,
        1920
      ],
      "parameters": {
        "jsCode": "function getWeekdaysNextTwoWeeks() {\n  const items = [];\n  const longDayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n\n  const today = new Date();\n  const endDate = new Date();\n  endDate.setDate(today.getDate() + 14); // 2 weeks ahead\n\n  const current = new Date(today);\n\n  while (current <= endDate) {\n    const dayOfWeek = current.getDay(); // 0 = Sunday, 6 = Saturday\n\n    // Only weekdays (Mon–Fri)\n    if (dayOfWeek >= 1 && dayOfWeek <= 5) {\n      const dateStr = current.toISOString().split('T')[0]; // YYYY-MM-DD\n      const output = `${longDayNames[dayOfWeek]} - ${dateStr}`;\n\n      items.push({\n        json: {\n          day: output\n        }\n      });\n    }\n\n    current.setDate(current.getDate() + 1); // Go to next day\n  }\n\n  return items;\n}\n\n// Example usage:\nreturn getWeekdaysNextTwoWeeks();\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f35d595e-6834-4898-bbcb-b17599d769b4",
      "name": "检查我的日历",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        420,
        1820
      ],
      "parameters": {
        "options": {
          "fields": ""
        },
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "rbreen.ynteractive@gmail.com",
          "cachedResultName": "rbreen.ynteractive@gmail.com"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "googleCalendarOAuth2Api": {
          "id": "nc5M45R7LyFadByw",
          "name": "Google Calendar account"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "29e3a097-b6f1-4a54-b943-d9ad9177b03b",
      "name": "将事件拆分为30分钟块",
      "type": "n8n-nodes-base.code",
      "position": [
        620,
        1820
      ],
      "parameters": {
        "jsCode": "const events = items.map(item => item.json);\nconst intervalMinutes = 30;\nconst timeZone = 'America/New_York';\n\nfunction formatToEastern(date) {\n  const tzDate = new Intl.DateTimeFormat('en-US', {\n    timeZone,\n    year: 'numeric',\n    month: '2-digit',\n    day: '2-digit',\n    hour: '2-digit',\n    minute: '2-digit',\n    second: '2-digit',\n    hour12: false\n  }).formatToParts(date).reduce((acc, part) => {\n    if (part.type !== 'literal') acc[part.type] = part.value;\n    return acc;\n  }, {});\n\n  const offset = getEasternOffset(date);\n  return `${tzDate.year}-${tzDate.month}-${tzDate.day}T${tzDate.hour}:${tzDate.minute}:${tzDate.second}${offset}`;\n}\n\nfunction getEasternOffset(date) {\n  const options = { timeZone, timeZoneName: 'short' };\n  const parts = new Intl.DateTimeFormat('en-US', options).formatToParts(date);\n  const tzName = parts.find(p => p.type === 'timeZoneName').value;\n  return tzName.includes('EDT') ? '-04:00' : '-05:00';\n}\n\nfunction alignToPreviousSlot(date) {\n  const aligned = new Date(date);\n  const minutes = aligned.getMinutes();\n  aligned.setMinutes(minutes < 30 ? 0 : 30, 0, 0);\n  return aligned;\n}\n\nfunction alignToNextSlot(date) {\n  const aligned = new Date(date);\n  const minutes = aligned.getMinutes();\n  if (minutes > 0 && minutes <= 30) {\n    aligned.setMinutes(30, 0, 0);\n  } else if (minutes > 30) {\n    aligned.setHours(aligned.getHours() + 1);\n    aligned.setMinutes(0, 0, 0);\n  } else {\n    aligned.setMinutes(0, 0, 0);\n  }\n  return aligned;\n}\n\nconst splitEventIntoETBlocks = (event) => {\n  const blocks = [];\n\n  let current = alignToPreviousSlot(new Date(event.start.dateTime));\n  const eventEnd = alignToNextSlot(new Date(event.end.dateTime));\n\n  while (current < eventEnd) {\n    const blockEnd = new Date(current);\n    blockEnd.setMinutes(current.getMinutes() + intervalMinutes);\n\n    blocks.push({\n      start: formatToEastern(current),\n      end: formatToEastern(blockEnd)\n    });\n\n    current = blockEnd;\n  }\n\n  return blocks;\n};\n\nlet allBlocks = [];\nfor (const event of events) {\n  if (event.start?.dateTime && event.end?.dateTime) {\n    const blocks = splitEventIntoETBlocks(event);\n    allBlocks = allBlocks.concat(blocks);\n  }\n}\n\nreturn allBlocks.map(block => ({ json: block }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f9297e8a-75dd-4f12-b0e1-d3fa372a7631",
      "name": "添加已阻止字段",
      "type": "n8n-nodes-base.set",
      "position": [
        800,
        1840
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "f1270be8-1d11-4086-8bc0-ae53c99507c1",
              "name": "start",
              "type": "string",
              "value": "={{ $json.start }}"
            },
            {
              "id": "1a5f24ff-7d0c-436d-bb0b-015fc0c85cb7",
              "name": "end",
              "type": "string",
              "value": "={{ $json.end }}"
            },
            {
              "id": "befe6645-c0c1-40eb-9ba6-eccf2a762247",
              "name": "Blocked",
              "type": "string",
              "value": "Blocked"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "8ba70f94-e9e6-44aa-b0e7-9a5294634e0e",
      "name": "生成30分钟时间段",
      "type": "n8n-nodes-base.code",
      "position": [
        440,
        2020
      ],
      "parameters": {
        "jsCode": "const slots = [];\nconst slotMinutes = 30;\nconst timeZone = 'America/New_York';\nconst businessStartHour = 9;\nconst businessEndHour = 17;\n\n// Get offset like -04:00 or -05:00\nfunction getEasternOffset(date) {\n  const options = { timeZone, timeZoneName: 'short' };\n  const parts = new Intl.DateTimeFormat('en-US', options).formatToParts(date);\n  const tz = parts.find(p => p.type === 'timeZoneName')?.value || 'EST';\n  return tz.includes('EDT') ? '-04:00' : '-05:00';\n}\n\n// Format Date as ISO with Eastern offset\nfunction formatToEasternISO(date) {\n  const formatter = new Intl.DateTimeFormat('en-CA', {\n    timeZone,\n    year: 'numeric',\n    month: '2-digit',\n    day: '2-digit',\n    hour: '2-digit',\n    minute: '2-digit',\n    second: '2-digit',\n    hour12: false,\n  });\n\n  const parts = formatter.formatToParts(date).reduce((acc, part) => {\n    if (part.type !== 'literal') acc[part.type] = part.value;\n    return acc;\n  }, {});\n\n  const offset = getEasternOffset(date);\n  return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}${offset}`;\n}\n\n// Convert a Date to the hour/minute of its Eastern time\nfunction getEasternTimeParts(date) {\n  const formatter = new Intl.DateTimeFormat('en-US', {\n    timeZone,\n    hour: '2-digit',\n    minute: '2-digit',\n    hour12: false,\n  });\n  const [hourStr, minStr] = formatter.format(date).split(':');\n  return { hour: parseInt(hourStr), minute: parseInt(minStr) };\n}\n\nconst now = new Date();\nconst endDate = new Date(now);\nendDate.setDate(now.getDate() + 7);\n\n// Set current time to 24 hours in the future\nconst current = new Date(now);\ncurrent.setHours(current.getHours() + 24);\n\n// Round to the next 30-minute block in Eastern time\nconst { minute } = getEasternTimeParts(current);\nif (minute < 30) {\n  current.setMinutes(30, 0, 0);\n} else {\n  current.setHours(current.getHours() + 1);\n  current.setMinutes(0, 0, 0);\n}\n\n// Generate 30-minute blocks only during business hours & weekdays\nwhile (current < endDate) {\n  const dayOfWeek = current.getDay(); // 0 = Sunday, 6 = Saturday\n\n  // Skip weekends\n  if (dayOfWeek !== 0 && dayOfWeek !== 6) {\n    const { hour } = getEasternTimeParts(current);\n\n    if (hour >= businessStartHour && hour < businessEndHour) {\n      const start = new Date(current);\n      const end = new Date(start);\n      end.setMinutes(start.getMinutes() + slotMinutes);\n\n      slots.push({\n        start: formatToEasternISO(start),\n        end: formatToEasternISO(end),\n      });\n    }\n  }\n\n  current.setMinutes(current.getMinutes() + slotMinutes);\n}\n\nreturn slots.map(slot => ({ json: slot }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "3ea13a0a-d496-40b8-9321-6bc3df415191",
      "name": "将我的日历与所有时间段合并",
      "type": "n8n-nodes-base.merge",
      "position": [
        780,
        2020
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "enrichInput2",
        "fieldsToMatchString": "start, end"
      },
      "typeVersion": 3
    },
    {
      "id": "ad57e0b4-43d0-4991-adc3-e325e2405e93",
      "name": "检查日历是否被阻止",
      "type": "n8n-nodes-base.if",
      "position": [
        1100,
        1820
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "af65c6c8-31c7-4f27-a073-cf7f72079882",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.Blocked }}",
              "rightValue": "Blocked"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "6e427266-1f64-4492-b4c0-30d03d6a20de",
      "name": "返回所有可用时间的字符串",
      "type": "n8n-nodes-base.code",
      "position": [
        1160,
        2000
      ],
      "parameters": {
        "jsCode": "const formatted = items.map(item => {\n  const start = item.json.start;\n  const end = item.json.end;\n  return `${start} - ${end}`;\n});\n\nconst combined = formatted.join(', ');\n\nreturn [\n  {\n    json: {\n      availableSlots: combined\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "3f26c921-2d4c-4e8a-a551-801c2a94086a",
      "name": "获取可用时间段",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        220,
        1920
      ],
      "parameters": {
        "inputSource": "passthrough"
      },
      "typeVersion": 1.1
    },
    {
      "id": "6d34f9e2-4c43-4e0b-a54d-2c8076ee6fe0",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -420,
        1160
      ],
      "parameters": {
        "color": 5,
        "width": 520,
        "height": 1000,
        "content": "如何在 n8n 中使用面试安排器工作流"
      },
      "typeVersion": 1
    },
    {
      "id": "07ef21ee-c02a-4145-a0fb-3ecc260ff585",
      "name": "当收到聊天消息时",
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "position": [
        280,
        1220
      ],
      "webhookId": "0c8f9f17-f5f3-4b5d-85e7-071ced0213ae",
      "parameters": {
        "public": true,
        "options": {}
      },
      "typeVersion": 1.1
    }
  ],
  "active": true,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "69e8aa1b-e404-44ed-aedc-7d8480e2383e",
  "connections": {
    "Parse to JSON": {
      "ai_outputParser": [
        [
          {
            "node": "Convert Output to JSON",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "If Final Output": {
      "main": [
        [
          {
            "node": "Convert Output to JSON",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond for More Info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "check day names": {
      "ai_tool": [
        [
          {
            "node": "Interview Scheduler",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Get Availability": {
      "main": [
        [
          {
            "node": "Check My Calendar",
            "type": "main",
            "index": 0
          },
          {
            "node": "Generate 30 Minute Timeslots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Blocked Field": {
      "main": [
        [
          {
            "node": "Combine My Calendar with All Slots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check My Calendar": {
      "main": [
        [
          {
            "node": "Split Events into 30 min blocks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model2": {
      "ai_languageModel": [
        [
          {
            "node": "Interview Scheduler",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model4": {
      "ai_languageModel": [
        [
          {
            "node": "Convert Output to JSON",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Interview Scheduler": {
      "main": [
        [
          {
            "node": "If Final Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Get Availability": {
      "ai_tool": [
        [
          {
            "node": "Interview Scheduler",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Respond for More Info": {
      "main": [
        []
      ]
    },
    "Window Buffer Memory2": {
      "ai_memory": [
        [
          {
            "node": "Interview Scheduler",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Convert Output to JSON": {
      "main": [
        [
          {
            "node": "Set Meeting with Google",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Final Response to User": {
      "main": [
        []
      ]
    },
    "Set Meeting with Google": {
      "main": [
        [
          {
            "node": "Final Response to User",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if Calendar Blocked": {
      "main": [
        [
          {
            "node": "Return string of all available times",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When chat message received": {
      "main": [
        [
          {
            "node": "Interview Scheduler",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate 30 Minute Timeslots": {
      "main": [
        [
          {
            "node": "Combine My Calendar with All Slots",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Split Events into 30 min blocks": {
      "main": [
        [
          {
            "node": "Add Blocked Field",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine My Calendar with All Slots": {
      "main": [
        [
          {
            "node": "Check if Calendar Blocked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

高级 - 人力资源, 人工智能

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量25
分类2
节点类型14
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

作者
Robert Breen

Robert Breen

@rbreen

Professional services consultant with over 10 years of experience solving complex business problems across industries. I specialize in n8n and process automation—designing custom workflows that integrate tools like Google Calendar, Airtable, GPT, and internal systems. Whether you need to automate scheduling, sync data, or streamline operations, I build solutions that save time and drive results.

外部链接
在 n8n.io 查看

分享此工作流