This workflow follows the Agent → Chat 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 →
{
"meta": {
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "173ca923-85be-44b1-b786-c2bd752d7b58",
"name": "OpenAI Chat Model2",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
140,
-340
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini"
},
"options": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "43fd8892-abe7-4d68-b37a-2e63bbb5d9d7",
"name": "Window Buffer Memory2",
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"position": [
340,
-340
],
"parameters": {
"sessionKey": "={{ $json.sessionId }}",
"sessionIdType": "customKey",
"contextWindowLength": 10
},
"typeVersion": 1.3
},
{
"id": "b505d8f2-f49b-4879-a5e0-2a39dad4266b",
"name": "OpenAI Chat Model4",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
1040,
-380
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini"
},
"options": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "ea6d87da-a17a-4660-84d1-7f2201bd9760",
"name": "Run Get Availability",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"position": [
540,
-300
],
"parameters": {
"name": "get_availability",
"source": "parameter",
"description": "Call this tool to get my availability",
"workflowJson": "{\n \"nodes\": [\n {\n \"parameters\": {\n \"operation\": \"getAll\",\n \"calendar\": {\n \"__rl\": true,\n \"value\": \"user@example.com\",\n \"mode\": \"list\",\n \"cachedResultName\": \"user@example.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": "b1dc95fc-af28-4944-818f-5c4f6bc542f3",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1260,
-160
],
"parameters": {
"color": 3,
"width": 520,
"height": 480,
"content": "\ud83d\ude80 Deployment Steps\n\nTo deploy the interview scheduler, import the provided workflow JSON into your n8n instance. Update the Google Calendar email, OpenAI and Google credential labels, system prompts, and branding as needed. Test the connections to ensure the API credentials are working correctly. Once everything is configured, copy and share the public chat URL from the Candidate Chat node. When candidates engage with the chat, the workflow will walk them through the interview booking process, check your availability, and finalize the booking automatically.\n\n\ud83d\udca1 Additional Tips\n\nBy default, the workflow avoids scheduling interviews on weekends and outside of 9\u20135 EST. Each interview lasts exactly 30 minutes, and overlapping with existing events is prevented. The assistant does not reveal details about other meetings. You can customize every part of this workflow to fit your use case, including subworkflows like Get Availability and check day names, or even white-label it for client use. This workflow is ready to become your AI-powered interview scheduling assistant.\n\nI\u2019m Robert Breen, founder of Ynteractive \u2014 a consulting firm that helps businesses automate operations using n8n, AI agents, and custom workflows. I\u2019ve helped clients build everything from intelligent chatbots to complex sales automations, and I\u2019m always excited to collaborate or support new projects.\n\nIf you found this workflow helpful or want to talk through an idea, I\u2019d love to hear from you.\n\n\ud83c\udf10 Website: https://www.ynteractive.com\n\n\ud83d\udcfa YouTube: @ynteractivetraining\n\n\ud83d\udcbc LinkedIn: https://www.linkedin.com/in/robert-breen\n\n\ud83d\udcec Email: rbreen@ynteractive.com\n\nRobert Breen\n\nRobert Breen\n\nRobert Breen\n\nDavide\n\nDvir Sharon\n\nLucas Peyrin\n\nn8n Team\n\nLucas Peyrin\n\nJimleuk\n\nOur customer\u2019s words, not ours.\n\nSkeptical? Try it out, and see for yourself.\n\nBuild complex workflows that other tools can't. I used other tools before. I got to know the N8N and I say it properly: it is better to do everything on the n8n! Congratulations on your work, you are a star!\n\n@igordisco\n\nThank you to the n8n community. I did the beginners course and promptly took an automation WAY beyond my skill level.\n\n@robm\n\nn8n is a beast for automation. self-hosting and low-code make it a dev\u2019s dream. if you\u2019re not automating yet, you\u2019re working too hard.\n\n@Anderoav\n\nI've said it many times. But I'll say it again. n8n is the GOAT. Anything is possible with n8n. You just need some technical knowledge + imagination. I'm actually looking to start a side project. Just to have an excuse to use n8n more \ud83d\ude05\n\n@maximpoulsen\n\nIt blows my mind. I was hating on no-code tools my whole life, but n8n changed everything. Made a Slack agent that can basically do everything, in half an hour.\n\n@felixleber\n\nI just have to say, n8n's integration with third-party services is absolutely mind-blowing. It's like having a Swiss Army knife for automation. So many tasks become a breeze, and I can quickly validate and implement my ideas without any hassle.\n\n@1ronben\n\nFound the holy grail of automation yesterday... Yesterday I tried n8n and it blew my mind \ud83e\udd2f What would've taken me 3 days to code from scratch? Done in 2 hours. The best part? If you still want to get your hands dirty with code (because let's be honest, we developers can't help ourselves \ud83d\ude05), you can just drop in custom code nodes. Zero restrictions.\n\n@francois-la\u00dfl\n\nAnything is possible with n8n. I think @n8n_io Cloud version is great, they are doing amazing stuff and I love that everything is available to look at on Github.\n\n@jodiem\n\nAutomate without limits\n\nImprint\n\n|\n\nSecurity\n\n|\n\nPrivacy\n\n|\n\nReport a vulnerability\n\n\u00a9 2025 n8n \u00a0 | \u00a0 All rights reserved."
},
"typeVersion": 1
},
{
"id": "346728e6-54c1-44b4-88f7-1059187e1bc3",
"name": "check day names",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"position": [
700,
-340
],
"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\u2013Fri)\\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": "b335dba5-1cd4-42f4-9535-f0ab6c5e614c",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
-160
],
"parameters": {
"width": 1200,
"height": 500,
"content": "Get Availability Execution. \n\n1. This part of the flow is just a copy of what is embedded in the \"Run Get Availability Tool\". It does not run. \n\n2. 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": "4e6195d3-0eb2-4bf0-8f58-2f8702fcf516",
"name": "Convert Output to JSON",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
1060,
-560
],
"parameters": {
"text": "={{ $json.output }}",
"options": {
"systemMessage": "=take in this message and output json"
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 1.7
},
{
"id": "00b034aa-f6a5-4b7c-9431-ec70989a27f8",
"name": "Interview Scheduler",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
300,
-620
],
"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": "cf239d5d-4fd9-4cc0-af69-18f1877d9599",
"name": "If Final Output",
"type": "n8n-nodes-base.if",
"position": [
780,
-660
],
"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": "c10d7577-826e-4de2-9603-abdd205029d3",
"name": "Respond for More Info",
"type": "n8n-nodes-base.noOp",
"position": [
860,
-420
],
"parameters": {},
"typeVersion": 1
},
{
"id": "890a4d28-9822-4edf-9244-9ab29da40bd5",
"name": "Parse to JSON",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
1220,
-320
],
"parameters": {
"jsonSchemaExample": "{\n \"interview\": {\n \"email\": \"user@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": "8372f453-5d15-47d9-b82e-2fee0520fed4",
"name": "Set Meeting with Google",
"type": "n8n-nodes-base.googleCalendar",
"position": [
1460,
-540
],
"parameters": {
"end": "={{ $json.output.interview.end_datetime }}",
"start": "={{ $json.output.interview.start_datetime }}",
"calendar": {
"__rl": true,
"mode": "list",
"value": "user@example.com",
"cachedResultName": "user@example.com"
},
"additionalFields": {
"summary": "Interview",
"attendees": [
"={{ $json.output.interview.email }}"
],
"description": "=I will call you at {{ $json.output.interview.phone }}"
}
},
"credentials": {
"googleCalendarOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "00f65c0a-0f27-4d9c-b520-6d77aa29ba4a",
"name": "Final Response to User",
"type": "n8n-nodes-base.code",
"position": [
1460,
-320
],
"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 = `\u2705 Interview Confirmed!\\n\\n\ud83d\udce7 Email: ${email}\\n\ud83d\udcde Phone: ${phone}\\n\ud83d\udd52 Start: ${start_datetime}\\n\ud83d\udd55 End: ${end_datetime}`;\n\nreturn { text };\n"
},
"typeVersion": 2
},
{
"id": "90babae7-212f-4b34-8fcf-c33475f99286",
"name": "Generate Interview Times",
"type": "n8n-nodes-base.code",
"position": [
1440,
100
],
"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\u2013Fri)\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": "52c1da22-4d17-4f4e-a602-5d6b252b41ae",
"name": "Check My Calendar",
"type": "n8n-nodes-base.googleCalendar",
"position": [
240,
0
],
"parameters": {
"options": {
"fields": ""
},
"calendar": {
"__rl": true,
"mode": "list",
"value": "user@example.com",
"cachedResultName": "user@example.com"
},
"operation": "getAll",
"returnAll": true
},
"credentials": {
"googleCalendarOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "1aafa569-974e-454d-baff-4e10ce15d3bd",
"name": "Split Events into 30 min blocks",
"type": "n8n-nodes-base.code",
"position": [
440,
0
],
"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": "250af200-d60e-4529-8286-9f4e890209b3",
"name": "Add Blocked Field",
"type": "n8n-nodes-base.set",
"position": [
620,
20
],
"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": "584fd926-fe7c-4b45-8aae-8cf16516b8dd",
"name": "Generate 30 Minute Timeslots",
"type": "n8n-nodes-base.code",
"position": [
260,
200
],
"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": "a129d8d8-1b2f-4a0f-9969-99b08218850d",
"name": "Combine My Calendar with All Slots",
"type": "n8n-nodes-base.merge",
"position": [
600,
200
],
"parameters": {
"mode": "combine",
"options": {},
"joinMode": "enrichInput2",
"fieldsToMatchString": "start, end"
},
"typeVersion": 3
},
{
"id": "f876b012-f691-4a7c-9dd9-8a59a0bef1ab",
"name": "Check if Calendar Blocked",
"type": "n8n-nodes-base.if",
"position": [
920,
0
],
"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": "ab6d8934-1e77-488c-9c14-d9bb62880651",
"name": "Return string of all available times",
"type": "n8n-nodes-base.code",
"position": [
980,
180
],
"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": "7cb1bd88-3db2-4670-affe-67d5376840af",
"name": "Get Availability",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"position": [
40,
100
],
"parameters": {
"inputSource": "passthrough"
},
"typeVersion": 1.1
},
{
"id": "2b4a8deb-19a5-4b96-9683-1edc4202525e",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-600,
-660
],
"parameters": {
"color": 5,
"width": 520,
"height": 1000,
"content": "How to Use the Interview Scheduler Workflow in n8n\n________________________________________\n\u2728 Overview\nThis 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.\n________________________________________\n\u26a1 Prerequisites\n1.\tOpenAI Account\no\tAPI Key with GPT-4o model access\n2.\tGoogle Account with Calendar Access\no\tYour calendar must be accessible via Google Calendar\n3.\tOAuth2 Credentials for Google Calendar API configured in n8n\n4.\tOpenAI Credentials configured in n8n\n________________________________________\n\ud83d\udd10 API Credentials Setup\nGoogle Calendar OAuth2:\n\u2022\tCreate a project called n8n in google cloud console\n\u2022\tGo to n8n > Credentials\n\u2022\tCreate new Google Calendar OAuth2 API credentials\n\u2022\tAuthorize your Google account (e.g., yourname@gmail.com)\nOpenAI:\n\u2022\tGo to Credentials\n\u2022\tCreate new OpenAI API credentials\n\u2022\tEnter your OpenAI API key and give it a label (e.g., \"My OpenAI Key\")\n________________________________________\n\ud83d\udd27 How to Make It Yours\n\u2705 Update These Workflow Fields:\n1.\tGoogle Calendar Email\no\tReplace all instances of rbreen.ynteractive@gmail.com with your own Google Calendar email.\no\tThis appears in:\n\uf0a7\tGoogle Calendar Nodes\n\uf0a7\tToolWorkflow JSON for \"Run Get Availability\"\n2.\tGoogle Calendar OAuth2 Credential Name\no\tReplace credential name Google Calendar account with your own credential name.\n3.\tOpenAI Credential Name\no\tReplace OpenAi account with your own OpenAI credential name.\n4.\tWebhook URL / Chat Interface\no\tGo to the Candidate Chat node\no\tCopy the webhook URL\no\tShare this public link with users to start the chatbot\n5.\tSystem Message Instructions (Optional)\no\tYou can tweak the system message in the Interview Scheduler agent node to change tone, questions, or rules.\n6.\tCustom Branding (Optional)\no\tUpdate the title and subtitle in the Candidate Chat node under options\no\tYou can also replace the final message in Final Response to User with your own branding/tone\n________________________________________\n\n\n"
},
"typeVersion": 1
},
{
"id": "d8208fb3-fe8e-4958-bc46-751ef193e705",
"name": "When chat message received",
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
"position": [
100,
-600
],
"parameters": {
"public": true,
"options": {}
},
"typeVersion": 1.1
},
{
"id": "2e243c03-7d21-401a-8e9a-70eece0592ed",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
-660,
-820
],
"parameters": {
"color": 5,
"width": 640,
"height": 140,
"content": "## Automated Interview Scheduling with GPT-4o and Google Calendar Chat Bot\n\n** Feel free to contact me if you need help implementing (rbreen@ynteractive.com) **"
},
"typeVersion": 1
}
],
"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
}
]
]
},
"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
}
]
]
},
"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
}
]
]
}
}
}
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.
googleCalendarOAuth2ApiopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Template 3363. Uses lmChatOpenAi, memoryBufferWindow, toolWorkflow, agent. Event-driven trigger; 26 nodes.
Source: https://gist.github.com/shraey96/944256a00933bccbfba2aeb6fdc23676 — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Agent Nodes. Uses lmChatOpenAi, slack, stopAndError, errorTrigger. Event-driven trigger; 72 nodes.
Who is this for? Agencies, consultants, and service providers who conduct discovery calls and need to quickly turn conversations into professional proposals.
Template Carnaval - time instagram. Uses toolWorkflow, lmChatOpenAi, memoryBufferWindow, agent. Event-driven trigger; 56 nodes.
Splitout Redis. Uses executeWorkflowTrigger, n8n, redis, splitOut. Event-driven trigger; 46 nodes.
3770. Uses executeWorkflowTrigger, n8n, redis, agent. Event-driven trigger; 46 nodes.