This is page 49 of 59. Use http://codebase.md/czlonkowski/n8n-mcp?format=md&lines=true&page={x} to view the full context.

# Directory Structure

```
├── _config.yml
├── .claude
│   └── agents
│       ├── code-reviewer.md
│       ├── context-manager.md
│       ├── debugger.md
│       ├── deployment-engineer.md
│       ├── mcp-backend-engineer.md
│       ├── n8n-mcp-tester.md
│       ├── technical-researcher.md
│       └── test-automator.md
├── .dockerignore
├── .env.docker
├── .env.example
├── .env.n8n.example
├── .env.test
├── .env.test.example
├── .github
│   ├── ABOUT.md
│   ├── BENCHMARK_THRESHOLDS.md
│   ├── FUNDING.yml
│   ├── gh-pages.yml
│   ├── secret_scanning.yml
│   └── workflows
│       ├── benchmark-pr.yml
│       ├── benchmark.yml
│       ├── docker-build-fast.yml
│       ├── docker-build-n8n.yml
│       ├── docker-build.yml
│       ├── release.yml
│       ├── test.yml
│       └── update-n8n-deps.yml
├── .gitignore
├── .npmignore
├── ATTRIBUTION.md
├── CHANGELOG.md
├── CLAUDE.md
├── codecov.yml
├── coverage.json
├── data
│   ├── .gitkeep
│   ├── nodes.db
│   ├── nodes.db-shm
│   ├── nodes.db-wal
│   └── templates.db
├── deploy
│   └── quick-deploy-n8n.sh
├── docker
│   ├── docker-entrypoint.sh
│   ├── n8n-mcp
│   ├── parse-config.js
│   └── README.md
├── docker-compose.buildkit.yml
├── docker-compose.extract.yml
├── docker-compose.n8n.yml
├── docker-compose.override.yml.example
├── docker-compose.test-n8n.yml
├── docker-compose.yml
├── Dockerfile
├── Dockerfile.railway
├── Dockerfile.test
├── docs
│   ├── AUTOMATED_RELEASES.md
│   ├── BENCHMARKS.md
│   ├── CHANGELOG.md
│   ├── CLAUDE_CODE_SETUP.md
│   ├── CLAUDE_INTERVIEW.md
│   ├── CODECOV_SETUP.md
│   ├── CODEX_SETUP.md
│   ├── CURSOR_SETUP.md
│   ├── DEPENDENCY_UPDATES.md
│   ├── DOCKER_README.md
│   ├── DOCKER_TROUBLESHOOTING.md
│   ├── FINAL_AI_VALIDATION_SPEC.md
│   ├── FLEXIBLE_INSTANCE_CONFIGURATION.md
│   ├── HTTP_DEPLOYMENT.md
│   ├── img
│   │   ├── cc_command.png
│   │   ├── cc_connected.png
│   │   ├── codex_connected.png
│   │   ├── cursor_tut.png
│   │   ├── Railway_api.png
│   │   ├── Railway_server_address.png
│   │   ├── vsc_ghcp_chat_agent_mode.png
│   │   ├── vsc_ghcp_chat_instruction_files.png
│   │   ├── vsc_ghcp_chat_thinking_tool.png
│   │   └── windsurf_tut.png
│   ├── INSTALLATION.md
│   ├── LIBRARY_USAGE.md
│   ├── local
│   │   ├── DEEP_DIVE_ANALYSIS_2025-10-02.md
│   │   ├── DEEP_DIVE_ANALYSIS_README.md
│   │   ├── Deep_dive_p1_p2.md
│   │   ├── integration-testing-plan.md
│   │   ├── integration-tests-phase1-summary.md
│   │   ├── N8N_AI_WORKFLOW_BUILDER_ANALYSIS.md
│   │   ├── P0_IMPLEMENTATION_PLAN.md
│   │   └── TEMPLATE_MINING_ANALYSIS.md
│   ├── MCP_ESSENTIALS_README.md
│   ├── MCP_QUICK_START_GUIDE.md
│   ├── N8N_DEPLOYMENT.md
│   ├── RAILWAY_DEPLOYMENT.md
│   ├── README_CLAUDE_SETUP.md
│   ├── README.md
│   ├── tools-documentation-usage.md
│   ├── VS_CODE_PROJECT_SETUP.md
│   ├── WINDSURF_SETUP.md
│   └── workflow-diff-examples.md
├── examples
│   └── enhanced-documentation-demo.js
├── fetch_log.txt
├── LICENSE
├── MEMORY_N8N_UPDATE.md
├── MEMORY_TEMPLATE_UPDATE.md
├── monitor_fetch.sh
├── N8N_HTTP_STREAMABLE_SETUP.md
├── n8n-nodes.db
├── P0-R3-TEST-PLAN.md
├── package-lock.json
├── package.json
├── package.runtime.json
├── PRIVACY.md
├── railway.json
├── README.md
├── renovate.json
├── scripts
│   ├── analyze-optimization.sh
│   ├── audit-schema-coverage.ts
│   ├── build-optimized.sh
│   ├── compare-benchmarks.js
│   ├── demo-optimization.sh
│   ├── deploy-http.sh
│   ├── deploy-to-vm.sh
│   ├── export-webhook-workflows.ts
│   ├── extract-changelog.js
│   ├── extract-from-docker.js
│   ├── extract-nodes-docker.sh
│   ├── extract-nodes-simple.sh
│   ├── format-benchmark-results.js
│   ├── generate-benchmark-stub.js
│   ├── generate-detailed-reports.js
│   ├── generate-test-summary.js
│   ├── http-bridge.js
│   ├── mcp-http-client.js
│   ├── migrate-nodes-fts.ts
│   ├── migrate-tool-docs.ts
│   ├── n8n-docs-mcp.service
│   ├── nginx-n8n-mcp.conf
│   ├── prebuild-fts5.ts
│   ├── prepare-release.js
│   ├── publish-npm-quick.sh
│   ├── publish-npm.sh
│   ├── quick-test.ts
│   ├── run-benchmarks-ci.js
│   ├── sync-runtime-version.js
│   ├── test-ai-validation-debug.ts
│   ├── test-code-node-enhancements.ts
│   ├── test-code-node-fixes.ts
│   ├── test-docker-config.sh
│   ├── test-docker-fingerprint.ts
│   ├── test-docker-optimization.sh
│   ├── test-docker.sh
│   ├── test-empty-connection-validation.ts
│   ├── test-error-message-tracking.ts
│   ├── test-error-output-validation.ts
│   ├── test-error-validation.js
│   ├── test-essentials.ts
│   ├── test-expression-code-validation.ts
│   ├── test-expression-format-validation.js
│   ├── test-fts5-search.ts
│   ├── test-fuzzy-fix.ts
│   ├── test-fuzzy-simple.ts
│   ├── test-helpers-validation.ts
│   ├── test-http-search.ts
│   ├── test-http.sh
│   ├── test-jmespath-validation.ts
│   ├── test-multi-tenant-simple.ts
│   ├── test-multi-tenant.ts
│   ├── test-n8n-integration.sh
│   ├── test-node-info.js
│   ├── test-node-type-validation.ts
│   ├── test-nodes-base-prefix.ts
│   ├── test-operation-validation.ts
│   ├── test-optimized-docker.sh
│   ├── test-release-automation.js
│   ├── test-search-improvements.ts
│   ├── test-security.ts
│   ├── test-single-session.sh
│   ├── test-sqljs-triggers.ts
│   ├── test-telemetry-debug.ts
│   ├── test-telemetry-direct.ts
│   ├── test-telemetry-env.ts
│   ├── test-telemetry-integration.ts
│   ├── test-telemetry-no-select.ts
│   ├── test-telemetry-security.ts
│   ├── test-telemetry-simple.ts
│   ├── test-typeversion-validation.ts
│   ├── test-url-configuration.ts
│   ├── test-user-id-persistence.ts
│   ├── test-webhook-validation.ts
│   ├── test-workflow-insert.ts
│   ├── test-workflow-sanitizer.ts
│   ├── test-workflow-tracking-debug.ts
│   ├── update-and-publish-prep.sh
│   ├── update-n8n-deps.js
│   ├── update-readme-version.js
│   ├── vitest-benchmark-json-reporter.js
│   └── vitest-benchmark-reporter.ts
├── SECURITY.md
├── src
│   ├── config
│   │   └── n8n-api.ts
│   ├── data
│   │   └── canonical-ai-tool-examples.json
│   ├── database
│   │   ├── database-adapter.ts
│   │   ├── migrations
│   │   │   └── add-template-node-configs.sql
│   │   ├── node-repository.ts
│   │   ├── nodes.db
│   │   ├── schema-optimized.sql
│   │   └── schema.sql
│   ├── errors
│   │   └── validation-service-error.ts
│   ├── http-server-single-session.ts
│   ├── http-server.ts
│   ├── index.ts
│   ├── loaders
│   │   └── node-loader.ts
│   ├── mappers
│   │   └── docs-mapper.ts
│   ├── mcp
│   │   ├── handlers-n8n-manager.ts
│   │   ├── handlers-workflow-diff.ts
│   │   ├── index.ts
│   │   ├── server.ts
│   │   ├── stdio-wrapper.ts
│   │   ├── tool-docs
│   │   │   ├── configuration
│   │   │   │   ├── get-node-as-tool-info.ts
│   │   │   │   ├── get-node-documentation.ts
│   │   │   │   ├── get-node-essentials.ts
│   │   │   │   ├── get-node-info.ts
│   │   │   │   ├── get-property-dependencies.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── search-node-properties.ts
│   │   │   ├── discovery
│   │   │   │   ├── get-database-statistics.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── list-ai-tools.ts
│   │   │   │   ├── list-nodes.ts
│   │   │   │   └── search-nodes.ts
│   │   │   ├── guides
│   │   │   │   ├── ai-agents-guide.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.ts
│   │   │   ├── system
│   │   │   │   ├── index.ts
│   │   │   │   ├── n8n-diagnostic.ts
│   │   │   │   ├── n8n-health-check.ts
│   │   │   │   ├── n8n-list-available-tools.ts
│   │   │   │   └── tools-documentation.ts
│   │   │   ├── templates
│   │   │   │   ├── get-template.ts
│   │   │   │   ├── get-templates-for-task.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── list-node-templates.ts
│   │   │   │   ├── list-tasks.ts
│   │   │   │   ├── search-templates-by-metadata.ts
│   │   │   │   └── search-templates.ts
│   │   │   ├── types.ts
│   │   │   ├── validation
│   │   │   │   ├── index.ts
│   │   │   │   ├── validate-node-minimal.ts
│   │   │   │   ├── validate-node-operation.ts
│   │   │   │   ├── validate-workflow-connections.ts
│   │   │   │   ├── validate-workflow-expressions.ts
│   │   │   │   └── validate-workflow.ts
│   │   │   └── workflow_management
│   │   │       ├── index.ts
│   │   │       ├── n8n-autofix-workflow.ts
│   │   │       ├── n8n-create-workflow.ts
│   │   │       ├── n8n-delete-execution.ts
│   │   │       ├── n8n-delete-workflow.ts
│   │   │       ├── n8n-get-execution.ts
│   │   │       ├── n8n-get-workflow-details.ts
│   │   │       ├── n8n-get-workflow-minimal.ts
│   │   │       ├── n8n-get-workflow-structure.ts
│   │   │       ├── n8n-get-workflow.ts
│   │   │       ├── n8n-list-executions.ts
│   │   │       ├── n8n-list-workflows.ts
│   │   │       ├── n8n-trigger-webhook-workflow.ts
│   │   │       ├── n8n-update-full-workflow.ts
│   │   │       ├── n8n-update-partial-workflow.ts
│   │   │       └── n8n-validate-workflow.ts
│   │   ├── tools-documentation.ts
│   │   ├── tools-n8n-friendly.ts
│   │   ├── tools-n8n-manager.ts
│   │   ├── tools.ts
│   │   └── workflow-examples.ts
│   ├── mcp-engine.ts
│   ├── mcp-tools-engine.ts
│   ├── n8n
│   │   ├── MCPApi.credentials.ts
│   │   └── MCPNode.node.ts
│   ├── parsers
│   │   ├── node-parser.ts
│   │   ├── property-extractor.ts
│   │   └── simple-parser.ts
│   ├── scripts
│   │   ├── debug-http-search.ts
│   │   ├── extract-from-docker.ts
│   │   ├── fetch-templates-robust.ts
│   │   ├── fetch-templates.ts
│   │   ├── rebuild-database.ts
│   │   ├── rebuild-optimized.ts
│   │   ├── rebuild.ts
│   │   ├── sanitize-templates.ts
│   │   ├── seed-canonical-ai-examples.ts
│   │   ├── test-autofix-documentation.ts
│   │   ├── test-autofix-workflow.ts
│   │   ├── test-execution-filtering.ts
│   │   ├── test-node-suggestions.ts
│   │   ├── test-protocol-negotiation.ts
│   │   ├── test-summary.ts
│   │   ├── test-webhook-autofix.ts
│   │   ├── validate.ts
│   │   └── validation-summary.ts
│   ├── services
│   │   ├── ai-node-validator.ts
│   │   ├── ai-tool-validators.ts
│   │   ├── confidence-scorer.ts
│   │   ├── config-validator.ts
│   │   ├── enhanced-config-validator.ts
│   │   ├── example-generator.ts
│   │   ├── execution-processor.ts
│   │   ├── expression-format-validator.ts
│   │   ├── expression-validator.ts
│   │   ├── n8n-api-client.ts
│   │   ├── n8n-validation.ts
│   │   ├── node-documentation-service.ts
│   │   ├── node-similarity-service.ts
│   │   ├── node-specific-validators.ts
│   │   ├── operation-similarity-service.ts
│   │   ├── property-dependencies.ts
│   │   ├── property-filter.ts
│   │   ├── resource-similarity-service.ts
│   │   ├── sqlite-storage-service.ts
│   │   ├── task-templates.ts
│   │   ├── universal-expression-validator.ts
│   │   ├── workflow-auto-fixer.ts
│   │   ├── workflow-diff-engine.ts
│   │   └── workflow-validator.ts
│   ├── telemetry
│   │   ├── batch-processor.ts
│   │   ├── config-manager.ts
│   │   ├── early-error-logger.ts
│   │   ├── error-sanitization-utils.ts
│   │   ├── error-sanitizer.ts
│   │   ├── event-tracker.ts
│   │   ├── event-validator.ts
│   │   ├── index.ts
│   │   ├── performance-monitor.ts
│   │   ├── rate-limiter.ts
│   │   ├── startup-checkpoints.ts
│   │   ├── telemetry-error.ts
│   │   ├── telemetry-manager.ts
│   │   ├── telemetry-types.ts
│   │   └── workflow-sanitizer.ts
│   ├── templates
│   │   ├── batch-processor.ts
│   │   ├── metadata-generator.ts
│   │   ├── README.md
│   │   ├── template-fetcher.ts
│   │   ├── template-repository.ts
│   │   └── template-service.ts
│   ├── types
│   │   ├── index.ts
│   │   ├── instance-context.ts
│   │   ├── n8n-api.ts
│   │   ├── node-types.ts
│   │   └── workflow-diff.ts
│   └── utils
│       ├── auth.ts
│       ├── bridge.ts
│       ├── cache-utils.ts
│       ├── console-manager.ts
│       ├── documentation-fetcher.ts
│       ├── enhanced-documentation-fetcher.ts
│       ├── error-handler.ts
│       ├── example-generator.ts
│       ├── fixed-collection-validator.ts
│       ├── logger.ts
│       ├── mcp-client.ts
│       ├── n8n-errors.ts
│       ├── node-source-extractor.ts
│       ├── node-type-normalizer.ts
│       ├── node-type-utils.ts
│       ├── node-utils.ts
│       ├── npm-version-checker.ts
│       ├── protocol-version.ts
│       ├── simple-cache.ts
│       ├── ssrf-protection.ts
│       ├── template-node-resolver.ts
│       ├── template-sanitizer.ts
│       ├── url-detector.ts
│       ├── validation-schemas.ts
│       └── version.ts
├── test-output.txt
├── test-reinit-fix.sh
├── tests
│   ├── __snapshots__
│   │   └── .gitkeep
│   ├── auth.test.ts
│   ├── benchmarks
│   │   ├── database-queries.bench.ts
│   │   ├── index.ts
│   │   ├── mcp-tools.bench.ts
│   │   ├── mcp-tools.bench.ts.disabled
│   │   ├── mcp-tools.bench.ts.skip
│   │   ├── node-loading.bench.ts.disabled
│   │   ├── README.md
│   │   ├── search-operations.bench.ts.disabled
│   │   └── validation-performance.bench.ts.disabled
│   ├── bridge.test.ts
│   ├── comprehensive-extraction-test.js
│   ├── data
│   │   └── .gitkeep
│   ├── debug-slack-doc.js
│   ├── demo-enhanced-documentation.js
│   ├── docker-tests-README.md
│   ├── error-handler.test.ts
│   ├── examples
│   │   └── using-database-utils.test.ts
│   ├── extracted-nodes-db
│   │   ├── database-import.json
│   │   ├── extraction-report.json
│   │   ├── insert-nodes.sql
│   │   ├── n8n-nodes-base__Airtable.json
│   │   ├── n8n-nodes-base__Discord.json
│   │   ├── n8n-nodes-base__Function.json
│   │   ├── n8n-nodes-base__HttpRequest.json
│   │   ├── n8n-nodes-base__If.json
│   │   ├── n8n-nodes-base__Slack.json
│   │   ├── n8n-nodes-base__SplitInBatches.json
│   │   └── n8n-nodes-base__Webhook.json
│   ├── factories
│   │   ├── node-factory.ts
│   │   └── property-definition-factory.ts
│   ├── fixtures
│   │   ├── .gitkeep
│   │   ├── database
│   │   │   └── test-nodes.json
│   │   ├── factories
│   │   │   ├── node.factory.ts
│   │   │   └── parser-node.factory.ts
│   │   └── template-configs.ts
│   ├── helpers
│   │   └── env-helpers.ts
│   ├── http-server-auth.test.ts
│   ├── integration
│   │   ├── ai-validation
│   │   │   ├── ai-agent-validation.test.ts
│   │   │   ├── ai-tool-validation.test.ts
│   │   │   ├── chat-trigger-validation.test.ts
│   │   │   ├── e2e-validation.test.ts
│   │   │   ├── helpers.ts
│   │   │   ├── llm-chain-validation.test.ts
│   │   │   ├── README.md
│   │   │   └── TEST_REPORT.md
│   │   ├── ci
│   │   │   └── database-population.test.ts
│   │   ├── database
│   │   │   ├── connection-management.test.ts
│   │   │   ├── empty-database.test.ts
│   │   │   ├── fts5-search.test.ts
│   │   │   ├── node-fts5-search.test.ts
│   │   │   ├── node-repository.test.ts
│   │   │   ├── performance.test.ts
│   │   │   ├── template-node-configs.test.ts
│   │   │   ├── template-repository.test.ts
│   │   │   ├── test-utils.ts
│   │   │   └── transactions.test.ts
│   │   ├── database-integration.test.ts
│   │   ├── docker
│   │   │   ├── docker-config.test.ts
│   │   │   ├── docker-entrypoint.test.ts
│   │   │   └── test-helpers.ts
│   │   ├── flexible-instance-config.test.ts
│   │   ├── mcp
│   │   │   └── template-examples-e2e.test.ts
│   │   ├── mcp-protocol
│   │   │   ├── basic-connection.test.ts
│   │   │   ├── error-handling.test.ts
│   │   │   ├── performance.test.ts
│   │   │   ├── protocol-compliance.test.ts
│   │   │   ├── README.md
│   │   │   ├── session-management.test.ts
│   │   │   ├── test-helpers.ts
│   │   │   ├── tool-invocation.test.ts
│   │   │   └── workflow-error-validation.test.ts
│   │   ├── msw-setup.test.ts
│   │   ├── n8n-api
│   │   │   ├── executions
│   │   │   │   ├── delete-execution.test.ts
│   │   │   │   ├── get-execution.test.ts
│   │   │   │   ├── list-executions.test.ts
│   │   │   │   └── trigger-webhook.test.ts
│   │   │   ├── scripts
│   │   │   │   └── cleanup-orphans.ts
│   │   │   ├── system
│   │   │   │   ├── diagnostic.test.ts
│   │   │   │   ├── health-check.test.ts
│   │   │   │   └── list-tools.test.ts
│   │   │   ├── test-connection.ts
│   │   │   ├── types
│   │   │   │   └── mcp-responses.ts
│   │   │   ├── utils
│   │   │   │   ├── cleanup-helpers.ts
│   │   │   │   ├── credentials.ts
│   │   │   │   ├── factories.ts
│   │   │   │   ├── fixtures.ts
│   │   │   │   ├── mcp-context.ts
│   │   │   │   ├── n8n-client.ts
│   │   │   │   ├── node-repository.ts
│   │   │   │   ├── response-types.ts
│   │   │   │   ├── test-context.ts
│   │   │   │   └── webhook-workflows.ts
│   │   │   └── workflows
│   │   │       ├── autofix-workflow.test.ts
│   │   │       ├── create-workflow.test.ts
│   │   │       ├── delete-workflow.test.ts
│   │   │       ├── get-workflow-details.test.ts
│   │   │       ├── get-workflow-minimal.test.ts
│   │   │       ├── get-workflow-structure.test.ts
│   │   │       ├── get-workflow.test.ts
│   │   │       ├── list-workflows.test.ts
│   │   │       ├── smart-parameters.test.ts
│   │   │       ├── update-partial-workflow.test.ts
│   │   │       ├── update-workflow.test.ts
│   │   │       └── validate-workflow.test.ts
│   │   ├── security
│   │   │   ├── command-injection-prevention.test.ts
│   │   │   └── rate-limiting.test.ts
│   │   ├── setup
│   │   │   ├── integration-setup.ts
│   │   │   └── msw-test-server.ts
│   │   ├── telemetry
│   │   │   ├── docker-user-id-stability.test.ts
│   │   │   └── mcp-telemetry.test.ts
│   │   ├── templates
│   │   │   └── metadata-operations.test.ts
│   │   └── workflow-creation-node-type-format.test.ts
│   ├── logger.test.ts
│   ├── MOCKING_STRATEGY.md
│   ├── mocks
│   │   ├── n8n-api
│   │   │   ├── data
│   │   │   │   ├── credentials.ts
│   │   │   │   ├── executions.ts
│   │   │   │   └── workflows.ts
│   │   │   ├── handlers.ts
│   │   │   └── index.ts
│   │   └── README.md
│   ├── node-storage-export.json
│   ├── setup
│   │   ├── global-setup.ts
│   │   ├── msw-setup.ts
│   │   ├── TEST_ENV_DOCUMENTATION.md
│   │   └── test-env.ts
│   ├── test-database-extraction.js
│   ├── test-direct-extraction.js
│   ├── test-enhanced-documentation.js
│   ├── test-enhanced-integration.js
│   ├── test-mcp-extraction.js
│   ├── test-mcp-server-extraction.js
│   ├── test-mcp-tools-integration.js
│   ├── test-node-documentation-service.js
│   ├── test-node-list.js
│   ├── test-package-info.js
│   ├── test-parsing-operations.js
│   ├── test-slack-node-complete.js
│   ├── test-small-rebuild.js
│   ├── test-sqlite-search.js
│   ├── test-storage-system.js
│   ├── unit
│   │   ├── __mocks__
│   │   │   ├── n8n-nodes-base.test.ts
│   │   │   ├── n8n-nodes-base.ts
│   │   │   └── README.md
│   │   ├── database
│   │   │   ├── __mocks__
│   │   │   │   └── better-sqlite3.ts
│   │   │   ├── database-adapter-unit.test.ts
│   │   │   ├── node-repository-core.test.ts
│   │   │   ├── node-repository-operations.test.ts
│   │   │   ├── node-repository-outputs.test.ts
│   │   │   ├── README.md
│   │   │   └── template-repository-core.test.ts
│   │   ├── docker
│   │   │   ├── config-security.test.ts
│   │   │   ├── edge-cases.test.ts
│   │   │   ├── parse-config.test.ts
│   │   │   └── serve-command.test.ts
│   │   ├── errors
│   │   │   └── validation-service-error.test.ts
│   │   ├── examples
│   │   │   └── using-n8n-nodes-base-mock.test.ts
│   │   ├── flexible-instance-security-advanced.test.ts
│   │   ├── flexible-instance-security.test.ts
│   │   ├── http-server
│   │   │   └── multi-tenant-support.test.ts
│   │   ├── http-server-n8n-mode.test.ts
│   │   ├── http-server-n8n-reinit.test.ts
│   │   ├── http-server-session-management.test.ts
│   │   ├── loaders
│   │   │   └── node-loader.test.ts
│   │   ├── mappers
│   │   │   └── docs-mapper.test.ts
│   │   ├── mcp
│   │   │   ├── get-node-essentials-examples.test.ts
│   │   │   ├── handlers-n8n-manager-simple.test.ts
│   │   │   ├── handlers-n8n-manager.test.ts
│   │   │   ├── handlers-workflow-diff.test.ts
│   │   │   ├── lru-cache-behavior.test.ts
│   │   │   ├── multi-tenant-tool-listing.test.ts.disabled
│   │   │   ├── parameter-validation.test.ts
│   │   │   ├── search-nodes-examples.test.ts
│   │   │   ├── tools-documentation.test.ts
│   │   │   └── tools.test.ts
│   │   ├── monitoring
│   │   │   └── cache-metrics.test.ts
│   │   ├── MULTI_TENANT_TEST_COVERAGE.md
│   │   ├── multi-tenant-integration.test.ts
│   │   ├── parsers
│   │   │   ├── node-parser-outputs.test.ts
│   │   │   ├── node-parser.test.ts
│   │   │   ├── property-extractor.test.ts
│   │   │   └── simple-parser.test.ts
│   │   ├── scripts
│   │   │   └── fetch-templates-extraction.test.ts
│   │   ├── services
│   │   │   ├── ai-node-validator.test.ts
│   │   │   ├── ai-tool-validators.test.ts
│   │   │   ├── confidence-scorer.test.ts
│   │   │   ├── config-validator-basic.test.ts
│   │   │   ├── config-validator-edge-cases.test.ts
│   │   │   ├── config-validator-node-specific.test.ts
│   │   │   ├── config-validator-security.test.ts
│   │   │   ├── debug-validator.test.ts
│   │   │   ├── enhanced-config-validator-integration.test.ts
│   │   │   ├── enhanced-config-validator-operations.test.ts
│   │   │   ├── enhanced-config-validator.test.ts
│   │   │   ├── example-generator.test.ts
│   │   │   ├── execution-processor.test.ts
│   │   │   ├── expression-format-validator.test.ts
│   │   │   ├── expression-validator-edge-cases.test.ts
│   │   │   ├── expression-validator.test.ts
│   │   │   ├── fixed-collection-validation.test.ts
│   │   │   ├── loop-output-edge-cases.test.ts
│   │   │   ├── n8n-api-client.test.ts
│   │   │   ├── n8n-validation.test.ts
│   │   │   ├── node-similarity-service.test.ts
│   │   │   ├── node-specific-validators.test.ts
│   │   │   ├── operation-similarity-service-comprehensive.test.ts
│   │   │   ├── operation-similarity-service.test.ts
│   │   │   ├── property-dependencies.test.ts
│   │   │   ├── property-filter-edge-cases.test.ts
│   │   │   ├── property-filter.test.ts
│   │   │   ├── resource-similarity-service-comprehensive.test.ts
│   │   │   ├── resource-similarity-service.test.ts
│   │   │   ├── task-templates.test.ts
│   │   │   ├── template-service.test.ts
│   │   │   ├── universal-expression-validator.test.ts
│   │   │   ├── validation-fixes.test.ts
│   │   │   ├── workflow-auto-fixer.test.ts
│   │   │   ├── workflow-diff-engine.test.ts
│   │   │   ├── workflow-fixed-collection-validation.test.ts
│   │   │   ├── workflow-validator-comprehensive.test.ts
│   │   │   ├── workflow-validator-edge-cases.test.ts
│   │   │   ├── workflow-validator-error-outputs.test.ts
│   │   │   ├── workflow-validator-expression-format.test.ts
│   │   │   ├── workflow-validator-loops-simple.test.ts
│   │   │   ├── workflow-validator-loops.test.ts
│   │   │   ├── workflow-validator-mocks.test.ts
│   │   │   ├── workflow-validator-performance.test.ts
│   │   │   ├── workflow-validator-with-mocks.test.ts
│   │   │   └── workflow-validator.test.ts
│   │   ├── telemetry
│   │   │   ├── batch-processor.test.ts
│   │   │   ├── config-manager.test.ts
│   │   │   ├── event-tracker.test.ts
│   │   │   ├── event-validator.test.ts
│   │   │   ├── rate-limiter.test.ts
│   │   │   ├── telemetry-error.test.ts
│   │   │   ├── telemetry-manager.test.ts
│   │   │   ├── v2.18.3-fixes-verification.test.ts
│   │   │   └── workflow-sanitizer.test.ts
│   │   ├── templates
│   │   │   ├── batch-processor.test.ts
│   │   │   ├── metadata-generator.test.ts
│   │   │   ├── template-repository-metadata.test.ts
│   │   │   └── template-repository-security.test.ts
│   │   ├── test-env-example.test.ts
│   │   ├── test-infrastructure.test.ts
│   │   ├── types
│   │   │   ├── instance-context-coverage.test.ts
│   │   │   └── instance-context-multi-tenant.test.ts
│   │   ├── utils
│   │   │   ├── auth-timing-safe.test.ts
│   │   │   ├── cache-utils.test.ts
│   │   │   ├── console-manager.test.ts
│   │   │   ├── database-utils.test.ts
│   │   │   ├── fixed-collection-validator.test.ts
│   │   │   ├── n8n-errors.test.ts
│   │   │   ├── node-type-normalizer.test.ts
│   │   │   ├── node-type-utils.test.ts
│   │   │   ├── node-utils.test.ts
│   │   │   ├── simple-cache-memory-leak-fix.test.ts
│   │   │   ├── ssrf-protection.test.ts
│   │   │   └── template-node-resolver.test.ts
│   │   └── validation-fixes.test.ts
│   └── utils
│       ├── assertions.ts
│       ├── builders
│       │   └── workflow.builder.ts
│       ├── data-generators.ts
│       ├── database-utils.ts
│       ├── README.md
│       └── test-helpers.ts
├── thumbnail.png
├── tsconfig.build.json
├── tsconfig.json
├── types
│   ├── mcp.d.ts
│   └── test-env.d.ts
├── verify-telemetry-fix.js
├── versioned-nodes.md
├── vitest.config.benchmark.ts
├── vitest.config.integration.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/src/services/workflow-validator.ts:
--------------------------------------------------------------------------------

```typescript
   1 | /**
   2 |  * Workflow Validator for n8n workflows
   3 |  * Validates complete workflow structure, connections, and node configurations
   4 |  */
   5 | 
   6 | import { NodeRepository } from '../database/node-repository';
   7 | import { EnhancedConfigValidator } from './enhanced-config-validator';
   8 | import { ExpressionValidator } from './expression-validator';
   9 | import { ExpressionFormatValidator } from './expression-format-validator';
  10 | import { NodeSimilarityService, NodeSuggestion } from './node-similarity-service';
  11 | import { NodeTypeNormalizer } from '../utils/node-type-normalizer';
  12 | import { Logger } from '../utils/logger';
  13 | import { validateAISpecificNodes, hasAINodes } from './ai-node-validator';
  14 | const logger = new Logger({ prefix: '[WorkflowValidator]' });
  15 | 
  16 | interface WorkflowNode {
  17 |   id: string;
  18 |   name: string;
  19 |   type: string;
  20 |   position: [number, number];
  21 |   parameters: any;
  22 |   credentials?: any;
  23 |   disabled?: boolean;
  24 |   notes?: string;
  25 |   notesInFlow?: boolean;
  26 |   typeVersion?: number;
  27 |   continueOnFail?: boolean;
  28 |   onError?: 'continueRegularOutput' | 'continueErrorOutput' | 'stopWorkflow';
  29 |   retryOnFail?: boolean;
  30 |   maxTries?: number;
  31 |   waitBetweenTries?: number;
  32 |   alwaysOutputData?: boolean;
  33 |   executeOnce?: boolean;
  34 | }
  35 | 
  36 | interface WorkflowConnection {
  37 |   [sourceNode: string]: {
  38 |     main?: Array<Array<{ node: string; type: string; index: number }>>;
  39 |     error?: Array<Array<{ node: string; type: string; index: number }>>;
  40 |     ai_tool?: Array<Array<{ node: string; type: string; index: number }>>;
  41 |   };
  42 | }
  43 | 
  44 | interface WorkflowJson {
  45 |   name?: string;
  46 |   nodes: WorkflowNode[];
  47 |   connections: WorkflowConnection;
  48 |   settings?: any;
  49 |   staticData?: any;
  50 |   pinData?: any;
  51 |   meta?: any;
  52 | }
  53 | 
  54 | interface ValidationIssue {
  55 |   type: 'error' | 'warning';
  56 |   nodeId?: string;
  57 |   nodeName?: string;
  58 |   message: string;
  59 |   details?: any;
  60 | }
  61 | 
  62 | export interface WorkflowValidationResult {
  63 |   valid: boolean;
  64 |   errors: ValidationIssue[];
  65 |   warnings: ValidationIssue[];
  66 |   statistics: {
  67 |     totalNodes: number;
  68 |     enabledNodes: number;
  69 |     triggerNodes: number;
  70 |     validConnections: number;
  71 |     invalidConnections: number;
  72 |     expressionsValidated: number;
  73 |   };
  74 |   suggestions: string[];
  75 | }
  76 | 
  77 | export class WorkflowValidator {
  78 |   private currentWorkflow: WorkflowJson | null = null;
  79 |   private similarityService: NodeSimilarityService;
  80 | 
  81 |   constructor(
  82 |     private nodeRepository: NodeRepository,
  83 |     private nodeValidator: typeof EnhancedConfigValidator
  84 |   ) {
  85 |     this.similarityService = new NodeSimilarityService(nodeRepository);
  86 |   }
  87 | 
  88 |   /**
  89 |    * Check if a node is a Sticky Note or other non-executable node
  90 |    */
  91 |   private isStickyNote(node: WorkflowNode): boolean {
  92 |     const stickyNoteTypes = [
  93 |       'n8n-nodes-base.stickyNote',
  94 |       'nodes-base.stickyNote',
  95 |       '@n8n/n8n-nodes-base.stickyNote'
  96 |     ];
  97 |     return stickyNoteTypes.includes(node.type);
  98 |   }
  99 | 
 100 |   /**
 101 |    * Validate a complete workflow
 102 |    */
 103 |   async validateWorkflow(
 104 |     workflow: WorkflowJson,
 105 |     options: {
 106 |       validateNodes?: boolean;
 107 |       validateConnections?: boolean;
 108 |       validateExpressions?: boolean;
 109 |       profile?: 'minimal' | 'runtime' | 'ai-friendly' | 'strict';
 110 |     } = {}
 111 |   ): Promise<WorkflowValidationResult> {
 112 |     // Store current workflow for access in helper methods
 113 |     this.currentWorkflow = workflow;
 114 | 
 115 |     const {
 116 |       validateNodes = true,
 117 |       validateConnections = true,
 118 |       validateExpressions = true,
 119 |       profile = 'runtime'
 120 |     } = options;
 121 | 
 122 |     const result: WorkflowValidationResult = {
 123 |       valid: true,
 124 |       errors: [],
 125 |       warnings: [],
 126 |       statistics: {
 127 |         totalNodes: 0,
 128 |         enabledNodes: 0,
 129 |         triggerNodes: 0,
 130 |         validConnections: 0,
 131 |         invalidConnections: 0,
 132 |         expressionsValidated: 0,
 133 |       },
 134 |       suggestions: []
 135 |     };
 136 | 
 137 |     try {
 138 |       // Handle null/undefined workflow
 139 |       if (!workflow) {
 140 |         result.errors.push({
 141 |           type: 'error',
 142 |           message: 'Invalid workflow structure: workflow is null or undefined'
 143 |         });
 144 |         result.valid = false;
 145 |         return result;
 146 |       }
 147 | 
 148 |       // Update statistics after null check (exclude sticky notes from counts)
 149 |       const executableNodes = Array.isArray(workflow.nodes) ? workflow.nodes.filter(n => !this.isStickyNote(n)) : [];
 150 |       result.statistics.totalNodes = executableNodes.length;
 151 |       result.statistics.enabledNodes = executableNodes.filter(n => !n.disabled).length;
 152 | 
 153 |       // Basic workflow structure validation
 154 |       this.validateWorkflowStructure(workflow, result);
 155 | 
 156 |       // Only continue if basic structure is valid
 157 |       if (workflow.nodes && Array.isArray(workflow.nodes) && workflow.connections && typeof workflow.connections === 'object') {
 158 |         // Validate each node if requested
 159 |         if (validateNodes && workflow.nodes.length > 0) {
 160 |           await this.validateAllNodes(workflow, result, profile);
 161 |         }
 162 | 
 163 |         // Validate connections if requested
 164 |         if (validateConnections) {
 165 |           this.validateConnections(workflow, result, profile);
 166 |         }
 167 | 
 168 |         // Validate expressions if requested
 169 |         if (validateExpressions && workflow.nodes.length > 0) {
 170 |           this.validateExpressions(workflow, result, profile);
 171 |         }
 172 | 
 173 |         // Check workflow patterns and best practices
 174 |         if (workflow.nodes.length > 0) {
 175 |           this.checkWorkflowPatterns(workflow, result, profile);
 176 |         }
 177 | 
 178 |         // Validate AI-specific nodes (AI Agent, Chat Trigger, AI tools)
 179 |         if (workflow.nodes.length > 0 && hasAINodes(workflow)) {
 180 |           const aiIssues = validateAISpecificNodes(workflow);
 181 |           // Convert AI validation issues to workflow validation format
 182 |           for (const issue of aiIssues) {
 183 |             const validationIssue: ValidationIssue = {
 184 |               type: issue.severity === 'error' ? 'error' : 'warning',
 185 |               nodeId: issue.nodeId,
 186 |               nodeName: issue.nodeName,
 187 |               message: issue.message,
 188 |               details: issue.code ? { code: issue.code } : undefined
 189 |             };
 190 | 
 191 |             if (issue.severity === 'error') {
 192 |               result.errors.push(validationIssue);
 193 |             } else {
 194 |               result.warnings.push(validationIssue);
 195 |             }
 196 |           }
 197 |         }
 198 | 
 199 |         // Add suggestions based on findings
 200 |         this.generateSuggestions(workflow, result);
 201 | 
 202 |         // Add AI-specific recovery suggestions if there are errors
 203 |         if (result.errors.length > 0) {
 204 |           this.addErrorRecoverySuggestions(result);
 205 |         }
 206 |       }
 207 | 
 208 |     } catch (error) {
 209 |       logger.error('Error validating workflow:', error);
 210 |       result.errors.push({
 211 |         type: 'error',
 212 |         message: `Workflow validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`
 213 |       });
 214 |     }
 215 | 
 216 |     result.valid = result.errors.length === 0;
 217 |     return result;
 218 |   }
 219 | 
 220 |   /**
 221 |    * Validate basic workflow structure
 222 |    */
 223 |   private validateWorkflowStructure(
 224 |     workflow: WorkflowJson,
 225 |     result: WorkflowValidationResult
 226 |   ): void {
 227 |     // Check for required fields
 228 |     if (!workflow.nodes) {
 229 |       result.errors.push({
 230 |         type: 'error',
 231 |         message: workflow.nodes === null ? 'nodes must be an array' : 'Workflow must have a nodes array'
 232 |       });
 233 |       return;
 234 |     }
 235 | 
 236 |     if (!Array.isArray(workflow.nodes)) {
 237 |       result.errors.push({
 238 |         type: 'error',
 239 |         message: 'nodes must be an array'
 240 |       });
 241 |       return;
 242 |     }
 243 | 
 244 |     if (!workflow.connections) {
 245 |       result.errors.push({
 246 |         type: 'error',
 247 |         message: workflow.connections === null ? 'connections must be an object' : 'Workflow must have a connections object'
 248 |       });
 249 |       return;
 250 |     }
 251 | 
 252 |     if (typeof workflow.connections !== 'object' || Array.isArray(workflow.connections)) {
 253 |       result.errors.push({
 254 |         type: 'error',
 255 |         message: 'connections must be an object'
 256 |       });
 257 |       return;
 258 |     }
 259 | 
 260 |     // Check for empty workflow - this should be a warning, not an error
 261 |     if (workflow.nodes.length === 0) {
 262 |       result.warnings.push({
 263 |         type: 'warning',
 264 |         message: 'Workflow is empty - no nodes defined'
 265 |       });
 266 |       return;
 267 |     }
 268 | 
 269 |     // Check for minimum viable workflow
 270 |     if (workflow.nodes.length === 1) {
 271 |       const singleNode = workflow.nodes[0];
 272 |       const normalizedType = NodeTypeNormalizer.normalizeToFullForm(singleNode.type);
 273 |       const isWebhook = normalizedType === 'nodes-base.webhook' ||
 274 |                        normalizedType === 'nodes-base.webhookTrigger';
 275 |       const isLangchainNode = normalizedType.startsWith('nodes-langchain.');
 276 | 
 277 |       // Langchain nodes can be validated standalone for AI tool purposes
 278 |       if (!isWebhook && !isLangchainNode) {
 279 |         result.errors.push({
 280 |           type: 'error',
 281 |           message: 'Single-node workflows are only valid for webhook endpoints. Add at least one more connected node to create a functional workflow.'
 282 |         });
 283 |       } else if (isWebhook && Object.keys(workflow.connections).length === 0) {
 284 |         result.warnings.push({
 285 |           type: 'warning',
 286 |           message: 'Webhook node has no connections. Consider adding nodes to process the webhook data.'
 287 |         });
 288 |       }
 289 |     }
 290 | 
 291 |     // Check for empty connections in multi-node workflows
 292 |     if (workflow.nodes.length > 1) {
 293 |       const hasEnabledNodes = workflow.nodes.some(n => !n.disabled);
 294 |       const hasConnections = Object.keys(workflow.connections).length > 0;
 295 |       
 296 |       if (hasEnabledNodes && !hasConnections) {
 297 |         result.errors.push({
 298 |           type: 'error',
 299 |           message: 'Multi-node workflow has no connections. Nodes must be connected to create a workflow. Use connections: { "Source Node Name": { "main": [[{ "node": "Target Node Name", "type": "main", "index": 0 }]] } }'
 300 |         });
 301 |       }
 302 |     }
 303 | 
 304 |     // Check for duplicate node names
 305 |     const nodeNames = new Set<string>();
 306 |     const nodeIds = new Set<string>();
 307 |     
 308 |     for (const node of workflow.nodes) {
 309 |       if (nodeNames.has(node.name)) {
 310 |         result.errors.push({
 311 |           type: 'error',
 312 |           nodeId: node.id,
 313 |           nodeName: node.name,
 314 |           message: `Duplicate node name: "${node.name}"`
 315 |         });
 316 |       }
 317 |       nodeNames.add(node.name);
 318 | 
 319 |       if (nodeIds.has(node.id)) {
 320 |         result.errors.push({
 321 |           type: 'error',
 322 |           nodeId: node.id,
 323 |           message: `Duplicate node ID: "${node.id}"`
 324 |         });
 325 |       }
 326 |       nodeIds.add(node.id);
 327 |     }
 328 | 
 329 |     // Count trigger nodes - normalize type names first
 330 |     const triggerNodes = workflow.nodes.filter(n => {
 331 |       const normalizedType = NodeTypeNormalizer.normalizeToFullForm(n.type);
 332 |       const lowerType = normalizedType.toLowerCase();
 333 |       return lowerType.includes('trigger') ||
 334 |              (lowerType.includes('webhook') && !lowerType.includes('respond')) ||
 335 |              normalizedType === 'nodes-base.start' ||
 336 |              normalizedType === 'nodes-base.manualTrigger' ||
 337 |              normalizedType === 'nodes-base.formTrigger';
 338 |     });
 339 |     result.statistics.triggerNodes = triggerNodes.length;
 340 | 
 341 |     // Check for at least one trigger node
 342 |     if (triggerNodes.length === 0 && workflow.nodes.filter(n => !n.disabled).length > 0) {
 343 |       result.warnings.push({
 344 |         type: 'warning',
 345 |         message: 'Workflow has no trigger nodes. It can only be executed manually.'
 346 |       });
 347 |     }
 348 |   }
 349 | 
 350 |   /**
 351 |    * Validate all nodes in the workflow
 352 |    */
 353 |   private async validateAllNodes(
 354 |     workflow: WorkflowJson,
 355 |     result: WorkflowValidationResult,
 356 |     profile: string
 357 |   ): Promise<void> {
 358 |     for (const node of workflow.nodes) {
 359 |       if (node.disabled || this.isStickyNote(node)) continue;
 360 | 
 361 |       try {
 362 |         // Validate node name length
 363 |         if (node.name && node.name.length > 255) {
 364 |           result.warnings.push({
 365 |             type: 'warning',
 366 |             nodeId: node.id,
 367 |             nodeName: node.name,
 368 |             message: `Node name is very long (${node.name.length} characters). Consider using a shorter name for better readability.`
 369 |           });
 370 |         }
 371 | 
 372 |         // Validate node position
 373 |         if (!Array.isArray(node.position) || node.position.length !== 2) {
 374 |           result.errors.push({
 375 |             type: 'error',
 376 |             nodeId: node.id,
 377 |             nodeName: node.name,
 378 |             message: 'Node position must be an array with exactly 2 numbers [x, y]'
 379 |           });
 380 |         } else {
 381 |           const [x, y] = node.position;
 382 |           if (typeof x !== 'number' || typeof y !== 'number' || 
 383 |               !isFinite(x) || !isFinite(y)) {
 384 |             result.errors.push({
 385 |               type: 'error',
 386 |               nodeId: node.id,
 387 |               nodeName: node.name,
 388 |               message: 'Node position values must be finite numbers'
 389 |             });
 390 |           }
 391 |         }
 392 |         // Normalize node type FIRST to ensure consistent lookup
 393 |         const normalizedType = NodeTypeNormalizer.normalizeToFullForm(node.type);
 394 | 
 395 |         // Update node type in place if it was normalized
 396 |         if (normalizedType !== node.type) {
 397 |           node.type = normalizedType;
 398 |         }
 399 | 
 400 |         // Get node definition using normalized type (needed for typeVersion validation)
 401 |         const nodeInfo = this.nodeRepository.getNode(normalizedType);
 402 | 
 403 |         if (!nodeInfo) {
 404 | 
 405 |           // Use NodeSimilarityService to find suggestions
 406 |           const suggestions = await this.similarityService.findSimilarNodes(node.type, 3);
 407 | 
 408 |           let message = `Unknown node type: "${node.type}".`;
 409 | 
 410 |           if (suggestions.length > 0) {
 411 |             message += '\n\nDid you mean one of these?';
 412 |             for (const suggestion of suggestions) {
 413 |               const confidence = Math.round(suggestion.confidence * 100);
 414 |               message += `\n• ${suggestion.nodeType} (${confidence}% match)`;
 415 |               if (suggestion.displayName) {
 416 |                 message += ` - ${suggestion.displayName}`;
 417 |               }
 418 |               message += `\n  → ${suggestion.reason}`;
 419 |               if (suggestion.confidence >= 0.9) {
 420 |                 message += ' (can be auto-fixed)';
 421 |               }
 422 |             }
 423 |           } else {
 424 |             message += ' No similar nodes found. Node types must include the package prefix (e.g., "n8n-nodes-base.webhook").';
 425 |           }
 426 | 
 427 |           const error: any = {
 428 |             type: 'error',
 429 |             nodeId: node.id,
 430 |             nodeName: node.name,
 431 |             message
 432 |           };
 433 | 
 434 |           // Add suggestions as metadata for programmatic access
 435 |           if (suggestions.length > 0) {
 436 |             error.suggestions = suggestions.map(s => ({
 437 |               nodeType: s.nodeType,
 438 |               confidence: s.confidence,
 439 |               reason: s.reason
 440 |             }));
 441 |           }
 442 | 
 443 |           result.errors.push(error);
 444 |           continue;
 445 |         }
 446 | 
 447 |         // Validate typeVersion for ALL versioned nodes (including langchain nodes)
 448 |         // CRITICAL: This MUST run BEFORE the langchain skip below!
 449 |         // Otherwise, langchain nodes with invalid typeVersion (e.g., 99999) would pass validation
 450 |         // but fail at runtime in n8n. This was the bug fixed in v2.17.4.
 451 |         if (nodeInfo.isVersioned) {
 452 |           // Check if typeVersion is missing
 453 |           if (!node.typeVersion) {
 454 |             result.errors.push({
 455 |               type: 'error',
 456 |               nodeId: node.id,
 457 |               nodeName: node.name,
 458 |               message: `Missing required property 'typeVersion'. Add typeVersion: ${nodeInfo.version || 1}`
 459 |             });
 460 |           }
 461 |           // Check if typeVersion is invalid (must be non-negative number, version 0 is valid)
 462 |           else if (typeof node.typeVersion !== 'number' || node.typeVersion < 0) {
 463 |             result.errors.push({
 464 |               type: 'error',
 465 |               nodeId: node.id,
 466 |               nodeName: node.name,
 467 |               message: `Invalid typeVersion: ${node.typeVersion}. Must be a non-negative number`
 468 |             });
 469 |           }
 470 |           // Check if typeVersion is outdated (less than latest)
 471 |           else if (nodeInfo.version && node.typeVersion < nodeInfo.version) {
 472 |             result.warnings.push({
 473 |               type: 'warning',
 474 |               nodeId: node.id,
 475 |               nodeName: node.name,
 476 |               message: `Outdated typeVersion: ${node.typeVersion}. Latest is ${nodeInfo.version}`
 477 |             });
 478 |           }
 479 |           // Check if typeVersion exceeds maximum supported
 480 |           else if (nodeInfo.version && node.typeVersion > nodeInfo.version) {
 481 |             result.errors.push({
 482 |               type: 'error',
 483 |               nodeId: node.id,
 484 |               nodeName: node.name,
 485 |               message: `typeVersion ${node.typeVersion} exceeds maximum supported version ${nodeInfo.version}`
 486 |             });
 487 |           }
 488 |         }
 489 | 
 490 |         // Skip PARAMETER validation for langchain nodes (but NOT typeVersion validation above!)
 491 |         // Langchain nodes have dedicated AI-specific validators in validateAISpecificNodes()
 492 |         // which handle their unique parameter structures (AI connections, tool ports, etc.)
 493 |         if (normalizedType.startsWith('nodes-langchain.')) {
 494 |           continue;
 495 |         }
 496 | 
 497 |         // Validate node configuration
 498 |         const nodeValidation = this.nodeValidator.validateWithMode(
 499 |           node.type,
 500 |           node.parameters,
 501 |           nodeInfo.properties || [],
 502 |           'operation',
 503 |           profile as any
 504 |         );
 505 | 
 506 |         // Add node-specific errors and warnings
 507 |         nodeValidation.errors.forEach((error: any) => {
 508 |           result.errors.push({
 509 |             type: 'error',
 510 |             nodeId: node.id,
 511 |             nodeName: node.name,
 512 |             message: typeof error === 'string' ? error : error.message || String(error)
 513 |           });
 514 |         });
 515 | 
 516 |         nodeValidation.warnings.forEach((warning: any) => {
 517 |           result.warnings.push({
 518 |             type: 'warning',
 519 |             nodeId: node.id,
 520 |             nodeName: node.name,
 521 |             message: typeof warning === 'string' ? warning : warning.message || String(warning)
 522 |           });
 523 |         });
 524 | 
 525 |       } catch (error) {
 526 |         result.errors.push({
 527 |           type: 'error',
 528 |           nodeId: node.id,
 529 |           nodeName: node.name,
 530 |           message: `Failed to validate node: ${error instanceof Error ? error.message : 'Unknown error'}`
 531 |         });
 532 |       }
 533 |     }
 534 |   }
 535 | 
 536 |   /**
 537 |    * Validate workflow connections
 538 |    */
 539 |   private validateConnections(
 540 |     workflow: WorkflowJson,
 541 |     result: WorkflowValidationResult,
 542 |     profile: string = 'runtime'
 543 |   ): void {
 544 |     const nodeMap = new Map(workflow.nodes.map(n => [n.name, n]));
 545 |     const nodeIdMap = new Map(workflow.nodes.map(n => [n.id, n]));
 546 | 
 547 |     // Check all connections
 548 |     for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
 549 |       const sourceNode = nodeMap.get(sourceName);
 550 |       
 551 |       if (!sourceNode) {
 552 |         // Check if this is an ID being used instead of a name
 553 |         const nodeById = nodeIdMap.get(sourceName);
 554 |         if (nodeById) {
 555 |           result.errors.push({
 556 |             type: 'error',
 557 |             nodeId: nodeById.id,
 558 |             nodeName: nodeById.name,
 559 |             message: `Connection uses node ID '${sourceName}' instead of node name '${nodeById.name}'. In n8n, connections must use node names, not IDs.`
 560 |           });
 561 |         } else {
 562 |           result.errors.push({
 563 |             type: 'error',
 564 |             message: `Connection from non-existent node: "${sourceName}"`
 565 |           });
 566 |         }
 567 |         result.statistics.invalidConnections++;
 568 |         continue;
 569 |       }
 570 | 
 571 |       // Check main outputs
 572 |       if (outputs.main) {
 573 |         this.validateConnectionOutputs(
 574 |           sourceName,
 575 |           outputs.main,
 576 |           nodeMap,
 577 |           nodeIdMap,
 578 |           result,
 579 |           'main'
 580 |         );
 581 |       }
 582 | 
 583 |       // Check error outputs
 584 |       if (outputs.error) {
 585 |         this.validateConnectionOutputs(
 586 |           sourceName,
 587 |           outputs.error,
 588 |           nodeMap,
 589 |           nodeIdMap,
 590 |           result,
 591 |           'error'
 592 |         );
 593 |       }
 594 | 
 595 |       // Check AI tool outputs
 596 |       if (outputs.ai_tool) {
 597 |         this.validateConnectionOutputs(
 598 |           sourceName,
 599 |           outputs.ai_tool,
 600 |           nodeMap,
 601 |           nodeIdMap,
 602 |           result,
 603 |           'ai_tool'
 604 |         );
 605 |       }
 606 |     }
 607 | 
 608 |     // Check for orphaned nodes (not connected and not triggers)
 609 |     const connectedNodes = new Set<string>();
 610 |     
 611 |     // Add all source nodes
 612 |     Object.keys(workflow.connections).forEach(name => connectedNodes.add(name));
 613 |     
 614 |     // Add all target nodes
 615 |     Object.values(workflow.connections).forEach(outputs => {
 616 |       if (outputs.main) {
 617 |         outputs.main.flat().forEach(conn => {
 618 |           if (conn) connectedNodes.add(conn.node);
 619 |         });
 620 |       }
 621 |       if (outputs.error) {
 622 |         outputs.error.flat().forEach(conn => {
 623 |           if (conn) connectedNodes.add(conn.node);
 624 |         });
 625 |       }
 626 |       if (outputs.ai_tool) {
 627 |         outputs.ai_tool.flat().forEach(conn => {
 628 |           if (conn) connectedNodes.add(conn.node);
 629 |         });
 630 |       }
 631 |     });
 632 | 
 633 |     // Check for orphaned nodes (exclude sticky notes)
 634 |     for (const node of workflow.nodes) {
 635 |       if (node.disabled || this.isStickyNote(node)) continue;
 636 | 
 637 |       const normalizedType = NodeTypeNormalizer.normalizeToFullForm(node.type);
 638 |       const isTrigger = normalizedType.toLowerCase().includes('trigger') ||
 639 |                        normalizedType.toLowerCase().includes('webhook') ||
 640 |                        normalizedType === 'nodes-base.start' ||
 641 |                        normalizedType === 'nodes-base.manualTrigger' ||
 642 |                        normalizedType === 'nodes-base.formTrigger';
 643 |       
 644 |       if (!connectedNodes.has(node.name) && !isTrigger) {
 645 |         result.warnings.push({
 646 |           type: 'warning',
 647 |           nodeId: node.id,
 648 |           nodeName: node.name,
 649 |           message: 'Node is not connected to any other nodes'
 650 |         });
 651 |       }
 652 |     }
 653 | 
 654 |     // Check for cycles (skip in minimal profile to reduce false positives)
 655 |     if (profile !== 'minimal' && this.hasCycle(workflow)) {
 656 |       result.errors.push({
 657 |         type: 'error',
 658 |         message: 'Workflow contains a cycle (infinite loop)'
 659 |       });
 660 |     }
 661 |   }
 662 | 
 663 |   /**
 664 |    * Validate connection outputs
 665 |    */
 666 |   private validateConnectionOutputs(
 667 |     sourceName: string,
 668 |     outputs: Array<Array<{ node: string; type: string; index: number }>>,
 669 |     nodeMap: Map<string, WorkflowNode>,
 670 |     nodeIdMap: Map<string, WorkflowNode>,
 671 |     result: WorkflowValidationResult,
 672 |     outputType: 'main' | 'error' | 'ai_tool'
 673 |   ): void {
 674 |     // Get source node for special validation
 675 |     const sourceNode = nodeMap.get(sourceName);
 676 | 
 677 |     // Special validation for main outputs with error handling
 678 |     if (outputType === 'main' && sourceNode) {
 679 |       this.validateErrorOutputConfiguration(sourceName, sourceNode, outputs, nodeMap, result);
 680 |     }
 681 |     
 682 |     outputs.forEach((outputConnections, outputIndex) => {
 683 |       if (!outputConnections) return;
 684 |       
 685 |       outputConnections.forEach(connection => {
 686 |         // Check for negative index
 687 |         if (connection.index < 0) {
 688 |           result.errors.push({
 689 |             type: 'error',
 690 |             message: `Invalid connection index ${connection.index} from "${sourceName}". Connection indices must be non-negative.`
 691 |           });
 692 |           result.statistics.invalidConnections++;
 693 |           return;
 694 |         }
 695 | 
 696 |         // Special validation for SplitInBatches node
 697 |         if (sourceNode && sourceNode.type === 'nodes-base.splitInBatches') {
 698 |           this.validateSplitInBatchesConnection(
 699 |             sourceNode,
 700 |             outputIndex,
 701 |             connection,
 702 |             nodeMap,
 703 |             result
 704 |           );
 705 |         }
 706 | 
 707 |         // Check for self-referencing connections
 708 |         if (connection.node === sourceName) {
 709 |           // This is only a warning for non-loop nodes
 710 |           if (sourceNode && sourceNode.type !== 'nodes-base.splitInBatches') {
 711 |             result.warnings.push({
 712 |               type: 'warning',
 713 |               message: `Node "${sourceName}" has a self-referencing connection. This can cause infinite loops.`
 714 |             });
 715 |           }
 716 |         }
 717 | 
 718 |         const targetNode = nodeMap.get(connection.node);
 719 |         
 720 |         if (!targetNode) {
 721 |           // Check if this is an ID being used instead of a name
 722 |           const nodeById = nodeIdMap.get(connection.node);
 723 |           if (nodeById) {
 724 |             result.errors.push({
 725 |               type: 'error',
 726 |               nodeId: nodeById.id,
 727 |               nodeName: nodeById.name,
 728 |               message: `Connection target uses node ID '${connection.node}' instead of node name '${nodeById.name}' (from ${sourceName}). In n8n, connections must use node names, not IDs.`
 729 |             });
 730 |           } else {
 731 |             result.errors.push({
 732 |               type: 'error',
 733 |               message: `Connection to non-existent node: "${connection.node}" from "${sourceName}"`
 734 |             });
 735 |           }
 736 |           result.statistics.invalidConnections++;
 737 |         } else if (targetNode.disabled) {
 738 |           result.warnings.push({
 739 |             type: 'warning',
 740 |             message: `Connection to disabled node: "${connection.node}" from "${sourceName}"`
 741 |           });
 742 |         } else {
 743 |           result.statistics.validConnections++;
 744 |           
 745 |           // Additional validation for AI tool connections
 746 |           if (outputType === 'ai_tool') {
 747 |             this.validateAIToolConnection(sourceName, targetNode, result);
 748 |           }
 749 |         }
 750 |       });
 751 |     });
 752 |   }
 753 | 
 754 |   /**
 755 |    * Validate error output configuration
 756 |    */
 757 |   private validateErrorOutputConfiguration(
 758 |     sourceName: string,
 759 |     sourceNode: WorkflowNode,
 760 |     outputs: Array<Array<{ node: string; type: string; index: number }>>,
 761 |     nodeMap: Map<string, WorkflowNode>,
 762 |     result: WorkflowValidationResult
 763 |   ): void {
 764 |     // Check if node has onError: 'continueErrorOutput'
 765 |     const hasErrorOutputSetting = sourceNode.onError === 'continueErrorOutput';
 766 |     const hasErrorConnections = outputs.length > 1 && outputs[1] && outputs[1].length > 0;
 767 | 
 768 |     // Validate mismatch between onError setting and connections
 769 |     if (hasErrorOutputSetting && !hasErrorConnections) {
 770 |       result.errors.push({
 771 |         type: 'error',
 772 |         nodeId: sourceNode.id,
 773 |         nodeName: sourceNode.name,
 774 |         message: `Node has onError: 'continueErrorOutput' but no error output connections in main[1]. Add error handler connections to main[1] or change onError to 'continueRegularOutput' or 'stopWorkflow'.`
 775 |       });
 776 |     }
 777 | 
 778 |     if (!hasErrorOutputSetting && hasErrorConnections) {
 779 |       result.warnings.push({
 780 |         type: 'warning',
 781 |         nodeId: sourceNode.id,
 782 |         nodeName: sourceNode.name,
 783 |         message: `Node has error output connections in main[1] but missing onError: 'continueErrorOutput'. Add this property to properly handle errors.`
 784 |       });
 785 |     }
 786 | 
 787 |     // Check for common mistake: multiple nodes in main[0] when error handling is intended
 788 |     if (outputs.length >= 1 && outputs[0] && outputs[0].length > 1) {
 789 |       // Check if any of the nodes in main[0] look like error handlers
 790 |       const potentialErrorHandlers = outputs[0].filter(conn => {
 791 |         const targetNode = nodeMap.get(conn.node);
 792 |         if (!targetNode) return false;
 793 | 
 794 |         const nodeName = targetNode.name.toLowerCase();
 795 |         const nodeType = targetNode.type.toLowerCase();
 796 | 
 797 |         // Common patterns for error handler nodes
 798 |         return nodeName.includes('error') ||
 799 |                nodeName.includes('fail') ||
 800 |                nodeName.includes('catch') ||
 801 |                nodeName.includes('exception') ||
 802 |                nodeType.includes('respondtowebhook') ||
 803 |                nodeType.includes('emailsend');
 804 |       });
 805 | 
 806 |       if (potentialErrorHandlers.length > 0) {
 807 |         const errorHandlerNames = potentialErrorHandlers.map(conn => `"${conn.node}"`).join(', ');
 808 |         result.errors.push({
 809 |           type: 'error',
 810 |           nodeId: sourceNode.id,
 811 |           nodeName: sourceNode.name,
 812 |           message: `Incorrect error output configuration. Nodes ${errorHandlerNames} appear to be error handlers but are in main[0] (success output) along with other nodes.\n\n` +
 813 |                    `INCORRECT (current):\n` +
 814 |                    `"${sourceName}": {\n` +
 815 |                    `  "main": [\n` +
 816 |                    `    [  // main[0] has multiple nodes mixed together\n` +
 817 |                    outputs[0].map(conn => `      {"node": "${conn.node}", "type": "${conn.type}", "index": ${conn.index}}`).join(',\n') + '\n' +
 818 |                    `    ]\n` +
 819 |                    `  ]\n` +
 820 |                    `}\n\n` +
 821 |                    `CORRECT (should be):\n` +
 822 |                    `"${sourceName}": {\n` +
 823 |                    `  "main": [\n` +
 824 |                    `    [  // main[0] = success output\n` +
 825 |                    outputs[0].filter(conn => !potentialErrorHandlers.includes(conn)).map(conn => `      {"node": "${conn.node}", "type": "${conn.type}", "index": ${conn.index}}`).join(',\n') + '\n' +
 826 |                    `    ],\n` +
 827 |                    `    [  // main[1] = error output\n` +
 828 |                    potentialErrorHandlers.map(conn => `      {"node": "${conn.node}", "type": "${conn.type}", "index": ${conn.index}}`).join(',\n') + '\n' +
 829 |                    `    ]\n` +
 830 |                    `  ]\n` +
 831 |                    `}\n\n` +
 832 |                    `Also add: "onError": "continueErrorOutput" to the "${sourceName}" node.`
 833 |         });
 834 |       }
 835 |     }
 836 |   }
 837 | 
 838 |   /**
 839 |    * Validate AI tool connections
 840 |    */
 841 |   private validateAIToolConnection(
 842 |     sourceName: string,
 843 |     targetNode: WorkflowNode,
 844 |     result: WorkflowValidationResult
 845 |   ): void {
 846 |     // For AI tool connections, we just need to check if this is being used as a tool
 847 |     // The source should be an AI Agent connecting to this target node as a tool
 848 |     
 849 |     // Get target node info to check if it can be used as a tool
 850 |     const normalizedType = NodeTypeNormalizer.normalizeToFullForm(targetNode.type);
 851 |     let targetNodeInfo = this.nodeRepository.getNode(normalizedType);
 852 | 
 853 |     // Try original type if normalization didn't help (fallback for edge cases)
 854 |     if (!targetNodeInfo && normalizedType !== targetNode.type) {
 855 |       targetNodeInfo = this.nodeRepository.getNode(targetNode.type);
 856 |     }
 857 |     
 858 |     if (targetNodeInfo && !targetNodeInfo.isAITool && targetNodeInfo.package !== 'n8n-nodes-base') {
 859 |       // It's a community node being used as a tool
 860 |       result.warnings.push({
 861 |         type: 'warning',
 862 |         nodeId: targetNode.id,
 863 |         nodeName: targetNode.name,
 864 |         message: `Community node "${targetNode.name}" is being used as an AI tool. Ensure N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true is set.`
 865 |       });
 866 |     }
 867 |   }
 868 | 
 869 |   /**
 870 |    * Check if workflow has cycles
 871 |    * Allow legitimate loops for SplitInBatches and similar loop nodes
 872 |    */
 873 |   private hasCycle(workflow: WorkflowJson): boolean {
 874 |     const visited = new Set<string>();
 875 |     const recursionStack = new Set<string>();
 876 |     const nodeTypeMap = new Map<string, string>();
 877 |     
 878 |     // Build node type map (exclude sticky notes)
 879 |     workflow.nodes.forEach(node => {
 880 |       if (!this.isStickyNote(node)) {
 881 |         nodeTypeMap.set(node.name, node.type);
 882 |       }
 883 |     });
 884 |     
 885 |     // Known legitimate loop node types
 886 |     const loopNodeTypes = [
 887 |       'n8n-nodes-base.splitInBatches',
 888 |       'nodes-base.splitInBatches',
 889 |       'n8n-nodes-base.itemLists',
 890 |       'nodes-base.itemLists',
 891 |       'n8n-nodes-base.loop',
 892 |       'nodes-base.loop'
 893 |     ];
 894 | 
 895 |     const hasCycleDFS = (nodeName: string, pathFromLoopNode: boolean = false): boolean => {
 896 |       visited.add(nodeName);
 897 |       recursionStack.add(nodeName);
 898 | 
 899 |       const connections = workflow.connections[nodeName];
 900 |       if (connections) {
 901 |         const allTargets: string[] = [];
 902 |         
 903 |         if (connections.main) {
 904 |           connections.main.flat().forEach(conn => {
 905 |             if (conn) allTargets.push(conn.node);
 906 |           });
 907 |         }
 908 |         
 909 |         if (connections.error) {
 910 |           connections.error.flat().forEach(conn => {
 911 |             if (conn) allTargets.push(conn.node);
 912 |           });
 913 |         }
 914 |         
 915 |         if (connections.ai_tool) {
 916 |           connections.ai_tool.flat().forEach(conn => {
 917 |             if (conn) allTargets.push(conn.node);
 918 |           });
 919 |         }
 920 | 
 921 |         const currentNodeType = nodeTypeMap.get(nodeName);
 922 |         const isLoopNode = loopNodeTypes.includes(currentNodeType || '');
 923 |         
 924 |         for (const target of allTargets) {
 925 |           if (!visited.has(target)) {
 926 |             if (hasCycleDFS(target, pathFromLoopNode || isLoopNode)) return true;
 927 |           } else if (recursionStack.has(target)) {
 928 |             // Allow cycles that involve legitimate loop nodes
 929 |             const targetNodeType = nodeTypeMap.get(target);
 930 |             const isTargetLoopNode = loopNodeTypes.includes(targetNodeType || '');
 931 |             
 932 |             // If this cycle involves a loop node, it's legitimate
 933 |             if (isTargetLoopNode || pathFromLoopNode || isLoopNode) {
 934 |               continue; // Allow this cycle
 935 |             }
 936 |             
 937 |             return true; // Reject other cycles
 938 |           }
 939 |         }
 940 |       }
 941 | 
 942 |       recursionStack.delete(nodeName);
 943 |       return false;
 944 |     };
 945 | 
 946 |     // Check from all executable nodes (exclude sticky notes)
 947 |     for (const node of workflow.nodes) {
 948 |       if (!this.isStickyNote(node) && !visited.has(node.name)) {
 949 |         if (hasCycleDFS(node.name)) return true;
 950 |       }
 951 |     }
 952 | 
 953 |     return false;
 954 |   }
 955 | 
 956 |   /**
 957 |    * Validate expressions in the workflow
 958 |    */
 959 |   private validateExpressions(
 960 |     workflow: WorkflowJson,
 961 |     result: WorkflowValidationResult,
 962 |     profile: string = 'runtime'
 963 |   ): void {
 964 |     const nodeNames = workflow.nodes.map(n => n.name);
 965 | 
 966 |     for (const node of workflow.nodes) {
 967 |       if (node.disabled || this.isStickyNote(node)) continue;
 968 | 
 969 |       // Skip expression validation for langchain nodes
 970 |       // They have AI-specific validators and different expression rules
 971 |       const normalizedType = NodeTypeNormalizer.normalizeToFullForm(node.type);
 972 |       if (normalizedType.startsWith('nodes-langchain.')) {
 973 |         continue;
 974 |       }
 975 | 
 976 |       // Create expression context
 977 |       const context = {
 978 |         availableNodes: nodeNames.filter(n => n !== node.name),
 979 |         currentNodeName: node.name,
 980 |         hasInputData: this.nodeHasInput(node.name, workflow),
 981 |         isInLoop: false // Could be enhanced to detect loop nodes
 982 |       };
 983 | 
 984 |       // Validate expressions in parameters
 985 |       const exprValidation = ExpressionValidator.validateNodeExpressions(
 986 |         node.parameters,
 987 |         context
 988 |       );
 989 | 
 990 |       // Count actual expressions found, not just unique variables
 991 |       const expressionCount = this.countExpressionsInObject(node.parameters);
 992 |       result.statistics.expressionsValidated += expressionCount;
 993 | 
 994 |       // Add expression errors and warnings
 995 |       exprValidation.errors.forEach(error => {
 996 |         result.errors.push({
 997 |           type: 'error',
 998 |           nodeId: node.id,
 999 |           nodeName: node.name,
1000 |           message: `Expression error: ${error}`
1001 |         });
1002 |       });
1003 | 
1004 |       exprValidation.warnings.forEach(warning => {
1005 |         result.warnings.push({
1006 |           type: 'warning',
1007 |           nodeId: node.id,
1008 |           nodeName: node.name,
1009 |           message: `Expression warning: ${warning}`
1010 |         });
1011 |       });
1012 | 
1013 |       // Validate expression format (check for missing = prefix and resource locator format)
1014 |       const formatContext = {
1015 |         nodeType: node.type,
1016 |         nodeName: node.name,
1017 |         nodeId: node.id
1018 |       };
1019 | 
1020 |       const formatIssues = ExpressionFormatValidator.validateNodeParameters(
1021 |         node.parameters,
1022 |         formatContext
1023 |       );
1024 | 
1025 |       // Add format errors and warnings
1026 |       formatIssues.forEach(issue => {
1027 |         const formattedMessage = ExpressionFormatValidator.formatErrorMessage(issue, formatContext);
1028 | 
1029 |         if (issue.severity === 'error') {
1030 |           result.errors.push({
1031 |             type: 'error',
1032 |             nodeId: node.id,
1033 |             nodeName: node.name,
1034 |             message: formattedMessage
1035 |           });
1036 |         } else {
1037 |           result.warnings.push({
1038 |             type: 'warning',
1039 |             nodeId: node.id,
1040 |             nodeName: node.name,
1041 |             message: formattedMessage
1042 |           });
1043 |         }
1044 |       });
1045 |     }
1046 |   }
1047 | 
1048 |   /**
1049 |    * Count expressions in an object recursively
1050 |    */
1051 |   private countExpressionsInObject(obj: any): number {
1052 |     let count = 0;
1053 |     
1054 |     if (typeof obj === 'string') {
1055 |       // Count expressions in string
1056 |       const matches = obj.match(/\{\{[\s\S]+?\}\}/g);
1057 |       if (matches) {
1058 |         count += matches.length;
1059 |       }
1060 |     } else if (Array.isArray(obj)) {
1061 |       // Recursively count in arrays
1062 |       for (const item of obj) {
1063 |         count += this.countExpressionsInObject(item);
1064 |       }
1065 |     } else if (obj && typeof obj === 'object') {
1066 |       // Recursively count in objects
1067 |       for (const value of Object.values(obj)) {
1068 |         count += this.countExpressionsInObject(value);
1069 |       }
1070 |     }
1071 |     
1072 |     return count;
1073 |   }
1074 | 
1075 |   /**
1076 |    * Check if a node has input connections
1077 |    */
1078 |   private nodeHasInput(nodeName: string, workflow: WorkflowJson): boolean {
1079 |     for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
1080 |       if (outputs.main) {
1081 |         for (const outputConnections of outputs.main) {
1082 |           if (outputConnections?.some(conn => conn.node === nodeName)) {
1083 |             return true;
1084 |           }
1085 |         }
1086 |       }
1087 |     }
1088 |     return false;
1089 |   }
1090 | 
1091 |   /**
1092 |    * Check workflow patterns and best practices
1093 |    */
1094 |   private checkWorkflowPatterns(
1095 |     workflow: WorkflowJson,
1096 |     result: WorkflowValidationResult,
1097 |     profile: string = 'runtime'
1098 |   ): void {
1099 |     // Check for error handling (n8n uses main[1] for error outputs, not outputs.error)
1100 |     const hasErrorHandling = Object.values(workflow.connections).some(
1101 |       outputs => outputs.main && outputs.main.length > 1 && outputs.main[1] && outputs.main[1].length > 0
1102 |     );
1103 | 
1104 |     // Only suggest error handling in stricter profiles
1105 |     if (!hasErrorHandling && workflow.nodes.length > 3 && profile !== 'minimal') {
1106 |       result.warnings.push({
1107 |         type: 'warning',
1108 |         message: 'Consider adding error handling to your workflow'
1109 |       });
1110 |     }
1111 | 
1112 |     // Check node-level error handling properties for ALL executable nodes
1113 |     for (const node of workflow.nodes) {
1114 |       if (!this.isStickyNote(node)) {
1115 |         this.checkNodeErrorHandling(node, workflow, result);
1116 |       }
1117 |     }
1118 | 
1119 |     // Check for very long linear workflows
1120 |     const linearChainLength = this.getLongestLinearChain(workflow);
1121 |     if (linearChainLength > 10) {
1122 |       result.warnings.push({
1123 |         type: 'warning',
1124 |         message: `Long linear chain detected (${linearChainLength} nodes). Consider breaking into sub-workflows.`
1125 |       });
1126 |     }
1127 | 
1128 |     // Generate error handling suggestions based on all nodes
1129 |     this.generateErrorHandlingSuggestions(workflow, result);
1130 | 
1131 |     // Check for missing credentials
1132 |     for (const node of workflow.nodes) {
1133 |       if (node.credentials && Object.keys(node.credentials).length > 0) {
1134 |         for (const [credType, credConfig] of Object.entries(node.credentials)) {
1135 |           if (!credConfig || (typeof credConfig === 'object' && !('id' in credConfig))) {
1136 |             result.warnings.push({
1137 |               type: 'warning',
1138 |               nodeId: node.id,
1139 |               nodeName: node.name,
1140 |               message: `Missing credentials configuration for ${credType}`
1141 |             });
1142 |           }
1143 |         }
1144 |       }
1145 |     }
1146 | 
1147 |     // Check for AI Agent workflows
1148 |     const aiAgentNodes = workflow.nodes.filter(n => 
1149 |       n.type.toLowerCase().includes('agent') || 
1150 |       n.type.includes('langchain.agent')
1151 |     );
1152 |     
1153 |     if (aiAgentNodes.length > 0) {
1154 |       // Check if AI agents have tools connected
1155 |       for (const agentNode of aiAgentNodes) {
1156 |         const connections = workflow.connections[agentNode.name];
1157 |         if (!connections?.ai_tool || connections.ai_tool.flat().filter(c => c).length === 0) {
1158 |           result.warnings.push({
1159 |             type: 'warning',
1160 |             nodeId: agentNode.id,
1161 |             nodeName: agentNode.name,
1162 |             message: 'AI Agent has no tools connected. Consider adding tools to enhance agent capabilities.'
1163 |           });
1164 |         }
1165 |       }
1166 |       
1167 |       // Check for community nodes used as tools
1168 |       const hasAIToolConnections = Object.values(workflow.connections).some(
1169 |         outputs => outputs.ai_tool && outputs.ai_tool.length > 0
1170 |       );
1171 |       
1172 |       if (hasAIToolConnections) {
1173 |         result.suggestions.push(
1174 |           'For community nodes used as AI tools, ensure N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true is set'
1175 |         );
1176 |       }
1177 |     }
1178 |   }
1179 | 
1180 |   /**
1181 |    * Get the longest linear chain in the workflow
1182 |    */
1183 |   private getLongestLinearChain(workflow: WorkflowJson): number {
1184 |     const memo = new Map<string, number>();
1185 |     const visiting = new Set<string>();
1186 | 
1187 |     const getChainLength = (nodeName: string): number => {
1188 |       // If we're already visiting this node, we have a cycle
1189 |       if (visiting.has(nodeName)) return 0;
1190 |       
1191 |       if (memo.has(nodeName)) return memo.get(nodeName)!;
1192 | 
1193 |       visiting.add(nodeName);
1194 | 
1195 |       let maxLength = 0;
1196 |       const connections = workflow.connections[nodeName];
1197 |       
1198 |       if (connections?.main) {
1199 |         for (const outputConnections of connections.main) {
1200 |           if (outputConnections) {
1201 |             for (const conn of outputConnections) {
1202 |               const length = getChainLength(conn.node);
1203 |               maxLength = Math.max(maxLength, length);
1204 |             }
1205 |           }
1206 |         }
1207 |       }
1208 | 
1209 |       visiting.delete(nodeName);
1210 |       const result = maxLength + 1;
1211 |       memo.set(nodeName, result);
1212 |       return result;
1213 |     };
1214 | 
1215 |     let maxChain = 0;
1216 |     for (const node of workflow.nodes) {
1217 |       if (!this.nodeHasInput(node.name, workflow)) {
1218 |         maxChain = Math.max(maxChain, getChainLength(node.name));
1219 |       }
1220 |     }
1221 | 
1222 |     return maxChain;
1223 |   }
1224 | 
1225 | 
1226 |   /**
1227 |    * Generate suggestions based on validation results
1228 |    */
1229 |   private generateSuggestions(
1230 |     workflow: WorkflowJson,
1231 |     result: WorkflowValidationResult
1232 |   ): void {
1233 |     // Suggest adding trigger if missing
1234 |     if (result.statistics.triggerNodes === 0) {
1235 |       result.suggestions.push(
1236 |         'Add a trigger node (e.g., Webhook, Schedule Trigger) to automate workflow execution'
1237 |       );
1238 |     }
1239 | 
1240 |     // Suggest proper connection structure for workflows with connection errors
1241 |     const hasConnectionErrors = result.errors.some(e =>
1242 |       typeof e.message === 'string' && (
1243 |         e.message.includes('connection') ||
1244 |         e.message.includes('Connection') ||
1245 |         e.message.includes('Multi-node workflow has no connections')
1246 |       )
1247 |     );
1248 |     
1249 |     if (hasConnectionErrors) {
1250 |       result.suggestions.push(
1251 |         'Example connection structure: connections: { "Manual Trigger": { "main": [[{ "node": "Set", "type": "main", "index": 0 }]] } }'
1252 |       );
1253 |       result.suggestions.push(
1254 |         'Remember: Use node NAMES (not IDs) in connections. The name is what you see in the UI, not the node type.'
1255 |       );
1256 |     }
1257 | 
1258 |     // Suggest error handling
1259 |     if (!Object.values(workflow.connections).some(o => o.error)) {
1260 |       result.suggestions.push(
1261 |         'Add error handling using the error output of nodes or an Error Trigger node'
1262 |       );
1263 |     }
1264 | 
1265 |     // Suggest optimization for large workflows
1266 |     if (workflow.nodes.length > 20) {
1267 |       result.suggestions.push(
1268 |         'Consider breaking this workflow into smaller sub-workflows for better maintainability'
1269 |       );
1270 |     }
1271 | 
1272 |     // Suggest using Code node for complex logic
1273 |     const complexExpressionNodes = workflow.nodes.filter(node => {
1274 |       const jsonString = JSON.stringify(node.parameters);
1275 |       const expressionCount = (jsonString.match(/\{\{/g) || []).length;
1276 |       return expressionCount > 5;
1277 |     });
1278 | 
1279 |     if (complexExpressionNodes.length > 0) {
1280 |       result.suggestions.push(
1281 |         'Consider using a Code node for complex data transformations instead of multiple expressions'
1282 |       );
1283 |     }
1284 | 
1285 |     // Suggest minimum workflow structure
1286 |     if (workflow.nodes.length === 1 && Object.keys(workflow.connections).length === 0) {
1287 |       result.suggestions.push(
1288 |         'A minimal workflow needs: 1) A trigger node (e.g., Manual Trigger), 2) An action node (e.g., Set, HTTP Request), 3) A connection between them'
1289 |       );
1290 |     }
1291 |   }
1292 | 
1293 |   /**
1294 |    * Check node-level error handling configuration for a single node
1295 |    */
1296 |   private checkNodeErrorHandling(
1297 |     node: WorkflowNode,
1298 |     workflow: WorkflowJson,
1299 |     result: WorkflowValidationResult
1300 |   ): void {
1301 |     // Only skip if disabled is explicitly true (not just truthy)
1302 |     if (node.disabled === true) return;
1303 | 
1304 |     // Define node types that typically interact with external services (lowercase for comparison)
1305 |     const errorProneNodeTypes = [
1306 |       'httprequest',
1307 |       'webhook',
1308 |       'emailsend',
1309 |       'slack',
1310 |       'discord',
1311 |       'telegram',
1312 |       'postgres',
1313 |       'mysql',
1314 |       'mongodb',
1315 |       'redis',
1316 |       'github',
1317 |       'gitlab',
1318 |       'jira',
1319 |       'salesforce',
1320 |       'hubspot',
1321 |       'airtable',
1322 |       'googlesheets',
1323 |       'googledrive',
1324 |       'dropbox',
1325 |       's3',
1326 |       'ftp',
1327 |       'ssh',
1328 |       'mqtt',
1329 |       'kafka',
1330 |       'rabbitmq',
1331 |       'graphql',
1332 |       'openai',
1333 |       'anthropic'
1334 |     ];
1335 | 
1336 |     const normalizedType = node.type.toLowerCase();
1337 |     const isErrorProne = errorProneNodeTypes.some(type => normalizedType.includes(type));
1338 | 
1339 |     // CRITICAL: Check for node-level properties in wrong location (inside parameters)
1340 |     const nodeLevelProps = [
1341 |       // Error handling properties
1342 |       'onError', 'continueOnFail', 'retryOnFail', 'maxTries', 'waitBetweenTries', 'alwaysOutputData',
1343 |       // Other node-level properties
1344 |       'executeOnce', 'disabled', 'notes', 'notesInFlow', 'credentials'
1345 |     ];
1346 |     const misplacedProps: string[] = [];
1347 |     
1348 |     if (node.parameters) {
1349 |       for (const prop of nodeLevelProps) {
1350 |         if (node.parameters[prop] !== undefined) {
1351 |           misplacedProps.push(prop);
1352 |         }
1353 |       }
1354 |     }
1355 |     
1356 |     if (misplacedProps.length > 0) {
1357 |         result.errors.push({
1358 |           type: 'error',
1359 |           nodeId: node.id,
1360 |           nodeName: node.name,
1361 |           message: `Node-level properties ${misplacedProps.join(', ')} are in the wrong location. They must be at the node level, not inside parameters.`,
1362 |           details: {
1363 |             fix: `Move these properties from node.parameters to the node level. Example:\n` +
1364 |                  `{\n` +
1365 |                  `  "name": "${node.name}",\n` +
1366 |                  `  "type": "${node.type}",\n` +
1367 |                  `  "parameters": { /* operation-specific params */ },\n` +
1368 |                  `  "onError": "continueErrorOutput",  // ✅ Correct location\n` +
1369 |                  `  "retryOnFail": true,               // ✅ Correct location\n` +
1370 |                  `  "executeOnce": true,               // ✅ Correct location\n` +
1371 |                  `  "disabled": false,                 // ✅ Correct location\n` +
1372 |                  `  "credentials": { /* ... */ }       // ✅ Correct location\n` +
1373 |                  `}`
1374 |           }
1375 |         });
1376 |     }
1377 | 
1378 |     // Validate error handling properties
1379 |     
1380 |     // Check for onError property (the modern approach)
1381 |     if (node.onError !== undefined) {
1382 |         const validOnErrorValues = ['continueRegularOutput', 'continueErrorOutput', 'stopWorkflow'];
1383 |         if (!validOnErrorValues.includes(node.onError)) {
1384 |           result.errors.push({
1385 |             type: 'error',
1386 |             nodeId: node.id,
1387 |             nodeName: node.name,
1388 |             message: `Invalid onError value: "${node.onError}". Must be one of: ${validOnErrorValues.join(', ')}`
1389 |           });
1390 |         }
1391 |     }
1392 | 
1393 |     // Check for deprecated continueOnFail
1394 |     if (node.continueOnFail !== undefined) {
1395 |         if (typeof node.continueOnFail !== 'boolean') {
1396 |           result.errors.push({
1397 |             type: 'error',
1398 |             nodeId: node.id,
1399 |             nodeName: node.name,
1400 |             message: 'continueOnFail must be a boolean value'
1401 |           });
1402 |         } else if (node.continueOnFail === true) {
1403 |           // Warn about using deprecated property
1404 |           result.warnings.push({
1405 |             type: 'warning',
1406 |             nodeId: node.id,
1407 |             nodeName: node.name,
1408 |             message: 'Using deprecated "continueOnFail: true". Use "onError: \'continueRegularOutput\'" instead for better control and UI compatibility.'
1409 |           });
1410 |         }
1411 |     }
1412 | 
1413 |     // Check for conflicting error handling properties
1414 |     if (node.continueOnFail !== undefined && node.onError !== undefined) {
1415 |         result.errors.push({
1416 |           type: 'error',
1417 |           nodeId: node.id,
1418 |           nodeName: node.name,
1419 |           message: 'Cannot use both "continueOnFail" and "onError" properties. Use only "onError" for modern workflows.'
1420 |         });
1421 |     }
1422 | 
1423 |     if (node.retryOnFail !== undefined) {
1424 |         if (typeof node.retryOnFail !== 'boolean') {
1425 |           result.errors.push({
1426 |             type: 'error',
1427 |             nodeId: node.id,
1428 |             nodeName: node.name,
1429 |             message: 'retryOnFail must be a boolean value'
1430 |           });
1431 |         }
1432 | 
1433 |         // If retry is enabled, check retry configuration
1434 |         if (node.retryOnFail === true) {
1435 |           if (node.maxTries !== undefined) {
1436 |             if (typeof node.maxTries !== 'number' || node.maxTries < 1) {
1437 |               result.errors.push({
1438 |                 type: 'error',
1439 |                 nodeId: node.id,
1440 |                 nodeName: node.name,
1441 |                 message: 'maxTries must be a positive number when retryOnFail is enabled'
1442 |               });
1443 |             } else if (node.maxTries > 10) {
1444 |               result.warnings.push({
1445 |                 type: 'warning',
1446 |                 nodeId: node.id,
1447 |                 nodeName: node.name,
1448 |                 message: `maxTries is set to ${node.maxTries}. Consider if this many retries is necessary.`
1449 |               });
1450 |             }
1451 |           } else {
1452 |             // maxTries defaults to 3 if not specified
1453 |             result.warnings.push({
1454 |               type: 'warning',
1455 |               nodeId: node.id,
1456 |               nodeName: node.name,
1457 |               message: 'retryOnFail is enabled but maxTries is not specified. Default is 3 attempts.'
1458 |             });
1459 |           }
1460 | 
1461 |           if (node.waitBetweenTries !== undefined) {
1462 |             if (typeof node.waitBetweenTries !== 'number' || node.waitBetweenTries < 0) {
1463 |               result.errors.push({
1464 |                 type: 'error',
1465 |                 nodeId: node.id,
1466 |                 nodeName: node.name,
1467 |                 message: 'waitBetweenTries must be a non-negative number (milliseconds)'
1468 |               });
1469 |             } else if (node.waitBetweenTries > 300000) { // 5 minutes
1470 |               result.warnings.push({
1471 |                 type: 'warning',
1472 |                 nodeId: node.id,
1473 |                 nodeName: node.name,
1474 |                 message: `waitBetweenTries is set to ${node.waitBetweenTries}ms (${(node.waitBetweenTries/1000).toFixed(1)}s). This seems excessive.`
1475 |               });
1476 |             }
1477 |           }
1478 |         }
1479 |     }
1480 | 
1481 |     if (node.alwaysOutputData !== undefined && typeof node.alwaysOutputData !== 'boolean') {
1482 |         result.errors.push({
1483 |           type: 'error',
1484 |           nodeId: node.id,
1485 |           nodeName: node.name,
1486 |           message: 'alwaysOutputData must be a boolean value'
1487 |         });
1488 |     }
1489 | 
1490 |     // Warnings for error-prone nodes without error handling
1491 |     const hasErrorHandling = node.onError || node.continueOnFail || node.retryOnFail;
1492 |     
1493 |     if (isErrorProne && !hasErrorHandling) {
1494 |         const nodeTypeSimple = normalizedType.split('.').pop() || normalizedType;
1495 |         
1496 |         // Special handling for specific node types
1497 |         if (normalizedType.includes('httprequest')) {
1498 |           result.warnings.push({
1499 |             type: 'warning',
1500 |             nodeId: node.id,
1501 |             nodeName: node.name,
1502 |             message: 'HTTP Request node without error handling. Consider adding "onError: \'continueRegularOutput\'" for non-critical requests or "retryOnFail: true" for transient failures.'
1503 |           });
1504 |         } else if (normalizedType.includes('webhook')) {
1505 |           result.warnings.push({
1506 |             type: 'warning',
1507 |             nodeId: node.id,
1508 |             nodeName: node.name,
1509 |             message: 'Webhook node without error handling. Consider adding "onError: \'continueRegularOutput\'" to prevent workflow failures from blocking webhook responses.'
1510 |           });
1511 |         } else if (errorProneNodeTypes.some(db => normalizedType.includes(db) && ['postgres', 'mysql', 'mongodb'].includes(db))) {
1512 |           result.warnings.push({
1513 |             type: 'warning',
1514 |             nodeId: node.id,
1515 |             nodeName: node.name,
1516 |             message: `Database operation without error handling. Consider adding "retryOnFail: true" for connection issues or "onError: \'continueRegularOutput\'" for non-critical queries.`
1517 |           });
1518 |         } else {
1519 |           result.warnings.push({
1520 |             type: 'warning',
1521 |             nodeId: node.id,
1522 |             nodeName: node.name,
1523 |             message: `${nodeTypeSimple} node without error handling. Consider using "onError" property for better error management.`
1524 |           });
1525 |         }
1526 |     }
1527 | 
1528 |     // Check for problematic combinations
1529 |     if (node.continueOnFail && node.retryOnFail) {
1530 |         result.warnings.push({
1531 |           type: 'warning',
1532 |           nodeId: node.id,
1533 |           nodeName: node.name,
1534 |           message: 'Both continueOnFail and retryOnFail are enabled. The node will retry first, then continue on failure.'
1535 |         });
1536 |     }
1537 | 
1538 |     // Validate additional node-level properties
1539 |     
1540 |     // Check executeOnce
1541 |     if (node.executeOnce !== undefined && typeof node.executeOnce !== 'boolean') {
1542 |         result.errors.push({
1543 |           type: 'error',
1544 |           nodeId: node.id,
1545 |           nodeName: node.name,
1546 |           message: 'executeOnce must be a boolean value'
1547 |         });
1548 |     }
1549 | 
1550 |     // Check disabled
1551 |     if (node.disabled !== undefined && typeof node.disabled !== 'boolean') {
1552 |         result.errors.push({
1553 |           type: 'error',
1554 |           nodeId: node.id,
1555 |           nodeName: node.name,
1556 |           message: 'disabled must be a boolean value'
1557 |         });
1558 |     }
1559 | 
1560 |     // Check notesInFlow
1561 |     if (node.notesInFlow !== undefined && typeof node.notesInFlow !== 'boolean') {
1562 |         result.errors.push({
1563 |           type: 'error',
1564 |           nodeId: node.id,
1565 |           nodeName: node.name,
1566 |           message: 'notesInFlow must be a boolean value'
1567 |         });
1568 |     }
1569 | 
1570 |     // Check notes
1571 |     if (node.notes !== undefined && typeof node.notes !== 'string') {
1572 |         result.errors.push({
1573 |           type: 'error',
1574 |           nodeId: node.id,
1575 |           nodeName: node.name,
1576 |           message: 'notes must be a string value'
1577 |         });
1578 |     }
1579 | 
1580 |     // Provide guidance for executeOnce
1581 |     if (node.executeOnce === true) {
1582 |         result.warnings.push({
1583 |           type: 'warning',
1584 |           nodeId: node.id,
1585 |           nodeName: node.name,
1586 |           message: 'executeOnce is enabled. This node will execute only once regardless of input items.'
1587 |         });
1588 |     }
1589 | 
1590 |     // Suggest alwaysOutputData for debugging
1591 |     if ((node.continueOnFail || node.retryOnFail) && !node.alwaysOutputData) {
1592 |         if (normalizedType.includes('httprequest') || normalizedType.includes('webhook')) {
1593 |           result.suggestions.push(
1594 |             `Consider enabling alwaysOutputData on "${node.name}" to capture error responses for debugging`
1595 |           );
1596 |         }
1597 |       }
1598 | 
1599 |   }
1600 | 
1601 |   /**
1602 |    * Generate error handling suggestions based on all nodes
1603 |    */
1604 |   private generateErrorHandlingSuggestions(
1605 |     workflow: WorkflowJson,
1606 |     result: WorkflowValidationResult
1607 |   ): void {
1608 |     // Add general suggestions based on findings
1609 |     const nodesWithoutErrorHandling = workflow.nodes.filter(n => 
1610 |       !n.disabled && !n.onError && !n.continueOnFail && !n.retryOnFail
1611 |     ).length;
1612 | 
1613 |     if (nodesWithoutErrorHandling > 5 && workflow.nodes.length > 5) {
1614 |       result.suggestions.push(
1615 |         'Most nodes lack error handling. Use "onError" property for modern error handling: "continueRegularOutput" (continue on error), "continueErrorOutput" (use error output), or "stopWorkflow" (stop execution).'
1616 |       );
1617 |     }
1618 | 
1619 |     // Check for nodes using deprecated continueOnFail
1620 |     const nodesWithDeprecatedErrorHandling = workflow.nodes.filter(n => 
1621 |       !n.disabled && n.continueOnFail === true
1622 |     ).length;
1623 | 
1624 |     if (nodesWithDeprecatedErrorHandling > 0) {
1625 |       result.suggestions.push(
1626 |         'Replace "continueOnFail: true" with "onError: \'continueRegularOutput\'" for better UI compatibility and control.'
1627 |       );
1628 |     }
1629 |   }
1630 | 
1631 |   /**
1632 |    * Validate SplitInBatches node connections for common mistakes
1633 |    */
1634 |   private validateSplitInBatchesConnection(
1635 |     sourceNode: WorkflowNode,
1636 |     outputIndex: number,
1637 |     connection: { node: string; type: string; index: number },
1638 |     nodeMap: Map<string, WorkflowNode>,
1639 |     result: WorkflowValidationResult
1640 |   ): void {
1641 |     const targetNode = nodeMap.get(connection.node);
1642 |     if (!targetNode) return;
1643 | 
1644 |     // Check if connections appear to be reversed
1645 |     // Output 0 = "done", Output 1 = "loop"
1646 |     
1647 |     if (outputIndex === 0) {
1648 |       // This is the "done" output (index 0)
1649 |       // Check if target looks like it should be in the loop
1650 |       const targetType = targetNode.type.toLowerCase();
1651 |       const targetName = targetNode.name.toLowerCase();
1652 |       
1653 |       // Common patterns that suggest this node should be inside the loop
1654 |       if (targetType.includes('function') || 
1655 |           targetType.includes('code') ||
1656 |           targetType.includes('item') ||
1657 |           targetName.includes('process') ||
1658 |           targetName.includes('transform') ||
1659 |           targetName.includes('handle')) {
1660 |         
1661 |         // Check if this node connects back to the SplitInBatches
1662 |         const hasLoopBack = this.checkForLoopBack(targetNode.name, sourceNode.name, nodeMap);
1663 |         
1664 |         if (hasLoopBack) {
1665 |           result.errors.push({
1666 |             type: 'error',
1667 |             nodeId: sourceNode.id,
1668 |             nodeName: sourceNode.name,
1669 |             message: `SplitInBatches outputs appear reversed! Node "${targetNode.name}" is connected to output 0 ("done") but connects back to the loop. It should be connected to output 1 ("loop") instead. Remember: Output 0 = "done" (post-loop), Output 1 = "loop" (inside loop).`
1670 |           });
1671 |         } else {
1672 |           result.warnings.push({
1673 |             type: 'warning',
1674 |             nodeId: sourceNode.id,
1675 |             nodeName: sourceNode.name,
1676 |             message: `Node "${targetNode.name}" is connected to the "done" output (index 0) but appears to be a processing node. Consider connecting it to the "loop" output (index 1) if it should process items inside the loop.`
1677 |           });
1678 |         }
1679 |       }
1680 |     } else if (outputIndex === 1) {
1681 |       // This is the "loop" output (index 1)
1682 |       // Check if target looks like it should be after the loop
1683 |       const targetType = targetNode.type.toLowerCase();
1684 |       const targetName = targetNode.name.toLowerCase();
1685 |       
1686 |       // Common patterns that suggest this node should be after the loop
1687 |       if (targetType.includes('aggregate') ||
1688 |           targetType.includes('merge') ||
1689 |           targetType.includes('email') ||
1690 |           targetType.includes('slack') ||
1691 |           targetName.includes('final') ||
1692 |           targetName.includes('complete') ||
1693 |           targetName.includes('summary') ||
1694 |           targetName.includes('report')) {
1695 |         
1696 |         result.warnings.push({
1697 |           type: 'warning',
1698 |           nodeId: sourceNode.id,
1699 |           nodeName: sourceNode.name,
1700 |           message: `Node "${targetNode.name}" is connected to the "loop" output (index 1) but appears to be a post-processing node. Consider connecting it to the "done" output (index 0) if it should run after all iterations complete.`
1701 |         });
1702 |       }
1703 |       
1704 |       // Check if loop output doesn't eventually connect back
1705 |       const hasLoopBack = this.checkForLoopBack(targetNode.name, sourceNode.name, nodeMap);
1706 |       if (!hasLoopBack) {
1707 |         result.warnings.push({
1708 |           type: 'warning',
1709 |           nodeId: sourceNode.id,
1710 |           nodeName: sourceNode.name,
1711 |           message: `The "loop" output connects to "${targetNode.name}" but doesn't connect back to the SplitInBatches node. The last node in the loop should connect back to complete the iteration.`
1712 |         });
1713 |       }
1714 |     }
1715 |   }
1716 | 
1717 |   /**
1718 |    * Check if a node eventually connects back to a target node
1719 |    */
1720 |   private checkForLoopBack(
1721 |     startNode: string,
1722 |     targetNode: string,
1723 |     nodeMap: Map<string, WorkflowNode>,
1724 |     visited: Set<string> = new Set(),
1725 |     maxDepth: number = 50
1726 |   ): boolean {
1727 |     if (maxDepth <= 0) return false; // Prevent stack overflow
1728 |     if (visited.has(startNode)) return false;
1729 |     visited.add(startNode);
1730 | 
1731 |     const node = nodeMap.get(startNode);
1732 |     if (!node) return false;
1733 | 
1734 |     // Access connections from the workflow structure, not the node
1735 |     // We need to access this.currentWorkflow.connections[startNode]
1736 |     const connections = (this as any).currentWorkflow?.connections[startNode];
1737 |     if (!connections) return false;
1738 | 
1739 |     for (const [outputType, outputs] of Object.entries(connections)) {
1740 |       if (!Array.isArray(outputs)) continue;
1741 |       
1742 |       for (const outputConnections of outputs) {
1743 |         if (!Array.isArray(outputConnections)) continue;
1744 |         
1745 |         for (const conn of outputConnections) {
1746 |           if (conn.node === targetNode) {
1747 |             return true;
1748 |           }
1749 |           
1750 |           // Recursively check connected nodes
1751 |           if (this.checkForLoopBack(conn.node, targetNode, nodeMap, visited, maxDepth - 1)) {
1752 |             return true;
1753 |           }
1754 |         }
1755 |       }
1756 |     }
1757 | 
1758 |     return false;
1759 |   }
1760 | 
1761 |   /**
1762 |    * Add AI-specific error recovery suggestions
1763 |    */
1764 |   private addErrorRecoverySuggestions(result: WorkflowValidationResult): void {
1765 |     // Categorize errors and provide specific recovery actions
1766 |     const errorTypes = {
1767 |       nodeType: result.errors.filter(e => e.message.includes('node type') || e.message.includes('Node type')),
1768 |       connection: result.errors.filter(e => e.message.includes('connection') || e.message.includes('Connection')),
1769 |       structure: result.errors.filter(e => e.message.includes('structure') || e.message.includes('nodes must be')),
1770 |       configuration: result.errors.filter(e => e.message.includes('property') || e.message.includes('field')),
1771 |       typeVersion: result.errors.filter(e => e.message.includes('typeVersion'))
1772 |     };
1773 | 
1774 |     // Add recovery suggestions based on error types
1775 |     if (errorTypes.nodeType.length > 0) {
1776 |       result.suggestions.unshift(
1777 |         '🔧 RECOVERY: Invalid node types detected. Use these patterns:',
1778 |         '   • For core nodes: "n8n-nodes-base.nodeName" (e.g., "n8n-nodes-base.webhook")',
1779 |         '   • For AI nodes: "@n8n/n8n-nodes-langchain.nodeName"',
1780 |         '   • Never use just the node name without package prefix'
1781 |       );
1782 |     }
1783 | 
1784 |     if (errorTypes.connection.length > 0) {
1785 |       result.suggestions.unshift(
1786 |         '🔧 RECOVERY: Connection errors detected. Fix with:',
1787 |         '   • Use node NAMES in connections, not IDs or types',
1788 |         '   • Structure: { "Source Node Name": { "main": [[{ "node": "Target Node Name", "type": "main", "index": 0 }]] } }',
1789 |         '   • Ensure all referenced nodes exist in the workflow'
1790 |       );
1791 |     }
1792 | 
1793 |     if (errorTypes.structure.length > 0) {
1794 |       result.suggestions.unshift(
1795 |         '🔧 RECOVERY: Workflow structure errors. Fix with:',
1796 |         '   • Ensure "nodes" is an array: "nodes": [...]',
1797 |         '   • Ensure "connections" is an object: "connections": {...}',
1798 |         '   • Add at least one node to create a valid workflow'
1799 |       );
1800 |     }
1801 | 
1802 |     if (errorTypes.configuration.length > 0) {
1803 |       result.suggestions.unshift(
1804 |         '🔧 RECOVERY: Node configuration errors. Fix with:',
1805 |         '   • Check required fields using validate_node_minimal first',
1806 |         '   • Use get_node_essentials to see what fields are needed',
1807 |         '   • Ensure operation-specific fields match the node\'s requirements'
1808 |       );
1809 |     }
1810 | 
1811 |     if (errorTypes.typeVersion.length > 0) {
1812 |       result.suggestions.unshift(
1813 |         '🔧 RECOVERY: TypeVersion errors. Fix with:',
1814 |         '   • Add "typeVersion": 1 (or latest version) to each node',
1815 |         '   • Use get_node_info to check the correct version for each node type'
1816 |       );
1817 |     }
1818 | 
1819 |     // Add general recovery workflow
1820 |     if (result.errors.length > 3) {
1821 |       result.suggestions.push(
1822 |         '📋 SUGGESTED WORKFLOW: Too many errors detected. Try this approach:',
1823 |         '   1. Fix structural issues first (nodes array, connections object)',
1824 |         '   2. Validate node types and fix invalid ones',
1825 |         '   3. Add required typeVersion to all nodes',
1826 |         '   4. Test connections step by step',
1827 |         '   5. Use validate_node_minimal on individual nodes to verify configuration'
1828 |       );
1829 |     }
1830 |   }
1831 | }
```

--------------------------------------------------------------------------------
/tests/extracted-nodes-db/n8n-nodes-base__If.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "node_type": "n8n-nodes-base.If",
  3 |   "name": "If",
  4 |   "package_name": "n8n-nodes-base",
  5 |   "code_hash": "7910ed9177a946b76f04ca847defb81226c37c698e4cdb63913f038c6c257ee1",
  6 |   "code_length": 20533,
  7 |   "source_location": "node_modules/n8n-nodes-base/dist/nodes/If/If.node.js",
  8 |   "has_credentials": false,
  9 |   "source_code": "\"use strict\";\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n    return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.If = void 0;\nconst moment_1 = __importDefault(require(\"moment\"));\nconst n8n_workflow_1 = require(\"n8n-workflow\");\nclass If {\n    constructor() {\n        this.description = {\n            displayName: 'IF',\n            name: 'if',\n            icon: 'fa:map-signs',\n            group: ['transform'],\n            version: 1,\n            description: 'Route items to different branches (true/false)',\n            defaults: {\n                name: 'IF',\n                color: '#408000',\n            },\n            inputs: ['main'],\n            outputs: ['main', 'main'],\n            outputNames: ['true', 'false'],\n            properties: [\n                {\n                    displayName: 'Conditions',\n                    name: 'conditions',\n                    placeholder: 'Add Condition',\n                    type: 'fixedCollection',\n                    typeOptions: {\n                        multipleValues: true,\n                        sortable: true,\n                    },\n                    description: 'The type of values to compare',\n                    default: {},\n                    options: [\n                        {\n                            name: 'boolean',\n                            displayName: 'Boolean',\n                            values: [\n                                {\n                                    displayName: 'Value 1',\n                                    name: 'value1',\n                                    type: 'boolean',\n                                    default: false,\n                                    description: 'The value to compare with the second one',\n                                },\n                                {\n                                    displayName: 'Operation',\n                                    name: 'operation',\n                                    type: 'options',\n                                    options: [\n                                        {\n                                            name: 'Equal',\n                                            value: 'equal',\n                                        },\n                                        {\n                                            name: 'Not Equal',\n                                            value: 'notEqual',\n                                        },\n                                    ],\n                                    default: 'equal',\n                                    description: 'Operation to decide where the the data should be mapped to',\n                                },\n                                {\n                                    displayName: 'Value 2',\n                                    name: 'value2',\n                                    type: 'boolean',\n                                    default: false,\n                                    description: 'The value to compare with the first one',\n                                },\n                            ],\n                        },\n                        {\n                            name: 'dateTime',\n                            displayName: 'Date & Time',\n                            values: [\n                                {\n                                    displayName: 'Value 1',\n                                    name: 'value1',\n                                    type: 'dateTime',\n                                    default: '',\n                                    description: 'The value to compare with the second one',\n                                },\n                                {\n                                    displayName: 'Operation',\n                                    name: 'operation',\n                                    type: 'options',\n                                    options: [\n                                        {\n                                            name: 'Occurred After',\n                                            value: 'after',\n                                        },\n                                        {\n                                            name: 'Occurred Before',\n                                            value: 'before',\n                                        },\n                                    ],\n                                    default: 'after',\n                                    description: 'Operation to decide where the the data should be mapped to',\n                                },\n                                {\n                                    displayName: 'Value 2',\n                                    name: 'value2',\n                                    type: 'dateTime',\n                                    default: '',\n                                    description: 'The value to compare with the first one',\n                                },\n                            ],\n                        },\n                        {\n                            name: 'number',\n                            displayName: 'Number',\n                            values: [\n                                {\n                                    displayName: 'Value 1',\n                                    name: 'value1',\n                                    type: 'number',\n                                    default: 0,\n                                    description: 'The value to compare with the second one',\n                                },\n                                {\n                                    displayName: 'Operation',\n                                    name: 'operation',\n                                    type: 'options',\n                                    noDataExpression: true,\n                                    options: [\n                                        {\n                                            name: 'Smaller',\n                                            value: 'smaller',\n                                        },\n                                        {\n                                            name: 'Smaller or Equal',\n                                            value: 'smallerEqual',\n                                        },\n                                        {\n                                            name: 'Equal',\n                                            value: 'equal',\n                                        },\n                                        {\n                                            name: 'Not Equal',\n                                            value: 'notEqual',\n                                        },\n                                        {\n                                            name: 'Larger',\n                                            value: 'larger',\n                                        },\n                                        {\n                                            name: 'Larger or Equal',\n                                            value: 'largerEqual',\n                                        },\n                                        {\n                                            name: 'Is Empty',\n                                            value: 'isEmpty',\n                                        },\n                                        {\n                                            name: 'Is Not Empty',\n                                            value: 'isNotEmpty',\n                                        },\n                                    ],\n                                    default: 'smaller',\n                                    description: 'Operation to decide where the the data should be mapped to',\n                                },\n                                {\n                                    displayName: 'Value 2',\n                                    name: 'value2',\n                                    type: 'number',\n                                    displayOptions: {\n                                        hide: {\n                                            operation: ['isEmpty', 'isNotEmpty'],\n                                        },\n                                    },\n                                    default: 0,\n                                    description: 'The value to compare with the first one',\n                                },\n                            ],\n                        },\n                        {\n                            name: 'string',\n                            displayName: 'String',\n                            values: [\n                                {\n                                    displayName: 'Value 1',\n                                    name: 'value1',\n                                    type: 'string',\n                                    default: '',\n                                    description: 'The value to compare with the second one',\n                                },\n                                {\n                                    displayName: 'Operation',\n                                    name: 'operation',\n                                    type: 'options',\n                                    noDataExpression: true,\n                                    options: [\n                                        {\n                                            name: 'Contains',\n                                            value: 'contains',\n                                        },\n                                        {\n                                            name: 'Not Contains',\n                                            value: 'notContains',\n                                        },\n                                        {\n                                            name: 'Ends With',\n                                            value: 'endsWith',\n                                        },\n                                        {\n                                            name: 'Not Ends With',\n                                            value: 'notEndsWith',\n                                        },\n                                        {\n                                            name: 'Equal',\n                                            value: 'equal',\n                                        },\n                                        {\n                                            name: 'Not Equal',\n                                            value: 'notEqual',\n                                        },\n                                        {\n                                            name: 'Regex Match',\n                                            value: 'regex',\n                                        },\n                                        {\n                                            name: 'Regex Not Match',\n                                            value: 'notRegex',\n                                        },\n                                        {\n                                            name: 'Starts With',\n                                            value: 'startsWith',\n                                        },\n                                        {\n                                            name: 'Not Starts With',\n                                            value: 'notStartsWith',\n                                        },\n                                        {\n                                            name: 'Is Empty',\n                                            value: 'isEmpty',\n                                        },\n                                        {\n                                            name: 'Is Not Empty',\n                                            value: 'isNotEmpty',\n                                        },\n                                    ],\n                                    default: 'equal',\n                                    description: 'Operation to decide where the the data should be mapped to',\n                                },\n                                {\n                                    displayName: 'Value 2',\n                                    name: 'value2',\n                                    type: 'string',\n                                    displayOptions: {\n                                        hide: {\n                                            operation: ['isEmpty', 'isNotEmpty', 'regex', 'notRegex'],\n                                        },\n                                    },\n                                    default: '',\n                                    description: 'The value to compare with the first one',\n                                },\n                                {\n                                    displayName: 'Regex',\n                                    name: 'value2',\n                                    type: 'string',\n                                    displayOptions: {\n                                        show: {\n                                            operation: ['regex', 'notRegex'],\n                                        },\n                                    },\n                                    default: '',\n                                    placeholder: '/text/i',\n                                    description: 'The regex which has to match',\n                                },\n                            ],\n                        },\n                    ],\n                },\n                {\n                    displayName: 'Combine',\n                    name: 'combineOperation',\n                    type: 'options',\n                    options: [\n                        {\n                            name: 'ALL',\n                            description: 'Only if all conditions are met it goes into \"true\" branch',\n                            value: 'all',\n                        },\n                        {\n                            name: 'ANY',\n                            description: 'If any of the conditions is met it goes into \"true\" branch',\n                            value: 'any',\n                        },\n                    ],\n                    default: 'all',\n                    description: 'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet',\n                },\n            ],\n        };\n    }\n    async execute() {\n        const returnDataTrue = [];\n        const returnDataFalse = [];\n        const items = this.getInputData();\n        let item;\n        let combineOperation;\n        const isDateObject = (value) => Object.prototype.toString.call(value) === '[object Date]';\n        const isDateInvalid = (value) => (value === null || value === void 0 ? void 0 : value.toString()) === 'Invalid Date';\n        const compareOperationFunctions = {\n            after: (value1, value2) => (value1 || 0) > (value2 || 0),\n            before: (value1, value2) => (value1 || 0) < (value2 || 0),\n            contains: (value1, value2) => (value1 || '').toString().includes((value2 || '').toString()),\n            notContains: (value1, value2) => !(value1 || '').toString().includes((value2 || '').toString()),\n            endsWith: (value1, value2) => value1.endsWith(value2),\n            notEndsWith: (value1, value2) => !value1.endsWith(value2),\n            equal: (value1, value2) => value1 === value2,\n            notEqual: (value1, value2) => value1 !== value2,\n            larger: (value1, value2) => (value1 || 0) > (value2 || 0),\n            largerEqual: (value1, value2) => (value1 || 0) >= (value2 || 0),\n            smaller: (value1, value2) => (value1 || 0) < (value2 || 0),\n            smallerEqual: (value1, value2) => (value1 || 0) <= (value2 || 0),\n            startsWith: (value1, value2) => value1.startsWith(value2),\n            notStartsWith: (value1, value2) => !value1.startsWith(value2),\n            isEmpty: (value1) => [undefined, null, '', NaN].includes(value1) ||\n                (typeof value1 === 'object' && value1 !== null && !isDateObject(value1)\n                    ? Object.entries(value1).length === 0\n                    : false) ||\n                (isDateObject(value1) && isDateInvalid(value1)),\n            isNotEmpty: (value1) => !([undefined, null, '', NaN].includes(value1) ||\n                (typeof value1 === 'object' && value1 !== null && !isDateObject(value1)\n                    ? Object.entries(value1).length === 0\n                    : false) ||\n                (isDateObject(value1) && isDateInvalid(value1))),\n            regex: (value1, value2) => {\n                const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));\n                let regex;\n                if (!regexMatch) {\n                    regex = new RegExp((value2 || '').toString());\n                }\n                else if (regexMatch.length === 1) {\n                    regex = new RegExp(regexMatch[1]);\n                }\n                else {\n                    regex = new RegExp(regexMatch[1], regexMatch[2]);\n                }\n                return !!(value1 || '').toString().match(regex);\n            },\n            notRegex: (value1, value2) => {\n                const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));\n                let regex;\n                if (!regexMatch) {\n                    regex = new RegExp((value2 || '').toString());\n                }\n                else if (regexMatch.length === 1) {\n                    regex = new RegExp(regexMatch[1]);\n                }\n                else {\n                    regex = new RegExp(regexMatch[1], regexMatch[2]);\n                }\n                return !(value1 || '').toString().match(regex);\n            },\n        };\n        const convertDateTime = (value) => {\n            let returnValue = undefined;\n            if (typeof value === 'string') {\n                returnValue = new Date(value).getTime();\n            }\n            else if (typeof value === 'number') {\n                returnValue = value;\n            }\n            if (moment_1.default.isMoment(value)) {\n                returnValue = value.unix();\n            }\n            if (value instanceof Date) {\n                returnValue = value.getTime();\n            }\n            if (returnValue === undefined || isNaN(returnValue)) {\n                throw new n8n_workflow_1.NodeOperationError(this.getNode(), `The value \"${value}\" is not a valid DateTime.`);\n            }\n            return returnValue;\n        };\n        const dataTypes = ['boolean', 'dateTime', 'number', 'string'];\n        let dataType;\n        let compareOperationResult;\n        let value1, value2;\n        itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {\n            item = items[itemIndex];\n            let compareData;\n            combineOperation = this.getNodeParameter('combineOperation', itemIndex);\n            for (dataType of dataTypes) {\n                for (compareData of this.getNodeParameter(`conditions.${dataType}`, itemIndex, [])) {\n                    value1 = compareData.value1;\n                    value2 = compareData.value2;\n                    if (dataType === 'dateTime') {\n                        value1 = convertDateTime(value1);\n                        value2 = convertDateTime(value2);\n                    }\n                    compareOperationResult = compareOperationFunctions[compareData.operation](value1, value2);\n                    if (compareOperationResult && combineOperation === 'any') {\n                        returnDataTrue.push(item);\n                        continue itemLoop;\n                    }\n                    else if (!compareOperationResult && combineOperation === 'all') {\n                        returnDataFalse.push(item);\n                        continue itemLoop;\n                    }\n                }\n            }\n            if (item.pairedItem === undefined) {\n                item.pairedItem = [{ item: itemIndex }];\n            }\n            if (combineOperation === 'all') {\n                returnDataTrue.push(item);\n            }\n            else {\n                returnDataFalse.push(item);\n            }\n        }\n        return [returnDataTrue, returnDataFalse];\n    }\n}\nexports.If = If;\n//# sourceMappingURL=If.node.js.map",
 10 |   "package_info": {
 11 |     "name": "n8n-nodes-base",
 12 |     "version": "1.14.1",
 13 |     "description": "Base nodes of n8n",
 14 |     "license": "SEE LICENSE IN LICENSE.md",
 15 |     "homepage": "https://n8n.io",
 16 |     "author": {
 17 |       "name": "Jan Oberhauser",
 18 |       "email": "jan@n8n.io"
 19 |     },
 20 |     "main": "index.js",
 21 |     "repository": {
 22 |       "type": "git",
 23 |       "url": "git+https://github.com/n8n-io/n8n.git"
 24 |     },
 25 |     "files": [
 26 |       "dist"
 27 |     ],
 28 |     "n8n": {
 29 |       "credentials": [
 30 |         "dist/credentials/ActionNetworkApi.credentials.js",
 31 |         "dist/credentials/ActiveCampaignApi.credentials.js",
 32 |         "dist/credentials/AcuitySchedulingApi.credentials.js",
 33 |         "dist/credentials/AcuitySchedulingOAuth2Api.credentials.js",
 34 |         "dist/credentials/AdaloApi.credentials.js",
 35 |         "dist/credentials/AffinityApi.credentials.js",
 36 |         "dist/credentials/AgileCrmApi.credentials.js",
 37 |         "dist/credentials/AirtableApi.credentials.js",
 38 |         "dist/credentials/AirtableOAuth2Api.credentials.js",
 39 |         "dist/credentials/AirtableTokenApi.credentials.js",
 40 |         "dist/credentials/AlienVaultApi.credentials.js",
 41 |         "dist/credentials/Amqp.credentials.js",
 42 |         "dist/credentials/ApiTemplateIoApi.credentials.js",
 43 |         "dist/credentials/AsanaApi.credentials.js",
 44 |         "dist/credentials/AsanaOAuth2Api.credentials.js",
 45 |         "dist/credentials/Auth0ManagementApi.credentials.js",
 46 |         "dist/credentials/AutomizyApi.credentials.js",
 47 |         "dist/credentials/AutopilotApi.credentials.js",
 48 |         "dist/credentials/Aws.credentials.js",
 49 |         "dist/credentials/BambooHrApi.credentials.js",
 50 |         "dist/credentials/BannerbearApi.credentials.js",
 51 |         "dist/credentials/BaserowApi.credentials.js",
 52 |         "dist/credentials/BeeminderApi.credentials.js",
 53 |         "dist/credentials/BitbucketApi.credentials.js",
 54 |         "dist/credentials/BitlyApi.credentials.js",
 55 |         "dist/credentials/BitlyOAuth2Api.credentials.js",
 56 |         "dist/credentials/BitwardenApi.credentials.js",
 57 |         "dist/credentials/BoxOAuth2Api.credentials.js",
 58 |         "dist/credentials/BrandfetchApi.credentials.js",
 59 |         "dist/credentials/BubbleApi.credentials.js",
 60 |         "dist/credentials/CalApi.credentials.js",
 61 |         "dist/credentials/CalendlyApi.credentials.js",
 62 |         "dist/credentials/CarbonBlackApi.credentials.js",
 63 |         "dist/credentials/ChargebeeApi.credentials.js",
 64 |         "dist/credentials/CircleCiApi.credentials.js",
 65 |         "dist/credentials/CiscoMerakiApi.credentials.js",
 66 |         "dist/credentials/CiscoSecureEndpointApi.credentials.js",
 67 |         "dist/credentials/CiscoWebexOAuth2Api.credentials.js",
 68 |         "dist/credentials/CiscoUmbrellaApi.credentials.js",
 69 |         "dist/credentials/CitrixAdcApi.credentials.js",
 70 |         "dist/credentials/CloudflareApi.credentials.js",
 71 |         "dist/credentials/ClearbitApi.credentials.js",
 72 |         "dist/credentials/ClickUpApi.credentials.js",
 73 |         "dist/credentials/ClickUpOAuth2Api.credentials.js",
 74 |         "dist/credentials/ClockifyApi.credentials.js",
 75 |         "dist/credentials/CockpitApi.credentials.js",
 76 |         "dist/credentials/CodaApi.credentials.js",
 77 |         "dist/credentials/ContentfulApi.credentials.js",
 78 |         "dist/credentials/ConvertKitApi.credentials.js",
 79 |         "dist/credentials/CopperApi.credentials.js",
 80 |         "dist/credentials/CortexApi.credentials.js",
 81 |         "dist/credentials/CrateDb.credentials.js",
 82 |         "dist/credentials/CrowdStrikeOAuth2Api.credentials.js",
 83 |         "dist/credentials/CrowdDevApi.credentials.js",
 84 |         "dist/credentials/CustomerIoApi.credentials.js",
 85 |         "dist/credentials/DeepLApi.credentials.js",
 86 |         "dist/credentials/DemioApi.credentials.js",
 87 |         "dist/credentials/DhlApi.credentials.js",
 88 |         "dist/credentials/DiscourseApi.credentials.js",
 89 |         "dist/credentials/DisqusApi.credentials.js",
 90 |         "dist/credentials/DriftApi.credentials.js",
 91 |         "dist/credentials/DriftOAuth2Api.credentials.js",
 92 |         "dist/credentials/DropboxApi.credentials.js",
 93 |         "dist/credentials/DropboxOAuth2Api.credentials.js",
 94 |         "dist/credentials/DropcontactApi.credentials.js",
 95 |         "dist/credentials/EgoiApi.credentials.js",
 96 |         "dist/credentials/ElasticsearchApi.credentials.js",
 97 |         "dist/credentials/ElasticSecurityApi.credentials.js",
 98 |         "dist/credentials/EmeliaApi.credentials.js",
 99 |         "dist/credentials/ERPNextApi.credentials.js",
100 |         "dist/credentials/EventbriteApi.credentials.js",
101 |         "dist/credentials/EventbriteOAuth2Api.credentials.js",
102 |         "dist/credentials/F5BigIpApi.credentials.js",
103 |         "dist/credentials/FacebookGraphApi.credentials.js",
104 |         "dist/credentials/FacebookGraphAppApi.credentials.js",
105 |         "dist/credentials/FacebookLeadAdsOAuth2Api.credentials.js",
106 |         "dist/credentials/FigmaApi.credentials.js",
107 |         "dist/credentials/FileMaker.credentials.js",
108 |         "dist/credentials/FlowApi.credentials.js",
109 |         "dist/credentials/FormIoApi.credentials.js",
110 |         "dist/credentials/FormstackApi.credentials.js",
111 |         "dist/credentials/FormstackOAuth2Api.credentials.js",
112 |         "dist/credentials/FortiGateApi.credentials.js",
113 |         "dist/credentials/FreshdeskApi.credentials.js",
114 |         "dist/credentials/FreshserviceApi.credentials.js",
115 |         "dist/credentials/FreshworksCrmApi.credentials.js",
116 |         "dist/credentials/Ftp.credentials.js",
117 |         "dist/credentials/GetResponseApi.credentials.js",
118 |         "dist/credentials/GetResponseOAuth2Api.credentials.js",
119 |         "dist/credentials/GhostAdminApi.credentials.js",
120 |         "dist/credentials/GhostContentApi.credentials.js",
121 |         "dist/credentials/GithubApi.credentials.js",
122 |         "dist/credentials/GithubOAuth2Api.credentials.js",
123 |         "dist/credentials/GitlabApi.credentials.js",
124 |         "dist/credentials/GitlabOAuth2Api.credentials.js",
125 |         "dist/credentials/GitPassword.credentials.js",
126 |         "dist/credentials/GmailOAuth2Api.credentials.js",
127 |         "dist/credentials/GoogleAdsOAuth2Api.credentials.js",
128 |         "dist/credentials/GoogleAnalyticsOAuth2Api.credentials.js",
129 |         "dist/credentials/GoogleApi.credentials.js",
130 |         "dist/credentials/GoogleBigQueryOAuth2Api.credentials.js",
131 |         "dist/credentials/GoogleBooksOAuth2Api.credentials.js",
132 |         "dist/credentials/GoogleCalendarOAuth2Api.credentials.js",
133 |         "dist/credentials/GoogleCloudNaturalLanguageOAuth2Api.credentials.js",
134 |         "dist/credentials/GoogleCloudStorageOAuth2Api.credentials.js",
135 |         "dist/credentials/GoogleContactsOAuth2Api.credentials.js",
136 |         "dist/credentials/GoogleDocsOAuth2Api.credentials.js",
137 |         "dist/credentials/GoogleDriveOAuth2Api.credentials.js",
138 |         "dist/credentials/GoogleFirebaseCloudFirestoreOAuth2Api.credentials.js",
139 |         "dist/credentials/GoogleFirebaseRealtimeDatabaseOAuth2Api.credentials.js",
140 |         "dist/credentials/GoogleOAuth2Api.credentials.js",
141 |         "dist/credentials/GooglePerspectiveOAuth2Api.credentials.js",
142 |         "dist/credentials/GoogleSheetsOAuth2Api.credentials.js",
143 |         "dist/credentials/GoogleSheetsTriggerOAuth2Api.credentials.js",
144 |         "dist/credentials/GoogleSlidesOAuth2Api.credentials.js",
145 |         "dist/credentials/GoogleTasksOAuth2Api.credentials.js",
146 |         "dist/credentials/GoogleTranslateOAuth2Api.credentials.js",
147 |         "dist/credentials/GotifyApi.credentials.js",
148 |         "dist/credentials/GoToWebinarOAuth2Api.credentials.js",
149 |         "dist/credentials/GristApi.credentials.js",
150 |         "dist/credentials/GrafanaApi.credentials.js",
151 |         "dist/credentials/GSuiteAdminOAuth2Api.credentials.js",
152 |         "dist/credentials/GumroadApi.credentials.js",
153 |         "dist/credentials/HaloPSAApi.credentials.js",
154 |         "dist/credentials/HarvestApi.credentials.js",
155 |         "dist/credentials/HarvestOAuth2Api.credentials.js",
156 |         "dist/credentials/HelpScoutOAuth2Api.credentials.js",
157 |         "dist/credentials/HighLevelApi.credentials.js",
158 |         "dist/credentials/HomeAssistantApi.credentials.js",
159 |         "dist/credentials/HttpBasicAuth.credentials.js",
160 |         "dist/credentials/HttpDigestAuth.credentials.js",
161 |         "dist/credentials/HttpHeaderAuth.credentials.js",
162 |         "dist/credentials/HttpCustomAuth.credentials.js",
163 |         "dist/credentials/HttpQueryAuth.credentials.js",
164 |         "dist/credentials/HubspotApi.credentials.js",
165 |         "dist/credentials/HubspotAppToken.credentials.js",
166 |         "dist/credentials/HubspotDeveloperApi.credentials.js",
167 |         "dist/credentials/HubspotOAuth2Api.credentials.js",
168 |         "dist/credentials/HumanticAiApi.credentials.js",
169 |         "dist/credentials/HunterApi.credentials.js",
170 |         "dist/credentials/HybridAnalysisApi.credentials.js",
171 |         "dist/credentials/Imap.credentials.js",
172 |         "dist/credentials/ImpervaWafApi.credentials.js",
173 |         "dist/credentials/IntercomApi.credentials.js",
174 |         "dist/credentials/InvoiceNinjaApi.credentials.js",
175 |         "dist/credentials/IterableApi.credentials.js",
176 |         "dist/credentials/JenkinsApi.credentials.js",
177 |         "dist/credentials/JiraSoftwareCloudApi.credentials.js",
178 |         "dist/credentials/JiraSoftwareServerApi.credentials.js",
179 |         "dist/credentials/JotFormApi.credentials.js",
180 |         "dist/credentials/Kafka.credentials.js",
181 |         "dist/credentials/KeapOAuth2Api.credentials.js",
182 |         "dist/credentials/KibanaApi.credentials.js",
183 |         "dist/credentials/KitemakerApi.credentials.js",
184 |         "dist/credentials/KoBoToolboxApi.credentials.js",
185 |         "dist/credentials/Ldap.credentials.js",
186 |         "dist/credentials/LemlistApi.credentials.js",
187 |         "dist/credentials/LinearApi.credentials.js",
188 |         "dist/credentials/LinearOAuth2Api.credentials.js",
189 |         "dist/credentials/LineNotifyOAuth2Api.credentials.js",
190 |         "dist/credentials/LingvaNexApi.credentials.js",
191 |         "dist/credentials/LinkedInOAuth2Api.credentials.js",
192 |         "dist/credentials/LoneScaleApi.credentials.js",
193 |         "dist/credentials/Magento2Api.credentials.js",
194 |         "dist/credentials/MailcheckApi.credentials.js",
195 |         "dist/credentials/MailchimpApi.credentials.js",
196 |         "dist/credentials/MailchimpOAuth2Api.credentials.js",
197 |         "dist/credentials/MailerLiteApi.credentials.js",
198 |         "dist/credentials/MailgunApi.credentials.js",
199 |         "dist/credentials/MailjetEmailApi.credentials.js",
200 |         "dist/credentials/MailjetSmsApi.credentials.js",
201 |         "dist/credentials/MandrillApi.credentials.js",
202 |         "dist/credentials/MarketstackApi.credentials.js",
203 |         "dist/credentials/MatrixApi.credentials.js",
204 |         "dist/credentials/MattermostApi.credentials.js",
205 |         "dist/credentials/MauticApi.credentials.js",
206 |         "dist/credentials/MauticOAuth2Api.credentials.js",
207 |         "dist/credentials/MediumApi.credentials.js",
208 |         "dist/credentials/MediumOAuth2Api.credentials.js",
209 |         "dist/credentials/MetabaseApi.credentials.js",
210 |         "dist/credentials/MessageBirdApi.credentials.js",
211 |         "dist/credentials/MetabaseApi.credentials.js",
212 |         "dist/credentials/MicrosoftDynamicsOAuth2Api.credentials.js",
213 |         "dist/credentials/MicrosoftEntraOAuth2Api.credentials.js",
214 |         "dist/credentials/MicrosoftExcelOAuth2Api.credentials.js",
215 |         "dist/credentials/MicrosoftGraphSecurityOAuth2Api.credentials.js",
216 |         "dist/credentials/MicrosoftOAuth2Api.credentials.js",
217 |         "dist/credentials/MicrosoftOneDriveOAuth2Api.credentials.js",
218 |         "dist/credentials/MicrosoftOutlookOAuth2Api.credentials.js",
219 |         "dist/credentials/MicrosoftSql.credentials.js",
220 |         "dist/credentials/MicrosoftTeamsOAuth2Api.credentials.js",
221 |         "dist/credentials/MicrosoftToDoOAuth2Api.credentials.js",
222 |         "dist/credentials/MindeeInvoiceApi.credentials.js",
223 |         "dist/credentials/MindeeReceiptApi.credentials.js",
224 |         "dist/credentials/MispApi.credentials.js",
225 |         "dist/credentials/MistApi.credentials.js",
226 |         "dist/credentials/MoceanApi.credentials.js",
227 |         "dist/credentials/MondayComApi.credentials.js",
228 |         "dist/credentials/MondayComOAuth2Api.credentials.js",
229 |         "dist/credentials/MongoDb.credentials.js",
230 |         "dist/credentials/MonicaCrmApi.credentials.js",
231 |         "dist/credentials/Mqtt.credentials.js",
232 |         "dist/credentials/Msg91Api.credentials.js",
233 |         "dist/credentials/MySql.credentials.js",
234 |         "dist/credentials/N8nApi.credentials.js",
235 |         "dist/credentials/NasaApi.credentials.js",
236 |         "dist/credentials/NetlifyApi.credentials.js",
237 |         "dist/credentials/NextCloudApi.credentials.js",
238 |         "dist/credentials/NextCloudOAuth2Api.credentials.js",
239 |         "dist/credentials/NocoDb.credentials.js",
240 |         "dist/credentials/NocoDbApiToken.credentials.js",
241 |         "dist/credentials/NotionApi.credentials.js",
242 |         "dist/credentials/NotionOAuth2Api.credentials.js",
243 |         "dist/credentials/NpmApi.credentials.js",
244 |         "dist/credentials/OAuth1Api.credentials.js",
245 |         "dist/credentials/OAuth2Api.credentials.js",
246 |         "dist/credentials/OdooApi.credentials.js",
247 |         "dist/credentials/OktaApi.credentials.js",
248 |         "dist/credentials/OneSimpleApi.credentials.js",
249 |         "dist/credentials/OnfleetApi.credentials.js",
250 |         "dist/credentials/OpenAiApi.credentials.js",
251 |         "dist/credentials/OpenCTIApi.credentials.js",
252 |         "dist/credentials/OpenWeatherMapApi.credentials.js",
253 |         "dist/credentials/OrbitApi.credentials.js",
254 |         "dist/credentials/OuraApi.credentials.js",
255 |         "dist/credentials/PaddleApi.credentials.js",
256 |         "dist/credentials/PagerDutyApi.credentials.js",
257 |         "dist/credentials/PagerDutyOAuth2Api.credentials.js",
258 |         "dist/credentials/PayPalApi.credentials.js",
259 |         "dist/credentials/PeekalinkApi.credentials.js",
260 |         "dist/credentials/PhantombusterApi.credentials.js",
261 |         "dist/credentials/PhilipsHueOAuth2Api.credentials.js",
262 |         "dist/credentials/PipedriveApi.credentials.js",
263 |         "dist/credentials/PipedriveOAuth2Api.credentials.js",
264 |         "dist/credentials/PlivoApi.credentials.js",
265 |         "dist/credentials/Postgres.credentials.js",
266 |         "dist/credentials/PostHogApi.credentials.js",
267 |         "dist/credentials/PostmarkApi.credentials.js",
268 |         "dist/credentials/ProfitWellApi.credentials.js",
269 |         "dist/credentials/PushbulletOAuth2Api.credentials.js",
270 |         "dist/credentials/PushcutApi.credentials.js",
271 |         "dist/credentials/PushoverApi.credentials.js",
272 |         "dist/credentials/QRadarApi.credentials.js",
273 |         "dist/credentials/QualysApi.credentials.js",
274 |         "dist/credentials/QuestDb.credentials.js",
275 |         "dist/credentials/QuickBaseApi.credentials.js",
276 |         "dist/credentials/QuickBooksOAuth2Api.credentials.js",
277 |         "dist/credentials/RabbitMQ.credentials.js",
278 |         "dist/credentials/RaindropOAuth2Api.credentials.js",
279 |         "dist/credentials/RecordedFutureApi.credentials.js",
280 |         "dist/credentials/RedditOAuth2Api.credentials.js",
281 |         "dist/credentials/Redis.credentials.js",
282 |         "dist/credentials/RocketchatApi.credentials.js",
283 |         "dist/credentials/RundeckApi.credentials.js",
284 |         "dist/credentials/S3.credentials.js",
285 |         "dist/credentials/SalesforceJwtApi.credentials.js",
286 |         "dist/credentials/SalesforceOAuth2Api.credentials.js",
287 |         "dist/credentials/SalesmateApi.credentials.js",
288 |         "dist/credentials/SeaTableApi.credentials.js",
289 |         "dist/credentials/SecurityScorecardApi.credentials.js",
290 |         "dist/credentials/SegmentApi.credentials.js",
291 |         "dist/credentials/SekoiaApi.credentials.js",
292 |         "dist/credentials/SendGridApi.credentials.js",
293 |         "dist/credentials/BrevoApi.credentials.js",
294 |         "dist/credentials/SendyApi.credentials.js",
295 |         "dist/credentials/SentryIoApi.credentials.js",
296 |         "dist/credentials/SentryIoOAuth2Api.credentials.js",
297 |         "dist/credentials/SentryIoServerApi.credentials.js",
298 |         "dist/credentials/ServiceNowOAuth2Api.credentials.js",
299 |         "dist/credentials/ServiceNowBasicApi.credentials.js",
300 |         "dist/credentials/Sftp.credentials.js",
301 |         "dist/credentials/ShopifyApi.credentials.js",
302 |         "dist/credentials/ShopifyAccessTokenApi.credentials.js",
303 |         "dist/credentials/ShopifyOAuth2Api.credentials.js",
304 |         "dist/credentials/Signl4Api.credentials.js",
305 |         "dist/credentials/SlackApi.credentials.js",
306 |         "dist/credentials/SlackOAuth2Api.credentials.js",
307 |         "dist/credentials/Sms77Api.credentials.js",
308 |         "dist/credentials/Smtp.credentials.js",
309 |         "dist/credentials/Snowflake.credentials.js",
310 |         "dist/credentials/SplunkApi.credentials.js",
311 |         "dist/credentials/SpontitApi.credentials.js",
312 |         "dist/credentials/SpotifyOAuth2Api.credentials.js",
313 |         "dist/credentials/ShufflerApi.credentials.js",
314 |         "dist/credentials/SshPassword.credentials.js",
315 |         "dist/credentials/SshPrivateKey.credentials.js",
316 |         "dist/credentials/StackbyApi.credentials.js",
317 |         "dist/credentials/StoryblokContentApi.credentials.js",
318 |         "dist/credentials/StoryblokManagementApi.credentials.js",
319 |         "dist/credentials/StrapiApi.credentials.js",
320 |         "dist/credentials/StrapiTokenApi.credentials.js",
321 |         "dist/credentials/StravaOAuth2Api.credentials.js",
322 |         "dist/credentials/StripeApi.credentials.js",
323 |         "dist/credentials/SupabaseApi.credentials.js",
324 |         "dist/credentials/SurveyMonkeyApi.credentials.js",
325 |         "dist/credentials/SurveyMonkeyOAuth2Api.credentials.js",
326 |         "dist/credentials/SyncroMspApi.credentials.js",
327 |         "dist/credentials/TaigaApi.credentials.js",
328 |         "dist/credentials/TapfiliateApi.credentials.js",
329 |         "dist/credentials/TelegramApi.credentials.js",
330 |         "dist/credentials/TheHiveProjectApi.credentials.js",
331 |         "dist/credentials/TheHiveApi.credentials.js",
332 |         "dist/credentials/TimescaleDb.credentials.js",
333 |         "dist/credentials/TodoistApi.credentials.js",
334 |         "dist/credentials/TodoistOAuth2Api.credentials.js",
335 |         "dist/credentials/TogglApi.credentials.js",
336 |         "dist/credentials/TotpApi.credentials.js",
337 |         "dist/credentials/TravisCiApi.credentials.js",
338 |         "dist/credentials/TrellixEpoApi.credentials.js",
339 |         "dist/credentials/TrelloApi.credentials.js",
340 |         "dist/credentials/TwakeCloudApi.credentials.js",
341 |         "dist/credentials/TwakeServerApi.credentials.js",
342 |         "dist/credentials/TwilioApi.credentials.js",
343 |         "dist/credentials/TwistOAuth2Api.credentials.js",
344 |         "dist/credentials/TwitterOAuth1Api.credentials.js",
345 |         "dist/credentials/TwitterOAuth2Api.credentials.js",
346 |         "dist/credentials/TypeformApi.credentials.js",
347 |         "dist/credentials/TypeformOAuth2Api.credentials.js",
348 |         "dist/credentials/UnleashedSoftwareApi.credentials.js",
349 |         "dist/credentials/UpleadApi.credentials.js",
350 |         "dist/credentials/UProcApi.credentials.js",
351 |         "dist/credentials/UptimeRobotApi.credentials.js",
352 |         "dist/credentials/UrlScanIoApi.credentials.js",
353 |         "dist/credentials/VeroApi.credentials.js",
354 |         "dist/credentials/VirusTotalApi.credentials.js",
355 |         "dist/credentials/VonageApi.credentials.js",
356 |         "dist/credentials/VenafiTlsProtectCloudApi.credentials.js",
357 |         "dist/credentials/VenafiTlsProtectDatacenterApi.credentials.js",
358 |         "dist/credentials/WebflowApi.credentials.js",
359 |         "dist/credentials/WebflowOAuth2Api.credentials.js",
360 |         "dist/credentials/WekanApi.credentials.js",
361 |         "dist/credentials/WhatsAppApi.credentials.js",
362 |         "dist/credentials/WiseApi.credentials.js",
363 |         "dist/credentials/WooCommerceApi.credentials.js",
364 |         "dist/credentials/WordpressApi.credentials.js",
365 |         "dist/credentials/WorkableApi.credentials.js",
366 |         "dist/credentials/WufooApi.credentials.js",
367 |         "dist/credentials/XeroOAuth2Api.credentials.js",
368 |         "dist/credentials/YourlsApi.credentials.js",
369 |         "dist/credentials/YouTubeOAuth2Api.credentials.js",
370 |         "dist/credentials/ZammadBasicAuthApi.credentials.js",
371 |         "dist/credentials/ZammadTokenAuthApi.credentials.js",
372 |         "dist/credentials/ZendeskApi.credentials.js",
373 |         "dist/credentials/ZendeskOAuth2Api.credentials.js",
374 |         "dist/credentials/ZohoOAuth2Api.credentials.js",
375 |         "dist/credentials/ZoomApi.credentials.js",
376 |         "dist/credentials/ZoomOAuth2Api.credentials.js",
377 |         "dist/credentials/ZscalerZiaApi.credentials.js",
378 |         "dist/credentials/ZulipApi.credentials.js"
379 |       ],
380 |       "nodes": [
381 |         "dist/nodes/ActionNetwork/ActionNetwork.node.js",
382 |         "dist/nodes/ActiveCampaign/ActiveCampaign.node.js",
383 |         "dist/nodes/ActiveCampaign/ActiveCampaignTrigger.node.js",
384 |         "dist/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.js",
385 |         "dist/nodes/Adalo/Adalo.node.js",
386 |         "dist/nodes/Affinity/Affinity.node.js",
387 |         "dist/nodes/Affinity/AffinityTrigger.node.js",
388 |         "dist/nodes/AgileCrm/AgileCrm.node.js",
389 |         "dist/nodes/Airtable/Airtable.node.js",
390 |         "dist/nodes/Airtable/AirtableTrigger.node.js",
391 |         "dist/nodes/Amqp/Amqp.node.js",
392 |         "dist/nodes/Amqp/AmqpTrigger.node.js",
393 |         "dist/nodes/ApiTemplateIo/ApiTemplateIo.node.js",
394 |         "dist/nodes/Asana/Asana.node.js",
395 |         "dist/nodes/Asana/AsanaTrigger.node.js",
396 |         "dist/nodes/Automizy/Automizy.node.js",
397 |         "dist/nodes/Autopilot/Autopilot.node.js",
398 |         "dist/nodes/Autopilot/AutopilotTrigger.node.js",
399 |         "dist/nodes/Aws/AwsLambda.node.js",
400 |         "dist/nodes/Aws/AwsSns.node.js",
401 |         "dist/nodes/Aws/AwsSnsTrigger.node.js",
402 |         "dist/nodes/Aws/CertificateManager/AwsCertificateManager.node.js",
403 |         "dist/nodes/Aws/Comprehend/AwsComprehend.node.js",
404 |         "dist/nodes/Aws/DynamoDB/AwsDynamoDB.node.js",
405 |         "dist/nodes/Aws/ELB/AwsElb.node.js",
406 |         "dist/nodes/Aws/Rekognition/AwsRekognition.node.js",
407 |         "dist/nodes/Aws/S3/AwsS3.node.js",
408 |         "dist/nodes/Aws/SES/AwsSes.node.js",
409 |         "dist/nodes/Aws/SQS/AwsSqs.node.js",
410 |         "dist/nodes/Aws/Textract/AwsTextract.node.js",
411 |         "dist/nodes/Aws/Transcribe/AwsTranscribe.node.js",
412 |         "dist/nodes/BambooHr/BambooHr.node.js",
413 |         "dist/nodes/Bannerbear/Bannerbear.node.js",
414 |         "dist/nodes/Baserow/Baserow.node.js",
415 |         "dist/nodes/Beeminder/Beeminder.node.js",
416 |         "dist/nodes/Bitbucket/BitbucketTrigger.node.js",
417 |         "dist/nodes/Bitly/Bitly.node.js",
418 |         "dist/nodes/Bitwarden/Bitwarden.node.js",
419 |         "dist/nodes/Box/Box.node.js",
420 |         "dist/nodes/Box/BoxTrigger.node.js",
421 |         "dist/nodes/Brandfetch/Brandfetch.node.js",
422 |         "dist/nodes/Bubble/Bubble.node.js",
423 |         "dist/nodes/Cal/CalTrigger.node.js",
424 |         "dist/nodes/Calendly/CalendlyTrigger.node.js",
425 |         "dist/nodes/Chargebee/Chargebee.node.js",
426 |         "dist/nodes/Chargebee/ChargebeeTrigger.node.js",
427 |         "dist/nodes/CircleCi/CircleCi.node.js",
428 |         "dist/nodes/Cisco/Webex/CiscoWebex.node.js",
429 |         "dist/nodes/Citrix/ADC/CitrixAdc.node.js",
430 |         "dist/nodes/Cisco/Webex/CiscoWebexTrigger.node.js",
431 |         "dist/nodes/Cloudflare/Cloudflare.node.js",
432 |         "dist/nodes/Clearbit/Clearbit.node.js",
433 |         "dist/nodes/ClickUp/ClickUp.node.js",
434 |         "dist/nodes/ClickUp/ClickUpTrigger.node.js",
435 |         "dist/nodes/Clockify/Clockify.node.js",
436 |         "dist/nodes/Clockify/ClockifyTrigger.node.js",
437 |         "dist/nodes/Cockpit/Cockpit.node.js",
438 |         "dist/nodes/Coda/Coda.node.js",
439 |         "dist/nodes/Code/Code.node.js",
440 |         "dist/nodes/CoinGecko/CoinGecko.node.js",
441 |         "dist/nodes/CompareDatasets/CompareDatasets.node.js",
442 |         "dist/nodes/Compression/Compression.node.js",
443 |         "dist/nodes/Contentful/Contentful.node.js",
444 |         "dist/nodes/ConvertKit/ConvertKit.node.js",
445 |         "dist/nodes/ConvertKit/ConvertKitTrigger.node.js",
446 |         "dist/nodes/Copper/Copper.node.js",
447 |         "dist/nodes/Copper/CopperTrigger.node.js",
448 |         "dist/nodes/Cortex/Cortex.node.js",
449 |         "dist/nodes/CrateDb/CrateDb.node.js",
450 |         "dist/nodes/Cron/Cron.node.js",
451 |         "dist/nodes/CrowdDev/CrowdDev.node.js",
452 |         "dist/nodes/CrowdDev/CrowdDevTrigger.node.js",
453 |         "dist/nodes/Crypto/Crypto.node.js",
454 |         "dist/nodes/CustomerIo/CustomerIo.node.js",
455 |         "dist/nodes/CustomerIo/CustomerIoTrigger.node.js",
456 |         "dist/nodes/DateTime/DateTime.node.js",
457 |         "dist/nodes/DebugHelper/DebugHelper.node.js",
458 |         "dist/nodes/DeepL/DeepL.node.js",
459 |         "dist/nodes/Demio/Demio.node.js",
460 |         "dist/nodes/Dhl/Dhl.node.js",
461 |         "dist/nodes/Discord/Discord.node.js",
462 |         "dist/nodes/Discourse/Discourse.node.js",
463 |         "dist/nodes/Disqus/Disqus.node.js",
464 |         "dist/nodes/Drift/Drift.node.js",
465 |         "dist/nodes/Dropbox/Dropbox.node.js",
466 |         "dist/nodes/Dropcontact/Dropcontact.node.js",
467 |         "dist/nodes/EditImage/EditImage.node.js",
468 |         "dist/nodes/E2eTest/E2eTest.node.js",
469 |         "dist/nodes/Egoi/Egoi.node.js",
470 |         "dist/nodes/Elastic/Elasticsearch/Elasticsearch.node.js",
471 |         "dist/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.js",
472 |         "dist/nodes/EmailReadImap/EmailReadImap.node.js",
473 |         "dist/nodes/EmailSend/EmailSend.node.js",
474 |         "dist/nodes/Emelia/Emelia.node.js",
475 |         "dist/nodes/Emelia/EmeliaTrigger.node.js",
476 |         "dist/nodes/ERPNext/ERPNext.node.js",
477 |         "dist/nodes/ErrorTrigger/ErrorTrigger.node.js",
478 |         "dist/nodes/Eventbrite/EventbriteTrigger.node.js",
479 |         "dist/nodes/ExecuteCommand/ExecuteCommand.node.js",
480 |         "dist/nodes/ExecuteWorkflow/ExecuteWorkflow.node.js",
481 |         "dist/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.js",
482 |         "dist/nodes/ExecutionData/ExecutionData.node.js",
483 |         "dist/nodes/Facebook/FacebookGraphApi.node.js",
484 |         "dist/nodes/Facebook/FacebookTrigger.node.js",
485 |         "dist/nodes/FacebookLeadAds/FacebookLeadAdsTrigger.node.js",
486 |         "dist/nodes/Figma/FigmaTrigger.node.js",
487 |         "dist/nodes/FileMaker/FileMaker.node.js",
488 |         "dist/nodes/Filter/Filter.node.js",
489 |         "dist/nodes/Flow/Flow.node.js",
490 |         "dist/nodes/Flow/FlowTrigger.node.js",
491 |         "dist/nodes/Form/FormTrigger.node.js",
492 |         "dist/nodes/FormIo/FormIoTrigger.node.js",
493 |         "dist/nodes/Formstack/FormstackTrigger.node.js",
494 |         "dist/nodes/Freshdesk/Freshdesk.node.js",
495 |         "dist/nodes/Freshservice/Freshservice.node.js",
496 |         "dist/nodes/FreshworksCrm/FreshworksCrm.node.js",
497 |         "dist/nodes/Ftp/Ftp.node.js",
498 |         "dist/nodes/Function/Function.node.js",
499 |         "dist/nodes/FunctionItem/FunctionItem.node.js",
500 |         "dist/nodes/GetResponse/GetResponse.node.js",
501 |         "dist/nodes/GetResponse/GetResponseTrigger.node.js",
502 |         "dist/nodes/Ghost/Ghost.node.js",
503 |         "dist/nodes/Git/Git.node.js",
504 |         "dist/nodes/Github/Github.node.js",
505 |         "dist/nodes/Github/GithubTrigger.node.js",
506 |         "dist/nodes/Gitlab/Gitlab.node.js",
507 |         "dist/nodes/Gitlab/GitlabTrigger.node.js",
508 |         "dist/nodes/Google/Ads/GoogleAds.node.js",
509 |         "dist/nodes/Google/Analytics/GoogleAnalytics.node.js",
510 |         "dist/nodes/Google/BigQuery/GoogleBigQuery.node.js",
511 |         "dist/nodes/Google/Books/GoogleBooks.node.js",
512 |         "dist/nodes/Google/Calendar/GoogleCalendar.node.js",
513 |         "dist/nodes/Google/Calendar/GoogleCalendarTrigger.node.js",
514 |         "dist/nodes/Google/Chat/GoogleChat.node.js",
515 |         "dist/nodes/Google/CloudNaturalLanguage/GoogleCloudNaturalLanguage.node.js",
516 |         "dist/nodes/Google/CloudStorage/GoogleCloudStorage.node.js",
517 |         "dist/nodes/Google/Contacts/GoogleContacts.node.js",
518 |         "dist/nodes/Google/Docs/GoogleDocs.node.js",
519 |         "dist/nodes/Google/Drive/GoogleDrive.node.js",
520 |         "dist/nodes/Google/Drive/GoogleDriveTrigger.node.js",
521 |         "dist/nodes/Google/Firebase/CloudFirestore/GoogleFirebaseCloudFirestore.node.js",
522 |         "dist/nodes/Google/Firebase/RealtimeDatabase/GoogleFirebaseRealtimeDatabase.node.js",
523 |         "dist/nodes/Google/Gmail/Gmail.node.js",
524 |         "dist/nodes/Google/Gmail/GmailTrigger.node.js",
525 |         "dist/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.js",
526 |         "dist/nodes/Google/Perspective/GooglePerspective.node.js",
527 |         "dist/nodes/Google/Sheet/GoogleSheets.node.js",
528 |         "dist/nodes/Google/Sheet/GoogleSheetsTrigger.node.js",
529 |         "dist/nodes/Google/Slides/GoogleSlides.node.js",
530 |         "dist/nodes/Google/Task/GoogleTasks.node.js",
531 |         "dist/nodes/Google/Translate/GoogleTranslate.node.js",
532 |         "dist/nodes/Google/YouTube/YouTube.node.js",
533 |         "dist/nodes/Gotify/Gotify.node.js",
534 |         "dist/nodes/GoToWebinar/GoToWebinar.node.js",
535 |         "dist/nodes/Grafana/Grafana.node.js",
536 |         "dist/nodes/GraphQL/GraphQL.node.js",
537 |         "dist/nodes/Grist/Grist.node.js",
538 |         "dist/nodes/Gumroad/GumroadTrigger.node.js",
539 |         "dist/nodes/HackerNews/HackerNews.node.js",
540 |         "dist/nodes/HaloPSA/HaloPSA.node.js",
541 |         "dist/nodes/Harvest/Harvest.node.js",
542 |         "dist/nodes/HelpScout/HelpScout.node.js",
543 |         "dist/nodes/HelpScout/HelpScoutTrigger.node.js",
544 |         "dist/nodes/HighLevel/HighLevel.node.js",
545 |         "dist/nodes/HomeAssistant/HomeAssistant.node.js",
546 |         "dist/nodes/HtmlExtract/HtmlExtract.node.js",
547 |         "dist/nodes/Html/Html.node.js",
548 |         "dist/nodes/HttpRequest/HttpRequest.node.js",
549 |         "dist/nodes/Hubspot/Hubspot.node.js",
550 |         "dist/nodes/Hubspot/HubspotTrigger.node.js",
551 |         "dist/nodes/HumanticAI/HumanticAi.node.js",
552 |         "dist/nodes/Hunter/Hunter.node.js",
553 |         "dist/nodes/ICalendar/ICalendar.node.js",
554 |         "dist/nodes/If/If.node.js",
555 |         "dist/nodes/Intercom/Intercom.node.js",
556 |         "dist/nodes/Interval/Interval.node.js",
557 |         "dist/nodes/InvoiceNinja/InvoiceNinja.node.js",
558 |         "dist/nodes/InvoiceNinja/InvoiceNinjaTrigger.node.js",
559 |         "dist/nodes/ItemLists/ItemLists.node.js",
560 |         "dist/nodes/Iterable/Iterable.node.js",
561 |         "dist/nodes/Jenkins/Jenkins.node.js",
562 |         "dist/nodes/Jira/Jira.node.js",
563 |         "dist/nodes/Jira/JiraTrigger.node.js",
564 |         "dist/nodes/JotForm/JotFormTrigger.node.js",
565 |         "dist/nodes/Kafka/Kafka.node.js",
566 |         "dist/nodes/Kafka/KafkaTrigger.node.js",
567 |         "dist/nodes/Keap/Keap.node.js",
568 |         "dist/nodes/Keap/KeapTrigger.node.js",
569 |         "dist/nodes/Kitemaker/Kitemaker.node.js",
570 |         "dist/nodes/KoBoToolbox/KoBoToolbox.node.js",
571 |         "dist/nodes/KoBoToolbox/KoBoToolboxTrigger.node.js",
572 |         "dist/nodes/Ldap/Ldap.node.js",
573 |         "dist/nodes/Lemlist/Lemlist.node.js",
574 |         "dist/nodes/Lemlist/LemlistTrigger.node.js",
575 |         "dist/nodes/Line/Line.node.js",
576 |         "dist/nodes/Linear/Linear.node.js",
577 |         "dist/nodes/Linear/LinearTrigger.node.js",
578 |         "dist/nodes/LingvaNex/LingvaNex.node.js",
579 |         "dist/nodes/LinkedIn/LinkedIn.node.js",
580 |         "dist/nodes/LocalFileTrigger/LocalFileTrigger.node.js",
581 |         "dist/nodes/LoneScale/LoneScaleTrigger.node.js",
582 |         "dist/nodes/LoneScale/LoneScale.node.js",
583 |         "dist/nodes/Magento/Magento2.node.js",
584 |         "dist/nodes/Mailcheck/Mailcheck.node.js",
585 |         "dist/nodes/Mailchimp/Mailchimp.node.js",
586 |         "dist/nodes/Mailchimp/MailchimpTrigger.node.js",
587 |         "dist/nodes/MailerLite/MailerLite.node.js",
588 |         "dist/nodes/MailerLite/MailerLiteTrigger.node.js",
589 |         "dist/nodes/Mailgun/Mailgun.node.js",
590 |         "dist/nodes/Mailjet/Mailjet.node.js",
591 |         "dist/nodes/Mailjet/MailjetTrigger.node.js",
592 |         "dist/nodes/Mandrill/Mandrill.node.js",
593 |         "dist/nodes/ManualTrigger/ManualTrigger.node.js",
594 |         "dist/nodes/Markdown/Markdown.node.js",
595 |         "dist/nodes/Marketstack/Marketstack.node.js",
596 |         "dist/nodes/Matrix/Matrix.node.js",
597 |         "dist/nodes/Mattermost/Mattermost.node.js",
598 |         "dist/nodes/Mautic/Mautic.node.js",
599 |         "dist/nodes/Mautic/MauticTrigger.node.js",
600 |         "dist/nodes/Medium/Medium.node.js",
601 |         "dist/nodes/Merge/Merge.node.js",
602 |         "dist/nodes/MessageBird/MessageBird.node.js",
603 |         "dist/nodes/Metabase/Metabase.node.js",
604 |         "dist/nodes/Microsoft/Dynamics/MicrosoftDynamicsCrm.node.js",
605 |         "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js",
606 |         "dist/nodes/Microsoft/GraphSecurity/MicrosoftGraphSecurity.node.js",
607 |         "dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js",
608 |         "dist/nodes/Microsoft/Outlook/MicrosoftOutlook.node.js",
609 |         "dist/nodes/Microsoft/Sql/MicrosoftSql.node.js",
610 |         "dist/nodes/Microsoft/Teams/MicrosoftTeams.node.js",
611 |         "dist/nodes/Microsoft/ToDo/MicrosoftToDo.node.js",
612 |         "dist/nodes/Mindee/Mindee.node.js",
613 |         "dist/nodes/Misp/Misp.node.js",
614 |         "dist/nodes/Mocean/Mocean.node.js",
615 |         "dist/nodes/MondayCom/MondayCom.node.js",
616 |         "dist/nodes/MongoDb/MongoDb.node.js",
617 |         "dist/nodes/MonicaCrm/MonicaCrm.node.js",
618 |         "dist/nodes/MoveBinaryData/MoveBinaryData.node.js",
619 |         "dist/nodes/MQTT/Mqtt.node.js",
620 |         "dist/nodes/MQTT/MqttTrigger.node.js",
621 |         "dist/nodes/Msg91/Msg91.node.js",
622 |         "dist/nodes/MySql/MySql.node.js",
623 |         "dist/nodes/N8n/N8n.node.js",
624 |         "dist/nodes/N8nTrainingCustomerDatastore/N8nTrainingCustomerDatastore.node.js",
625 |         "dist/nodes/N8nTrainingCustomerMessenger/N8nTrainingCustomerMessenger.node.js",
626 |         "dist/nodes/N8nTrigger/N8nTrigger.node.js",
627 |         "dist/nodes/Nasa/Nasa.node.js",
628 |         "dist/nodes/Netlify/Netlify.node.js",
629 |         "dist/nodes/Netlify/NetlifyTrigger.node.js",
630 |         "dist/nodes/NextCloud/NextCloud.node.js",
631 |         "dist/nodes/NocoDB/NocoDB.node.js",
632 |         "dist/nodes/Brevo/Brevo.node.js",
633 |         "dist/nodes/Brevo/BrevoTrigger.node.js",
634 |         "dist/nodes/StickyNote/StickyNote.node.js",
635 |         "dist/nodes/NoOp/NoOp.node.js",
636 |         "dist/nodes/Onfleet/Onfleet.node.js",
637 |         "dist/nodes/Onfleet/OnfleetTrigger.node.js",
638 |         "dist/nodes/Notion/Notion.node.js",
639 |         "dist/nodes/Notion/NotionTrigger.node.js",
640 |         "dist/nodes/Npm/Npm.node.js",
641 |         "dist/nodes/Odoo/Odoo.node.js",
642 |         "dist/nodes/OneSimpleApi/OneSimpleApi.node.js",
643 |         "dist/nodes/OpenAi/OpenAi.node.js",
644 |         "dist/nodes/OpenThesaurus/OpenThesaurus.node.js",
645 |         "dist/nodes/OpenWeatherMap/OpenWeatherMap.node.js",
646 |         "dist/nodes/Orbit/Orbit.node.js",
647 |         "dist/nodes/Oura/Oura.node.js",
648 |         "dist/nodes/Paddle/Paddle.node.js",
649 |         "dist/nodes/PagerDuty/PagerDuty.node.js",
650 |         "dist/nodes/PayPal/PayPal.node.js",
651 |         "dist/nodes/PayPal/PayPalTrigger.node.js",
652 |         "dist/nodes/Peekalink/Peekalink.node.js",
653 |         "dist/nodes/Phantombuster/Phantombuster.node.js",
654 |         "dist/nodes/PhilipsHue/PhilipsHue.node.js",
655 |         "dist/nodes/Pipedrive/Pipedrive.node.js",
656 |         "dist/nodes/Pipedrive/PipedriveTrigger.node.js",
657 |         "dist/nodes/Plivo/Plivo.node.js",
658 |         "dist/nodes/PostBin/PostBin.node.js",
659 |         "dist/nodes/Postgres/Postgres.node.js",
660 |         "dist/nodes/Postgres/PostgresTrigger.node.js",
661 |         "dist/nodes/PostHog/PostHog.node.js",
662 |         "dist/nodes/Postmark/PostmarkTrigger.node.js",
663 |         "dist/nodes/ProfitWell/ProfitWell.node.js",
664 |         "dist/nodes/Pushbullet/Pushbullet.node.js",
665 |         "dist/nodes/Pushcut/Pushcut.node.js",
666 |         "dist/nodes/Pushcut/PushcutTrigger.node.js",
667 |         "dist/nodes/Pushover/Pushover.node.js",
668 |         "dist/nodes/QuestDb/QuestDb.node.js",
669 |         "dist/nodes/QuickBase/QuickBase.node.js",
670 |         "dist/nodes/QuickBooks/QuickBooks.node.js",
671 |         "dist/nodes/QuickChart/QuickChart.node.js",
672 |         "dist/nodes/RabbitMQ/RabbitMQ.node.js",
673 |         "dist/nodes/RabbitMQ/RabbitMQTrigger.node.js",
674 |         "dist/nodes/Raindrop/Raindrop.node.js",
675 |         "dist/nodes/ReadBinaryFile/ReadBinaryFile.node.js",
676 |         "dist/nodes/ReadBinaryFiles/ReadBinaryFiles.node.js",
677 |         "dist/nodes/ReadPdf/ReadPDF.node.js",
678 |         "dist/nodes/Reddit/Reddit.node.js",
679 |         "dist/nodes/Redis/Redis.node.js",
680 |         "dist/nodes/Redis/RedisTrigger.node.js",
681 |         "dist/nodes/RenameKeys/RenameKeys.node.js",
682 |         "dist/nodes/RespondToWebhook/RespondToWebhook.node.js",
683 |         "dist/nodes/Rocketchat/Rocketchat.node.js",
684 |         "dist/nodes/RssFeedRead/RssFeedRead.node.js",
685 |         "dist/nodes/RssFeedRead/RssFeedReadTrigger.node.js",
686 |         "dist/nodes/Rundeck/Rundeck.node.js",
687 |         "dist/nodes/S3/S3.node.js",
688 |         "dist/nodes/Salesforce/Salesforce.node.js",
689 |         "dist/nodes/Salesmate/Salesmate.node.js",
690 |         "dist/nodes/Schedule/ScheduleTrigger.node.js",
691 |         "dist/nodes/SeaTable/SeaTable.node.js",
692 |         "dist/nodes/SeaTable/SeaTableTrigger.node.js",
693 |         "dist/nodes/SecurityScorecard/SecurityScorecard.node.js",
694 |         "dist/nodes/Segment/Segment.node.js",
695 |         "dist/nodes/SendGrid/SendGrid.node.js",
696 |         "dist/nodes/Sendy/Sendy.node.js",
697 |         "dist/nodes/SentryIo/SentryIo.node.js",
698 |         "dist/nodes/ServiceNow/ServiceNow.node.js",
699 |         "dist/nodes/Set/Set.node.js",
700 |         "dist/nodes/Shopify/Shopify.node.js",
701 |         "dist/nodes/Shopify/ShopifyTrigger.node.js",
702 |         "dist/nodes/Signl4/Signl4.node.js",
703 |         "dist/nodes/Slack/Slack.node.js",
704 |         "dist/nodes/Sms77/Sms77.node.js",
705 |         "dist/nodes/Snowflake/Snowflake.node.js",
706 |         "dist/nodes/SplitInBatches/SplitInBatches.node.js",
707 |         "dist/nodes/Splunk/Splunk.node.js",
708 |         "dist/nodes/Spontit/Spontit.node.js",
709 |         "dist/nodes/Spotify/Spotify.node.js",
710 |         "dist/nodes/SpreadsheetFile/SpreadsheetFile.node.js",
711 |         "dist/nodes/SseTrigger/SseTrigger.node.js",
712 |         "dist/nodes/Ssh/Ssh.node.js",
713 |         "dist/nodes/Stackby/Stackby.node.js",
714 |         "dist/nodes/Start/Start.node.js",
715 |         "dist/nodes/StopAndError/StopAndError.node.js",
716 |         "dist/nodes/Storyblok/Storyblok.node.js",
717 |         "dist/nodes/Strapi/Strapi.node.js",
718 |         "dist/nodes/Strava/Strava.node.js",
719 |         "dist/nodes/Strava/StravaTrigger.node.js",
720 |         "dist/nodes/Stripe/Stripe.node.js",
721 |         "dist/nodes/Stripe/StripeTrigger.node.js",
722 |         "dist/nodes/Supabase/Supabase.node.js",
723 |         "dist/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.js",
724 |         "dist/nodes/Switch/Switch.node.js",
725 |         "dist/nodes/SyncroMSP/SyncroMsp.node.js",
726 |         "dist/nodes/Taiga/Taiga.node.js",
727 |         "dist/nodes/Taiga/TaigaTrigger.node.js",
728 |         "dist/nodes/Tapfiliate/Tapfiliate.node.js",
729 |         "dist/nodes/Telegram/Telegram.node.js",
730 |         "dist/nodes/Telegram/TelegramTrigger.node.js",
731 |         "dist/nodes/TheHiveProject/TheHiveProject.node.js",
732 |         "dist/nodes/TheHiveProject/TheHiveProjectTrigger.node.js",
733 |         "dist/nodes/TheHive/TheHive.node.js",
734 |         "dist/nodes/TheHive/TheHiveTrigger.node.js",
735 |         "dist/nodes/TimescaleDb/TimescaleDb.node.js",
736 |         "dist/nodes/Todoist/Todoist.node.js",
737 |         "dist/nodes/Toggl/TogglTrigger.node.js",
738 |         "dist/nodes/Totp/Totp.node.js",
739 |         "dist/nodes/TravisCi/TravisCi.node.js",
740 |         "dist/nodes/Trello/Trello.node.js",
741 |         "dist/nodes/Trello/TrelloTrigger.node.js",
742 |         "dist/nodes/Twake/Twake.node.js",
743 |         "dist/nodes/Twilio/Twilio.node.js",
744 |         "dist/nodes/Twist/Twist.node.js",
745 |         "dist/nodes/Twitter/Twitter.node.js",
746 |         "dist/nodes/Typeform/TypeformTrigger.node.js",
747 |         "dist/nodes/UnleashedSoftware/UnleashedSoftware.node.js",
748 |         "dist/nodes/Uplead/Uplead.node.js",
749 |         "dist/nodes/UProc/UProc.node.js",
750 |         "dist/nodes/UptimeRobot/UptimeRobot.node.js",
751 |         "dist/nodes/UrlScanIo/UrlScanIo.node.js",
752 |         "dist/nodes/Vero/Vero.node.js",
753 |         "dist/nodes/Venafi/ProtectCloud/VenafiTlsProtectCloud.node.js",
754 |         "dist/nodes/Venafi/ProtectCloud/VenafiTlsProtectCloudTrigger.node.js",
755 |         "dist/nodes/Venafi/Datacenter/VenafiTlsProtectDatacenter.node.js",
756 |         "dist/nodes/Vonage/Vonage.node.js",
757 |         "dist/nodes/Wait/Wait.node.js",
758 |         "dist/nodes/Webflow/Webflow.node.js",
759 |         "dist/nodes/Webflow/WebflowTrigger.node.js",
760 |         "dist/nodes/Webhook/Webhook.node.js",
761 |         "dist/nodes/Wekan/Wekan.node.js",
762 |         "dist/nodes/WhatsApp/WhatsApp.node.js",
763 |         "dist/nodes/Wise/Wise.node.js",
764 |         "dist/nodes/Wise/WiseTrigger.node.js",
765 |         "dist/nodes/WooCommerce/WooCommerce.node.js",
766 |         "dist/nodes/WooCommerce/WooCommerceTrigger.node.js",
767 |         "dist/nodes/Wordpress/Wordpress.node.js",
768 |         "dist/nodes/Workable/WorkableTrigger.node.js",
769 |         "dist/nodes/WorkflowTrigger/WorkflowTrigger.node.js",
770 |         "dist/nodes/WriteBinaryFile/WriteBinaryFile.node.js",
771 |         "dist/nodes/Wufoo/WufooTrigger.node.js",
772 |         "dist/nodes/Xero/Xero.node.js",
773 |         "dist/nodes/Xml/Xml.node.js",
774 |         "dist/nodes/Yourls/Yourls.node.js",
775 |         "dist/nodes/Zammad/Zammad.node.js",
776 |         "dist/nodes/Zendesk/Zendesk.node.js",
777 |         "dist/nodes/Zendesk/ZendeskTrigger.node.js",
778 |         "dist/nodes/Zoho/ZohoCrm.node.js",
779 |         "dist/nodes/Zoom/Zoom.node.js",
780 |         "dist/nodes/Zulip/Zulip.node.js"
781 |       ]
782 |     },
783 |     "devDependencies": {
784 |       "@types/amqplib": "^0.10.1",
785 |       "@types/aws4": "^1.5.1",
786 |       "@types/basic-auth": "^1.1.3",
787 |       "@types/cheerio": "^0.22.15",
788 |       "@types/cron": "~1.7.1",
789 |       "@types/eventsource": "^1.1.2",
790 |       "@types/express": "^4.17.6",
791 |       "@types/gm": "^1.25.0",
792 |       "@types/imap-simple": "^4.2.0",
793 |       "@types/js-nacl": "^1.3.0",
794 |       "@types/jsonwebtoken": "^9.0.1",
795 |       "@types/lodash": "^4.14.195",
796 |       "@types/lossless-json": "^1.0.0",
797 |       "@types/mailparser": "^2.7.3",
798 |       "@types/mime-types": "^2.1.0",
799 |       "@types/mssql": "^6.0.2",
800 |       "@types/node-ssh": "^7.0.1",
801 |       "@types/nodemailer": "^6.4.0",
802 |       "@types/promise-ftp": "^1.3.4",
803 |       "@types/redis": "^2.8.11",
804 |       "@types/request-promise-native": "~1.0.15",
805 |       "@types/rfc2047": "^2.0.1",
806 |       "@types/showdown": "^1.9.4",
807 |       "@types/snowflake-sdk": "^1.6.12",
808 |       "@types/ssh2-sftp-client": "^5.1.0",
809 |       "@types/tmp": "^0.2.0",
810 |       "@types/uuid": "^8.3.2",
811 |       "@types/xml2js": "^0.4.11",
812 |       "eslint-plugin-n8n-nodes-base": "^1.16.0",
813 |       "gulp": "^4.0.0",
814 |       "n8n-core": "1.14.1"
815 |     },
816 |     "dependencies": {
817 |       "@kafkajs/confluent-schema-registry": "1.0.6",
818 |       "@n8n/vm2": "^3.9.20",
819 |       "amqplib": "^0.10.3",
820 |       "aws4": "^1.8.0",
821 |       "basic-auth": "^2.0.1",
822 |       "change-case": "^4.1.1",
823 |       "cheerio": "1.0.0-rc.6",
824 |       "chokidar": "3.5.2",
825 |       "cron": "~1.7.2",
826 |       "csv-parse": "^5.5.0",
827 |       "currency-codes": "^2.1.0",
828 |       "eventsource": "^2.0.2",
829 |       "fast-glob": "^3.2.5",
830 |       "fflate": "^0.7.0",
831 |       "get-system-fonts": "^2.0.2",
832 |       "gm": "^1.25.0",
833 |       "iconv-lite": "^0.6.2",
834 |       "ics": "^2.27.0",
835 |       "imap-simple": "^4.3.0",
836 |       "isbot": "^3.6.13",
837 |       "iso-639-1": "^2.1.3",
838 |       "js-nacl": "^1.4.0",
839 |       "jsonwebtoken": "^9.0.0",
840 |       "kafkajs": "^1.14.0",
841 |       "ldapts": "^4.2.6",
842 |       "lodash": "^4.17.21",
843 |       "lossless-json": "^1.0.4",
844 |       "luxon": "^3.3.0",
845 |       "mailparser": "^3.2.0",
846 |       "minifaker": "^1.34.1",
847 |       "moment": "~2.29.2",
848 |       "moment-timezone": "^0.5.28",
849 |       "mongodb": "^4.17.1",
850 |       "mqtt": "^5.0.2",
851 |       "mssql": "^8.1.2",
852 |       "mysql2": "~2.3.0",
853 |       "nanoid": "^3.3.6",
854 |       "node-html-markdown": "^1.1.3",
855 |       "node-ssh": "^12.0.0",
856 |       "nodemailer": "^6.7.1",
857 |       "otpauth": "^9.1.1",
858 |       "pdfjs-dist": "^2.16.105",
859 |       "pg": "^8.3.0",
860 |       "pg-promise": "^10.5.8",
861 |       "pretty-bytes": "^5.6.0",
862 |       "promise-ftp": "^1.3.5",
863 |       "pyodide": "^0.23.4",
864 |       "redis": "^3.1.1",
865 |       "rfc2047": "^4.0.1",
866 |       "rhea": "^1.0.11",
867 |       "rss-parser": "^3.7.0",
868 |       "semver": "^7.5.4",
869 |       "showdown": "^2.0.3",
870 |       "simple-git": "^3.17.0",
871 |       "snowflake-sdk": "^1.8.0",
872 |       "ssh2-sftp-client": "^7.0.0",
873 |       "tmp-promise": "^3.0.2",
874 |       "typedi": "^0.10.0",
875 |       "uuid": "^8.3.2",
876 |       "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz",
877 |       "xml2js": "^0.5.0",
878 |       "n8n-workflow": "1.14.1"
879 |     },
880 |     "scripts": {
881 |       "clean": "rimraf dist .turbo",
882 |       "dev": "pnpm watch",
883 |       "typecheck": "tsc",
884 |       "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && gulp build:icons && gulp build:translations && pnpm build:metadata",
885 |       "build:translations": "gulp build:translations",
886 |       "build:metadata": "pnpm n8n-generate-known && pnpm n8n-generate-ui-types",
887 |       "format": "prettier --write . --ignore-path ../../.prettierignore",
888 |       "lint": "eslint . --quiet && node ./scripts/validate-load-options-methods.js",
889 |       "lintfix": "eslint . --fix",
890 |       "watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\" --onSuccess \"pnpm n8n-generate-ui-types\"",
891 |       "test": "jest"
892 |     }
893 |   },
894 |   "extraction_time_ms": 4,
895 |   "extracted_at": "2025-06-07T17:49:22.724Z"
896 | }
```

