This workflow follows the Google Calendar → Slack 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 →
{
"id": "HrXWJRDS0O4PpRJ5",
"name": "Send interview reminders (10 minutes) from Google Calendar to Slack DMs",
"tags": [],
"nodes": [
{
"id": "171b7858-d94e-409f-b368-7a59a11d09b8",
"name": "Cron Trigger",
"type": "n8n-nodes-base.cron",
"position": [
-1600,
100
],
"parameters": {},
"typeVersion": 1
},
{
"id": "a569b753-23c3-4226-906e-6bfc5992b4ef",
"name": "Prepare Pings",
"type": "n8n-nodes-base.function",
"position": [
-940,
100
],
"parameters": {
"functionCode": "const now = new Date();\nconst leadMinutes = parseInt($node['Set: Config'].json['LEAD_MINUTES']);\nconst companyDomain = $node['Set: Config'].json['COMPANY_DOMAIN'];\nconst fileritems = [];\n\n// Process each calendar event\nfor (const item of items) {\n const eventData = item.json;\n \n // Skip if already started\n if (!eventData.attendees || !eventData.start || !eventData.start.dateTime) {\n continue;\n }\n \n const startTime = new Date(eventData.start.dateTime);\n const timeDiff = (startTime - now) / (1000 * 60); // minutes\n \n // Only process events starting within lead time\n\n if (timeDiff > 0 && timeDiff <= leadMinutes) {\n\n // Extract links from description\n const description = eventData.description || '';\n const cvMatch = description.match(/CV:\\s*(https?:\\/\\/[^\\s\\n]+)/i);\n const notesMatch = description.match(/Notes:\\s*(https?:\\/\\/[^\\s\\n]+)/i);\n const meetingLinkMatch = description.match(/(https?:\\/\\/[^\\s\\n]*(?:meet\\.google\\.com|zoom\\.us|teams\\.microsoft\\.com)[^\\s\\n]*)/i) || \n eventData.hangoutLink ? [eventData.hangoutLink] : null;\n\n // Process each attendee\n\n for (const attendee of eventData.attendees) {\n // Skip organizer, declined attendees, and external emails\n if (attendee.organizer || \n attendee.responseStatus === 'declined' ||\n !attendee.email) {\n continue;\n }\n \n // Create item for each valid attendee\n fileritems.push({\n eventId: eventData.id,\n eventTitle: eventData.summary || 'Interview',\n startTime: eventData.start.dateTime,\n attendeeEmail: attendee.email,\n attendeeName: attendee.displayName || attendee.email.split('@')[0],\n cvLink: cvMatch ? cvMatch[1] : null,\n notesLink: notesMatch ? notesMatch[1] : null,\n meetingLink: meetingLinkMatch ? meetingLinkMatch[0] : eventData.htmlLink,\n localStartTime: new Date(eventData.start.dateTime).toLocaleString('en-IN', {\n timeZone: $node['Set: Config'].json['TIMEZONE'],\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit'\n }),\n uniqueKey: `${eventData.id}_${attendee.email}`\n });\n }\n }\n}\n\nreturn fileritems;"
},
"typeVersion": 1
},
{
"id": "d3e1408e-d8a4-4536-bb8e-afba9eb29048",
"name": "Check Ledger",
"type": "n8n-nodes-base.function",
"position": [
-720,
100
],
"parameters": {
"functionCode": "const uniqueKey = $json.uniqueKey;\nconst sentPings = this.getWorkflowStaticData('global').sentPings || {};\n\n// Check if already sent\nconst alreadySent = sentPings[uniqueKey] || false;\n\nreturn {\n json: {\n ...$json,\n alreadySent: alreadySent\n }\n};"
},
"typeVersion": 1
},
{
"id": "979bf30e-5ada-4e3b-a523-2a66d34a174c",
"name": "If Not Already Sent",
"type": "n8n-nodes-base.if",
"position": [
-500,
100
],
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{$json.alreadySent}}"
}
]
}
},
"typeVersion": 1
},
{
"id": "f3bb5c60-887b-4263-8624-82506edf1bdf",
"name": "User Found?",
"type": "n8n-nodes-base.if",
"position": [
-280,
100
],
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{$json.ok}}",
"value2": true
}
]
}
},
"typeVersion": 1
},
{
"id": "2157b372-c81a-4563-9c38-a46bf7b16192",
"name": "Send DM",
"type": "n8n-nodes-base.slack",
"position": [
-60,
0
],
"parameters": {
"text": "\ud83d\udd59 *Interview starting in {{$node['Set: Config'].json['LEAD_MINUTES']}} minutes*\n\n*Candidate:* {{$json.eventTitle.replace('Interview', '').replace(' - ', '').trim() || 'TBD'}}\n*Time:* {{$json.localStartTime}}\n*Role:* {{$json.eventTitle.includes(' - ') ? $json.eventTitle.split(' - ')[1] : 'Interview'}}\n\n\ud83d\udcc5 <{{$json.meetingLink}}|Join Meeting>{{$json.cvLink ? '\n\ud83d\udccb <' + $json.cvLink + '|View CV>' : ''}}{{$json.notesLink ? '\n\ud83d\udcdd <' + $json.notesLink + '|View Notes>' : ''}}",
"channel": "={{$json.user.id}}",
"attachments": [],
"otherOptions": {
"mrkdwn": true
},
"authentication": "oAuth2"
},
"typeVersion": 1
},
{
"id": "63b10066-efa9-4801-a817-4a8eab96c0b4",
"name": "Post to Fallback Channel",
"type": "n8n-nodes-base.slack",
"position": [
-60,
200
],
"parameters": {
"text": "=\ud83d\udd59 *Interview Reminder - Unable to DM*\n\n*Interviewer:* <mailto:{{ $('Check Ledger').item.json.attendeeEmail }}|{{ $('Check Ledger').item.json.attendeeName }}>\n*Candidate:* {{$('Check Ledger').item.json.eventTitle.replace('Interview', '').replace(' - ', '').trim() || 'TBD'}}\n*Time:* {{ $('Check Ledger').item.json.startTime }}\n*Role:* {{$('Check Ledger').item.json.eventTitle.includes(' - ') ? $('Check Ledger').item.json.eventTitle.split(' - ')[1] : 'Interview'}}\n\n\ud83d\udcc5 <{{ $('Check Ledger').item.json.meetingLink }}|Join Meeting>\n{{$('Check Ledger').item.json.cvLink ? '\ud83d\udccb <' + $('Check Ledger').item.json.cvLink + '|View CV>' : ''}}\n{{$('Check Ledger').item.notesLink ? '\ud83d\udcdd <' + $('Check Ledger').item.notesLink + '|View Notes>' : ''}}",
"channel": "={{$node['Set: Config'].json['FALLBACK_CHANNEL']}}",
"attachments": [],
"otherOptions": {
"mrkdwn": true
},
"authentication": "oAuth2"
},
"typeVersion": 1
},
{
"id": "ddfb1f3a-98f6-4426-bbfd-5afeebd50425",
"name": "Record Ping Sent",
"type": "n8n-nodes-base.function",
"position": [
160,
100
],
"parameters": {
"functionCode": "// Record that ping was sent\nconst uniqueKey = $json.uniqueKey;\nconst staticData = this.getWorkflowStaticData('global');\n\nif (!staticData.sentPings) {\n staticData.sentPings = {};\n}\n\nstaticData.sentPings[uniqueKey] = {\n sentAt: new Date().toISOString(),\n eventId: $json.eventId,\n attendeeEmail: $json.attendeeEmail\n};\n\nreturn {\n json: {\n ...$json,\n recorded: true\n }\n};"
},
"typeVersion": 1
},
{
"id": "f30eecea-8611-4653-8d75-cda59233246c",
"name": "Set: Config",
"type": "n8n-nodes-base.set",
"position": [
-1380,
100
],
"parameters": {
"values": {
"string": [
{
"name": "CALENDAR_NAME",
"value": "Interviews"
},
{
"name": "COMPANY_DOMAIN",
"value": "domain.com"
},
{
"name": "TIMEZONE",
"value": "Asia/Kolkata"
},
{
"name": "LEAD_MINUTES",
"value": "10"
},
{
"name": "FALLBACK_CHANNEL",
"value": "#recruiting-alerts"
}
]
},
"options": {}
},
"typeVersion": 1
},
{
"id": "af6a6ed8-0370-4f7a-9573-eeec96eb001b",
"name": "Get many events",
"type": "n8n-nodes-base.googleCalendar",
"position": [
-1160,
100
],
"parameters": {
"options": {},
"calendar": {
"__rl": true,
"mode": "list",
"value": "",
"cachedResultName": ""
},
"operation": "getAll"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "97c7d32f-1785-4a9b-87fd-d3b58ddf3de5",
"connections": {
"Send DM": {
"main": [
[
{
"node": "Record Ping Sent",
"type": "main",
"index": 0
}
]
]
},
"Set: Config": {
"main": [
[
{
"node": "Get many events",
"type": "main",
"index": 0
}
]
]
},
"User Found?": {
"main": [
[
{
"node": "Send DM",
"type": "main",
"index": 0
}
],
[
{
"node": "Post to Fallback Channel",
"type": "main",
"index": 0
}
]
]
},
"Check Ledger": {
"main": [
[
{
"node": "If Not Already Sent",
"type": "main",
"index": 0
}
]
]
},
"Cron Trigger": {
"main": [
[
{
"node": "Set: Config",
"type": "main",
"index": 0
}
]
]
},
"Prepare Pings": {
"main": [
[
{
"node": "Check Ledger",
"type": "main",
"index": 0
}
]
]
},
"Get many events": {
"main": [
[
{
"node": "Prepare Pings",
"type": "main",
"index": 0
}
]
]
},
"If Not Already Sent": {
"main": [
[
{
"node": "User Found?",
"type": "main",
"index": 0
}
]
]
},
"Post to Fallback Channel": {
"main": [
[
{
"node": "Record Ping Sent",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
How this works
Ensure candidates arrive prepared and on time for interviews by automatically sending personalised reminders ten minutes before scheduled sessions, directly to their Slack direct messages. This workflow suits recruitment teams or HR professionals managing multiple hires, freeing them from manual notifications and reducing no-shows. It pulls events from Google Calendar, checks if a reminder has already been sent, locates the recipient's Slack user details, and dispatches the message via Slack, with a fallback post to a channel if the DM fails.
Use this when interviews are booked in Google Calendar and candidates are active on your team's Slack workspace, especially for high-volume or remote hiring processes. Avoid it for non-Slack users or events without associated calendar entries, as it relies on precise integration between the two. Common variations include adjusting the reminder timing to 30 minutes prior or adding custom message templates for different interview stages.
About this workflow
Send interview reminders (10 minutes) from Google Calendar to Slack DMs. Uses slack, googleCalendar. Scheduled trigger; 10 nodes.
Source: https://github.com/weblineindia/n8n-Send-interview-reminders-10-minutes-from-Google-Calendar-to-Slack-DMs/blob/main/main.json — 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.
Who’s it for
Optimize your performance review process with this automated workflow. Running daily at 8 AM, it retrieves scheduled reviews from a Google Sheet, validates upcoming sessions, processes each review, an
Send A Daily Summary Of Your Google Calendar Events To Slack. Uses googleCalendar, dateTime, slack. Scheduled trigger; 12 nodes.
This workflow will trigger daily at 6am to retrieve your day's calendar events from Google Calendar and send them as a summary message to Slack.
Busy professionals who want a quick daily update combining their calendar, weather, and top news.