{ "nodes": [ { "id": "36816ae7-414a-482e-8a50-021885237273", "name": "Event Type", "type": "n8n-nodes-base.switch", "position": [ -220, -140 ], "parameters": { "rules": { "values": [ { "outputKey": "row.updated", "conditions": { "options": { "version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict" }, "combinator": "and", "conditions": [ { "id": "2162daf8-d23d-4b8f-8257-bdfc5400a3a8", "operator": { "name": "filter.operator.equals", "type": "string", "operation": "equals" }, "leftValue": "={{ $json.event_type }}", "rightValue": "row.updated" } ] }, "renameOutput": true }, { "outputKey": "field.created", "conditions": { "options": { "version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict" }, "combinator": "and", "conditions": [ { "id": "48e112f6-afe8-40bf-b673-b37446934a62", "operator": { "name": "filter.operator.equals", "type": "string", "operation": "equals" }, "leftValue": "={{ $json.event_type }}", "rightValue": "field.created" } ] }, "renameOutput": true }, { "outputKey": "field.updated", "conditions": { "options": { "version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict" }, "combinator": "and", "conditions": [ { "id": "5aa258cd-15c2-4156-a32d-afeed662a38e", "operator": { "name": "filter.operator.equals", "type": "string", "operation": "equals" }, "leftValue": "={{ $json.event_type }}", "rightValue": "field.updated" } ] }, "renameOutput": true } ] }, "options": {} }, "typeVersion": 3.2 }, { "id": "920ca6d8-7a6e-4482-b003-fa643f550a85", "name": "Get Prompt Fields", "type": "n8n-nodes-base.code", "position": [ -900, -140 ], "parameters": { "jsCode": "const fields = $input.first().json.fields\n .filter(item => item.description)\n .map((item, idx) => ({\n id: item.id,\n order: idx,\n name: item.name,\n type: item.type,\n description: item.description,\n }));\n\nreturn { json: { fields } };" }, "typeVersion": 2 }, { "id": "3b73b2f5-9081-4633-911f-ef3041600a00", "name": "Get File Data", "type": "n8n-nodes-base.httpRequest", "position": [ 1220, 320 ], "parameters": { "url": "={{ $json.File[0].url }}", "options": {} }, "typeVersion": 4.2 }, { "id": "e96edca8-9e8b-4ca4-bef9-dae673d3aba4", "name": "Extract from File", "type": "n8n-nodes-base.extractFromFile", "position": [ 1380, 320 ], "parameters": { "options": {}, "operation": "pdf" }, "typeVersion": 1 }, { "id": "b5c2b87b-5756-4810-84c9-34ea420bdcef", "name": "Get Result", "type": "n8n-nodes-base.set", "position": [ 2000, 380 ], "parameters": { "options": {}, "assignments": { "assignments": [ { "id": "63d7c52e-d5bf-4f4c-9e37-1d5feaea20f4", "name": "id", "type": "string", "value": "={{ $('Row Reference').item.json.id }}" }, { "id": "3ad72567-1d17-4910-b916-4c34a43b1060", "name": "={{ $('Event Ref').first().json.field.name }}", "type": "string", "value": "={{ $json.text.trim() }}" } ] } }, "typeVersion": 3.4 }, { "id": "a5cb0510-620b-469d-bf66-26ab64d6f88f", "name": "Loop Over Items", "type": "n8n-nodes-base.splitInBatches", "position": [ 800, 220 ], "parameters": { "options": {} }, "typeVersion": 3 }, { "id": "20e24946-59d8-4b19-bfab-eebb02f7e46d", "name": "Row Reference", "type": "n8n-nodes-base.noOp", "position": [ 980, 320 ], "parameters": {}, "typeVersion": 1 }, { "id": "4090c53e-e635-4421-ab2b-475bfc62cea4", "name": "Generate Field Value", "type": "@n8n/n8n-nodes-langchain.chainLlm", "position": [ 1540, 320 ], "parameters": { "text": "=\n{{ $json.text }}\n\n\nData to extract: {{ $('Event Ref').first().json.field.description }}\noutput format is: {{ $('Event Ref').first().json.field.type }}", "messages": { "messageValues": [ { "message": "=You assist the user in extracting the required data from the given file.\n* Keep you answer short.\n* If you cannot extract the requested data, give you response as \"n/a\"." } ] }, "promptType": "define" }, "typeVersion": 1.5 }, { "id": "582d4008-4871-4798-bc24-abf774ad29b5", "name": "Fields to Update", "type": "n8n-nodes-base.code", "position": [ 1560, -300 ], "parameters": { "jsCode": "const row = $('Row Ref').first().json;\nconst fields = $('Get Prompt Fields').first().json.fields;\nconst missingFields = fields\n .filter(field => field.description && !row[field.name]);\n\nreturn missingFields;" }, "typeVersion": 2 }, { "id": "051c6a99-cec3-42df-9de7-47cb69b51682", "name": "Loop Over Items1", "type": "n8n-nodes-base.splitInBatches", "position": [ 820, -420 ], "parameters": { "options": {} }, "typeVersion": 3 }, { "id": "f559c8ff-2ee5-478d-84ee-6b0ca2fe2050", "name": "Row Ref", "type": "n8n-nodes-base.noOp", "position": [ 1000, -300 ], "parameters": {}, "typeVersion": 1 }, { "id": "7b82cc73-67cb-46d7-a1d4-19712c86890a", "name": "Get File Data1", "type": "n8n-nodes-base.httpRequest", "position": [ 1240, -300 ], "parameters": { "url": "={{ $('Row Ref').item.json.File[0].url }}", "options": {} }, "typeVersion": 4.2 }, { "id": "7ef1556c-96a3-4988-982d-ec8c5fba4601", "name": "Extract from File1", "type": "n8n-nodes-base.extractFromFile", "position": [ 1400, -300 ], "parameters": { "options": {}, "operation": "pdf" }, "typeVersion": 1 }, { "id": "9916f1c1-f413-4996-ad45-380a899b4a88", "name": "Get Result1", "type": "n8n-nodes-base.set", "position": [ 2120, -260 ], "parameters": { "options": {}, "assignments": { "assignments": [ { "id": "e376ba60-8692-4962-9af7-466b6a3f44a2", "name": "={{ $('Fields to Update').item.json.name }}", "type": "string", "value": "={{ $json.text.trim() }}" } ] } }, "typeVersion": 3.4 }, { "id": "f62f612d-c288-4062-ab3c-dbc24c9b4b38", "name": "Generate Field Value1", "type": "@n8n/n8n-nodes-langchain.chainLlm", "position": [ 1720, -300 ], "parameters": { "text": "=\n{{ $('Extract from File1').first().json.text }}\n\n\nData to extract: {{ $json.description }}\noutput format is: {{ $json.type }}", "messages": { "messageValues": [ { "message": "=You assist the user in extracting the required data from the given file.\n* Keep you answer short.\n* If you cannot extract the requested data, give you response as \"n/a\" followed by \"(reason)\" where reason is replaced with reason why data could not be extracted." } ] }, "promptType": "define" }, "typeVersion": 1.5 }, { "id": "615f7436-f280-4033-8ec8-a34f1bd78075", "name": "Filter Valid Rows", "type": "n8n-nodes-base.filter", "position": [ 520, -420 ], "parameters": { "options": {}, "conditions": { "options": { "version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict" }, "combinator": "and", "conditions": [ { "id": "7ad58f0b-0354-49a9-ab2f-557652d7b416", "operator": { "type": "string", "operation": "notEmpty", "singleValue": true }, "leftValue": "={{ $json.File[0].url }}", "rightValue": "" } ] } }, "typeVersion": 2.2 }, { "id": "281b9fb0-305c-4a0c-b73b-82b6ba876d12", "name": "Filter Valid Fields", "type": "n8n-nodes-base.filter", "position": [ 340, 220 ], "parameters": { "options": {}, "conditions": { "options": { "version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict" }, "combinator": "and", "conditions": [ { "id": "5b4a7393-788c-42dc-ac1f-e76f833f8534", "operator": { "type": "string", "operation": "notEmpty", "singleValue": true }, "leftValue": "={{ $json.field.description }}", "rightValue": "" } ] } }, "typeVersion": 2.2 }, { "id": "dd0fa792-791f-4d31-a7e8-9b72a25b6a07", "name": "Event Ref", "type": "n8n-nodes-base.noOp", "position": [ 160, 220 ], "parameters": {}, "typeVersion": 1 }, { "id": "ca1174b3-da18-4d3c-86ef-3028cd5b12a7", "name": "Event Ref1", "type": "n8n-nodes-base.noOp", "position": [ 160, -420 ], "parameters": {}, "typeVersion": 1 }, { "id": "8800b355-0fa8-4297-b13b-d3da8a01c3b7", "name": "Sticky Note", "type": "n8n-nodes-base.stickyNote", "position": [ -1180, -340 ], "parameters": { "color": 7, "width": 480, "height": 440, "content": "### 1. 테이블 스키마 가져오기\n[Airtable 노드에 대해 자세히 알아보기](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.airtable/)\n\n이 작업을 위해 편리한 Airtable 노드를 사용할 것입니다. Airtable에 대한 모든 필요를 위해 이 노드에 익숙해지는 것을 추천합니다!" }, "typeVersion": 1 }, { "id": "a90876d3-8a93-4d90-9e2a-f23de452259d", "name": "Sticky Note1", "type": "n8n-nodes-base.stickyNote", "position": [ -260, -440 ], "parameters": { "color": 5, "width": 330, "height": 80, "content": "### 2a. 최소 행 수 업데이트\n이 브랜치가 영향을 받은 행만 업데이트합니다." }, "typeVersion": 1 }, { "id": "319adf97-8b14-4069-b4cc-594a6ea479c1", "name": "Sticky Note2", "type": "n8n-nodes-base.stickyNote", "position": [ -320, 140 ], "parameters": { "color": 5, "width": 390, "height": 120, "content": "### 2b. 필드 아래의 모든 행 업데이트\n이 브랜치가 필드 아래의 모든 적용 가능한 행을 필드/열이 생성되거나 변경될 때 업데이트합니다. 주의하세요 - 행이 수천 개 있을 경우, 이 작업이 시간이 오래 걸릴 수 있습니다!" }, "typeVersion": 1 }, { "id": "42a60c8c-476f-4930-bac5-4d36a7185f4f", "name": "Sticky Note3", "type": "n8n-nodes-base.stickyNote", "position": [ -2240, -1000 ], "parameters": { "width": 520, "height": 1120, "content": "## 시도해 보세요!\n### 이 n8n 템플릿은 [Airtable](https://airtable.com/invite/r/cKzxFYVc) 테이블에 대한 PDF 워크플로 패턴에서 \"동적\" 또는 \"사용자 정의\" 프롬프트를 구동합니다. 간단히 말해, 사용자가 기본 템플릿을 건드리지 않고 프롬프트로 스프레드시트를 채울 수 있게 합니다.\n\n**n8n Studio에서 제가 한 비디오 데모 확인하기**: https://www.youtube.com/watch?v=_fNAD1u8BZw\n\n**예시 Airtable 확인하기:** https://airtable.com/appAyH3GCBJ56cfXl/shrXzR1Tj99kuQbyL\n\n이 템플릿은 Airtable의 웹훅 소스로 사용하도록 설계되었습니다. **Baserow 버전이 필요하신가요? [여기 클릭](https://n8n.io/workflows/2780-ai-data-extraction-with-dynamic-prompts-and-baserow)**\n\n## 작동 원리\n* Airtable.io 테이블은 테이블 변경 사항을 액세스 가능한 웹훅으로 이벤트로 보내는 통합 기능을 제공합니다. 이는 반응형 트리거 패턴을 가능하게 하며, 이 워크플로를 실현할 수 있게 합니다. 우리 사례에서, 우리는 `row_updated`, `field_created` 및 `field_updated` 이벤트를 캡처합니다.\n* 다음으로, Airtable.io 테이블에 \"input\" 열이 필요합니다. 이 열은 프롬프트를 평가할 컨텍스트가 있는 곳입니다. 이 예제에서 \"input\" 열 이름은 \"file\"이며, 여기에 PDF를 업로드합니다. 이 \"input\" 필드는 인간이 제어하며, 이 템플릿에서 업데이트되지 않습니다.\n* 이제 열(즉, Airtable의 \"fields\")에 대해 설명하겠습니다. 각 필드는 이름, 유형 및 설명을 정의할 수 있으며, 이들이 함께 스키마를 형성합니다. 처음 두 가지는 자명하지만, \"description\"은 사용자가 프롬프트를 제공하는 곳으로, 각 필드가 포함해야 할 데이터에 대한 내용입니다.\n* 이 템플릿에서 웹훅 트리거는 행이나 열이 업데이트될 때 대기합니다. 들어오는 이벤트에는 영향을 받은 테이블, 행 및/또는 열 ID와 같은 많은 세부 정보가 포함됩니다.\n* 이 정보를 사용하여 테이블의 스키마를 가져와 열의 설명(즉, 동적 프롬프트)을 얻습니다.\n* 각 트리거된 이벤트에 대해, 우리는 입력(즉, PDF)을 다운로드하여 AI/LLM에 준비합니다. 사용 가능한 열을 반복하며 동적 프롬프트를 제공하여, LLM이 PDF에 대해 각 프롬프트를 실행하고 각 셀에 대한 값 응답을 생성합니다.\n* 이러한 값들을 수집하여 Airtable 레코드를 업데이트합니다.\n\n## 사용 방법\n* 이 워크플로를 게시하여 Airtable 인스턴스에서 접근할 수 있게 해야 합니다.\n* Airtable과 연결하기 위해 \"Create Airtable Webhooks\" 미니-플로를 실행해야 합니다.\n* 이 템플릿은 다른 Airtable에도 재사용 가능하지만, 각 테이블마다 웹훅을 새로 생성해야 합니다.\n\n### 도움이 필요하신가요?\n[Discord](https://discord.com/invite/XPKeKXeB7d) 에 참여하거나 [Forum](https://community.n8n.io/)에서 문의하세요!\n\n즐거운 플로우그램밍!" }, "typeVersion": 1 }, { "id": "c6d037e9-1bf7-47a7-9c46-940220e0786b", "name": "Sticky Note4", "type": "n8n-nodes-base.stickyNote", "position": [ -680, -340 ], "parameters": { "color": 7, "width": 760, "height": 440, "content": "### 2. 이벤트 라우터 패턴 \n[Switch 노드에 대해 더 알아보기](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.switch/) \n\n단순한 스위치 노드를 사용하여 처리할 이벤트를 결정할 수 있습니다. 우리의 행 이벤트와 필드 이벤트의 차이점은 행 이벤트가 하나의 행에 영향을 미치는 반면 필드 이벤트는 모든 행에 영향을 미친다는 것입니다." }, "typeVersion": 1 }, { "id": "897cec32-3a4c-4a76-bffe-b1456c287b44", "name": "Sticky Note5", "type": "n8n-nodes-base.stickyNote", "position": [ 100, -620 ], "parameters": { "color": 7, "width": 620, "height": 400, "content": "### 3. 유효 입력이 있는 행만 필터링\n\n[Split Out 노드에 대해 자세히 알아보기](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.splitout/)\n\n이 단계는 하나 이상의 업데이트된 행을 처리합니다. 여기서 \"updated\"는 해당 행의 \"input\" 열(즉, 우리 예제에서의 \"file\")이 변경되었음을 의미합니다. 각 영향을 받은 행에 대해, 우리는 전체 행을 가져와 업데이트해야 할 열들만 파악합니다 - 이는 이미 값이 있는 열에 대한 값을 생성하는 등의 불필요한 작업을 피하기 위한 최적화입니다." }, "typeVersion": 1 }, { "id": "a5999ca3-4418-42c5-aa1c-fbdfb1c04fef", "name": "Sticky Note7", "type": "n8n-nodes-base.stickyNote", "position": [ 2060, -480 ], "parameters": { "color": 7, "width": 600, "height": 440, "content": "### 6. Airtable 레코드 업데이트 \n[Edit Fields 노드에 대해 자세히 알아보기](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.set/) \n\n마침내, LLM 응답을 수집하여 결합하여 Airtable 레코드를 업데이트하기 위한 API 요청을 구축할 수 있습니다 - 이 레코드의 ID는 초기 웹훅에서 얻었습니다. 이 작업이 완료된 후, 다음 행으로 이동하여 프로세스를 반복할 수 있습니다." }, "typeVersion": 1 }, { "id": "38192929-a387-4240-8373-290499b40e5a", "name": "Sticky Note8", "type": "n8n-nodes-base.stickyNote", "position": [ 1180, -580 ], "parameters": { "color": 7, "width": 860, "height": 580, "content": "### 5. PDF, LLM 및 동적 프롬프트? 오 마이!\n\n[기본 LLM 노드에 대해 자세히 알아보기](https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.chainllm/)\n\n이 단계에서 모든 것이 합쳐집니다! 간단히 말해서, 우리는 LLM에게 PDF 내용을 컨텍스트로 제공하고, 이전에 가져온 스키마에서 나온 동적 프롬프트를 해당 행에 대해 반복합니다. 결국, LLM은 요청된 각 열에 대한 값을 생성해야 합니다.\n\n**노트**: PDF 캐싱에 대한 최적화가 확실히 가능하지만, 이는 이 데모의 범위를 넘어갑니다." }, "typeVersion": 1 }, { "id": "19a9b93a-d18f-4ffd-ae93-ed41cf398e90", "name": "Sticky Note9", "type": "n8n-nodes-base.stickyNote", "position": [ 740, -580 ], "parameters": { "color": 7, "width": 420, "height": 460, "content": "### 4. 항목 루프 사용\n\n[Split in Batches 노드에 대해 자세히 알아보기](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.splitinbatches/)\n\n여기서 split in batches 노드는 한 번에 하나의 행을 업데이트하기 위해 사용됩니다. 그러나 이는 사용자 경험을 위한 선호사항입니다 - Airtable에서 변경 사항이 더 빠르게 반영됩니다." }, "typeVersion": 1 }, { "id": "5407fead-ee7c-47c8-94ed-5b89e74e50e8", "name": "Sticky Note10", "type": "n8n-nodes-base.stickyNote", "position": [ 100, 40 ], "parameters": { "color": 7, "width": 600, "height": 360, "content": "### 7. 해당 열 아래에 모든 적용 가능한 행 나열\n\n[필터 노드에 대해 자세히 알아보기](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.filter)\n\n성능을 유지하기 위해, 입력 필드가 채워진 행만 가져오기로 결정할 수 있습니다. 이는 추출을 수행하는 데 필요하기 때문입니다. 이는 Airtable 필터를 사용하여 쉽게 달성할 수 있습니다." }, "typeVersion": 1 }, { "id": "43b0e330-b79a-4577-b4fc-314e8b790cf7", "name": "Sticky Note11", "type": "n8n-nodes-base.stickyNote", "position": [ 1160, 140 ], "parameters": { "color": 7, "width": 700, "height": 500, "content": "### 9. LLM을 사용하여 값 생성\n[Extract From File 노드에 대해 자세히 알아보기](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.extractfromfile/)\n\nStep 5와 거의 동일하지만, 모든 필드/열을 업데이트하는 대신, 하나의 값만 생성하면 됩니다." }, "typeVersion": 1 }, { "id": "0665fe56-48d2-4215-8d95-d4c01f9266ed", "name": "OpenAI Chat Model", "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", "position": [ 1720, -140 ], "parameters": { "options": {} }, "credentials": { "openAiApi": { "id": "8gccIjcuf3gvaoEr", "name": "OpenAi account" } }, "typeVersion": 1.1 }, { "id": "1997fb8b-73eb-4016-bab6-eb8f02fee368", "name": "Sticky Note12", "type": "n8n-nodes-base.stickyNote", "position": [ 720, 40 ], "parameters": { "color": 7, "width": 420, "height": 460, "content": "### 8. 항목 루프 사용\n[Split in Batches 노드에 대해 더 알아보기](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.splitinbatches/)\n\n4단계와 유사하게, Split in Batches 노드는 사용자 경험을 위한 선호사항입니다 - 변경 사항이 Airtable에서 더 빠르게 보입니다." }, "typeVersion": 1 }, { "id": "c2799ded-b742-43a2-80ce-7a0c8f1df96e", "name": "OpenAI Chat Model1", "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", "position": [ 1540, 500 ], "parameters": { "options": {} }, "credentials": { "openAiApi": { "id": "8gccIjcuf3gvaoEr", "name": "OpenAi account" } }, "typeVersion": 1.1 }, { "id": "e5b42790-fc86-4134-9d04-e6bcad4a5f20", "name": "Sticky Note13", "type": "n8n-nodes-base.stickyNote", "position": [ 1880, 140 ], "parameters": { "color": 7, "width": 500, "height": 440, "content": "### 10. Airtable 레코드 업데이트\n\n[Edit Fields 노드에 대해 자세히 알아보기](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.set/)\n\nStep 6과 마찬가지로, LLM 응답은 생성되거나 변경된 필드에서만 행을 업데이트하는 데 사용됩니다. 작업이 완료되면, 루프가 계속되어 다음 행이 처리됩니다." }, "typeVersion": 1 }, { "id": "b1e98631-a440-4c66-b2d2-8236f6889b65", "name": "Sticky Note6", "type": "n8n-nodes-base.stickyNote", "position": [ -2240, -1140 ], "parameters": { "color": 7, "width": 300, "height": 120, "content": "에어테이블.io [](https://airtable.com/invite/r/cKzxFYVc)" }, "typeVersion": 1 }, { "id": "9d293b3a-954d-4e3b-8773-b6c3dded9520", "name": "Get Webhook Payload", "type": "n8n-nodes-base.httpRequest", "position": [ -580, -140 ], "parameters": { "url": "=https://api.airtable.com/v0/bases/{{ $('Airtable Webhook').first().json.body.base.id }}/webhooks/{{ $('Airtable Webhook').first().json.body.webhook.id }}/payloads", "options": {}, "authentication": "predefinedCredentialType", "nodeCredentialType": "airtableTokenApi" }, "credentials": { "airtableTokenApi": { "id": "Und0frCQ6SNVX3VV", "name": "Airtable Personal Access Token account" } }, "typeVersion": 4.2 }, { "id": "5f8d919b-14cd-4cb4-8604-731e56cc9402", "name": "Parse Event", "type": "n8n-nodes-base.code", "position": [ -400, -140 ], "parameters": { "jsCode": "const webhook = $('Airtable Webhook').first().json;\nconst schema = $('Get Prompt Fields').first().json;\nconst { payloads } = $input.first().json;\nif (!payloads.length) return [];\n\nconst event = payloads[payloads.length - 1];\nconst baseId = webhook.body.base.id;\nconst tableId = Object.keys(event.changedTablesById)[0];\nconst table = event.changedTablesById[tableId];\n\nreturn {\n baseId,\n tableId,\n event_type: getEventType(table),\n fieldId: getFieldId(table),\n field: getField(getFieldId(table)),\n rowId: getRecordId(table),\n}\n\nfunction getEventType(changedTableByIdObject) {\n if (changedTableByIdObject['createdFieldsById']) return 'field.created';\n if (changedTableByIdObject['changedFieldsById']) return 'field.updated'\n if (changedTableByIdObject['changedRecordsById']) return 'row.updated';\n return 'unknown';\n}\n\nfunction getFieldId(changedTableByIdObject) {\n const field = changedTableByIdObject.createdFieldsById\n || changedTableByIdObject.changedFieldsById\n || null;\n\n return field ? Object.keys(field)[0] : null;\n}\n\nfunction getField(id) {\n return schema.fields.find(field => field.id === id);\n}\n\nfunction getRecordId(changedTableByIdObject) {\n const record = changedTableByIdObject.changedRecordsById\n || null;\n\n return record ? Object.keys(record)[0] : null;\n}" }, "typeVersion": 2 }, { "id": "9b99d939-94d6-4fef-8b73-58c702503221", "name": "Get Table Schema", "type": "n8n-nodes-base.airtable", "position": [ -1080, -140 ], "parameters": { "base": { "__rl": true, "mode": "id", "value": "={{ $('Airtable Webhook').item.json.body.base.id }}" }, "resource": "base", "operation": "getSchema" }, "credentials": { "airtableTokenApi": { "id": "Und0frCQ6SNVX3VV", "name": "Airtable Personal Access Token account" } }, "typeVersion": 2.1 }, { "id": "c29fc911-a852-46f2-bbb1-5092cc1aaa9d", "name": "Fetch Records", "type": "n8n-nodes-base.airtable", "position": [ 520, 220 ], "parameters": { "base": { "__rl": true, "mode": "id", "value": "={{ $json.baseId }}" }, "table": { "__rl": true, "mode": "id", "value": "={{ $json.tableId }}" }, "options": {}, "operation": "search", "filterByFormula": "NOT({File} = \"\")" }, "credentials": { "airtableTokenApi": { "id": "Und0frCQ6SNVX3VV", "name": "Airtable Personal Access Token account" } }, "typeVersion": 2.1 }, { "id": "86d3c8d8-709f-4d9d-99bc-5d1b4aeb8603", "name": "Update Row", "type": "n8n-nodes-base.airtable", "position": [ 2180, 380 ], "parameters": { "base": { "__rl": true, "mode": "id", "value": "={{ $('Event Ref').first().json.baseId }}" }, "table": { "__rl": true, "mode": "id", "value": "={{ $('Event Ref').first().json.tableId }}" }, "columns": { "value": {}, "schema": [ { "id": "id", "type": "string", "display": true, "removed": false, "readOnly": true, "required": false, "displayName": "id", "defaultMatch": true }, { "id": "Name", "type": "string", "display": true, "removed": false, "readOnly": false, "required": false, "displayName": "Name", "defaultMatch": false, "canBeUsedToMatch": true }, { "id": "File", "type": "array", "display": true, "removed": false, "readOnly": false, "required": false, "displayName": "File", "defaultMatch": false, "canBeUsedToMatch": true }, { "id": "Full Name", "type": "string", "display": true, "removed": false, "readOnly": false, "required": false, "displayName": "Full Name", "defaultMatch": false, "canBeUsedToMatch": true }, { "id": "Created", "type": "string", "display": true, "removed": false, "readOnly": true, "required": false, "displayName": "Created", "defaultMatch": false, "canBeUsedToMatch": true }, { "id": "Last Modified", "type": "string", "display": true, "removed": false, "readOnly": true, "required": false, "displayName": "Last Modified", "defaultMatch": false, "canBeUsedToMatch": true }, { "id": "Address", "type": "string", "display": true, "removed": false, "readOnly": false, "required": false, "displayName": "Address", "defaultMatch": false, "canBeUsedToMatch": true } ], "mappingMode": "autoMapInputData", "matchingColumns": [ "id" ] }, "options": {}, "operation": "update" }, "credentials": { "airtableTokenApi": { "id": "Und0frCQ6SNVX3VV", "name": "Airtable Personal Access Token account" } }, "typeVersion": 2.1 }, { "id": "95d08439-59a2-4e74-bd5a-b71cf079b621", "name": "Get Row", "type": "n8n-nodes-base.airtable", "position": [ 340, -420 ], "parameters": { "id": "={{ $json.rowId }}", "base": { "__rl": true, "mode": "id", "value": "={{ $json.baseId }}" }, "table": { "__rl": true, "mode": "id", "value": "={{ $json.tableId }}" }, "options": {} }, "credentials": { "airtableTokenApi": { "id": "Und0frCQ6SNVX3VV", "name": "Airtable Personal Access Token account" } }, "typeVersion": 2.1 }, { "id": "50888ac5-30c9-4036-aade-6ccfdf605c3b", "name": "Add Row ID to Payload", "type": "n8n-nodes-base.set", "position": [ 2300, -260 ], "parameters": { "mode": "raw", "options": {}, "jsonOutput": "={{\n{\n id: $('Row Ref').item.json.id,\n ...$input.all()\n .map(item => item.json)\n .reduce((acc, item) => ({\n ...acc,\n ...item,\n }), {})\n}\n}}" }, "executeOnce": true, "typeVersion": 3.4 }, { "id": "e3ebeb45-45d9-44a4-a2e6-bde89f5da125", "name": "Update Record", "type": "n8n-nodes-base.airtable", "position": [ 2480, -260 ], "parameters": { "base": { "__rl": true, "mode": "id", "value": "={{ $('Event Ref1').first().json.baseId }}" }, "table": { "__rl": true, "mode": "id", "value": "={{ $('Event Ref1').first().json.tableId }}" }, "columns": { "value": {}, "schema": [ { "id": "id", "type": "string", "display": true, "removed": false, "readOnly": true, "required": false, "displayName": "id", "defaultMatch": true }, { "id": "Name", "type": "string", "display": true, "removed": false, "readOnly": false, "required": false, "displayName": "Name", "defaultMatch": false, "canBeUsedToMatch": true }, { "id": "File", "type": "array", "display": true, "removed": false, "readOnly": false, "required": false, "displayName": "File", "defaultMatch": false, "canBeUsedToMatch": true }, { "id": "Full Name", "type": "string", "display": true, "removed": false, "readOnly": false, "required": false, "displayName": "Full Name", "defaultMatch": false, "canBeUsedToMatch": true }, { "id": "Address", "type": "string", "display": true, "removed": false, "readOnly": false, "required": false, "displayName": "Address", "defaultMatch": false, "canBeUsedToMatch": true }, { "id": "Created", "type": "string", "display": true, "removed": false, "readOnly": true, "required": false, "displayName": "Created", "defaultMatch": false, "canBeUsedToMatch": true }, { "id": "Last Modified", "type": "string", "display": true, "removed": false, "readOnly": true, "required": false, "displayName": "Last Modified", "defaultMatch": false, "canBeUsedToMatch": true } ], "mappingMode": "autoMapInputData", "matchingColumns": [ "id" ] }, "options": {}, "operation": "update" }, "credentials": { "airtableTokenApi": { "id": "Und0frCQ6SNVX3VV", "name": "Airtable Personal Access Token account" } }, "typeVersion": 2.1 }, { "id": "ac01ec4b-e030-4608-af38-64558408832f", "name": "Airtable Webhook", "type": "n8n-nodes-base.webhook", "position": [ -1400, -140 ], "webhookId": "a82f0ae7-678e-49d9-8219-7281e8a2a1b2", "parameters": { "path": "a82f0ae7-678e-49d9-8219-7281e8a2a1b2", "options": {}, "httpMethod": "POST" }, "typeVersion": 2 }, { "id": "90178da9-2000-474e-ba93-a02d03ec6a1d", "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "position": [ -1600, -640 ], "parameters": {}, "typeVersion": 1 }, { "id": "b8b887ce-f891-4a3c-993b-0aaccadf1b52", "name": "Set Airtable Vars", "type": "n8n-nodes-base.set", "position": [ -1420, -640 ], "parameters": { "options": {}, "assignments": { "assignments": [ { "id": "012cb420-1455-4796-a2ac-a31e6abf59ba", "name": "appId", "type": "string", "value": "" }, { "id": "e863b66c-420f-43c6-aee2-43aa5087a0a5", "name": "tableId", "type": "string", "value": "" }, { "id": "e470be1a-5833-47ed-9e2f-988ef5479738", "name": "notificationUrl", "type": "string", "value": "" }, { "id": "e4b3213b-e3bd-479b-99ec-d1aa31eaa4c8", "name": "inputField", "type": "string", "value": "File" } ] } }, "typeVersion": 3.4 }, { "id": "a3ef1a4a-fd22-4a37-8edb-48037f44fa4b", "name": "Get Table Schema1", "type": "n8n-nodes-base.airtable", "position": [ -1240, -820 ], "parameters": { "base": { "__rl": true, "mode": "id", "value": "={{ $json.appId }}" }, "resource": "base", "operation": "getSchema" }, "credentials": { "airtableTokenApi": { "id": "Und0frCQ6SNVX3VV", "name": "Airtable Personal Access Token account" } }, "typeVersion": 2.1 }, { "id": "2490bbc6-2ea1-4146-b0b8-5a406e89ea2c", "name": "Get \"Input\" Field", "type": "n8n-nodes-base.set", "position": [ -1060, -820 ], "parameters": { "mode": "raw", "options": {}, "jsonOutput": "={{\n$input.all()\n .map(item => item.json)\n .find(item => item.id === $('Set Airtable Vars').first().json.tableId)\n .fields\n .find(field => field.name === $('Set Airtable Vars').first().json.inputField)\n}}" }, "executeOnce": true, "typeVersion": 3.4 }, { "id": "a3de141f-0ce8-4f8e-ae8e-f10f635d14ec", "name": "RecordsChanged Webhook", "type": "n8n-nodes-base.httpRequest", "position": [ -880, -820 ], "parameters": { "url": "=https://api.airtable.com/v0/bases/{{ $('Set Airtable Vars').first().json.appId }}/webhooks", "method": "POST", "options": {}, "jsonBody": "={{\n{\n \"notificationUrl\": $('Set Airtable Vars').first().json.notificationUrl,\n \"specification\": {\n \"options\": {\n \"filters\": {\n \"fromSources\": [ \"client\" ],\n \"dataTypes\": [ \"tableData\" ],\n \"changeTypes\": [ \"update\" ],\n \"recordChangeScope\": $('Set Airtable Vars').first().json.tableId,\n \"watchDataInFieldIds\": [$json.id]\n }\n }\n }\n}\n}}", "sendBody": true, "specifyBody": "json", "authentication": "predefinedCredentialType", "nodeCredentialType": "airtableTokenApi" }, "credentials": { "airtableTokenApi": { "id": "Und0frCQ6SNVX3VV", "name": "Airtable Personal Access Token account" } }, "typeVersion": 4.2 }, { "id": "21b0fae8-2046-4647-83c4-132d1d63503a", "name": "FieldsChanged Webhook", "type": "n8n-nodes-base.httpRequest", "position": [ -880, -640 ], "parameters": { "url": "=https://api.airtable.com/v0/bases/{{ $('Set Airtable Vars').first().json.appId }}/webhooks", "method": "POST", "options": {}, "jsonBody": "={{\n{\n \"notificationUrl\": $('Set Airtable Vars').first().json.notificationUrl,\n \"specification\": {\n \"options\": {\n \"filters\": {\n \"fromSources\": [ \"client\" ],\n \"dataTypes\": [ \"tableFields\" ],\n \"changeTypes\": [ \"add\", \"update\" ],\n \"recordChangeScope\": $('Set Airtable Vars').first().json.tableId\n }\n }\n }\n}\n}}", "sendBody": true, "specifyBody": "json", "authentication": "predefinedCredentialType", "nodeCredentialType": "airtableTokenApi" }, "credentials": { "airtableTokenApi": { "id": "Und0frCQ6SNVX3VV", "name": "Airtable Personal Access Token account" } }, "typeVersion": 4.2 }, { "id": "f31c36cb-98da-4688-a83a-f06e46d2b8a2", "name": "Sticky Note14", "type": "n8n-nodes-base.stickyNote", "position": [ -1680, -1000 ], "parameters": { "color": 5, "width": 1020, "height": 580, "content": "## ⭐️ Airtable 웹훅 생성\n이 워크플로를 Airtable과 연결하려면 베이스에 웹훅을 생성해야 합니다.\n이 작업은 한 번만 하면 되지만, 이 웹훅들이 7일 후에 비활성화되면 다시 생성해야 합니다.\n\n더 많은 정보를 위해 Airtable 개발자 문서를 확인하세요: [https://airtable.com/developers/web/api/webhooks-overview](https://airtable.com/developers/web/api/webhooks-overview)" }, "typeVersion": 1 } ], "pinData": {}, "connections": { "Get Row": { "main": [ [ { "node": "Filter Valid Rows", "type": "main", "index": 0 } ] ] }, "Row Ref": { "main": [ [ { "node": "Get File Data1", "type": "main", "index": 0 } ] ] }, "Event Ref": { "main": [ [ { "node": "Filter Valid Fields", "type": "main", "index": 0 } ] ] }, "Event Ref1": { "main": [ [ { "node": "Get Row", "type": "main", "index": 0 } ] ] }, "Event Type": { "main": [ [ { "node": "Event Ref1", "type": "main", "index": 0 } ], [ { "node": "Event Ref", "type": "main", "index": 0 } ], [ { "node": "Event Ref", "type": "main", "index": 0 } ] ] }, "Get Result": { "main": [ [ { "node": "Update Row", "type": "main", "index": 0 } ] ] }, "Update Row": { "main": [ [ { "node": "Loop Over Items", "type": "main", "index": 0 } ] ] }, "Get Result1": { "main": [ [ { "node": "Add Row ID to Payload", "type": "main", "index": 0 } ] ] }, "Parse Event": { "main": [ [ { "node": "Event Type", "type": "main", "index": 0 } ] ] }, "Fetch Records": { "main": [ [ { "node": "Loop Over Items", "type": "main", "index": 0 } ] ] }, "Get File Data": { "main": [ [ { "node": "Extract from File", "type": "main", "index": 0 } ] ] }, "Row Reference": { "main": [ [ { "node": "Get File Data", "type": "main", "index": 0 } ] ] }, "Update Record": { "main": [ [ { "node": "Loop Over Items1", "type": "main", "index": 0 } ] ] }, "Get File Data1": { "main": [ [ { "node": "Extract from File1", "type": "main", "index": 0 } ] ] }, "Loop Over Items": { "main": [ [], [ { "node": "Row Reference", "type": "main", "index": 0 } ] ] }, "Airtable Webhook": { "main": [ [ { "node": "Get Table Schema", "type": "main", "index": 0 } ] ] }, "Fields to Update": { "main": [ [ { "node": "Generate Field Value1", "type": "main", "index": 0 } ] ] }, "Get Table Schema": { "main": [ [ { "node": "Get Prompt Fields", "type": "main", "index": 0 } ] ] }, "Loop Over Items1": { "main": [ [], [ { "node": "Row Ref", "type": "main", "index": 0 } ] ] }, "Extract from File": { "main": [ [ { "node": "Generate Field Value", "type": "main", "index": 0 } ] ] }, "Filter Valid Rows": { "main": [ [ { "node": "Loop Over Items1", "type": "main", "index": 0 } ] ] }, "Get \"Input\" Field": { "main": [ [ { "node": "RecordsChanged Webhook", "type": "main", "index": 0 } ] ] }, "Get Prompt Fields": { "main": [ [ { "node": "Get Webhook Payload", "type": "main", "index": 0 } ] ] }, "Get Table Schema1": { "main": [ [ { "node": "Get \"Input\" Field", "type": "main", "index": 0 } ] ] }, "OpenAI Chat Model": { "ai_languageModel": [ [ { "node": "Generate Field Value1", "type": "ai_languageModel", "index": 0 } ] ] }, "Set Airtable Vars": { "main": [ [ { "node": "Get Table Schema1", "type": "main", "index": 0 }, { "node": "FieldsChanged Webhook", "type": "main", "index": 0 } ] ] }, "Extract from File1": { "main": [ [ { "node": "Fields to Update", "type": "main", "index": 0 } ] ] }, "OpenAI Chat Model1": { "ai_languageModel": [ [ { "node": "Generate Field Value", "type": "ai_languageModel", "index": 0 } ] ] }, "Filter Valid Fields": { "main": [ [ { "node": "Fetch Records", "type": "main", "index": 0 } ] ] }, "Get Webhook Payload": { "main": [ [ { "node": "Parse Event", "type": "main", "index": 0 } ] ] }, "Generate Field Value": { "main": [ [ { "node": "Get Result", "type": "main", "index": 0 } ] ] }, "Add Row ID to Payload": { "main": [ [ { "node": "Update Record", "type": "main", "index": 0 } ] ] }, "FieldsChanged Webhook": { "main": [ [] ] }, "Generate Field Value1": { "main": [ [ { "node": "Get Result1", "type": "main", "index": 0 } ] ] }, "RecordsChanged Webhook": { "main": [ [] ] }, "When clicking ‘Test workflow’": { "main": [ [ { "node": "Set Airtable Vars", "type": "main", "index": 0 } ] ] } } }