{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "6a5ff5a1-b75f-4cf2-a952-ff20bf29c054",
      "name": "trigger",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        -112,
        -16
      ],
      "parameters": {
        "workflowInputs": {
          "values": [
            {
              "name": "message_text"
            },
            {
              "name": "timezone"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "fe99e9c0-9104-4b2b-add6-ef68b331491f",
      "name": "normalize_trigger_input",
      "type": "n8n-nodes-base.set",
      "position": [
        192,
        -16
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "e55d98cb-b0f1-4bd9-9d39-8ffdde608ffd",
              "name": "message_text",
              "type": "string",
              "value": "={{ $('trigger').first().json.message_text }}"
            },
            {
              "id": "33ce078d-3592-480c-b43f-42e78c44f39c",
              "name": "now",
              "type": "string",
              "value": "={{ new Date().toISOString() }}"
            },
            {
              "id": "55a26125-534d-428a-8690-a8767400fc61",
              "name": "timezone",
              "type": "string",
              "value": "={{ $('trigger').first().json.timezone }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "cc5675cc-44b6-4a68-befd-b46062440ae4",
      "name": "load_eval_data",
      "type": "n8n-nodes-base.evaluationTrigger",
      "position": [
        -96,
        336
      ],
      "parameters": {
        "limitRows": true,
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U89nPsasM2WNv1D7gEYINhDwylyxYw7BOd_i8ipFC0M/edit#gid=0",
          "cachedResultName": "meeting_extractor"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1U89nPsasM2WNv1D7gEYINhDwylyxYw7BOd_i8ipFC0M"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "fcd81c55-e173-4cd2-b870-8b5af138e4dd",
      "name": "merged_inputs",
      "type": "n8n-nodes-base.noOp",
      "position": [
        480,
        -16
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "91fca1e3-7d83-4798-8b59-b135eb7deac4",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        960,
        224
      ],
      "parameters": {
        "autoFix": true,
        "schemaType": "manual",
        "inputSchema": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Meeting Details Extraction\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"meeting\": {\n      \"type\": \"object\",\n      \"description\": \"Extracted meeting/event details from the message\",\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"title\": {\n          \"type\": [\"string\", \"null\"],\n          \"minLength\": 1,\n          \"description\": \"Meeting title or subject\"\n        },\n        \"date\": {\n          \"type\": [\"string\", \"null\"],\n          \"pattern\": \"^\\\\d{4}-\\\\d{2}-\\\\d{2}$\",\n          \"description\": \"Meeting date in YYYY-MM-DD format\"\n        },\n        \"time\": {\n          \"type\": [\"string\", \"null\"],\n          \"pattern\": \"^\\\\d{2}:\\\\d{2}$\",\n          \"description\": \"Meeting time in HH:mm 24-hour format\"\n        },\n        \"location\": {\n          \"type\": [\"string\", \"null\"],\n          \"description\": \"Physical location or venue\"\n        },\n        \"meeting_link\": {\n          \"type\": [\"string\", \"null\"],\n          \"format\": \"uri\",\n          \"description\": \"URL for virtual meeting or location map\"\n        },\n        \"attendees\": {\n          \"type\": [\"string\", \"null\"],\n          \"description\": \"Who is invited or attending\"\n        },\n        \"notes\": {\n          \"type\": [\"string\", \"null\"],\n          \"maxLength\": 500,\n          \"description\": \"Additional context or instructions\"\n        }\n      },\n      \"required\": [\"title\", \"date\", \"time\"]\n    },\n    \"reasoning\": {\n      \"type\": \"string\",\n      \"minLength\": 1,\n      \"maxLength\": 200,\n      \"description\": \"Explanation of how relative date/time terms were resolved\"\n    }\n  },\n  \"required\": [\"meeting\", \"reasoning\"],\n  \"additionalProperties\": false,\n  \"examples\": [\n    {\n      \"meeting\": {\n        \"title\": \"Q1 planning session\",\n        \"date\": \"2025-03-12\",\n        \"time\": \"14:30\",\n        \"location\": \"main conference room\",\n        \"meeting_link\": \"https://zoom.us/j/123456789\",\n        \"attendees\": \"team\",\n        \"notes\": \"bring department budgets and proposals\"\n      },\n      \"reasoning\": \"Explicit date 'March 12th' and time '2:30 PM' provided\"\n    }\n  ]\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "06c934d2-e0dc-449f-8a87-945043c84bf0",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        736,
        224
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini",
          "cachedResultName": "gpt-4.1-mini"
        },
        "options": {
          "maxRetries": 3,
          "temperature": 0,
          "responseFormat": "json_object"
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "a37b5bfa-5671-4854-a18e-1ab5b0e25e7d",
      "name": "output_fixer",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        944,
        448
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-mini",
          "cachedResultName": "gpt-5-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "4725d4f7-1e51-4042-b6fe-f3d98630be9d",
      "name": "OpenAI Chat Model1",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        2560,
        464
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-mini",
          "cachedResultName": "gpt-5-mini"
        },
        "options": {},
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1.3
    },
    {
      "id": "4b62e05b-86b8-4699-9c25-33b93b771912",
      "name": "not_evaluating",
      "type": "n8n-nodes-base.if",
      "position": [
        2048,
        -48
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "b8664dcf-79e2-4ea8-8b60-6499c926e65d",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ !!$('merged_inputs').item.json.is_evaluating }}",
              "rightValue": false
            },
            {
              "id": "ca933c48-3547-4237-8342-f580c8eb7d5d",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "d142e14e-2764-4524-97ff-d5fe200f293c",
      "name": "handle_error",
      "type": "n8n-nodes-base.stopAndError",
      "position": [
        1552,
        288
      ],
      "parameters": {
        "errorType": "errorObject",
        "errorObject": "={{ JSON.stringify({\n  error: {\n    message: 'AI agent failed to normalize event strings',\n    execution: $execution,\n    workflow: $workflow\n  },\n  timestamp: new Date().toISOString()\n}) }}"
      },
      "typeVersion": 1
    },
    {
      "id": "78818b79-e7b4-4510-84b1-536dd8f5f993",
      "name": "normalize_eval_data",
      "type": "n8n-nodes-base.set",
      "position": [
        208,
        336
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "d3c3e6f8-7914-4339-8569-08d1ef00b68e",
              "name": "message_text",
              "type": "string",
              "value": "={{ $('load_eval_data').item.json.input }}"
            },
            {
              "id": "b83909cc-24f2-4aea-ac1a-78b48f1026a0",
              "name": "is_evaluating",
              "type": "boolean",
              "value": true
            },
            {
              "id": "0af096b4-c8b5-43fe-97b3-ebe36b497579",
              "name": "=now",
              "type": "string",
              "value": "={{ new Date('2026-01-06T15:30:00Z').toISOString() }}"
            },
            {
              "id": "49eee69b-52d0-45fb-9c2d-3cef6a88ab8d",
              "name": "timezone",
              "type": "string",
              "value": "America/Argentina/Buenos_Aires"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "23c9765a-cc3f-4335-9b47-93034435c48c",
      "name": "record_eval_output",
      "type": "n8n-nodes-base.evaluation",
      "position": [
        2768,
        224
      ],
      "parameters": {
        "source": "googleSheets",
        "outputs": {
          "values": [
            {
              "outputName": "actual_output",
              "outputValue": "={{ JSON.stringify($('normalize_agent_output').first().json.meeting) }}"
            },
            {
              "outputName": "metadata",
              "outputValue": "={{ JSON.stringify($('normalize_agent_output').first().json.reasoning) }}"
            },
            {
              "outputName": "match",
              "outputValue": "={{ $('evaluate_match').first().json.match }}"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/12ZpnquNrd3bxK0y9iCxNi1kQ8QKOZioYMA5Inx6SNz0/edit#gid=0",
          "cachedResultName": "meeting_extractor"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1U89nPsasM2WNv1D7gEYINhDwylyxYw7BOd_i8ipFC0M"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.8
    },
    {
      "id": "d819daba-640c-4539-bdd1-afddfac010ce",
      "name": "evaluate_match",
      "type": "n8n-nodes-base.evaluation",
      "position": [
        2416,
        224
      ],
      "parameters": {
        "prompt": "=Compare actual_meeting to expected_meeting and score from 1 to 5.\n\nEXACT MATCH fields (byte-for-byte):\n- date (YYYY-MM-DD): \"2026-01-07\" \u2260 \"2026-01-08\"\n- time (HH:mm): \"15:00\" \u2260 \"15:01\"  \n- meeting_link (full URL): \"https://zoom.us/j/123\" \u2260 \"https://zoom.us/j/124\"\n\nSEMANTIC MATCH fields (meaning matters, wording flexible):\n- title: \"dentist appointment\" = \"dentist appt\"  = \"dental appointment\"\n- location: \"731 Libertad\" = \"Libertad 731\" = \"Libertad, 731\"\n- attendees: \"team\" = \"the team\" (null expected \u2192 null actual preferred)\n- notes: \"bring documents\" = \"bring your documents\" \"bring your documents\" (null expected \u2192 null actual preferred)\n\nNULL handling:\n- If expected is null, actual should be null (exact fields) or tolerate minor differences (semantic fields)\n\nSCORING:\n5 = Perfect match (all exact fields identical, all semantic fields equivalent)\n4 = Minor semantic differences (exact fields match, semantic slightly off)\n3 = Some exact fields wrong OR multiple semantic mismatches\n2 = Major exact field errors (date/time/link wrong)\n1 = Completely failed (multiple exact fields wrong)\n\nOutput only the number: 1, 2, 3, 4, or 5",
        "options": {
          "metricName": "match"
        },
        "operation": "setMetrics",
        "actualAnswer": "={{ JSON.stringify($('normalize_agent_output').first().json.meeting) }}",
        "expectedAnswer": "={{ JSON.stringify($('load_eval_data').item.json.expected_output) }}"
      },
      "typeVersion": 4.8
    },
    {
      "id": "aeded02e-86ef-4243-a06f-03a71ad56951",
      "name": "extract_meeting_details",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "onError": "continueErrorOutput",
      "position": [
        784,
        -32
      ],
      "parameters": {
        "text": "=Inputs:\n- message_text: {{ $('merged_inputs').first().json.message_text }}\n- reference_datetime_utc: {{ $('merged_inputs').first().json.now }}\n- timezone: {{ $('merged_inputs').first().json.timezone }}",
        "options": {
          "systemMessage": "=You are a meeting details extractor.\n\nExtract structured information from messages about meetings, appointments, or events.\n\nInput:\n- message_text: The message to extract from\n- reference_datetime_iso: Current date/time for resolving relative terms\n- timezone: IANA timezone for date/time resolution\n\nOutput (STRICT JSON only):\n{\n  \"meeting\": {\n    \"title\": \"<event title/subject>\",\n    \"date\": \"<YYYY-MM-DD>\",\n    \"time\": \"<HH:mm in 24h format>\",\n    \"location\": \"<physical location if mentioned>\",\n    \"meeting_link\": \"<URL if present>\",\n    \"attendees\": \"<who's invited/attending>\",\n    \"notes\": \"<any additional context>\"\n  },\n  \"reasoning\": \"<explain how date/time was resolved from relative terms>\"\n}\n\nRules:\n- Resolve relative dates (\"tomorrow\", \"next Friday\") using reference_datetime_iso and timezone\n- Date format: YYYY-MM-DD (always future date)\n- Time format: HH:mm (24-hour, e.g., \"14:30\" not \"2:30 PM\")\n- If field is not mentioned, use null\n- Extract location AND link if both present\n- Keep notes brief and relevant\n- Attendees: extract only actual people/groups mentioned. Ignore message recipients like \"@channel\", \"@everyone\", \"@here\"\n- Reasoning (separate key): explain how you resolved relative terms like \"tomorrow\", \"next Friday\", \"in 2 hours\"\n  - Example: \"'tomorrow' from 2026-01-06 15:30 EST \u2192 2026-01-07\"\n  - Example: \"'next Tuesday' from 2026-01-06 \u2192 2026-01-13\"\n  - Example: \"'in 1 hour' from 2026-01-06 15:30 \u2192 16:30\"\n  - If no relative terms, state: \"Explicit date/time provided\"\n\nReturn only valid JSON. No markdown, no extra text."
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "retryOnFail": true,
      "typeVersion": 2.2,
      "alwaysOutputData": false
    },
    {
      "id": "ee15e729-f594-4692-949e-78e7845d6c27",
      "name": "validate_output",
      "type": "n8n-nodes-base.if",
      "position": [
        1520,
        -32
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "check_has_output",
              "operator": {
                "type": "object",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $('extract_meeting_details').first().json.output }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "381c017a-5248-47ea-a723-ad8dcf379a67",
      "name": "normalize_agent_output",
      "type": "n8n-nodes-base.set",
      "position": [
        1776,
        -48
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "1b6532c7-3aa9-4e66-aa5c-9b7393b012e9",
              "name": "meeting",
              "type": "object",
              "value": "={{ $('extract_meeting_details').first().json.output.meeting }}"
            },
            {
              "id": "021c3a90-77a6-44d4-a4ee-7b34d4c20d66",
              "name": "reasoning",
              "type": "string",
              "value": "={{ $('extract_meeting_details').first().json.output.reasoning }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "974f63f9-4151-4ed2-a05b-3217d164dfd4",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -800,
        -736
      ],
      "parameters": {
        "width": 496,
        "height": 1744,
        "content": "# AI Meeting Extractor with Evaluation Framework\n\nProduction-ready AI agent subworkflow that extracts structured meeting details from natural language messages. Demonstrates best practices for validation, error handling, and automated testing.\n\n### How it works\n\n1. **Input**: Receives message text and timezone via Execute Workflow Trigger\n2. **AI Extraction**: Agent parses the message and extracts meeting details (title, date, time, location, links, attendees, notes)\n3. **Validation**: Checks that AI returned valid structured output\n4. **Output**: Returns extracted meeting object with reasoning for date/time resolution\n\n**Evaluation mode**: When triggered from Google Sheets, runs automated tests comparing actual vs expected outputs, scoring accuracy on a 1-5 scale.\n\n### Setup steps\n\n1. **Connect credentials**: \n   - Add your OpenAI API key to the `extract_meeting_details` AI Agent node\n   - Add Google Sheets OAuth credential for evaluation nodes\n\n2. **Copy the test data sheet**:\n   - Make a copy of this sheet: https://docs.google.com/spreadsheets/d/1U89nPsasM2WNv1D7gEYINhDwylyxYw7BOd_i8ipFC0M/edit?usp=sharing\n   - Replace document ID in `load_eval_data` node (line 7 in parameters)\n   - Replace document ID in `record_eval_output` node\n\n3. **Test normal extraction**:\n   - Use the pinned data in `trigger` node or provide your own message\n   - Execute workflow normally to test extraction\n\n4. **Test evaluation mode**:\n   - Click \"Execute workflow\" button\n   - Select \"from load_eval_data\" to run evaluations against your dataset\n   - Review results in the `match` column of your Google Sheet\n\n5. **Use in parent workflows**:\n   - Add Execute Workflow node in your main workflow\n   - Select this workflow\n   - Pass `message_text` (string) and `timezone` (string, e.g., \"America/New_York\")\n\n### Customization\n\n- **Change extracted fields**: Modify the AI agent system prompt and JSON schema in `Structured Output Parser`\n- **Adjust evaluation criteria**: Edit the `evaluate_match` prompt (currently uses 1-5 scoring with semantic matching)\n- **Add test cases**: Extend your Google Sheet with more examples\n- **Change timezone**: Update the default timezone in `normalize_eval_data` node\n\n### Why subworkflow architecture?\n\nThis pattern makes AI agents **reusable** across multiple parent workflows. Call it from any workflow that needs meeting extraction, and get consistent, validated results every time.\n\n**Key benefit**: Wrapping AI agents in subworkflows allows you to set up **evaluation for each agent individually**. This makes your workflows more testable and maintainable\u2014you can verify each component works correctly before integrating it into larger systems.\n\nPerfect for learning AI agent best practices or adapting to your own extraction needs."
      },
      "typeVersion": 1
    },
    {
      "id": "fada81c5-4293-419f-80a1-6a184fb00e8b",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -160,
        496
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 448,
        "content": "## Load Evaluation Data\n\nGets test cases from Google Sheet and normalizes them to match the format from the trigger node.\n\n**load_eval_data**: Change the spreadsheet ID here to point to your copy of the test data sheet.\n\n**normalize_eval_data**: \n- Sets `is_evaluating` to `true` to enable evaluation mode\n- Hardcodes `now` to `2026-01-06T15:30:00Z` for consistent test results\n- Sets timezone to `America/Argentina/Buenos_Aires`\n\n\u26a0\ufe0f **Important**: The `now` and `timezone` values are linked to the test data inputs and expected outputs. If you change these values, you must also update the corresponding dates in your evaluation sheet, or tests will fail."
      },
      "typeVersion": 1
    },
    {
      "id": "55d30f15-6fe4-4c11-a25f-e4b9a4fd173f",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        624,
        -592
      ],
      "parameters": {
        "color": 7,
        "width": 688,
        "height": 1184,
        "content": "## AI Agent - Core Extraction Logic\n\nThis is the heart of the workflow: an AI agent that extracts meeting details from natural language text.\n\n**What it does**:\n- Takes message text, reference datetime, and timezone as input\n- Uses GPT-4.1-mini to parse the message\n- Returns structured JSON with meeting fields (title, date, time, location, link, attendees, notes)\n- Includes reasoning explaining how relative dates/times were resolved\n- Enforces output format with Structured Output Parser\n\n**Reusing this pattern for other AI agents**:\nTo adapt this subworkflow for a different AI task:\n1. Replace `extract_meeting_details` with your AI agent (can include tools, memory, RAG, etc.)\n2. Update the Structured Output Parser schema to match your output structure\n3. Adjust evaluation criteria in `evaluate_match` node\n4. Update test cases in your Google Sheet with your agent's inputs/outputs\n\nThe validation, error handling, and evaluation framework stay the same regardless of what your AI agent does\u2014whether it's using tools, accessing memory, searching documents, or anything else!"
      },
      "typeVersion": 1
    },
    {
      "id": "2a960655-689d-4d66-9229-7613b5dc9d70",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1456,
        -976
      ],
      "parameters": {
        "color": 7,
        "height": 1440,
        "content": "## Output Validation & Error Handling\n\n**validate_output**: Checks that the AI agent actually returned data.\n\n\u26a0\ufe0f **Why this is needed**: The AI Agent node with \"Continue on Error Output\" sometimes takes the success path even when it fails, returning empty output. This validation catches those cases before they cause issues downstream.\n\n**handle_error**: Stops execution and bubbles the error up to the parent workflow.\n\nThe error includes:\n- Execution ID and workflow metadata for traceability\n- Timestamp for logging\n- Clear error message\n\nThis structured error format allows parent workflows to:\n- Log errors to error tracking systems\n- Send notifications with execution links\n- Debug failures by jumping directly to the failed execution\n\nWithout this explicit structure, you'd only get the default error message\u2014losing critical context like execution IDs needed for debugging and traceability."
      },
      "typeVersion": 1
    },
    {
      "id": "7b5cdd55-b731-4d13-95ab-6b4cdfb81253",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1984,
        -896
      ],
      "parameters": {
        "color": 7,
        "height": 1056,
        "content": "## Evaluation Mode Router\n\n**not_evaluating**: Routes the workflow based on execution mode.\n\n- **False path (evaluation mode)**: Continues to evaluation nodes\n- **True path (normal mode)**: Ends workflow, returns output to parent\n\n\u26a0\ufe0f **Why manual routing instead of \"Check If Evaluating\" node**: \n\nThe built-in \"Check If Evaluating\" node outputs non-evaluation executions to the FALSE branch (second output). In subworkflows, this prevents the output from being returned to the parent workflow properly.\n\nBy using a manual IF node with explicit `is_evaluating` flag, we control which path returns data to the parent, ensuring the workflow works correctly both standalone and when called from other workflows."
      },
      "typeVersion": 1
    },
    {
      "id": "967293ff-bcc4-4cdc-984e-286026662f95",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2384,
        -464
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 1104,
        "content": "## Evaluation: Compare Actual vs Expected\n\n**evaluate_match**: Compares AI output against ground truth from test data.\n\nUses GPT-5-mini to perform semantic matching:\n- **Exact match**: date, time, meeting_link (byte-for-byte)\n- **Semantic match**: title, location, attendees, notes (meaning-based)\n- Scores from 1-5 based on accuracy\n\n\ud83d\udca1 **Best practice**: Use \"Categorization\" metric (exact match) whenever possible\u2014it's faster, more precise, and doesn't use LLM tokens. Only use AI-powered evaluation when you need semantic/fuzzy matching like in this example.\n\n**Customization**: \n- Adjust the evaluation prompt to match your matching criteria\n- Use more powerful models (GPT-5) for complex evaluations\n- Switch to exact matching if your outputs are deterministic\n\n**record_eval_output**: Writes results back to Google Sheet.\n\n\u26a0\ufe0f Change the spreadsheet ID here to match your test data sheet.\n\nOutputs:\n- `actual_output`: What the AI extracted\n- `metadata`: Reasoning from the AI agent\n- `match`: Score from 1-5"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "trigger": {
      "main": [
        [
          {
            "node": "normalize_trigger_input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "output_fixer": {
      "ai_languageModel": [
        [
          {
            "node": "Structured Output Parser",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "merged_inputs": {
      "main": [
        [
          {
            "node": "extract_meeting_details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "evaluate_match": {
      "main": [
        [
          {
            "node": "record_eval_output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "load_eval_data": {
      "main": [
        [
          {
            "node": "normalize_eval_data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "not_evaluating": {
      "main": [
        [],
        [
          {
            "node": "evaluate_match",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "validate_output": {
      "main": [
        [
          {
            "node": "normalize_agent_output",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "handle_error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "extract_meeting_details",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "evaluate_match",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "normalize_eval_data": {
      "main": [
        [
          {
            "node": "merged_inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "normalize_agent_output": {
      "main": [
        [
          {
            "node": "not_evaluating",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "extract_meeting_details": {
      "main": [
        [
          {
            "node": "validate_output",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "handle_error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "normalize_trigger_input": {
      "main": [
        [
          {
            "node": "merged_inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "extract_meeting_details",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    }
  }
}