This workflow corresponds to n8n.io template #10211 — we link there as the canonical source.
This workflow follows the Agent → Google Drive Trigger 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": "3y5cbX89D0mFj9N5",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "AI Recommender: From Food Photo to Restaurant and Book (Google Books Integrated) ## What it does Analyzes a food photo with",
"tags": [],
"nodes": [
{
"id": "1269407f-2761-4611-af3c-d61e9fbeb614",
"name": "Google Drive Trigger",
"type": "n8n-nodes-base.googleDriveTrigger",
"position": [
1680,
800
],
"parameters": {
"event": "fileCreated",
"options": {},
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
},
"triggerOn": "specificFolder",
"folderToWatch": {
"__rl": true,
"mode": "list",
"value": "1uoky4OAFYULqTrWkkfm8JFGBGNcLaLT0",
"cachedResultUrl": "https://drive.google.com/drive/folders/1uoky4OAFYULqTrWkkfm8JFGBGNcLaLT0",
"cachedResultName": "n8n_folder"
}
},
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "d804b5e1-f574-4c81-8c57-f9de592df449",
"name": "Dish Classifier",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
2128,
800
],
"parameters": {
"text": "={\n \"model\": \"gpt-4o-mini\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You are a strict JSON generator. Always return a JSON object that includes only the following keys: dish_name, category, calories_kcal, protein_g, fat_g, carbs_g. Do not include any explanations.\"\n },\n {\n \"role\": \"user\",\n \"content\": [\n {\n \"type\": \"image_url\",\n \"image_url\": {\n \"url\": \"https://drive.google.com/uc?export=download&id={{$json.id}}\"\n }\n },\n {\n \"type\": \"text\",\n \"text\": \"From the image, return the dish name, category, and estimated nutrition per serving. Output only valid JSON.\"\n }\n ]\n }\n ],\n \"response_format\": { \"type\": \"json_object\" }\n}\n",
"options": {},
"promptType": "define"
},
"typeVersion": 2.2
},
{
"id": "d1f36934-6055-4c93-9396-fb7ef3377ddb",
"name": "Fetch Image (Drive)",
"type": "n8n-nodes-base.httpRequest",
"position": [
1904,
800
],
"parameters": {
"url": "https://drive.google.com/uc?id=1DRoHd4SVbu3lvw6pH5oWAKUf-TejnVXT&export=download",
"options": {}
},
"typeVersion": 4.3
},
{
"id": "0ed6a504-a545-4bc2-a10b-902d8d21d299",
"name": "LLM (Vision/JSON)",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"position": [
2208,
1024
],
"parameters": {
"model": "openai/gpt-5",
"options": {}
},
"credentials": {
"openRouterApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "6862ca06-be98-4a63-899b-b94f02969447",
"name": "Normalize Classification",
"type": "n8n-nodes-base.code",
"position": [
2480,
800
],
"parameters": {
"jsCode": "// --- Dish Classifier \u306e output(JSON\u6587\u5b57\u5217) \u3092\u30d1\u30fc\u30b9 ---\nlet parsed = {};\ntry {\n parsed = JSON.parse($json.output || '{}');\n} catch (e) {\n parsed = {};\n}\n\n// --- \u30ab\u30c6\u30b4\u30ea\u53d6\u5f97 ---\nconst raw = parsed.category || '';\nconst top = raw.split('/')[0].trim(); // \"\u4e2d\u83ef\" \u306e\u3088\u3046\u306b\u5148\u982d\u90e8\u5206\u3092\u62bd\u51fa\n\n// --- \u30de\u30c3\u30d4\u30f3\u30b0\u5b9a\u7fa9 ---\nconst mapping = {\n '\u548c\u98df': { type: 'restaurant', keyword: '\u548c\u98df \u5b9a\u98df \u305d\u3070 \u3046\u3069\u3093' },\n '\u6d0b\u98df': { type: 'restaurant', keyword: '\u6d0b\u98df \u30d3\u30b9\u30c8\u30ed \u30d1\u30b9\u30bf \u30cf\u30f3\u30d0\u30fc\u30b0' },\n '\u4e2d\u83ef': { type: 'restaurant', keyword: '\u4e2d\u83ef \u30e9\u30fc\u30e1\u30f3 \u9903\u5b50 \u30c1\u30e3\u30fc\u30cf\u30f3' },\n '\u30e9\u30fc\u30e1\u30f3': { type: 'restaurant', keyword: '\u30e9\u30fc\u30e1\u30f3 \u3064\u3051\u9eba \u4e2d\u83ef\u305d\u3070' },\n '\u5bff\u53f8': { type: 'restaurant', keyword: '\u5bff\u53f8 \u3059\u3057 \u9b5a\u4ecb' },\n '\u30ab\u30ec\u30fc': { type: 'restaurant', keyword: '\u30ab\u30ec\u30fc \u30b9\u30d1\u30a4\u30b9 \u30a4\u30f3\u30c9\u6599\u7406' },\n '\u30c7\u30b6\u30fc\u30c8': { type: 'cafe', keyword: '\u30b9\u30a4\u30fc\u30c4 \u30b1\u30fc\u30ad \u30d1\u30d5\u30a7 \u30c7\u30b6\u30fc\u30c8' }\n};\n\n// --- \u30de\u30c3\u30d4\u30f3\u30b0\u9069\u7528 ---\nconst conf = mapping[top] || { type: 'restaurant', keyword: top || '\u30ec\u30b9\u30c8\u30e9\u30f3' };\n\n// --- \u51fa\u529b ---\nreturn {\n json: {\n dish_name: parsed.dish_name || '\u4e0d\u660e',\n category: top || '\u672a\u5206\u985e',\n confidence: parsed.confidence || 0,\n ...conf\n }\n};"
},
"typeVersion": 2
},
{
"id": "8e43608e-0602-4cea-bab8-72e66251df8f",
"name": "Set Origin & Radius",
"type": "n8n-nodes-base.set",
"position": [
2704,
800
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "95af2f82-9212-4d14-87ac-ae42ef08f3ec",
"name": "originLabel",
"type": "string",
"value": "\u4e2d\u91ce\u99c5 \u6771\u4eac\u90fd\u4e2d\u91ce\u533a"
},
{
"id": "f2d60ee4-287e-4d7a-9e52-87248e2c550d",
"name": "originLat",
"type": "number",
"value": "=35.7073"
},
{
"id": "ddfd5750-6524-48d8-aead-11903a434972",
"name": "radiusM",
"type": "number",
"value": "=1200"
},
{
"id": "8e8d2e00-7069-4558-bc42-d3e64b307cc1",
"name": "originLng",
"type": "string",
"value": "139.6655"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "985089c9-6a40-41b4-a344-6442de7c2d20",
"name": "Search Google Places",
"type": "n8n-nodes-base.httpRequest",
"position": [
3152,
800
],
"parameters": {
"url": "https://places.googleapis.com/v1/places:searchText",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify({\n textQuery: $('Normalize Classification').item.json.category || \"\u30ec\u30b9\u30c8\u30e9\u30f3\",\n languageCode: \"ja\",\n maxResultCount: 3,\n locationBias: {\n circle: {\n center: {\n latitude: Number($('Set Origin & Radius').item.json.originLat),\n longitude: Number($('Set Origin & Radius').item.json.originLng)\n },\n radius: Number($('Set Origin & Radius').item.json.radiusM ?? 1000)\n }\n }\n}) }}\n",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "X-Goog-Api-Key",
"value": "={{ $json[\"GOOGLE_API_KEY\"] }}"
},
{
"name": "X-Goog-FieldMask",
"value": "places.displayName,places.formattedAddress,places.rating,places.id"
}
]
}
},
"typeVersion": 4.3
},
{
"id": "e42be32f-f334-4570-9de1-12efe2b94024",
"name": "Summarize Place List",
"type": "n8n-nodes-base.code",
"position": [
3376,
800
],
"parameters": {
"jsCode": "// Google Places \u306e\u51fa\u529b\u304b\u3089\u5fc5\u8981\u306a\u60c5\u5831\u3060\u3051\u62bd\u51fa\nconst places = $json.places.map(p => ({\n name: p.displayName?.text || \"\u4e0d\u660e\",\n rating: p.rating || 0,\n reviews: p.userRatingCount || 0,\n address: p.shortFormattedAddress || \"\u4f4f\u6240\u4e0d\u660e\"\n}));\n\n// AI\u306b\u6e21\u3059\u305f\u3081\u306e\u7c21\u6f54\u306a\u30ea\u30b9\u30c8\u6587\u5b57\u5217\u3092\u751f\u6210\nconst summary = places.map(\n p => `${p.name}\uff08\u8a55\u4fa1: ${p.rating}, \u30ec\u30d3\u30e5\u30fc\u6570: ${p.reviews}, \u4f4f\u6240: ${p.address}\uff09`\n).join(\"\\n\");\n\nreturn {\n json: {\n places,\n summary\n }\n};\n"
},
"typeVersion": 2
},
{
"id": "98a9cba5-0d79-4641-ac49-a4e2cdfd5a8f",
"name": "LLM (Selection)",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"position": [
3680,
1024
],
"parameters": {
"options": {}
},
"credentials": {
"openRouterApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "7c8508b1-cede-451b-9844-04e7106e2d8c",
"name": "Select Best Place (AI)",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
3600,
800
],
"parameters": {
"text": "=You are a strict evaluator. Use ONLY the data provided below.\n\nHere is the restaurant list:\n{{ $json.summary }}\n\nFrom the above list, select the single best restaurant based on:\n1. Highest rating\n2. If ratings are equal, most user reviews\n\nReturn the result ONLY in this JSON format:\n\n{\n \"best_place\": \"\u5e97\u8217\u540d\",\n \"rating\": \u6570\u5024,\n \"reviews\": \u6570\u5024,\n \"address\": \"\u4f4f\u6240\",\n \"reason\": \"\u9078\u3093\u3060\u7406\u7531\uff0830\u6587\u5b57\u4ee5\u5185\uff09\"\n}\n",
"options": {},
"promptType": "define"
},
"typeVersion": 3
},
{
"id": "c5c9a61e-d894-40ff-aba1-13d86f9af726",
"name": "Format Best Place JSON",
"type": "n8n-nodes-base.code",
"position": [
3952,
800
],
"parameters": {
"jsCode": "// AI Agent\u306e\u51fa\u529b\uff08\u6587\u5b57\u5217\uff09\u3092\u53d6\u5f97\nconst raw = $json.output || \"\";\n\n// JSON\u3092\u5b89\u5168\u306b\u30d1\u30fc\u30b9\nlet parsed = {};\ntry {\n parsed = JSON.parse(raw);\n} catch (e) {\n parsed = {};\n}\n\n// \u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u660e\u793a\u7684\u306b\u6574\u5f62\nreturn {\n best_place: parsed.best_place || \"\",\n reason: parsed.reason || \"\",\n};\n"
},
"typeVersion": 2
},
{
"id": "f721cd9f-30eb-4fc7-8810-2d02291ace90",
"name": "LLM (Books)",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"position": [
4256,
1024
],
"parameters": {
"options": {}
},
"credentials": {
"openRouterApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "99be8853-41de-44ed-8596-657a0a8fedac",
"name": "Recommend Book (AI)",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
4176,
800
],
"parameters": {
"text": "=You are a creative book curator.\nGiven the name of a restaurant, infer its theme, cuisine, or atmosphere,\nand recommend one *real* book that exists on Google Books which matches the vibe.\n\nReturn ONLY JSON (no markdown, no prose). Fill all fields (no empty strings).\n\nInput:\n{{ $json.best_place }}\n\nOutput JSON fields:\n- restaurant: echo the input restaurant name (string)\n- recommended_book.title: book title (string, non-empty, real book)\n- recommended_book.author: author name (string, non-empty)\n- reason: why this book fits the restaurant (<=100 Japanese chars)\n",
"options": {},
"promptType": "define"
},
"typeVersion": 3
},
{
"id": "b22512e2-07f1-4b25-89b0-2026305f2341",
"name": "Normalize Book JSON",
"type": "n8n-nodes-base.code",
"position": [
4528,
800
],
"parameters": {
"jsCode": "// 1) AI\u51fa\u529b\u3092\u62fe\u3046\uff08\u3059\u3067\u306b\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u306a\u3089\u305d\u308c\u3092\u4f7f\u3046\uff09\nlet raw = $json.output ?? $json.data ?? $json.text ?? $json;\nlet parsed = {};\n\n// 2) \u6587\u5b57\u5217\u306a\u3089\u524d\u5f8c\u306e\u4f59\u5206\u3092\u9664\u53bb\uff06```json\u30d5\u30a7\u30f3\u30b9\u3092\u5265\u304c\u3059\nif (typeof raw === 'string') {\n let s = raw.trim();\n s = s.replace(/^```(?:json)?/i, '').replace(/```$/, '').trim();\n try {\n parsed = JSON.parse(s);\n } catch {\n parsed = {};\n }\n} else if (typeof raw === 'object') {\n parsed = raw;\n}\n\n// 3) \u6700\u4f4e\u9650\u306e\u30ad\u30fc\u3092\u7528\u610f\uff08\u7a7a\u3067\u3082\u69cb\u9020\u306f\u4f5c\u308b\uff09\nparsed.recommended_book = parsed.recommended_book || {};\nparsed.recommended_book.title = parsed.recommended_book.title || '';\nparsed.recommended_book.author = parsed.recommended_book.author || '';\nparsed.reason = parsed.reason || '';\nparsed.restaurant = parsed.restaurant || '';\n\nreturn parsed;\n"
},
"typeVersion": 2
},
{
"id": "9af6fc3d-aabd-4f02-aa94-a96491125968",
"name": "Search Google Books",
"type": "n8n-nodes-base.googleBooks",
"position": [
4752,
800
],
"parameters": {
"resource": "volume",
"operation": "getAll",
"searchQuery": "={{ '\"' + $json.recommended_book.title + '\"' }}"
},
"credentials": {
"googleBooksOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2
},
{
"id": "41f90290-f831-43b5-ac26-d1159611763f",
"name": "Format Book Details",
"type": "n8n-nodes-base.code",
"position": [
4976,
800
],
"parameters": {
"jsCode": "// \u300cGet many volumes\u300d\u30ce\u30fc\u30c9\u306e 1 \u51fa\u529b\u76ee(run 0 / output 0)\u306e\u914d\u5217\u3092\u53d6\u5f97\nconst vols = $items(\"Search Google Books\", 0, 0);\n\n// \u30a2\u30a4\u30c6\u30e0\u304c\u306a\u3051\u308c\u3070\u7a7a\u3067\u8fd4\u3059\nif (!vols || vols.length === 0) {\n return [{ json: { title: \"\uff08\u30bf\u30a4\u30c8\u30eb\u4e0d\u660e\uff09\" } }];\n}\n\n// \u5148\u982d\u306e\u672c\u3060\u3051\u4f7f\u3046\uff08\u5fc5\u8981\u306a\u3089\u4efb\u610f\u306e\u9078\u3073\u65b9\u306b\u5909\u66f4\uff09\nconst v = vols[0].json || {};\nconst vi = v.volumeInfo || {};\n\nreturn [\n {\n json: {\n title: vi.title ?? \"\uff08\u30bf\u30a4\u30c8\u30eb\u4e0d\u660e\uff09\",\n authors: Array.isArray(vi.authors) ? vi.authors.join(\", \") : \"\uff08\u8457\u8005\u60c5\u5831\u306a\u3057\uff09\",\n publisher: vi.publisher ?? \"\uff08\u51fa\u7248\u793e\u4e0d\u660e\uff09\",\n publishedDate: vi.publishedDate ?? \"\u2015\",\n selfLink: v.selfLink ?? \"\"\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "26c9dce5-bda6-4a5e-b474-fe1148ac73f1",
"name": "Post to Slack",
"type": "n8n-nodes-base.slack",
"position": [
5200,
800
],
"parameters": {
"text": "=The dish in the photo is {{$node[\"Normalize Classification\"].json[\"dish_name\"]}}, and its category is {{$node[\"Normalize Classification\"].json[\"category\"]}}.\nThe best {{$node[\"Normalize Classification\"].json[\"category\"]}} spot around Nakano Station, Nakano City, Tokyo is {{$node[\"Format Best Place JSON\"].json[\"best_place\"]}}.\n\nA book that pairs well with this place\u2026\n\n\ud83d\udcd6 Title: {{$json.title}}\n\ud83d\udc64 Author(s): {{$json.authors}}\n\ud83c\udfe2 Publisher: {{$json.publisher}}\n\ud83d\udcc5 Published: {{$json.publishedDate}}\n\ud83d\udd17 Link: <{{$json.selfLink}}|Google Books>",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "CKUCBTG0H",
"cachedResultName": "general"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "4b6256c4-9c26-4d24-abee-0d7800b121c6",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
608,
592
],
"parameters": {
"width": 560,
"height": 960,
"content": "## AI Recommender: From Food Photo to Restaurant and Book (Google Books Integrated)\n## What it does\n\nAnalyzes a food photo with an AI vision model to extract dish name + category\n\nSearches nearby restaurants with Google Places and selects the single best (rating \u2192 reviews tie-break)\n\nFinds a matching book via Google Books and posts a tidy summary to Slack\n\n## Who it\u2019s for\n\nFoodies, bloggers, and teams who want a plug-and-play flow that turns a single food photo into a dining pick + themed reading.\n\n## How it works\n\nGoogle Drive Trigger detects a new photo\n\nDish Classifier (Vision LLM) \u2192 JSON (dish_name, category, basic macros)\n\nSearch Google Places near your origin; Select Best Place (AI)\n\nRecommend Book (AI) \u2192 Search Google Books \u2192 format details\n\nPost to Slack (JP/EN both possible)\n\n## Requirements\n\nGoogle Drive / Google Places / Google Books credentials, LLM access (OpenRouter/OpenAI), Slack OAuth.\n\n## Customize\n\nEdit origin/radius in Set Origin & Radius, tweak category\u2192keyword mapping in Normalize Classification, adjust Slack channel & message in Post to Slack."
},
"typeVersion": 1
},
{
"id": "0f73fbe4-f430-446e-b5a0-3ed56c6a04a3",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1200,
512
],
"parameters": {
"color": 7,
"width": 448,
"height": 512,
"content": "## \ud83e\udde9 Setup & Credentials\n\nConnect these **before running** the workflow:\n\n- **Google Drive Trigger** \u2192 Drive OAuth \n- **Search Google Places** \u2192 Move API key to a Credential (don\u2019t hardcode in headers) \n- **Search Google Books** \u2192 Books OAuth \n- **LLM nodes (Vision / Selection / Books)** \u2192 OpenRouter / OpenAI \n- **Post to Slack** \u2192 Slack OAuth2 \n\n### Config (Set node)\nKeep all user-editable variables centralized here:\n\n- `originLabel` (e.g., \u201cNakano Station, Tokyo\u201d) \n- `originLat`, `originLng` (coordinates) \n- `radiusM` (search radius, meters)\n\n\ud83d\udca1 *Add your own Google API key here or via Credential for security and reusability.*\n"
},
"typeVersion": 1
},
{
"id": "f9164e78-b671-475b-9f3c-2538b9b230a4",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
2064,
1168
],
"parameters": {
"color": 7,
"width": 384,
"height": 256,
"content": "## Photo \u2192 Category\n\nFetch Image (Drive) \u2013 downloads the image\n\nDish Classifier \u2013 vision prompt; returns strict JSON\n\nNormalize Classification (Code) \u2013 parses JSON safely, maps category to Places-friendly keyword/type; outputs dish_name, category, type, keyword"
},
"typeVersion": 1
},
{
"id": "9e12b78c-2ed0-40d7-9ed2-8d25dda64d89",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2976,
1056
],
"parameters": {
"color": 7,
"width": 384,
"height": 352,
"content": "## Find & Select Restaurant\n\nSet Origin & Radius (Set) \u2013 coordinates/radius\n\nSearch Google Places (HTTP POST /v1/places:searchText) \u2013 gets top candidates\n\nSummarize Place List (Code) \u2013 flattens to readable summary\n\nSelect Best Place (AI) \u2013 strictly picks 1 by rating, then user reviews\n\nFormat Best Place JSON (Code) \u2013 outputs { best_place, reason }"
},
"typeVersion": 1
},
{
"id": "dd635986-5749-40fc-b7a2-2f5217404339",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
3904,
1184
],
"parameters": {
"color": 7,
"width": 336,
"height": 304,
"content": "## Book Recommendation\n\nRecommend Book (AI) \u2013 returns recommended_book.title/author + reason\n\nNormalize Book JSON (Code) \u2013 strips code fences, guarantees fields\n\nSearch Google Books \u2013 quoted title search for precision\n\nFormat Book Details (Code) \u2013 single record: title, authors, publisher, publishedDate, selfLink"
},
"typeVersion": 1
},
{
"id": "8b4f9ed3-0ba1-4d0d-ac50-665041345636",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
4816,
1040
],
"parameters": {
"color": 7,
"width": 336,
"height": 128,
"content": "## Output\n\nPost to Slack \u2013 assembles dish/category + best place + book details"
},
"typeVersion": 1
},
{
"id": "3653a047-b561-46f0-a730-c1ebb026274a",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
5344,
1056
],
"parameters": {
"color": 7,
"width": 336,
"height": 416,
"content": "## Troubleshooting\n\nLLM returns prose, not JSON \u2192 tighten prompts; enable \u201cspecific output format\u201d; add a Code node to parse & default missing fields\n\nWrong book results \u2192 ensure quoted title / intitle: / langRestrict\n\nSlack shows undefined \u2192 execute previous nodes; verify field paths; prefer a Code step that outputs a single, flat record for Slack\n\nPlaces returns off-topic venues \u2192 adjust mapping keywords; increase maxResultCount; tweak radius or origin\n\nRate limits \u2192 slow Drive polling; add batching/schedules"
},
"typeVersion": 1
},
{
"id": "5e8d70ee-a1fc-4dc7-a0e6-be208c3c1b1d",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
5344,
608
],
"parameters": {
"color": 7,
"width": 336,
"height": 416,
"content": "## Security & Review Checklist\n\n\u2705 No hardcoded secrets in HTTP headers/body\u2014store API keys in Credentials\n\n\u2705 Remove personal IDs (private Sheet IDs, channels) before publishing\n\n\u2705 Centralize user settings in a Set (Config) node\n\n\u2705 Node names are descriptive and consistent\n\n\u2705 Add a small workflow image or short Loom for setup (recommended)"
},
"typeVersion": 1
},
{
"id": "3f180369-5d9f-4802-bae0-4ac007ed6e0a",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
4880,
368
],
"parameters": {
"color": 7,
"width": 336,
"height": 416,
"content": "## Google Books & Slack Output (quality tips)\n\nImprove Books accuracy\n\nUse quoted title: searchQuery = \"={{ '\\\"' + $json.recommended_book.title + '\\\"' }}\"\n\nIf needed, add intitle:\"{{title}}\" or restrict language via langRestrict (ja/en)\n\nSlack formatting\n\nKeep a single message with clear sections, or send JP then reply with EN using thread TS\n\nUse <URL|Label> to avoid noisy link previews; disable \u201cUnfurl Links/Media\u201d if needed"
},
"typeVersion": 1
},
{
"id": "8d2c4e24-cfd8-4b0c-bf31-2e672196923b",
"name": "Pass the API",
"type": "n8n-nodes-base.set",
"position": [
2928,
800
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "{\n \"GOOGLE_API_KEY\": \"\"\n}"
},
"typeVersion": 3.4
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "f64c9658-7481-4e4e-8ed2-083fffa4bc2d",
"connections": {
"LLM (Books)": {
"ai_languageModel": [
[
{
"node": "Recommend Book (AI)",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Pass the API": {
"main": [
[
{
"node": "Search Google Places",
"type": "main",
"index": 0
}
]
]
},
"Dish Classifier": {
"main": [
[
{
"node": "Normalize Classification",
"type": "main",
"index": 0
}
]
]
},
"LLM (Selection)": {
"ai_languageModel": [
[
{
"node": "Select Best Place (AI)",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"LLM (Vision/JSON)": {
"ai_languageModel": [
[
{
"node": "Dish Classifier",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Fetch Image (Drive)": {
"main": [
[
{
"node": "Dish Classifier",
"type": "main",
"index": 0
}
]
]
},
"Format Book Details": {
"main": [
[
{
"node": "Post to Slack",
"type": "main",
"index": 0
}
]
]
},
"Normalize Book JSON": {
"main": [
[
{
"node": "Search Google Books",
"type": "main",
"index": 0
}
]
]
},
"Recommend Book (AI)": {
"main": [
[
{
"node": "Normalize Book JSON",
"type": "main",
"index": 0
}
]
]
},
"Search Google Books": {
"main": [
[
{
"node": "Format Book Details",
"type": "main",
"index": 0
}
]
]
},
"Set Origin & Radius": {
"main": [
[
{
"node": "Pass the API",
"type": "main",
"index": 0
}
]
]
},
"Google Drive Trigger": {
"main": [
[
{
"node": "Fetch Image (Drive)",
"type": "main",
"index": 0
}
]
]
},
"Search Google Places": {
"main": [
[
{
"node": "Summarize Place List",
"type": "main",
"index": 0
}
]
]
},
"Summarize Place List": {
"main": [
[
{
"node": "Select Best Place (AI)",
"type": "main",
"index": 0
}
]
]
},
"Format Best Place JSON": {
"main": [
[
{
"node": "Recommend Book (AI)",
"type": "main",
"index": 0
}
]
]
},
"Select Best Place (AI)": {
"main": [
[
{
"node": "Format Best Place JSON",
"type": "main",
"index": 0
}
]
]
},
"Normalize Classification": {
"main": [
[
{
"node": "Set Origin & Radius",
"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.
googleBooksOAuth2ApigoogleDriveOAuth2ApiopenRouterApislackOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Analyzes a food photo with an AI vision model to extract dish name + category
Source: https://n8n.io/workflows/10211/ — 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 is this for? Agencies, consultants, and service providers who conduct discovery calls and need to quickly turn conversations into professional proposals.
YouTube Strategist. Uses formTrigger, splitOut, splitInBatches, agent. Event-driven trigger; 50 nodes.
This advanced multi-phase n8n workflow automates the complete research, analysis, and ideation pipeline for a YouTube strategist. It scrapes competitor channels, analyzes top-performing titles and thu
Before adding a new npm package as a dependency, you should know if it's actively maintained, widely used, and safe to build on. This workflow does that analysis automatically.
This n8n template builds a WhatsApp support copilot that answers **order status and product availability** from Shopify using LLM "agents," then replies to the customer in WhatsApp or routes to human