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
LINE Image Handler. Uses executeWorkflowTrigger, googleSheets, httpRequest, googleDrive. Event-driven trigger; 17 nodes.
PCN. Uses googleSheets, httpRequest, @n-octo-n/n8n-nodes-json-database, itemLists. Event-driven trigger; 60 nodes.
The workflow automates the process of gathering extensive keyword data for a "Main Keyword." It starts by reading initial parameters from a Google Sheets template, creates a new dedicated Google Sheet
🔥 March Sale – n8n Community Members Get ideoGener8r for Just $27! (Reg. $47) Use Coupon Code: (Valid until 3/31/2025 for n8n community members)