This workflow follows the Execute Workflow Trigger → HTTP Request 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 →
{
"name": "Gmail-Calendar",
"nodes": [
{
"parameters": {},
"id": "cal-trigger",
"name": "Workflow Input",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1,
"position": [
240,
300
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": false
},
"combinator": "and",
"conditions": [
{
"id": "cal-1",
"leftValue": "={{ $json.calendar_action || $json.action }}",
"rightValue": "check_today",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"renameOutput": true,
"outputKey": "Check Today"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"combinator": "and",
"conditions": [
{
"id": "cal-2",
"leftValue": "={{ $json.calendar_action || $json.action }}",
"rightValue": "check_next_meeting",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"renameOutput": true,
"outputKey": "Next Meeting"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"combinator": "and",
"conditions": [
{
"id": "cal-3",
"leftValue": "={{ $json.calendar_action || $json.action }}",
"rightValue": "check_24h",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"renameOutput": true,
"outputKey": "Check 24h"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"combinator": "and",
"conditions": [
{
"id": "cal-4",
"leftValue": "={{ $json.calendar_action || $json.action }}",
"rightValue": "availability",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"renameOutput": true,
"outputKey": "Availability"
},
{
"conditions": {
"options": {
"caseSensitive": false
},
"combinator": "and",
"conditions": [
{
"id": "cal-5",
"leftValue": "={{ $json.calendar_action || $json.action }}",
"rightValue": "detect_meeting_worthy",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"renameOutput": true,
"outputKey": "Detect Meeting"
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "cal-router",
"name": "Action Router",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
460,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT * FROM calendar_events_cache WHERE event_date = CURRENT_DATE AND cache_expires_at > NOW() ORDER BY start_time ASC",
"options": {}
},
"id": "check-cache-today",
"name": "Check Today Cache",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
700,
140
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "cache-check",
"leftValue": "={{ $json.event_id }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "exists"
}
}
],
"combinator": "and"
}
},
"id": "cache-hit",
"name": "Cache Hit?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
920,
140
]
},
{
"parameters": {
"resource": "event",
"operation": "getAll",
"calendar": {
"__rl": true,
"value": "primary",
"mode": "list"
},
"returnAll": false,
"limit": 50,
"options": {
"timeMin": "={{ $now.startOf('day').toISO() }}",
"timeMax": "={{ $now.endOf('day').toISO() }}",
"singleEvents": true,
"orderBy": "startTime"
}
},
"id": "get-today-events",
"name": "Get Today Events",
"type": "n8n-nodes-base.googleCalendar",
"typeVersion": 1.2,
"position": [
1140,
200
],
"credentials": {
"googleCalendarOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const events = $input.all();\nconst results = [];\nfor (const item of events) {\n const e = item.json;\n results.push({\n json: {\n event_id: e.id || '',\n event_date: new Date().toISOString().split('T')[0],\n summary: e.summary || '',\n start_time: e.start?.dateTime || e.start?.date || '',\n end_time: e.end?.dateTime || e.end?.date || '',\n attendees: (e.attendees || []).map(a => a.email).join(','),\n location: e.location || '',\n calendar_id: 'primary'\n }\n });\n}\nreturn results.length > 0 ? results : [{ json: { message: 'No events today' } }];"
},
"id": "normalize-events",
"name": "Normalize Events",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1360,
200
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO calendar_events_cache (event_id, event_date, summary, start_time, end_time, attendees, location, calendar_id, cached_at, cache_expires_at) VALUES ($1, $2::date, $3, $4::timestamptz, $5::timestamptz, $6, $7, 'primary', NOW(), NOW() + INTERVAL '1 hour') ON CONFLICT (event_id) DO UPDATE SET summary = $3, start_time = $4::timestamptz, end_time = $5::timestamptz, attendees = $6, cached_at = NOW(), cache_expires_at = NOW() + INTERVAL '1 hour'",
"options": {
"queryParameters": "={{ $json.event_id }},={{ $json.event_date }},={{ $json.summary }},={{ $json.start_time }},={{ $json.end_time }},={{ $json.attendees }},={{ $json.location }}"
}
},
"id": "update-cache",
"name": "Update Cache",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1580,
200
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"resource": "event",
"operation": "getAll",
"calendar": {
"__rl": true,
"value": "primary",
"mode": "list"
},
"returnAll": false,
"limit": 50,
"options": {
"timeMin": "={{ $now.toISO() }}",
"timeMax": "={{ $now.plus({ hours: 24 }).toISO() }}",
"singleEvents": true,
"orderBy": "startTime"
}
},
"id": "get-24h-events",
"name": "Get 24h Events",
"type": "n8n-nodes-base.googleCalendar",
"typeVersion": 1.2,
"position": [
700,
360
],
"credentials": {
"googleCalendarOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const events = $input.all().map(i => i.json);\nconst now = new Date();\n// Find next event\nconst upcoming = events.filter(e => new Date(e.start?.dateTime || e.start?.date) > now);\nif (upcoming.length === 0) return [{ json: { message: 'No upcoming meetings in next 24h' } }];\nconst next = upcoming[0];\nreturn [{ json: { next_meeting_summary: next.summary || '', next_meeting_start: next.start?.dateTime || next.start?.date, next_meeting_end: next.end?.dateTime || next.end?.date, next_meeting_attendees: (next.attendees || []).map(a => a.email).join(','), next_meeting_location: next.location || '', total_upcoming: upcoming.length } }];"
},
"id": "find-next",
"name": "Find Next Meeting",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
920,
320
]
},
{
"parameters": {
"jsCode": "const events = $input.all().map(i => i.json);\nconst today = new Date();\nconst startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 8, 0);\nconst endOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 18, 0);\n\n// Build busy slots\nconst busy = events.filter(e => e.start?.dateTime).map(e => ({\n start: new Date(e.start.dateTime),\n end: new Date(e.end.dateTime)\n})).sort((a, b) => a.start - b.start);\n\n// Find free slots (minimum 30 min)\nconst free = [];\nlet cursor = startOfDay;\nfor (const slot of busy) {\n if (slot.start > cursor) {\n const gap = (slot.start - cursor) / 60000;\n if (gap >= 30) free.push({ from: cursor.toISOString(), to: slot.start.toISOString(), minutes: Math.round(gap) });\n }\n if (slot.end > cursor) cursor = slot.end;\n}\nif (endOfDay > cursor) {\n const gap = (endOfDay - cursor) / 60000;\n if (gap >= 30) free.push({ from: cursor.toISOString(), to: endOfDay.toISOString(), minutes: Math.round(gap) });\n}\n\nreturn [{ json: { availability_windows: free, total_free_slots: free.length, total_free_minutes: free.reduce((s, f) => s + f.minutes, 0) } }];"
},
"id": "find-availability",
"name": "Find Availability",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
920,
480
]
},
{
"parameters": {
"method": "POST",
"url": "https://super-agent-production.up.railway.app/chat",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-Token",
"value": "={{ $env.SUPER_AGENT_PASSWORD }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ message: 'Does this email warrant creating a calendar event? From: ' + ($json.sender || '') + ', Subject: ' + ($json.subject || '') + ', Snippet: ' + ($json.snippet || '') + '. Reply ONLY with JSON: {\"create_event\": true/false, \"suggested_title\": \"...\", \"suggested_date\": \"YYYY-MM-DD\", \"suggested_time\": \"HH:MM\", \"reason\": \"...\"}', session_id: 'gmail-calendar' }) }}",
"options": {
"timeout": 30000
}
},
"id": "detect-meeting",
"name": "Detect Meeting-Worthy",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
700,
600
]
},
{
"parameters": {
"jsCode": "const raw = $input.first().json.response || '';\nlet parsed;\ntry {\n const match = raw.match(/\\{[\\s\\S]*\\}/);\n parsed = JSON.parse(match ? match[0] : raw);\n} catch (e) {\n parsed = { create_event: false, reason: 'Could not parse AI response' };\n}\nreturn [{ json: parsed }];"
},
"id": "parse-meeting-response",
"name": "Parse Meeting Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
920,
600
]
}
],
"connections": {
"Workflow Input": {
"main": [
[
{
"node": "Action Router",
"type": "main",
"index": 0
}
]
]
},
"Action Router": {
"main": [
[
{
"node": "Check Today Cache",
"type": "main",
"index": 0
}
],
[
{
"node": "Get 24h Events",
"type": "main",
"index": 0
}
],
[
{
"node": "Get 24h Events",
"type": "main",
"index": 0
}
],
[
{
"node": "Get 24h Events",
"type": "main",
"index": 0
}
],
[
{
"node": "Detect Meeting-Worthy",
"type": "main",
"index": 0
}
]
]
},
"Check Today Cache": {
"main": [
[
{
"node": "Cache Hit?",
"type": "main",
"index": 0
}
]
]
},
"Cache Hit?": {
"main": [
[],
[
{
"node": "Get Today Events",
"type": "main",
"index": 0
}
]
]
},
"Get Today Events": {
"main": [
[
{
"node": "Normalize Events",
"type": "main",
"index": 0
}
]
]
},
"Normalize Events": {
"main": [
[
{
"node": "Update Cache",
"type": "main",
"index": 0
}
]
]
},
"Get 24h Events": {
"main": [
[
{
"node": "Find Next Meeting",
"type": "main",
"index": 0
}
],
[
{
"node": "Find Availability",
"type": "main",
"index": 0
}
]
]
},
"Detect Meeting-Worthy": {
"main": [
[
{
"node": "Parse Meeting Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"saveExecutionProgress": true
},
"meta": {
"templateCredsSetupCompleted": false,
"description": "Gmail Calendar Intelligence sub-workflow: checks today's events, finds next meeting, calculates availability, detects meeting-worthy emails using AI."
},
"tags": []
}
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.
googleCalendarOAuth2Apipostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Gmail-Calendar. Uses executeWorkflowTrigger, postgres, googleCalendar, httpRequest. Event-driven trigger; 12 nodes.
Source: https://github.com/gelson12/super-agent/blob/c6b7435c3ef54ed3f7432efdc42e0aed35990df8/n8n/gmail_calendar.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.
Gmail-Triage. Uses executeWorkflowTrigger, httpRequest, postgres. Event-driven trigger; 10 nodes.
Who is this for? Agencies, consultants, and service providers who conduct discovery calls and need to quickly turn conversations into professional proposals.
6_Multi-Agent_4vaEvzlaMrgovhNz. Uses postgres, httpRequest, lmChatOpenAi, outputParserStructured. Event-driven trigger; 54 nodes.
Actioning Your Meeting Next Steps Using Transcripts And Ai. Uses lmChatOpenAi, httpRequest, googleDrive, manualTrigger. Event-driven trigger; 28 nodes.
Splitout Googlecalendar. Uses lmChatOpenAi, httpRequest, googleDrive, manualTrigger. Event-driven trigger; 28 nodes.