This workflow follows the Execute Workflow Trigger → Google Drive 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": "__Create-doc",
"nodes": [
{
"parameters": {
"inputSource": "passthrough"
},
"id": "54bf80ac-69f1-4cb3-b807-ab7ed4c8d760",
"name": "When Executed by Another Workflow",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
0,
0
]
},
{
"parameters": {
"jsCode": "// Updated Format Document code to handle both API and scraping paths\n// Log the incoming raw data for debugging\nconsole.log(\"Format Document - Received Data:\", JSON.stringify($json));\n\n// Get the workflow execution context to trace back to original input\nconst workflowInput = $items(\"When Executed by Another Workflow\")[0]?.json || {};\nconsole.log(\"Original workflow input:\", JSON.stringify(workflowInput));\n\n// Handle multiple possible data formats\nlet parsedData = {};\nlet transcript = \"\";\nlet originalUrl = \"\";\n\n// Look for data directly in parameters field - this is how the agent passes data to tools\nconst params = $json.parameters || {};\nif (params && Object.keys(params).length > 0) {\n console.log(\"Found parameters object with keys:\", Object.keys(params));\n \n // Extract values from parameters if available\n if (params.style) parsedData.style = params.style;\n if (params.videoId) parsedData.videoId = params.videoId;\n if (params.summary) parsedData.summary = params.summary;\n if (params.transcript) {\n parsedData.transcript = params.transcript;\n transcript = params.transcript;\n }\n if (params.prompt) parsedData.prompt = params.prompt;\n if (params.URL || params.url) {\n parsedData.URL = params.URL || params.url;\n originalUrl = params.URL || params.url;\n }\n}\n\n// Check workflow input for URL if not found yet\nif (!originalUrl && workflowInput.URL) {\n originalUrl = workflowInput.URL;\n parsedData.URL = workflowInput.URL;\n}\nif (!originalUrl && workflowInput.url) {\n originalUrl = workflowInput.url;\n parsedData.URL = workflowInput.url;\n}\n\n// Check for data in query string if it exists\nif ($json.query && typeof $json.query === 'string') {\n try {\n // Attempt to parse as JSON\n const queryData = JSON.parse($json.query);\n console.log(\"Successfully parsed query as JSON:\", Object.keys(queryData));\n \n // Merge with parsedData, prioritizing existing values\n Object.keys(queryData).forEach(key => {\n if (!parsedData[key]) parsedData[key] = queryData[key];\n });\n \n // Special handling for URL\n if (queryData.URL && !originalUrl) {\n originalUrl = queryData.URL;\n parsedData.URL = queryData.URL;\n }\n } catch (e) {\n console.log(\"Query couldn't be parsed as JSON:\", e.message);\n \n // If it's not JSON but looks like a summary, store as summary\n if ($json.query.includes('Learning Objectives') || \n $json.query.includes('Presentation Structure') || \n $json.query.includes('CISCO')) {\n \n parsedData.summary = $json.query;\n // Try to determine style from content\n if (!parsedData.style) {\n parsedData.style = $json.query.includes('Learning Objectives') ? 'educational' : \n $json.query.includes('Presentation Structure') ? 'presentation' : \n $json.query.includes('CISCO') ? 'llm_prompt' : 'unknown';\n }\n }\n }\n}\n\n// Look for values in various locations with specific properties\nconst possibleFields = ['style', 'videoId', 'summary', 'transcript', 'prompt', 'URL', 'url'];\nconst possibleContainers = ['output', 'response', 'success', 'result', 'data'];\n\n// Search in top level fields\npossibleFields.forEach(field => {\n const normalized = field.toLowerCase();\n if (!parsedData[normalized] && $json[field] !== undefined && $json[field] !== null) {\n parsedData[normalized] = $json[field];\n if (normalized === 'transcript') transcript = $json[field];\n if ((normalized === 'url' || field === 'URL') && !originalUrl) {\n originalUrl = $json[field];\n parsedData.URL = $json[field];\n }\n console.log(`Found ${field} in top level`);\n }\n});\n\n// Search in nested containers\npossibleContainers.forEach(container => {\n if ($json[container] && typeof $json[container] === 'object') {\n possibleFields.forEach(field => {\n const normalized = field.toLowerCase();\n if (!parsedData[normalized] && $json[container][field] !== undefined && $json[container][field] !== null) {\n parsedData[normalized] = $json[container][field];\n if (normalized === 'transcript') transcript = $json[container][field];\n if ((normalized === 'url' || field === 'URL') && !originalUrl) {\n originalUrl = $json[container][field];\n parsedData.URL = $json[container][field];\n }\n console.log(`Found ${field} in ${container} object`);\n }\n });\n }\n});\n\n// If still no URL, try to construct it from videoId\nif (!originalUrl && parsedData.videoId) {\n originalUrl = `https://www.youtube.com/watch?v=${parsedData.videoId}`;\n parsedData.URL = originalUrl;\n console.log(\"Constructed URL from videoId:\", originalUrl);\n}\n\n// Format the content based on the style type\nfunction formatContent(content, style, videoId, transcriptText) {\n // Get current date for the document\n const currentDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD\n \n // Create a document header based on style\n let formattedContent = '';\n \n // Add document title and metadata\n if (style === 'educational') {\n formattedContent += `# Educational Summary: YouTube Video ${videoId}\\n`;\n formattedContent += `*Generated on ${currentDate}*\\n\\n`;\n formattedContent += `---\\n\\n`;\n } \n else if (style === 'llm_prompt') {\n formattedContent += `# LLM Prompt Conversion: YouTube Video ${videoId}\\n`;\n formattedContent += `*Generated on ${currentDate}*\\n\\n`;\n formattedContent += `---\\n\\n`;\n }\n else if (style === 'presentation') {\n formattedContent += `# Presentation Structure Analysis: YouTube Video ${videoId}\\n`;\n formattedContent += `*Generated on ${currentDate}*\\n\\n`;\n formattedContent += `---\\n\\n`;\n }\n else {\n formattedContent += `# Summary: YouTube Video ${videoId}\\n`;\n formattedContent += `*Generated on ${currentDate}*\\n\\n`;\n formattedContent += `---\\n\\n`;\n }\n \n // Add the main content\n if (content && content.length > 0) {\n formattedContent += content;\n } else {\n formattedContent += \"No summary content available.\";\n }\n \n // If we have a transcript and it's not already included, add it\n if (transcriptText && transcriptText.length > 0 && !content.includes(transcriptText)) {\n formattedContent += \"\\n\\n## Original Transcript\\n\\n```\\n\" + transcriptText + \"\\n```\";\n }\n \n // Add footer\n formattedContent += `\\n\\n---\\n\\n*This document was automatically generated by the YouTube Summarizer Bot.*`;\n \n return formattedContent;\n}\n\n// Set final values with fallbacks\nconst style = parsedData.style || \"unknown\";\nconst videoId = parsedData.videoId || \"unknown\";\nconst summary = parsedData.summary || parsedData.content || \"No summary content available.\";\nconst prompt = parsedData.prompt || null;\nconst url = parsedData.url || parsedData.URL || originalUrl || \"\";\n\n// CRITICAL: Ensure we have a URL for the Update Sheet node\nif (!url) {\n console.error(\"WARNING: No URL found for Update Sheet node. This will cause an error.\");\n // Try one more time to construct from videoId\n if (videoId && videoId !== \"unknown\") {\n const constructedUrl = `https://www.youtube.com/watch?v=${videoId}`;\n parsedData.URL = constructedUrl;\n console.log(\"Emergency URL construction:\", constructedUrl);\n }\n}\n\n// Log the extracted parameters\nconsole.log(\"Document Parameters:\", {\n style: style,\n videoId: videoId,\n summary_length: summary ? summary.length : 0,\n transcript_length: transcript ? transcript.length : 0,\n has_prompt: Boolean(prompt),\n prompt_length: prompt ? prompt.length : 0,\n url: url || parsedData.URL || \"MISSING\"\n});\n\n// Create the document title\nlet docTitle;\nconst customTitle = parsedData.title || null;\n\nif (customTitle) {\n docTitle = customTitle;\n} else {\n // Default titles based on style\n if (style === 'educational') {\n docTitle = `Educational Summary: YouTube ${videoId}`;\n } \n else if (style === 'llm_prompt') {\n docTitle = `LLM Prompt Conversion: YouTube ${videoId}`;\n }\n else if (style === 'presentation') {\n docTitle = `Presentation Structure Analysis: YouTube ${videoId}`;\n }\n else {\n docTitle = `Summary: YouTube ${videoId}`;\n }\n}\n\n// Format the content for Google Docs\nconst formattedContent = formatContent(summary, style, videoId, transcript);\n\n// Return the parameters for Google Docs creation with ALL available fields\nreturn {\n json: {\n title: docTitle,\n content: formattedContent,\n mimeType: \"application/vnd.google-apps.document\",\n // Original input fields preserved - ENSURE URL IS SET\n style: style,\n videoId: videoId,\n prompt: prompt,\n summary: summary,\n transcript: transcript,\n URL: url || parsedData.URL || originalUrl || `https://www.youtube.com/watch?v=${videoId}`,\n // Add any extra fields we can extract\n timestamp: new Date().toISOString(),\n // Include the original source data \n sourceData: parsedData,\n // Debug info to track URL\n _debug: {\n hadOriginalUrl: !!originalUrl,\n hadParsedUrl: !!parsedData.URL,\n finalUrl: url || parsedData.URL || originalUrl,\n urlSource: originalUrl ? \"original\" : parsedData.URL ? \"parsed\" : \"constructed\"\n }\n }\n};"
},
"id": "d6bd3030-5ea2-4c49-bc23-2df96102f016",
"name": "Format Document",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
208,
0
]
},
{
"parameters": {
"fileContent": "={{ $json.content }}",
"name": "={{ $json.title }}",
"resolveData": true,
"parents": [
"1iy25hLcBkJ_awr6qp0DDug03RFYWUTb0"
],
"options": {}
},
"id": "ed719abb-d8a8-4b5e-b1ed-b8decdff4aba",
"name": "Create Document",
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 2,
"position": [
400,
0
],
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "appendOrUpdate",
"documentId": {
"__rl": true,
"value": "1RARvRfDAb07rOHM9FULpcidicQZb4KxuhnTknFAa-cQ",
"mode": "list",
"cachedResultName": "vidSummarizer",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1RARvRfDAb07rOHM9FULpcidicQZb4KxuhnTknFAa-cQ/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": "gid=0",
"mode": "list",
"cachedResultName": "Sheet1",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1RARvRfDAb07rOHM9FULpcidicQZb4KxuhnTknFAa-cQ/edit#gid=0"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"URL": "={{ $('Format Document').first().json.URL }}",
"Summary Style": "={{ $('Format Document').first().json.style }}",
"Document URL": "={{ $json.webViewLink }}",
"Document ID": "={{ $json.id }}"
},
"matchingColumns": [
"URL",
"Summary Style"
],
"schema": [
{
"id": "URL",
"displayName": "URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Transcript",
"displayName": "Transcript",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Summary Style",
"displayName": "Summary Style",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Document URL",
"displayName": "Document URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Document ID",
"displayName": "Document ID",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"id": "41853d08-7437-49c4-bcee-487b1646b959",
"name": "Update Sheet",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
800,
0
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Get input data\nconst input = $input.item.json;\n\n// Log what we're receiving for debugging\nconsole.log(\"Prepare Response - Raw input:\", input);\n\n// Extract values safely with fallbacks\n// Fix: Use webViewLink as primary source for document URL\nconst style = input.style || \"unknown\";\nconst videoId = input.videoId || input.id || \"unknown\";\nconst docUrl = input.webViewLink || input.alternateLink || input.webContentLink || \"\";\nconst prompt = input.prompt || null;\n\n// Add more debugging to trace what's happening\nconsole.log(\"Prepare Response - Data extracted:\", {\n input_keys: Object.keys(input),\n style: style,\n videoId: videoId,\n docUrl: docUrl,\n has_prompt: Boolean(prompt),\n webViewLink: input.webViewLink, \n alternateLink: input.alternateLink,\n webContentLink: input.webContentLink,\n inputId: input.id\n});\n\n// Create a response based on the summary style\nlet responseMessage = '';\n\n// Check if we have a valid doc URL, if not provide an error message\nif (!docUrl) {\n console.log(\"WARNING: No document URL found in input\");\n responseMessage = `I processed your YouTube video (${videoId}), but I couldn't generate a valid document link. Please try again or check the system logs.`;\n} else {\n if (style === 'educational') {\n responseMessage = `I've created an Educational Summary for the YouTube video (${videoId}). You can view it here: ${docUrl}`;\n }\n else if (style === 'llm_prompt') {\n responseMessage = `I've created an LLM Prompt Conversion for the YouTube video (${videoId}). You can view the full analysis here: ${docUrl}`;\n \n // Add the extracted prompt if available\n if (prompt) {\n responseMessage += `\\n\\nHere's the CISCO-formatted prompt that you can copy and paste:\\n\\n${prompt}`;\n }\n }\n else if (style === 'presentation') {\n responseMessage = `I've created a Presentation Structure Analysis for the YouTube video (${videoId}). You can view it here: ${docUrl}`;\n }\n else {\n responseMessage = `I've created a summary for the YouTube video (${videoId}). You can view it here: ${docUrl}`;\n }\n}\n\n// Return the final response with all available data for maximum traceability\nreturn {\n json: {\n success: Boolean(docUrl),\n docUrl: docUrl,\n style: style,\n videoId: videoId,\n prompt: prompt,\n response: responseMessage,\n output: responseMessage,\n sourceData: input // Include the original input for debugging\n }\n};"
},
"id": "41ed58bd-639d-4f0c-80cf-1e95691d054a",
"name": "Prepare Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1008,
0
]
}
],
"connections": {
"When Executed by Another Workflow": {
"main": [
[
{
"node": "Format Document",
"type": "main",
"index": 0
}
]
]
},
"Format Document": {
"main": [
[
{
"node": "Create Document",
"type": "main",
"index": 0
}
]
]
},
"Create Document": {
"main": [
[
{
"node": "Update Sheet",
"type": "main",
"index": 0
}
]
]
},
"Update Sheet": {
"main": [
[
{
"node": "Prepare Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {},
"versionId": "9c0c4b16-df7e-49c6-9fe1-6fc19fbefc70",
"id": "PYCushSh8ivRbKG4",
"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.
googleDriveOAuth2ApigoogleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
__Create-doc. Uses executeWorkflowTrigger, googleDrive, googleSheets. Event-driven trigger; 5 nodes.
Source: https://github.com/MyAiAd/MyVA-prev/blob/ac00431a79390d6df43def92b40ef2291d7ce0ef/workflows/__Create-doc.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.
Intelligent URL Validation - Validates PDF URLs before attempting download, extracting filenames from URLs and generating fallback names when needed, preventing wasted processing time Binary File Hand
Gdrive-Recursion. Uses executeWorkflowTrigger, googleSheets, googleDrive. Event-driven trigger; 19 nodes.
LINE Image Handler. Uses executeWorkflowTrigger, googleSheets, httpRequest, googleDrive. Event-driven trigger; 17 nodes.
Wattlift Onboarding - Sub-G - Drive Folders. Uses executeWorkflowTrigger, googleDrive, googleSheets. Event-driven trigger; 17 nodes.
Smart-Folder2Table. Uses executeWorkflowTrigger, httpRequest, chainLlm, lmChatGroq. Event-driven trigger; 26 nodes.