Inverview Scheduler
工作流概述
这是一个包含25个节点的复杂工作流,主要用于自动化处理各种任务。
工作流源代码
{
"id": "bh3H2b654RSYgIm9",
"meta": {
"instanceId": "efb474b59b0341d7791932605bd9ff04a6c7ed9941fdd53dc4a2e4b99a6f9439",
"templateCredsSetupCompleted": true
},
"name": "Inverview Scheduler",
"tags": [],
"nodes": [
{
"id": "cd5664f9-0b6b-491a-a0a0-1d8b3b2f2461",
"name": "OpenAI Chat Model2",
"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": "Window Buffer Memory2",
"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 Chat Model4",
"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": "Run Get Availability",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"position": [
720,
1520
],
"parameters": {
"name": "get_availability",
"source": "parameter",
"description": "Call this tool to get my availability",
"workflowJson": "{
\"nodes\": [
{
\"parameters\": {
\"operation\": \"getAll\",
\"calendar\": {
\"__rl\": true,
\"value\": \"rbreen.ynteractive@gmail.com\",
\"mode\": \"list\",
\"cachedResultName\": \"rbreen.ynteractive@gmail.com\"
},
\"returnAll\": true,
\"options\": {
\"fields\": \"\"
}
},
\"type\": \"n8n-nodes-base.googleCalendar\",
\"typeVersion\": 1.3,
\"position\": [
-500,
220
],
\"id\": \"a1017705-8866-469f-83e0-9f5d5f37af53\",
\"name\": \"Check My Calendar\",
\"credentials\": {
\"googleCalendarOAuth2Api\": {
\"id\": \"nc5M45R7LyFadByw\",
\"name\": \"Google Calendar account\"
}
}
},
{
\"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\"
},
\"type\": \"n8n-nodes-base.code\",
\"typeVersion\": 2,
\"position\": [
-280,
240
],
\"id\": \"fb9063c2-de6b-4513-8901-d12625f5d772\",
\"name\": \"Split Events into 30 min blocks\"
},
{
\"parameters\": {
\"assignments\": {
\"assignments\": [
{
\"id\": \"f1270be8-1d11-4086-8bc0-ae53c99507c1\",
\"name\": \"start\",
\"value\": \"={{ $json.start }}\",
\"type\": \"string\"
},
{
\"id\": \"1a5f24ff-7d0c-436d-bb0b-015fc0c85cb7\",
\"name\": \"end\",
\"value\": \"={{ $json.end }}\",
\"type\": \"string\"
},
{
\"id\": \"befe6645-c0c1-40eb-9ba6-eccf2a762247\",
\"name\": \"Blocked\",
\"value\": \"Blocked\",
\"type\": \"string\"
}
]
},
\"options\": {}
},
\"type\": \"n8n-nodes-base.set\",
\"typeVersion\": 3.4,
\"position\": [
-80,
240
],
\"id\": \"23d8ed50-131f-49ea-9ce8-72a0067fe828\",
\"name\": \"Add Blocked Field\"
},
{
\"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\"
},
\"type\": \"n8n-nodes-base.code\",
\"typeVersion\": 2,
\"position\": [
-400,
460
],
\"id\": \"01597a94-d94b-47e7-9488-adea3abb741c\",
\"name\": \"Generate 30 Minute Timeslots\"
},
{
\"parameters\": {
\"mode\": \"combine\",
\"fieldsToMatchString\": \"start, end\",
\"joinMode\": \"enrichInput2\",
\"options\": {}
},
\"type\": \"n8n-nodes-base.merge\",
\"typeVersion\": 3,
\"position\": [
180,
300
],
\"id\": \"2d9f98a1-02ac-4332-a288-635a48ea3ee8\",
\"name\": \"Combine My Calendar with All Slots\"
},
{
\"parameters\": {
\"conditions\": {
\"options\": {
\"caseSensitive\": true,
\"leftValue\": \"\",
\"typeValidation\": \"strict\",
\"version\": 2
},
\"conditions\": [
{
\"id\": \"af65c6c8-31c7-4f27-a073-cf7f72079882\",
\"leftValue\": \"={{ $json.Blocked }}\",
\"rightValue\": \"Blocked\",
\"operator\": {
\"type\": \"string\",
\"operation\": \"notEquals\"
}
}
],
\"combinator\": \"and\"
},
\"options\": {}
},
\"type\": \"n8n-nodes-base.if\",
\"typeVersion\": 2.2,
\"position\": [
420,
280
],
\"id\": \"0438b5be-b3c4-4645-9604-303ace7bfead\",
\"name\": \"Check if Calendar Blocked\"
},
{
\"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\"
},
\"type\": \"n8n-nodes-base.code\",
\"typeVersion\": 2,
\"position\": [
660,
300
],
\"id\": \"4a6bfde4-7d9f-4837-bc6c-66bf968e782a\",
\"name\": \"Return string of all available times\"
},
{
\"parameters\": {
\"inputSource\": \"passthrough\"
},
\"type\": \"n8n-nodes-base.executeWorkflowTrigger\",
\"typeVersion\": 1.1,
\"position\": [
-760,
340
],
\"id\": \"8bde95cb-7239-4b7d-aca1-0adacf2ea257\",
\"name\": \"Get Availability\"
}
],
\"connections\": {
\"Check My Calendar\": {
\"main\": [
[
{
\"node\": \"Split Events into 30 min blocks\",
\"type\": \"main\",
\"index\": 0
}
]
]
},
\"Split Events into 30 min blocks\": {
\"main\": [
[
{
\"node\": \"Add Blocked Field\",
\"type\": \"main\",
\"index\": 0
}
]
]
},
\"Add Blocked Field\": {
\"main\": [
[
{
\"node\": \"Combine My Calendar with All Slots\",
\"type\": \"main\",
\"index\": 0
}
]
]
},
\"Generate 30 Minute Timeslots\": {
\"main\": [
[
{
\"node\": \"Combine My Calendar with All Slots\",
\"type\": \"main\",
\"index\": 1
}
]
]
},
\"Combine My Calendar with All Slots\": {
\"main\": [
[
{
\"node\": \"Check if Calendar Blocked\",
\"type\": \"main\",
\"index\": 0
}
]
]
},
\"Check if Calendar Blocked\": {
\"main\": [
[
{
\"node\": \"Return string of all available times\",
\"type\": \"main\",
\"index\": 0
}
]
]
},
\"Get Availability\": {
\"main\": [
[
{
\"node\": \"Check My Calendar\",
\"type\": \"main\",
\"index\": 0
},
{
\"node\": \"Generate 30 Minute Timeslots\",
\"type\": \"main\",
\"index\": 0
}
]
]
}
},
\"pinData\": {},
\"meta\": {
\"instanceId\": \"efb474b59b0341d7791932605bd9ff04a6c7ed9941fdd53dc4a2e4b99a6f9439\"
}
}"
},
"typeVersion": 2.1
},
{
"id": "8892f883-aaae-4616-bb50-bbe0f9dacb23",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1440,
1660
],
"parameters": {
"color": 3,
"width": 520,
"height": 480,
"content": "Check Day Names Tool
1. This part of the flow is just a copy of what is embedded in the \"Check Day Names Tool\". It does not run.
2. If you update this part of the flow, copy it with ctrl-c and paste it into another workbook. Add a sub-workflow execution. Set the workflow to accept all data. Copy the flow. Paste the Workflow JSON field in the \"Check Day Names Tool\" tool node
"
},
"typeVersion": 1
},
{
"id": "234b89da-9003-43d5-842a-4ecf92522b51",
"name": "check day names",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"position": [
880,
1480
],
"parameters": {
"name": "check_days",
"source": "parameter",
"workflowJson": "{
\"nodes\": [
{
\"parameters\": {
\"inputSource\": \"passthrough\"
},
\"type\": \"n8n-nodes-base.executeWorkflowTrigger\",
\"typeVersion\": 1.1,
\"position\": [
-400,
-120
],
\"id\": \"dec37e15-3695-4911-91a6-1f97018ab982\",
\"name\": \"When Executed by Another Workflow\"
},
{
\"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\"
},
\"type\": \"n8n-nodes-base.code\",
\"typeVersion\": 2,
\"position\": [
-180,
-120
],
\"id\": \"cbbe4248-d1cc-48e3-9ea8-67a844f3de29\",
\"name\": \"Check Day Names\"
}
],
\"connections\": {
\"When Executed by Another Workflow\": {
\"main\": [
[
{
\"node\": \"Check Day Names\",
\"type\": \"main\",
\"index\": 0
}
]
]
}
},
\"pinData\": {},
\"meta\": {
\"instanceId\": \"efb474b59b0341d7791932605bd9ff04a6c7ed9941fdd53dc4a2e4b99a6f9439\"
}
}"
},
"typeVersion": 2.1
},
{
"id": "c052c7e4-1587-4c7e-9a8e-043c8571338d",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
180,
1660
],
"parameters": {
"width": 1200,
"height": 500,
"content": "Get Availability Execution.
1. This part of the flow is just a copy of what is embedded in the \"Run Get Availability Tool\". It does not run.
2. If you update this part of the flow, copy it with ctrl-c and paste it into another workbook. Add a sub-workflow execution. Set the workflow to accept all data. Copy the flow. Paste the Workflow JSON field in the \"Run Get Availability\" tool node"
},
"typeVersion": 1
},
{
"id": "b7c71153-fbd1-45ac-8dbf-d4beb241daaf",
"name": "Convert Output to 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": "Interview Scheduler",
"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.
Today's date is {{ $now }}
1. 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
3. If I am not available, look at get_availability tool again and propose a similar time where I am available
2. use the check_days tool if the user mentions something like next tuesday so you know what date they are talking about
3. Once a time is aggreed upon, output json in this format
2025-03-28T13:00:00-04:00.
4. once you have the email, phone start and end time, output only the json and nothing else
{
\"interview\": {
\"email\": \"applicant@example.com\",
\"phone\": \"814-882-1293\",
\"start_datetime\": \"2025-03-28T10:00:00\",
\"end_datetime\": \"2025-03-28T11:00:00\"
}
}
## Rules
- If the calendar is not available at the time requested, do not double book. Send a new time.
- Interviews are all 30 minutes long
- Do not book over another meeting
- do not give details about what is on the interviewers calendar
- do not converse with the user about anything else",
"returnIntermediateSteps": true
},
"promptType": "define"
},
"typeVersion": 1.7
},
{
"id": "ba0fb82e-a280-4392-833e-04f00a47170c",
"name": "If Final Output",
"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": "Respond for More Info",
"type": "n8n-nodes-base.noOp",
"position": [
1040,
1400
],
"parameters": {},
"typeVersion": 1
},
{
"id": "efd03308-0da1-4797-b899-3d4446eba722",
"name": "Parse to JSON",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
1400,
1500
],
"parameters": {
"jsonSchemaExample": "{
\"interview\": {
\"email\": \"applicant@example.com\",
\"phone\": \"814-882-1293\",
\"start_datetime\": \"2025-03-28T10:00:00\",
\"end_datetime\": \"2025-03-28T11:00:00\"
}
}"
},
"typeVersion": 1.2
},
{
"id": "11abd142-d509-4459-bdf5-861dcf4263bf",
"name": "Set Meeting with 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": "Final Response to User",
"type": "n8n-nodes-base.code",
"position": [
1640,
1500
],
"parameters": {
"jsCode": "const email = $('Convert Output to JSON').first().json.output.interview.email;
const phone = $('Convert Output to JSON').first().json.output.interview.phone;
const start_datetime = $('Convert Output to JSON').first().json.output.interview.start_datetime;
const end_datetime = $('Convert Output to JSON').first().json.output.interview.end_datetime;
let text = `✅ Interview Confirmed!\n\n📧 Email: ${email}\n📞 Phone: ${phone}\n🕒 Start: ${start_datetime}\n🕕 End: ${end_datetime}`;
return { text };
"
},
"typeVersion": 2
},
{
"id": "a06664e2-d5d2-40a7-98a5-a3de2d775b7c",
"name": "Generate Interview Times",
"type": "n8n-nodes-base.code",
"position": [
1620,
1920
],
"parameters": {
"jsCode": "function getWeekdaysNextTwoWeeks() {
const items = [];
const longDayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const today = new Date();
const endDate = new Date();
endDate.setDate(today.getDate() + 14); // 2 weeks ahead
const current = new Date(today);
while (current <= endDate) {
const dayOfWeek = current.getDay(); // 0 = Sunday, 6 = Saturday
// Only weekdays (Mon–Fri)
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
const dateStr = current.toISOString().split('T')[0]; // YYYY-MM-DD
const output = `${longDayNames[dayOfWeek]} - ${dateStr}`;
items.push({
json: {
day: output
}
});
}
current.setDate(current.getDate() + 1); // Go to next day
}
return items;
}
// Example usage:
return getWeekdaysNextTwoWeeks();
"
},
"typeVersion": 2
},
{
"id": "f35d595e-6834-4898-bbcb-b17599d769b4",
"name": "Check My Calendar",
"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": "Split Events into 30 min blocks",
"type": "n8n-nodes-base.code",
"position": [
620,
1820
],
"parameters": {
"jsCode": "const events = items.map(item => item.json);
const intervalMinutes = 30;
const timeZone = 'America/New_York';
function formatToEastern(date) {
const tzDate = new Intl.DateTimeFormat('en-US', {
timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).formatToParts(date).reduce((acc, part) => {
if (part.type !== 'literal') acc[part.type] = part.value;
return acc;
}, {});
const offset = getEasternOffset(date);
return `${tzDate.year}-${tzDate.month}-${tzDate.day}T${tzDate.hour}:${tzDate.minute}:${tzDate.second}${offset}`;
}
function getEasternOffset(date) {
const options = { timeZone, timeZoneName: 'short' };
const parts = new Intl.DateTimeFormat('en-US', options).formatToParts(date);
const tzName = parts.find(p => p.type === 'timeZoneName').value;
return tzName.includes('EDT') ? '-04:00' : '-05:00';
}
function alignToPreviousSlot(date) {
const aligned = new Date(date);
const minutes = aligned.getMinutes();
aligned.setMinutes(minutes < 30 ? 0 : 30, 0, 0);
return aligned;
}
function alignToNextSlot(date) {
const aligned = new Date(date);
const minutes = aligned.getMinutes();
if (minutes > 0 && minutes <= 30) {
aligned.setMinutes(30, 0, 0);
} else if (minutes > 30) {
aligned.setHours(aligned.getHours() + 1);
aligned.setMinutes(0, 0, 0);
} else {
aligned.setMinutes(0, 0, 0);
}
return aligned;
}
const splitEventIntoETBlocks = (event) => {
const blocks = [];
let current = alignToPreviousSlot(new Date(event.start.dateTime));
const eventEnd = alignToNextSlot(new Date(event.end.dateTime));
while (current < eventEnd) {
const blockEnd = new Date(current);
blockEnd.setMinutes(current.getMinutes() + intervalMinutes);
blocks.push({
start: formatToEastern(current),
end: formatToEastern(blockEnd)
});
current = blockEnd;
}
return blocks;
};
let allBlocks = [];
for (const event of events) {
if (event.start?.dateTime && event.end?.dateTime) {
const blocks = splitEventIntoETBlocks(event);
allBlocks = allBlocks.concat(blocks);
}
}
return allBlocks.map(block => ({ json: block }));
"
},
"typeVersion": 2
},
{
"id": "f9297e8a-75dd-4f12-b0e1-d3fa372a7631",
"name": "Add Blocked Field",
"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": "Generate 30 Minute Timeslots",
"type": "n8n-nodes-base.code",
"position": [
440,
2020
],
"parameters": {
"jsCode": "const slots = [];
const slotMinutes = 30;
const timeZone = 'America/New_York';
const businessStartHour = 9;
const businessEndHour = 17;
// Get offset like -04:00 or -05:00
function getEasternOffset(date) {
const options = { timeZone, timeZoneName: 'short' };
const parts = new Intl.DateTimeFormat('en-US', options).formatToParts(date);
const tz = parts.find(p => p.type === 'timeZoneName')?.value || 'EST';
return tz.includes('EDT') ? '-04:00' : '-05:00';
}
// Format Date as ISO with Eastern offset
function formatToEasternISO(date) {
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
const parts = formatter.formatToParts(date).reduce((acc, part) => {
if (part.type !== 'literal') acc[part.type] = part.value;
return acc;
}, {});
const offset = getEasternOffset(date);
return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}${offset}`;
}
// Convert a Date to the hour/minute of its Eastern time
function getEasternTimeParts(date) {
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone,
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
const [hourStr, minStr] = formatter.format(date).split(':');
return { hour: parseInt(hourStr), minute: parseInt(minStr) };
}
const now = new Date();
const endDate = new Date(now);
endDate.setDate(now.getDate() + 7);
// Set current time to 24 hours in the future
const current = new Date(now);
current.setHours(current.getHours() + 24);
// Round to the next 30-minute block in Eastern time
const { minute } = getEasternTimeParts(current);
if (minute < 30) {
current.setMinutes(30, 0, 0);
} else {
current.setHours(current.getHours() + 1);
current.setMinutes(0, 0, 0);
}
// Generate 30-minute blocks only during business hours & weekdays
while (current < endDate) {
const dayOfWeek = current.getDay(); // 0 = Sunday, 6 = Saturday
// Skip weekends
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
const { hour } = getEasternTimeParts(current);
if (hour >= businessStartHour && hour < businessEndHour) {
const start = new Date(current);
const end = new Date(start);
end.setMinutes(start.getMinutes() + slotMinutes);
slots.push({
start: formatToEasternISO(start),
end: formatToEasternISO(end),
});
}
}
current.setMinutes(current.getMinutes() + slotMinutes);
}
return slots.map(slot => ({ json: slot }));
"
},
"typeVersion": 2
},
{
"id": "3ea13a0a-d496-40b8-9321-6bc3df415191",
"name": "Combine My Calendar with All Slots",
"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": "Check if Calendar Blocked",
"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": "Return string of all available times",
"type": "n8n-nodes-base.code",
"position": [
1160,
2000
],
"parameters": {
"jsCode": "const formatted = items.map(item => {
const start = item.json.start;
const end = item.json.end;
return `${start} - ${end}`;
});
const combined = formatted.join(', ');
return [
{
json: {
availableSlots: combined
}
}
];
"
},
"typeVersion": 2
},
{
"id": "3f26c921-2d4c-4e8a-a551-801c2a94086a",
"name": "Get Availability",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"position": [
220,
1920
],
"parameters": {
"inputSource": "passthrough"
},
"typeVersion": 1.1
},
{
"id": "6d34f9e2-4c43-4e0b-a54d-2c8076ee6fe0",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-420,
1160
],
"parameters": {
"color": 5,
"width": 520,
"height": 1000,
"content": "How to Use the Interview Scheduler Workflow in n8n
________________________________________
✨ Overview
This workflow allows candidates to schedule interviews by chatting with an AI assistant. It checks your Google Calendar availability, identifies free 30-minute weekday slots between 9am-5pm EST, and automatically books a meeting once details are confirmed.
________________________________________
⚡ Prerequisites
1. OpenAI Account
o API Key with GPT-4o model access
2. Google Account with Calendar Access
o Your calendar must be accessible via Google Calendar
3. OAuth2 Credentials for Google Calendar API configured in n8n
4. OpenAI Credentials configured in n8n
________________________________________
🔐 API Credentials Setup
Google Calendar OAuth2:
• Create a project called n8n in google cloud console
• Go to n8n > Credentials
• Create new Google Calendar OAuth2 API credentials
• Authorize your Google account (e.g., yourname@gmail.com)
OpenAI:
• Go to Credentials
• Create new OpenAI API credentials
• Enter your OpenAI API key and give it a label (e.g., \"My OpenAI Key\")
________________________________________
🔧 How to Make It Yours
✅ Update These Workflow Fields:
1. Google Calendar Email
o Replace all instances of rbreen.ynteractive@gmail.com with your own Google Calendar email.
o This appears in:
Google Calendar Nodes
ToolWorkflow JSON for \"Run Get Availability\"
2. Google Calendar OAuth2 Credential Name
o Replace credential name Google Calendar account with your own credential name.
3. OpenAI Credential Name
o Replace OpenAi account with your own OpenAI credential name.
4. Webhook URL / Chat Interface
o Go to the Candidate Chat node
o Copy the webhook URL
o Share this public link with users to start the chatbot
5. System Message Instructions (Optional)
o You can tweak the system message in the Interview Scheduler agent node to change tone, questions, or rules.
6. Custom Branding (Optional)
o Update the title and subtitle in the Candidate Chat node under options
o You can also replace the final message in Final Response to User with your own branding/tone
________________________________________
"
},
"typeVersion": 1
},
{
"id": "07ef21ee-c02a-4145-a0fb-3ecc260ff585",
"name": "When chat message received",
"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
}
]
]
}
}
}
功能特点
- 自动检测新邮件
- AI智能内容分析
- 自定义分类规则
- 批量处理能力
- 详细的处理日志
技术分析
节点类型及作用
- @N8N/N8N Nodes Langchain.Lmchatopenai
- @N8N/N8N Nodes Langchain.Memorybufferwindow
- @N8N/N8N Nodes Langchain.Toolworkflow
- Stickynote
- @N8N/N8N Nodes Langchain.Agent
复杂度评估
配置难度:
维护难度:
扩展性:
实施指南
前置条件
- 有效的Gmail账户
- n8n平台访问权限
- Google API凭证
- AI分类服务订阅
配置步骤
- 在n8n中导入工作流JSON文件
- 配置Gmail节点的认证信息
- 设置AI分类器的API密钥
- 自定义分类规则和标签映射
- 测试工作流执行
- 配置定时触发器(可选)
关键参数
| 参数名称 | 默认值 | 说明 |
|---|---|---|
| maxEmails | 50 | 单次处理的最大邮件数量 |
| confidenceThreshold | 0.8 | 分类置信度阈值 |
| autoLabel | true | 是否自动添加标签 |
最佳实践
优化建议
- 定期更新AI分类模型以提高准确性
- 根据邮件量调整处理批次大小
- 设置合理的分类置信度阈值
- 定期清理过期的分类规则
安全注意事项
- 妥善保管API密钥和认证信息
- 限制工作流的访问权限
- 定期审查处理日志
- 启用双因素认证保护Gmail账户
性能优化
- 使用增量处理减少重复工作
- 缓存频繁访问的数据
- 并行处理多个邮件分类任务
- 监控系统资源使用情况
故障排除
常见问题
邮件未被正确分类
检查AI分类器的置信度阈值设置,适当降低阈值或更新训练数据。
Gmail认证失败
确认Google API凭证有效且具有正确的权限范围,重新进行OAuth授权。
调试技巧
- 启用详细日志记录查看每个步骤的执行情况
- 使用测试邮件验证分类逻辑
- 检查网络连接和API服务状态
- 逐步执行工作流定位问题节点
错误处理
工作流包含以下错误处理机制:
- 网络超时自动重试(最多3次)
- API错误记录和告警
- 处理失败邮件的隔离机制
- 异常情况下的回滚操作