This workflow follows the Execute Workflow Trigger → OpenAI 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 →
{
"updatedAt": "2026-01-18T19:26:33.483Z",
"createdAt": "2025-12-02T18:46:42.377Z",
"id": "ZQp61SzkePFnnurz",
"name": "Enrich Book by ID",
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"workflowInputs": {
"values": [
{
"name": "id"
}
]
}
},
"id": "c59ccf0a-891f-4aad-85f9-32084c107c92",
"typeVersion": 1.1,
"name": "Start",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"position": [
-960,
16
]
},
{
"parameters": {
"modelId": {
"__rl": true,
"value": "gpt-4.1-mini",
"mode": "list",
"cachedResultName": "GPT-4.1-MINI"
},
"responses": {
"values": [
{
"content": "=Partial Title: \"{{ $json.title }}\"\nAuthor: \"{{ $json.author }}\""
},
{
"role": "system",
"content": "=You are a book metadata recovery agent. You receive a partial book title extracted from a filename. You MUST retrieve the most likely complete and correct book title. You have access to tools, including WEB SEARCH. You will be retrieving the data for the following fields:\n\n- title\n- author\n- summary\n- isbn\n- publication_year\n- page_count\n\n\n-----------------------------\nTOOL USE \u2014 VERY IMPORTANT\n-----------------------------\nYou have access to a WEB SEARCH tool.\n\nYou MUST call the WEB SEARCH tool to find:\n- complete title if it appears incomplete or uncertain \n- author \n- isbn\n- publication_year \n- page_count \n- summary\n\nYour WEB SEARCH query MUST be:\n\n\"{{ $json.title }} {{ $json.author }} book metadata\"\n\nAfter receiving search results:\n1. Interpret all results \u2014 prioritize authoritative or majority consensus. \n2. Extract canonical metadata:\n - **title** \u2192 complete, widely recognized title \n - **author** \u2192 correct full author name \n - **isbn** \u2192 prefer ISBN-13; do NOT invent one \n - **publication_year** \u2192 first edition year unless majority reference a specific edition \n - **page_count** \u2192 choose the most common count across sources \n - **summary** \u2192 write an objective summary - should be no more than 50 words long. Reference the book by its title, not generic phrases like \"this book\" or \"this self-help book\"\n3. If results contain multiple possibilities \u2192 choose the most widely published \n4. If the search returns nothing \u2192 leave uncertain fields as `null` (except title, which MUST have your best inference)\n\n-----------------------------\nSTRICT OUTPUT FORMAT RULES\n-----------------------------\nYou MUST output ONLY a **single JSON object**. \nABSOLUTELY NO:\n- markdown \n- backticks \n- code fences \n- prose \n- explanations \n- tool call artifacts \n- \"Here is your JSON:\" prefixes \n\nThe ONLY valid output is a compact JSON object like this:\n\n{\n \"title\": \"\",\n \"author\": \"\",\n \"summary\": \"\",\n \"isbn\": \"\",\n \"publication_year\": null,\n \"page_count\": null\n}\n\nRules:\n- Use `null` for unknown numbers \n- Strings MUST be plain strings (never wrapped in extra quotes) \n- Do NOT invent ISBNs, years, or page counts \n- Summary must be factual, not fictional \n- No trailing commas \n- No additional fields \n\n-----------------------------\nFINAL INSTRUCTION\n-----------------------------\nIf you require external information \u2192 you MUST call WEB SEARCH before producing the final JSON.\n\nWhen you generate your final answer \u2192 it MUST be ONLY the JSON object and nothing else."
}
]
},
"builtInTools": {
"webSearch": {
"searchContextSize": "medium"
}
},
"options": {
"temperature": 0.1
}
},
"type": "@n8n/n8n-nodes-langchain.openAi",
"typeVersion": 2,
"position": [
-512,
16
],
"id": "5466e6ae-66fd-45d7-939f-1f1de8fd2dbe",
"name": "Enrich Book",
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// INPUT: Full response from the \"Message A Model\" node\n// GOAL: Extract the inner JSON string and return a parsed JSON object\n\n// 1. Get raw model output safely\nlet raw = \"\";\n\ntry {\n raw =\n $json.output?.[0]?.content?.[0]?.text ||\n $json.output?.[0]?.content?.[0]?.output_text ||\n $json.data ||\n \"\";\n} catch (e) {\n raw = \"\";\n}\n\nif (!raw) {\n throw new Error(\"No content found in model output\");\n}\n\n// 2. Normalize: trim, remove accidental markdown, remove code fences\nraw = raw.trim()\n .replace(/^```json/i, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n// 3. Sometimes the model wraps JSON in extra quotes \u2192 unwrap if needed\nif (\n (raw.startsWith('\"') && raw.endsWith('\"')) ||\n (raw.startsWith(\"'\") && raw.endsWith(\"'\"))\n) {\n try {\n raw = JSON.parse(raw);\n } catch (err) {\n // leave it as-is if parse fails\n }\n}\n\n// 4. Extract the FIRST \"{\" and LAST \"}\" to isolate JSON cleanly\nconst start = raw.indexOf(\"{\");\nconst end = raw.lastIndexOf(\"}\");\n\nif (start === -1 || end === -1) {\n throw new Error(\"JSON braces not found in model output\");\n}\n\n// slice out the JSON substring\nconst jsonString = raw.slice(start, end + 1);\n\n// 5. Parse JSON safely\nlet parsed;\ntry {\n parsed = JSON.parse(jsonString);\n} catch (err) {\n throw new Error(\"Failed to parse JSON: \" + err.message + \"\\nRaw: \" + jsonString);\n}\n\n// 6. Return parsed metadata as the output item\nreturn [\n {\n json: parsed\n }\n];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-160,
16
],
"id": "ae0218c8-8e0d-4fe3-9312-8636b312c5ea",
"name": "Code in JavaScript1"
},
{
"parameters": {
"operation": "update",
"tableId": "books",
"filters": {
"conditions": [
{
"keyName": "id",
"condition": "eq",
"keyValue": "={{ $('Start').item.json.id }}"
}
]
},
"fieldsUi": {
"fieldValues": [
{
"fieldId": "title",
"fieldValue": "={{ $('Code in JavaScript1').item.json.title }}"
},
{
"fieldId": "author",
"fieldValue": "={{ $('Code in JavaScript1').item.json.author }}"
},
{
"fieldId": "isbn",
"fieldValue": "={{ $('Code in JavaScript1').item.json.isbn }}"
},
{
"fieldId": "publication_year",
"fieldValue": "={{ $('Code in JavaScript1').item.json.publication_year }}"
},
{
"fieldId": "page_count",
"fieldValue": "={{ $('Code in JavaScript1').item.json.page_count }}"
},
{
"fieldId": "summary",
"fieldValue": "={{ $('Code in JavaScript1').item.json.summary }}"
}
]
}
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
64,
16
],
"id": "965704ac-2657-4cf0-a81b-872ebf3ab361",
"name": "Update a row",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "get",
"tableId": "books",
"filters": {
"conditions": [
{
"keyName": "id",
"keyValue": "={{ $json.id }}"
}
]
}
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
-736,
16
],
"id": "4defe766-dcce-409a-b392-4a30e4b49f0d",
"name": "Get a row",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"content": "## AI Book Enrichment\n for pre-existing Book (by Supabase ID)",
"height": 576,
"width": 1216,
"color": 6
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-992,
-304
],
"id": "048c9d37-ac93-49b5-a419-c2a3b41b0dc0",
"name": "Sticky Note"
}
],
"connections": {
"Start": {
"main": [
[
{
"node": "Get a row",
"type": "main",
"index": 0
}
]
]
},
"Enrich Book": {
"main": [
[
{
"node": "Code in JavaScript1",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript1": {
"main": [
[
{
"node": "Update a row",
"type": "main",
"index": 0
}
]
]
},
"Get a row": {
"main": [
[
{
"node": "Enrich Book",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"meta": {
"templateCredsSetupCompleted": true
},
"versionId": "b063f825-0177-429b-881a-ffe20d6f68e7",
"activeVersionId": "37e3109c-c1bc-4cb1-a389-0e52ac834d57",
"triggerCount": 0,
"shared": [
{
"updatedAt": "2025-12-02T18:46:42.387Z",
"createdAt": "2025-12-02T18:46:42.387Z",
"role": "workflow:owner",
"workflowId": "ZQp61SzkePFnnurz",
"projectId": "B7QJE85HA2Vij1it"
}
],
"activeVersion": {
"updatedAt": "2026-01-18T17:38:36.000Z",
"createdAt": "2026-01-18T17:38:34.190Z",
"versionId": "37e3109c-c1bc-4cb1-a389-0e52ac834d57",
"workflowId": "ZQp61SzkePFnnurz",
"nodes": [
{
"parameters": {
"workflowInputs": {
"values": [
{
"name": "id"
}
]
}
},
"id": "c59ccf0a-891f-4aad-85f9-32084c107c92",
"typeVersion": 1.1,
"name": "Start",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"position": [
-960,
16
]
},
{
"parameters": {
"modelId": {
"__rl": true,
"value": "gpt-4.1-mini",
"mode": "list",
"cachedResultName": "GPT-4.1-MINI"
},
"responses": {
"values": [
{
"content": "=Partial Title: \"{{ $json.title }}\"\nAuthor: \"{{ $json.author }}\""
},
{
"role": "system",
"content": "=You are a book metadata recovery agent. You receive a partial book title extracted from a filename. You MUST retrieve the most likely complete and correct book title. You have access to tools, including WEB SEARCH. You will be retrieving the data for the following fields:\n\n- title\n- author\n- summary\n- isbn\n- publication_year\n- page_count\n\n\n-----------------------------\nTOOL USE \u2014 VERY IMPORTANT\n-----------------------------\nYou have access to a WEB SEARCH tool.\n\nYou MUST call the WEB SEARCH tool to find:\n- complete title if it appears incomplete or uncertain \n- author \n- isbn\n- publication_year \n- page_count \n- summary\n\nYour WEB SEARCH query MUST be:\n\n\"{{ $json.title }} {{ $json.author }} book metadata\"\n\nAfter receiving search results:\n1. Interpret all results \u2014 prioritize authoritative or majority consensus. \n2. Extract canonical metadata:\n - **title** \u2192 complete, widely recognized title \n - **author** \u2192 correct full author name \n - **isbn** \u2192 prefer ISBN-13; do NOT invent one \n - **publication_year** \u2192 first edition year unless majority reference a specific edition \n - **page_count** \u2192 choose the most common count across sources \n - **summary** \u2192 write an objective summary - about 50 words long based on real sources. Reference the book by its title, not generic phrases like \"this book\" or \"this self-help book\"\n3. If results contain multiple possibilities \u2192 choose the most widely published \n4. If the search returns nothing \u2192 leave uncertain fields as `null` (except title, which MUST have your best inference)\n\n-----------------------------\nSTRICT OUTPUT FORMAT RULES\n-----------------------------\nYou MUST output ONLY a **single JSON object**. \nABSOLUTELY NO:\n- markdown \n- backticks \n- code fences \n- prose \n- explanations \n- tool call artifacts \n- \"Here is your JSON:\" prefixes \n\nThe ONLY valid output is a compact JSON object like this:\n\n{\n \"title\": \"\",\n \"author\": \"\",\n \"summary\": \"\",\n \"isbn\": \"\",\n \"publication_year\": null,\n \"page_count\": null\n}\n\nRules:\n- Use `null` for unknown numbers \n- Strings MUST be plain strings (never wrapped in extra quotes) \n- Do NOT invent ISBNs, years, or page counts \n- Summary must be factual, not fictional \n- No trailing commas \n- No additional fields \n\n-----------------------------\nFINAL INSTRUCTION\n-----------------------------\nIf you require external information \u2192 you MUST call WEB SEARCH before producing the final JSON.\n\nWhen you generate your final answer \u2192 it MUST be ONLY the JSON object and nothing else."
}
]
},
"builtInTools": {
"webSearch": {
"searchContextSize": "medium"
}
},
"options": {
"temperature": 0.1
}
},
"type": "@n8n/n8n-nodes-langchain.openAi",
"typeVersion": 2,
"position": [
-512,
16
],
"id": "5466e6ae-66fd-45d7-939f-1f1de8fd2dbe",
"name": "Enrich Book",
"credentials": {
"openAiApi": {
"id": "BRRf66J5aSwt4UDP",
"name": "OpenAi account"
}
}
},
{
"parameters": {
"jsCode": "// INPUT: Full response from the \"Message A Model\" node\n// GOAL: Extract the inner JSON string and return a parsed JSON object\n\n// 1. Get raw model output safely\nlet raw = \"\";\n\ntry {\n raw =\n $json.output?.[0]?.content?.[0]?.text ||\n $json.output?.[0]?.content?.[0]?.output_text ||\n $json.data ||\n \"\";\n} catch (e) {\n raw = \"\";\n}\n\nif (!raw) {\n throw new Error(\"No content found in model output\");\n}\n\n// 2. Normalize: trim, remove accidental markdown, remove code fences\nraw = raw.trim()\n .replace(/^```json/i, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n// 3. Sometimes the model wraps JSON in extra quotes \u2192 unwrap if needed\nif (\n (raw.startsWith('\"') && raw.endsWith('\"')) ||\n (raw.startsWith(\"'\") && raw.endsWith(\"'\"))\n) {\n try {\n raw = JSON.parse(raw);\n } catch (err) {\n // leave it as-is if parse fails\n }\n}\n\n// 4. Extract the FIRST \"{\" and LAST \"}\" to isolate JSON cleanly\nconst start = raw.indexOf(\"{\");\nconst end = raw.lastIndexOf(\"}\");\n\nif (start === -1 || end === -1) {\n throw new Error(\"JSON braces not found in model output\");\n}\n\n// slice out the JSON substring\nconst jsonString = raw.slice(start, end + 1);\n\n// 5. Parse JSON safely\nlet parsed;\ntry {\n parsed = JSON.parse(jsonString);\n} catch (err) {\n throw new Error(\"Failed to parse JSON: \" + err.message + \"\\nRaw: \" + jsonString);\n}\n\n// 6. Return parsed metadata as the output item\nreturn [\n {\n json: parsed\n }\n];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-160,
16
],
"id": "ae0218c8-8e0d-4fe3-9312-8636b312c5ea",
"name": "Code in JavaScript1"
},
{
"parameters": {
"operation": "update",
"tableId": "books",
"filters": {
"conditions": [
{
"keyName": "id",
"condition": "eq",
"keyValue": "={{ $('Start').item.json.id }}"
}
]
},
"fieldsUi": {
"fieldValues": [
{
"fieldId": "title",
"fieldValue": "={{ $('Code in JavaScript1').item.json.title }}"
},
{
"fieldId": "author",
"fieldValue": "={{ $('Code in JavaScript1').item.json.author }}"
},
{
"fieldId": "isbn",
"fieldValue": "={{ $('Code in JavaScript1').item.json.isbn }}"
},
{
"fieldId": "publication_year",
"fieldValue": "={{ $('Code in JavaScript1').item.json.publication_year }}"
},
{
"fieldId": "page_count",
"fieldValue": "={{ $('Code in JavaScript1').item.json.page_count }}"
},
{
"fieldId": "summary",
"fieldValue": "={{ $('Code in JavaScript1').item.json.summary }}"
}
]
}
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
64,
16
],
"id": "965704ac-2657-4cf0-a81b-872ebf3ab361",
"name": "Update a row",
"credentials": {
"supabaseApi": {
"id": "fdzgJDGuPA2JozKn",
"name": "Supabase account"
}
}
},
{
"parameters": {
"operation": "get",
"tableId": "books",
"filters": {
"conditions": [
{
"keyName": "id",
"keyValue": "={{ $json.id }}"
}
]
}
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
-736,
16
],
"id": "4defe766-dcce-409a-b392-4a30e4b49f0d",
"name": "Get a row",
"credentials": {
"supabaseApi": {
"id": "fdzgJDGuPA2JozKn",
"name": "Supabase account"
}
}
},
{
"parameters": {
"content": "## AI Book Enrichment\n for pre-existing Book (by Supabase ID)",
"height": 576,
"width": 1216,
"color": 6
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-992,
-304
],
"id": "048c9d37-ac93-49b5-a419-c2a3b41b0dc0",
"name": "Sticky Note"
}
],
"connections": {
"Start": {
"main": [
[
{
"node": "Get a row",
"type": "main",
"index": 0
}
]
]
},
"Enrich Book": {
"main": [
[
{
"node": "Code in JavaScript1",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript1": {
"main": [
[
{
"node": "Update a row",
"type": "main",
"index": 0
}
]
]
},
"Get a row": {
"main": [
[
{
"node": "Enrich Book",
"type": "main",
"index": 0
}
]
]
}
},
"nodeGroups": [],
"authors": "Gordan Kljajic",
"name": "Version 37e3109c",
"description": "",
"autosaved": false
},
"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.
openAiApisupabaseApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Enrich Book by ID. Uses executeWorkflowTrigger, openAi, supabase. Event-driven trigger; 6 nodes.
Source: https://github.com/adamhaley/megyk-automations/blob/main/workflows/Enrich_Book_by_ID.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.
Pitch Paul. Uses lmChatOpenRouter, telegram, outputParserStructured, supabaseTool. Event-driven trigger; 33 nodes.
Pitch Paul. Uses lmChatOpenRouter, telegram, outputParserStructured, supabaseTool. Event-driven trigger; 33 nodes.
image_generation. Uses executeWorkflowTrigger, openAi, supabase. Event-driven trigger; 6 nodes.
How it Works
The best content automation template in the market is now even better—with “deep research” on time-sensitive topics\! Unlike most n8n content automation templates that are mainly for “demo purposes,”