This workflow corresponds to n8n.io template #7161 — we link there as the canonical source.
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": "LvEhaVWxPx14FYiD",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "TripTeller",
"tags": [
{
"id": "TOmp11D0RTZnlXKe",
"name": "Creator Hub",
"createdAt": "2025-07-27T06:39:31.509Z",
"updatedAt": "2025-07-27T06:39:31.509Z"
}
],
"nodes": [
{
"id": "split-photos",
"name": "Split Photos Array",
"type": "n8n-nodes-base.splitOut",
"position": [
384,
112
],
"parameters": {
"options": {
"destinationFieldName": "data"
},
"fieldToSplitOut": "photos"
},
"typeVersion": 1
},
{
"id": "analyze-image",
"name": "Vision Analysis",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
896,
-16
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4o"
},
"options": {},
"resource": "image",
"inputType": "base64",
"operation": "analyze"
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.8
},
{
"id": "generate-story",
"name": "Generate Travel Story",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
1600,
112
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1",
"cachedResultName": "GPT-4.1"
},
"options": {
"topP": 1,
"temperature": 0.7,
"presence_penalty": 0,
"frequency_penalty": 0
},
"messages": {
"values": [
{
"content": "=Based on these trip photos and analysis, create an engaging travel story:\n\n{{ JSON.stringify($json.tripDays, null, 2) }}\n\nFormat the output as a structured story with:\n\n1. Trip title\n2. Brief introduction\n3. Day-by-day narrative with titles\n4. Memorable moments highlights\n5. Closing reflection"
},
{
"role": "system",
"content": "=You are a professional storyteller crafting a first-person narrative using the user's photo sequence. Write in a narrative style that brings the journey to life. Include sensory details, emotions, and cultural observations. \n\n**Structure the story by days** with creative titles for each day. Make it personal and immersive. \n\n**Context Summary**\n- Submission Time: {{ $now.toUTC()}}\n- Number of Photos: {{ $json.totalPhotos }}\n- Time Range: {{ $json.dateRange.start }} to {{ $json.dateRange.end }}\n\n**Photos Provided (chronological order):**\n\n\n**Your Task**\n- Create an immersive travel or personal story based on the moments captured in the photos\n- Title the story creatively and meaningfully\n- Write in **first-person past tense**\n- Include **sensory details** (sights, sounds, feelings, etc.)\n- Format using **Markdown**: use headings, paragraphs, and line breaks\n- Do **not** reference filenames or technical details\n- **focus** on emotion, story, and setting\n- Conclude with a reflective insight or emotional takeaway\n\n**Tone & Style Options**\nChoose one based on image mood:\n- Warm and nostalgic\n- Playful and lighthearted\n- Poetic and introspective\n- Adventurous and energetic\n\n**Output Format**\nTitle: <Title for Day 1>\n<Story for the tripDay 1>\n\nTitle: <Title for Day 2>\n<Story for the tripDay 2>\n\nTitle: <Title for Day 3>\n<Story for the tripDay 3>\n\nSummary: <Summarize all trip days in less than 500 words>\n\n"
}
]
},
"jsonOutput": true
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.8
},
{
"id": "format-response",
"name": "Format Final Response",
"type": "n8n-nodes-base.set",
"position": [
2016,
112
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "success",
"name": "success",
"type": "boolean",
"value": true
},
{
"id": "storybook",
"name": "storybook",
"type": "object",
"value": "={{ $json.message.content }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "1bc0b873-5253-4768-8c82-5e33e8834a25",
"name": "Convert to File",
"type": "n8n-nodes-base.convertToFile",
"position": [
656,
-16
],
"parameters": {
"options": {
"fileName": "={{ $json.data.filename }}"
},
"operation": "toBinary",
"sourceProperty": "data.data"
},
"typeVersion": 1.1
},
{
"id": "edit-fields",
"name": "Get Photos Array",
"type": "n8n-nodes-base.set",
"position": [
112,
112
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "photos",
"name": "photos",
"type": "array",
"value": "={{ $item(\"0\").$node[\"Photo Upload Webhook\"].json[\"body\"][\"photos\"] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "ed9d8186-d2fe-462d-bc6d-1471e7ef59ce",
"name": "Set Photo Attribs",
"type": "n8n-nodes-base.set",
"position": [
656,
384
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "df19c366-e65d-4614-a3c5-f1a317b3c1e7",
"name": "filename",
"type": "string",
"value": "={{ $json.data.filename }}"
},
{
"id": "1177488a-ba24-4a04-9cfc-fde61cbfbeb0",
"name": "size",
"type": "number",
"value": "={{ $json.data.size }}"
},
{
"id": "6c83cb35-d22e-4074-b685-a5a229f3edb0",
"name": "timestamp",
"type": "string",
"value": "={{ $json.data.lastModified }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "9108850e-3a7a-482d-9f6c-de02e39d2da1",
"name": "Combine",
"type": "n8n-nodes-base.merge",
"position": [
1136,
112
],
"parameters": {
"mode": "combine",
"options": {
"clashHandling": {
"values": {
"resolveClash": "preferLast"
}
}
},
"combineBy": "combineByPosition"
},
"typeVersion": 3.2
},
{
"id": "0dfdbab7-3250-4f33-9340-7ee247b0d30c",
"name": "Group Photos by Day",
"type": "n8n-nodes-base.code",
"position": [
1344,
112
],
"parameters": {
"jsCode": "// Group photos by date\nconst photosByDate = {};\n\nfor (const item of $input.all()) {\n const dateFormatted = new Date (item.json.timestamp).toDateString();\n const date = dateFormatted;\n if (!photosByDate[date]) {\n photosByDate[date] = [];\n }\n photosByDate[date].push(item.json);\n}\n\n// Sort dates and create day entries\nconst sortedDates = Object.keys(photosByDate).sort();\nconst dayEntries = [];\n\nfor (const date of sortedDates) {\n const photos = photosByDate[date];\n \n // Sort photos by timestamp within each day\n photos.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));\n \n // Extract themes for the day\n const dayThemes = [...new Set(photos.flatMap(p => p.themes))];\n \n dayEntries.push({\n date: date,\n dayNumber: sortedDates.indexOf(date) + 1,\n photos: photos,\n themes: dayThemes,\n photoCount: photos.length\n });\n}\n\nreturn [{\n json: {\n tripDays: dayEntries,\n totalDays: dayEntries.length,\n totalPhotos: $input.all().length,\n dateRange: {\n start: sortedDates[0],\n end: sortedDates[sortedDates.length - 1]\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "1867d2c7-1505-4ac6-926a-d072a5dcc219",
"name": "Photo Upload Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-160,
112
],
"parameters": {
"path": "tripteller-upload",
"options": {},
"httpMethod": "POST",
"responseMode": "lastNode"
},
"typeVersion": 2.1
},
{
"id": "3e8ff04a-792d-4a04-99d7-e6b10c8aa105",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-256,
-112
],
"parameters": {
"width": 304,
"height": 416,
"content": "\ud83d\udccc ENTRY POINT\n- Listens for POST requests at `/tripteller-upload`\n- Expects photos array in request body"
},
"typeVersion": 1
},
{
"id": "c35d16d4-226b-4a38-9214-3b9bbc7aca47",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
64,
-112
],
"parameters": {
"height": 416,
"content": "\ud83d\udccc DATA EXTRACTION\n- Extracts 'photos' array from webhook payload\n- Creates clean data structure for downstream processing\n- Input: body.photos from webhook\n- Output: Structured photos array"
},
"typeVersion": 1
},
{
"id": "617d4322-bddb-4c82-92aa-649c85ebc714",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
320,
-112
],
"parameters": {
"height": 416,
"content": "\ud83d\udccc PHOTO SEPARATION\n- Splits photos array into individual items\n- Each photo becomes separate workflow execution\n- Field: 'photos' \u2192 destination: 'data'"
},
"typeVersion": 1
},
{
"id": "fe77f420-c0b9-4a87-8c83-9b98b4530fdf",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
576,
-256
],
"parameters": {
"height": 400,
"content": "\ud83d\udccc BINARY CONVERSION\n- Converts base64 photo data to binary format\n- Required for OpenAI Analyze Image operation\n- Preserves original filename from upload\n- Input: data.data \u2192 Output: Binary file object"
},
"typeVersion": 1
},
{
"id": "54a27f54-7b44-48ff-ad0c-b00e402a641d",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
576,
160
],
"parameters": {
"height": 368,
"content": "\ud83d\udccc METADATA EXTRACTION\n- Captures: filename, size, timestamp (lastModified)\n- Creates photo metadata for story context\n- Runs parallel with file conversion\n- Essential for chronological ordering"
},
"typeVersion": 1
},
{
"id": "9ffe7801-4a33-4015-b2cd-22a1b7a9874d",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
832,
-256
],
"parameters": {
"height": 400,
"content": "\ud83d\udccc AI IMAGE ANALYSIS\n- Model: GPT-4o (Vision enabled)\n- Analyzes photo content, objects, scenes, emotions\n- Input: Binary image file\n- Output: Detailed photo description and themes\n- Credentials: OpenAI API account"
},
"typeVersion": 1
},
{
"id": "e1619f17-e377-43fd-8c92-c9b7ecd65cda",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
1264,
-256
],
"parameters": {
"width": 256,
"height": 544,
"content": "\ud83d\udccc CHRONOLOGICAL ORGANIZATION\n- JavaScript function groups photos by date\n- Sorts photos within each day by timestamp\n- Extracts themes for each day\n- Creates structured trip days with:\n - Date, day number, photo count\n - Sorted photos, combined themes\n - Total days and date range\n"
},
"typeVersion": 1
},
{
"id": "b073140d-6aec-4314-8f4f-35cbe692aae9",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
1536,
-256
],
"parameters": {
"width": 352,
"height": 544,
"content": "\ud83d\udccc AI STORY CREATION\n- Model: GPT-4O for advanced narrative generation\n- Input: Grouped photo data with analysis\n- Prompts for:\n - First-person past tense narrative\n - Day-by-day structure with creative titles\n - Sensory details and emotional depth\n - Markdown formatting\n- Temperature: 0.7 (creative but focused)\n- JSON output format"
},
"typeVersion": 1
},
{
"id": "59dcd8e4-8970-464c-a002-aef80df9dd49",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
1904,
-48
],
"parameters": {
"width": 304,
"height": 336,
"content": "\ud83d\udccc OUTPUT FORMATTING\n- Creates final response structure\n- Sets success: true\n- Packages story content in 'storybook' object\n- Ready for client consumption"
},
"typeVersion": 1
}
],
"active": true,
"settings": {
"callerPolicy": "workflowsFromSameOwner",
"executionOrder": "v1",
"executionTimeout": 300,
"saveDataErrorExecution": "all",
"saveDataSuccessExecution": "all"
},
"versionId": "496dfebf-ab04-45dd-a011-2695e4baaa83",
"connections": {
"Combine": {
"main": [
[
{
"node": "Group Photos by Day",
"type": "main",
"index": 0
}
]
]
},
"Convert to File": {
"main": [
[
{
"node": "Vision Analysis",
"type": "main",
"index": 0
}
]
]
},
"Vision Analysis": {
"main": [
[
{
"node": "Combine",
"type": "main",
"index": 0
}
]
]
},
"Get Photos Array": {
"main": [
[
{
"node": "Split Photos Array",
"type": "main",
"index": 0
}
]
]
},
"Set Photo Attribs": {
"main": [
[
{
"node": "Combine",
"type": "main",
"index": 1
}
]
]
},
"Split Photos Array": {
"main": [
[
{
"node": "Convert to File",
"type": "main",
"index": 0
},
{
"node": "Set Photo Attribs",
"type": "main",
"index": 0
}
]
]
},
"Group Photos by Day": {
"main": [
[
{
"node": "Generate Travel Story",
"type": "main",
"index": 0
}
]
]
},
"Photo Upload Webhook": {
"main": [
[
{
"node": "Get Photos Array",
"type": "main",
"index": 0
}
]
]
},
"Format Final Response": {
"main": [
[]
]
},
"Generate Travel Story": {
"main": [
[
{
"node": "Format Final Response",
"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.
openAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow is designed for travel bloggers, content creators, social media managers, and anyone who wants to transform their travel photos into engaging written narratives. It's perfect for travelers looking to create compelling stories from their photo collections without…
Source: https://n8n.io/workflows/7161/ — 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.
This powerful n8n automation workflow is designed to execute advanced B2B lead enrichment and hyper-personalization for cold email outreach. By orchestrating a complex chain of data scraping, AI analy
Eu Clara – Funil Kiwify Completo. Uses postgres, openAi, httpRequest, gmail. Webhook trigger; 70 nodes.
This workflow bridges the gap between raw product data and revenue sales tools. It automates the entire Product Qualified Lead (PQL) lifecycle—from real-time intent routing to churn prevention—reducin
Lua Nova - Sistema Completo. Uses postgres, httpRequest, openAi. Webhook trigger; 55 nodes.
User Signup & Verification: The workflow starts when a user signs up. It generates a verification code and sends it via SMS using Twilio. Code Validation: The user replies with the code. The workflow