{
"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
}
]
]
}
}
}