This is page 41 of 59. Use http://codebase.md/czlonkowski/n8n-mcp?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 -------------------------------------------------------------------------------- /tests/unit/services/n8n-validation.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeEach, vi } from 'vitest'; 2 | import { 3 | workflowNodeSchema, 4 | workflowConnectionSchema, 5 | workflowSettingsSchema, 6 | defaultWorkflowSettings, 7 | validateWorkflowNode, 8 | validateWorkflowConnections, 9 | validateWorkflowSettings, 10 | cleanWorkflowForCreate, 11 | cleanWorkflowForUpdate, 12 | validateWorkflowStructure, 13 | hasWebhookTrigger, 14 | getWebhookUrl, 15 | getWorkflowStructureExample, 16 | getWorkflowFixSuggestions, 17 | } from '../../../src/services/n8n-validation'; 18 | import { WorkflowBuilder } from '../../utils/builders/workflow.builder'; 19 | import { z } from 'zod'; 20 | import { WorkflowNode, WorkflowConnection, Workflow } from '../../../src/types/n8n-api'; 21 | 22 | describe('n8n-validation', () => { 23 | describe('Zod Schemas', () => { 24 | describe('workflowNodeSchema', () => { 25 | it('should validate a complete valid node', () => { 26 | const validNode = { 27 | id: 'node-1', 28 | name: 'Test Node', 29 | type: 'n8n-nodes-base.set', 30 | typeVersion: 3, 31 | position: [100, 200], 32 | parameters: { key: 'value' }, 33 | credentials: { api: 'cred-id' }, 34 | disabled: false, 35 | notes: 'Test notes', 36 | notesInFlow: true, 37 | continueOnFail: true, 38 | retryOnFail: true, 39 | maxTries: 3, 40 | waitBetweenTries: 1000, 41 | alwaysOutputData: true, 42 | executeOnce: false, 43 | }; 44 | 45 | const result = workflowNodeSchema.parse(validNode); 46 | expect(result).toEqual(validNode); 47 | }); 48 | 49 | it('should validate a minimal valid node', () => { 50 | const minimalNode = { 51 | id: 'node-1', 52 | name: 'Test Node', 53 | type: 'n8n-nodes-base.set', 54 | typeVersion: 3, 55 | position: [100, 200], 56 | parameters: {}, 57 | }; 58 | 59 | const result = workflowNodeSchema.parse(minimalNode); 60 | expect(result).toEqual(minimalNode); 61 | }); 62 | 63 | it('should reject node with missing required fields', () => { 64 | const invalidNode = { 65 | name: 'Test Node', 66 | type: 'n8n-nodes-base.set', 67 | }; 68 | 69 | expect(() => workflowNodeSchema.parse(invalidNode)).toThrow(); 70 | }); 71 | 72 | it('should reject node with invalid position format', () => { 73 | const invalidNode = { 74 | id: 'node-1', 75 | name: 'Test Node', 76 | type: 'n8n-nodes-base.set', 77 | typeVersion: 3, 78 | position: [100], // Should be tuple of 2 numbers 79 | parameters: {}, 80 | }; 81 | 82 | expect(() => workflowNodeSchema.parse(invalidNode)).toThrow(); 83 | }); 84 | 85 | it('should reject node with invalid type values', () => { 86 | const invalidNode = { 87 | id: 'node-1', 88 | name: 'Test Node', 89 | type: 'n8n-nodes-base.set', 90 | typeVersion: '3', // Should be number 91 | position: [100, 200], 92 | parameters: {}, 93 | }; 94 | 95 | expect(() => workflowNodeSchema.parse(invalidNode)).toThrow(); 96 | }); 97 | }); 98 | 99 | describe('workflowConnectionSchema', () => { 100 | it('should validate valid connections', () => { 101 | const validConnections = { 102 | 'node-1': { 103 | main: [[{ node: 'node-2', type: 'main', index: 0 }]], 104 | }, 105 | 'node-2': { 106 | main: [ 107 | [ 108 | { node: 'node-3', type: 'main', index: 0 }, 109 | { node: 'node-4', type: 'main', index: 0 }, 110 | ], 111 | ], 112 | }, 113 | }; 114 | 115 | const result = workflowConnectionSchema.parse(validConnections); 116 | expect(result).toEqual(validConnections); 117 | }); 118 | 119 | it('should validate empty connections', () => { 120 | const emptyConnections = {}; 121 | const result = workflowConnectionSchema.parse(emptyConnections); 122 | expect(result).toEqual(emptyConnections); 123 | }); 124 | 125 | it('should reject invalid connection structure', () => { 126 | const invalidConnections = { 127 | 'node-1': { 128 | main: [{ node: 'node-2', type: 'main', index: 0 }], // Should be array of arrays 129 | }, 130 | }; 131 | 132 | expect(() => workflowConnectionSchema.parse(invalidConnections)).toThrow(); 133 | }); 134 | 135 | it('should reject connections missing required fields', () => { 136 | const invalidConnections = { 137 | 'node-1': { 138 | main: [[{ node: 'node-2' }]], // Missing type and index 139 | }, 140 | }; 141 | 142 | expect(() => workflowConnectionSchema.parse(invalidConnections)).toThrow(); 143 | }); 144 | }); 145 | 146 | describe('workflowSettingsSchema', () => { 147 | it('should validate complete settings', () => { 148 | const completeSettings = { 149 | executionOrder: 'v1' as const, 150 | timezone: 'America/New_York', 151 | saveDataErrorExecution: 'all' as const, 152 | saveDataSuccessExecution: 'all' as const, 153 | saveManualExecutions: true, 154 | saveExecutionProgress: true, 155 | executionTimeout: 300, 156 | errorWorkflow: 'error-handler-workflow', 157 | }; 158 | 159 | const result = workflowSettingsSchema.parse(completeSettings); 160 | expect(result).toEqual(completeSettings); 161 | }); 162 | 163 | it('should apply defaults for missing fields', () => { 164 | const minimalSettings = {}; 165 | const result = workflowSettingsSchema.parse(minimalSettings); 166 | 167 | expect(result).toEqual({ 168 | executionOrder: 'v1', 169 | saveDataErrorExecution: 'all', 170 | saveDataSuccessExecution: 'all', 171 | saveManualExecutions: true, 172 | saveExecutionProgress: true, 173 | }); 174 | }); 175 | 176 | it('should reject invalid enum values', () => { 177 | const invalidSettings = { 178 | executionOrder: 'v2', // Invalid enum value 179 | }; 180 | 181 | expect(() => workflowSettingsSchema.parse(invalidSettings)).toThrow(); 182 | }); 183 | }); 184 | }); 185 | 186 | describe('Validation Functions', () => { 187 | describe('validateWorkflowNode', () => { 188 | it('should validate and return a valid node', () => { 189 | const node = { 190 | id: 'test-1', 191 | name: 'Test', 192 | type: 'n8n-nodes-base.webhook', 193 | typeVersion: 2, 194 | position: [250, 300] as [number, number], 195 | parameters: {}, 196 | }; 197 | 198 | const result = validateWorkflowNode(node); 199 | expect(result).toEqual(node); 200 | }); 201 | 202 | it('should throw for invalid node', () => { 203 | const invalidNode = { name: 'Test' }; 204 | expect(() => validateWorkflowNode(invalidNode)).toThrow(); 205 | }); 206 | }); 207 | 208 | describe('validateWorkflowConnections', () => { 209 | it('should validate and return valid connections', () => { 210 | const connections = { 211 | 'Node1': { 212 | main: [[{ node: 'Node2', type: 'main', index: 0 }]], 213 | }, 214 | }; 215 | 216 | const result = validateWorkflowConnections(connections); 217 | expect(result).toEqual(connections); 218 | }); 219 | 220 | it('should throw for invalid connections', () => { 221 | const invalidConnections = { 222 | 'Node1': { 223 | main: 'invalid', // Should be array 224 | }, 225 | }; 226 | 227 | expect(() => validateWorkflowConnections(invalidConnections)).toThrow(); 228 | }); 229 | }); 230 | 231 | describe('validateWorkflowSettings', () => { 232 | it('should validate and return valid settings', () => { 233 | const settings = { 234 | executionOrder: 'v1' as const, 235 | timezone: 'UTC', 236 | }; 237 | 238 | const result = validateWorkflowSettings(settings); 239 | expect(result).toMatchObject(settings); 240 | }); 241 | 242 | it('should apply defaults and validate', () => { 243 | const result = validateWorkflowSettings({}); 244 | expect(result).toMatchObject(defaultWorkflowSettings); 245 | }); 246 | }); 247 | }); 248 | 249 | describe('Workflow Cleaning Functions', () => { 250 | describe('cleanWorkflowForCreate', () => { 251 | it('should remove read-only fields', () => { 252 | const workflow = { 253 | id: 'should-be-removed', 254 | name: 'Test Workflow', 255 | nodes: [], 256 | connections: {}, 257 | createdAt: '2023-01-01', 258 | updatedAt: '2023-01-01', 259 | versionId: 'v123', 260 | meta: { test: 'data' }, 261 | active: true, 262 | tags: ['tag1'], 263 | }; 264 | 265 | const cleaned = cleanWorkflowForCreate(workflow as any); 266 | 267 | expect(cleaned).not.toHaveProperty('id'); 268 | expect(cleaned).not.toHaveProperty('createdAt'); 269 | expect(cleaned).not.toHaveProperty('updatedAt'); 270 | expect(cleaned).not.toHaveProperty('versionId'); 271 | expect(cleaned).not.toHaveProperty('meta'); 272 | expect(cleaned).not.toHaveProperty('active'); 273 | expect(cleaned).not.toHaveProperty('tags'); 274 | expect(cleaned.name).toBe('Test Workflow'); 275 | }); 276 | 277 | it('should add default settings if not present', () => { 278 | const workflow = { 279 | name: 'Test Workflow', 280 | nodes: [], 281 | connections: {}, 282 | }; 283 | 284 | const cleaned = cleanWorkflowForCreate(workflow as Workflow); 285 | expect(cleaned.settings).toEqual(defaultWorkflowSettings); 286 | }); 287 | 288 | it('should preserve existing settings', () => { 289 | const customSettings = { 290 | executionOrder: 'v0' as const, 291 | timezone: 'America/New_York', 292 | }; 293 | 294 | const workflow = { 295 | name: 'Test Workflow', 296 | nodes: [], 297 | connections: {}, 298 | settings: customSettings, 299 | }; 300 | 301 | const cleaned = cleanWorkflowForCreate(workflow as Workflow); 302 | expect(cleaned.settings).toEqual(customSettings); 303 | }); 304 | }); 305 | 306 | describe('cleanWorkflowForUpdate', () => { 307 | it('should remove all read-only and computed fields', () => { 308 | const workflow = { 309 | id: 'keep-id', 310 | name: 'Updated Workflow', 311 | nodes: [], 312 | connections: {}, 313 | createdAt: '2023-01-01', 314 | updatedAt: '2023-01-01', 315 | versionId: 'v123', 316 | meta: { test: 'data' }, 317 | staticData: { some: 'data' }, 318 | pinData: { pin: 'data' }, 319 | tags: ['tag1'], 320 | isArchived: false, 321 | usedCredentials: ['cred1'], 322 | sharedWithProjects: ['proj1'], 323 | triggerCount: 5, 324 | shared: true, 325 | active: true, 326 | settings: { executionOrder: 'v1' }, 327 | } as any; 328 | 329 | const cleaned = cleanWorkflowForUpdate(workflow); 330 | 331 | // Should remove all these fields 332 | expect(cleaned).not.toHaveProperty('id'); 333 | expect(cleaned).not.toHaveProperty('createdAt'); 334 | expect(cleaned).not.toHaveProperty('updatedAt'); 335 | expect(cleaned).not.toHaveProperty('versionId'); 336 | expect(cleaned).not.toHaveProperty('meta'); 337 | expect(cleaned).not.toHaveProperty('staticData'); 338 | expect(cleaned).not.toHaveProperty('pinData'); 339 | expect(cleaned).not.toHaveProperty('tags'); 340 | expect(cleaned).not.toHaveProperty('isArchived'); 341 | expect(cleaned).not.toHaveProperty('usedCredentials'); 342 | expect(cleaned).not.toHaveProperty('sharedWithProjects'); 343 | expect(cleaned).not.toHaveProperty('triggerCount'); 344 | expect(cleaned).not.toHaveProperty('shared'); 345 | expect(cleaned).not.toHaveProperty('active'); 346 | 347 | // Should keep name and filter settings to safe properties 348 | expect(cleaned.name).toBe('Updated Workflow'); 349 | expect(cleaned.settings).toEqual({ executionOrder: 'v1' }); 350 | }); 351 | 352 | it('should add empty settings object for cloud API compatibility', () => { 353 | const workflow = { 354 | name: 'Test Workflow', 355 | nodes: [], 356 | connections: {}, 357 | } as any; 358 | 359 | const cleaned = cleanWorkflowForUpdate(workflow); 360 | expect(cleaned.settings).toEqual({}); 361 | }); 362 | 363 | it('should filter settings to safe properties to prevent API errors (Issue #248 - final fix)', () => { 364 | const workflow = { 365 | name: 'Test Workflow', 366 | nodes: [], 367 | connections: {}, 368 | settings: { 369 | executionOrder: 'v1' as const, 370 | saveDataSuccessExecution: 'none' as const, 371 | callerPolicy: 'workflowsFromSameOwner' as const, // Filtered out (not in OpenAPI spec) 372 | timeSavedPerExecution: 5, // Filtered out (UI-only property) 373 | }, 374 | } as any; 375 | 376 | const cleaned = cleanWorkflowForUpdate(workflow); 377 | 378 | // Unsafe properties filtered out, safe properties kept 379 | expect(cleaned.settings).toEqual({ 380 | executionOrder: 'v1', 381 | saveDataSuccessExecution: 'none' 382 | }); 383 | expect(cleaned.settings).not.toHaveProperty('callerPolicy'); 384 | expect(cleaned.settings).not.toHaveProperty('timeSavedPerExecution'); 385 | }); 386 | 387 | it('should filter out callerPolicy (Issue #248 - API limitation)', () => { 388 | const workflow = { 389 | name: 'Test Workflow', 390 | nodes: [], 391 | connections: {}, 392 | settings: { 393 | executionOrder: 'v1' as const, 394 | callerPolicy: 'workflowsFromSameOwner' as const, // Filtered out 395 | errorWorkflow: 'N2O2nZy3aUiBRGFN', 396 | }, 397 | } as any; 398 | 399 | const cleaned = cleanWorkflowForUpdate(workflow); 400 | 401 | // callerPolicy filtered out (causes API errors), safe properties kept 402 | expect(cleaned.settings).toEqual({ 403 | executionOrder: 'v1', 404 | errorWorkflow: 'N2O2nZy3aUiBRGFN' 405 | }); 406 | expect(cleaned.settings).not.toHaveProperty('callerPolicy'); 407 | }); 408 | 409 | it('should filter all settings properties correctly (Issue #248 - API design)', () => { 410 | const workflow = { 411 | name: 'Test Workflow', 412 | nodes: [], 413 | connections: {}, 414 | settings: { 415 | executionOrder: 'v0' as const, 416 | timezone: 'UTC', 417 | saveDataErrorExecution: 'all' as const, 418 | saveDataSuccessExecution: 'none' as const, 419 | saveManualExecutions: false, 420 | saveExecutionProgress: false, 421 | executionTimeout: 300, 422 | errorWorkflow: 'error-workflow-id', 423 | callerPolicy: 'workflowsFromAList' as const, // Filtered out (not in OpenAPI spec) 424 | }, 425 | } as any; 426 | 427 | const cleaned = cleanWorkflowForUpdate(workflow); 428 | 429 | // Safe properties kept, unsafe properties filtered out 430 | // See: https://community.n8n.io/t/api-workflow-update-endpoint-doesnt-support-setting-callerpolicy/161916 431 | expect(cleaned.settings).toEqual({ 432 | executionOrder: 'v0', 433 | timezone: 'UTC', 434 | saveDataErrorExecution: 'all', 435 | saveDataSuccessExecution: 'none', 436 | saveManualExecutions: false, 437 | saveExecutionProgress: false, 438 | executionTimeout: 300, 439 | errorWorkflow: 'error-workflow-id' 440 | }); 441 | expect(cleaned.settings).not.toHaveProperty('callerPolicy'); 442 | }); 443 | 444 | it('should handle workflows without settings gracefully', () => { 445 | const workflow = { 446 | name: 'Test Workflow', 447 | nodes: [], 448 | connections: {}, 449 | } as any; 450 | 451 | const cleaned = cleanWorkflowForUpdate(workflow); 452 | expect(cleaned.settings).toEqual({}); 453 | }); 454 | }); 455 | }); 456 | 457 | describe('validateWorkflowStructure', () => { 458 | it('should return no errors for valid workflow', () => { 459 | const workflow = new WorkflowBuilder('Valid Workflow') 460 | .addWebhookNode({ id: 'webhook-1', name: 'Webhook' }) 461 | .addSlackNode({ id: 'slack-1', name: 'Send Slack' }) 462 | .connect('Webhook', 'Send Slack') 463 | .build(); 464 | 465 | const errors = validateWorkflowStructure(workflow as any); 466 | expect(errors).toEqual([]); 467 | }); 468 | 469 | it('should detect missing workflow name', () => { 470 | const workflow = { 471 | nodes: [], 472 | connections: {}, 473 | }; 474 | 475 | const errors = validateWorkflowStructure(workflow as any); 476 | expect(errors).toContain('Workflow name is required'); 477 | }); 478 | 479 | it('should detect missing nodes', () => { 480 | const workflow = { 481 | name: 'Test', 482 | connections: {}, 483 | }; 484 | 485 | const errors = validateWorkflowStructure(workflow as any); 486 | expect(errors).toContain('Workflow must have at least one node'); 487 | }); 488 | 489 | it('should detect empty nodes array', () => { 490 | const workflow = { 491 | name: 'Test', 492 | nodes: [], 493 | connections: {}, 494 | }; 495 | 496 | const errors = validateWorkflowStructure(workflow as any); 497 | expect(errors).toContain('Workflow must have at least one node'); 498 | }); 499 | 500 | it('should detect missing connections', () => { 501 | const workflow = { 502 | name: 'Test', 503 | nodes: [{ id: 'node-1', name: 'Node 1', type: 'n8n-nodes-base.set', typeVersion: 1, position: [0, 0] as [number, number], parameters: {} }], 504 | }; 505 | 506 | const errors = validateWorkflowStructure(workflow as any); 507 | expect(errors).toContain('Workflow connections are required'); 508 | }); 509 | 510 | it('should allow single webhook node workflow', () => { 511 | const workflow = { 512 | name: 'Webhook Only', 513 | nodes: [{ 514 | id: 'webhook-1', 515 | name: 'Webhook', 516 | type: 'n8n-nodes-base.webhook', 517 | typeVersion: 2, 518 | position: [250, 300] as [number, number], 519 | parameters: {}, 520 | }], 521 | connections: {}, 522 | }; 523 | 524 | const errors = validateWorkflowStructure(workflow as any); 525 | expect(errors).toEqual([]); 526 | }); 527 | 528 | it('should reject single non-webhook node workflow', () => { 529 | const workflow = { 530 | name: 'Invalid Single Node', 531 | nodes: [{ 532 | id: 'set-1', 533 | name: 'Set', 534 | type: 'n8n-nodes-base.set', 535 | typeVersion: 3, 536 | position: [250, 300] as [number, number], 537 | parameters: {}, 538 | }], 539 | connections: {}, 540 | }; 541 | 542 | const errors = validateWorkflowStructure(workflow); 543 | expect(errors).toContain('Single-node workflows are only valid for webhooks. Add at least one more node and connect them. Example: Manual Trigger → Set node'); 544 | }); 545 | 546 | it('should detect empty connections in multi-node workflow', () => { 547 | const workflow = { 548 | name: 'Disconnected Nodes', 549 | nodes: [ 550 | { 551 | id: 'node-1', 552 | name: 'Node 1', 553 | type: 'n8n-nodes-base.set', 554 | typeVersion: 3, 555 | position: [250, 300] as [number, number], 556 | parameters: {}, 557 | }, 558 | { 559 | id: 'node-2', 560 | name: 'Node 2', 561 | type: 'n8n-nodes-base.set', 562 | typeVersion: 3, 563 | position: [550, 300] as [number, number], 564 | parameters: {}, 565 | }, 566 | ], 567 | connections: {}, 568 | }; 569 | 570 | const errors = validateWorkflowStructure(workflow); 571 | expect(errors).toContain('Multi-node workflow has empty connections. Connect nodes like this: connections: { "Node1 Name": { "main": [[{ "node": "Node2 Name", "type": "main", "index": 0 }]] } }'); 572 | }); 573 | 574 | it('should validate node type format - missing package prefix', () => { 575 | const workflow = { 576 | name: 'Invalid Node Type', 577 | nodes: [{ 578 | id: 'node-1', 579 | name: 'Node 1', 580 | type: 'webhook', // Missing package prefix 581 | typeVersion: 2, 582 | position: [250, 300] as [number, number], 583 | parameters: {}, 584 | }], 585 | connections: {}, 586 | }; 587 | 588 | const errors = validateWorkflowStructure(workflow); 589 | expect(errors).toContain('Invalid node type "webhook" at index 0. Node types must include package prefix (e.g., "n8n-nodes-base.webhook").'); 590 | }); 591 | 592 | it('should validate node type format - wrong prefix format', () => { 593 | const workflow = { 594 | name: 'Invalid Node Type', 595 | nodes: [{ 596 | id: 'node-1', 597 | name: 'Node 1', 598 | type: 'nodes-base.webhook', // Wrong prefix 599 | typeVersion: 2, 600 | position: [250, 300] as [number, number], 601 | parameters: {}, 602 | }], 603 | connections: {}, 604 | }; 605 | 606 | const errors = validateWorkflowStructure(workflow); 607 | expect(errors).toContain('Invalid node type "nodes-base.webhook" at index 0. Use "n8n-nodes-base.webhook" instead.'); 608 | }); 609 | 610 | it('should detect invalid node structure', () => { 611 | const workflow = { 612 | name: 'Invalid Node', 613 | nodes: [{ 614 | name: 'Missing Required Fields', 615 | // Missing id, type, typeVersion, position, parameters 616 | } as any], 617 | connections: {}, 618 | }; 619 | 620 | const errors = validateWorkflowStructure(workflow); 621 | // The validation will fail because the node is missing required fields 622 | expect(errors.some(e => e.includes('Invalid node at index 0'))).toBe(true); 623 | }); 624 | 625 | it('should detect non-existent connection source by name', () => { 626 | const workflow = { 627 | name: 'Bad Connection', 628 | nodes: [{ 629 | id: 'node-1', 630 | name: 'Node 1', 631 | type: 'n8n-nodes-base.set', 632 | typeVersion: 3, 633 | position: [250, 300] as [number, number], 634 | parameters: {}, 635 | }], 636 | connections: { 637 | 'Non-existent Node': { 638 | main: [[{ node: 'Node 1', type: 'main', index: 0 }]], 639 | }, 640 | }, 641 | }; 642 | 643 | const errors = validateWorkflowStructure(workflow); 644 | expect(errors).toContain('Connection references non-existent node: Non-existent Node'); 645 | }); 646 | 647 | it('should detect non-existent connection target by name', () => { 648 | const workflow = { 649 | name: 'Bad Connection Target', 650 | nodes: [{ 651 | id: 'node-1', 652 | name: 'Node 1', 653 | type: 'n8n-nodes-base.set', 654 | typeVersion: 3, 655 | position: [250, 300] as [number, number], 656 | parameters: {}, 657 | }], 658 | connections: { 659 | 'Node 1': { 660 | main: [[{ node: 'Non-existent Node', type: 'main', index: 0 }]], 661 | }, 662 | }, 663 | }; 664 | 665 | const errors = validateWorkflowStructure(workflow); 666 | expect(errors).toContain('Connection references non-existent target node: Non-existent Node (from Node 1[0][0])'); 667 | }); 668 | 669 | it('should detect when node ID is used instead of name in connection source', () => { 670 | const workflow = { 671 | name: 'ID Instead of Name', 672 | nodes: [ 673 | { 674 | id: 'node-1', 675 | name: 'First Node', 676 | type: 'n8n-nodes-base.set', 677 | typeVersion: 3, 678 | position: [250, 300] as [number, number], 679 | parameters: {}, 680 | }, 681 | { 682 | id: 'node-2', 683 | name: 'Second Node', 684 | type: 'n8n-nodes-base.set', 685 | typeVersion: 3, 686 | position: [550, 300] as [number, number], 687 | parameters: {}, 688 | }, 689 | ], 690 | connections: { 691 | 'node-1': { // Using ID instead of name 692 | main: [[{ node: 'Second Node', type: 'main', index: 0 }]], 693 | }, 694 | }, 695 | }; 696 | 697 | const errors = validateWorkflowStructure(workflow); 698 | expect(errors).toContain("Connection uses node ID 'node-1' but must use node name 'First Node'. Change connections.node-1 to connections['First Node']"); 699 | }); 700 | 701 | it('should detect when node ID is used instead of name in connection target', () => { 702 | const workflow = { 703 | name: 'ID Instead of Name in Target', 704 | nodes: [ 705 | { 706 | id: 'node-1', 707 | name: 'First Node', 708 | type: 'n8n-nodes-base.set', 709 | typeVersion: 3, 710 | position: [250, 300] as [number, number], 711 | parameters: {}, 712 | }, 713 | { 714 | id: 'node-2', 715 | name: 'Second Node', 716 | type: 'n8n-nodes-base.set', 717 | typeVersion: 3, 718 | position: [550, 300] as [number, number], 719 | parameters: {}, 720 | }, 721 | ], 722 | connections: { 723 | 'First Node': { 724 | main: [[{ node: 'node-2', type: 'main', index: 0 }]], // Using ID instead of name 725 | }, 726 | }, 727 | }; 728 | 729 | const errors = validateWorkflowStructure(workflow); 730 | expect(errors).toContain("Connection target uses node ID 'node-2' but must use node name 'Second Node' (from First Node[0][0])"); 731 | }); 732 | 733 | it('should handle complex multi-output connections', () => { 734 | const workflow = { 735 | name: 'Complex Connections', 736 | nodes: [ 737 | { 738 | id: 'if-1', 739 | name: 'IF Node', 740 | type: 'n8n-nodes-base.if', 741 | typeVersion: 2, 742 | position: [250, 300] as [number, number], 743 | parameters: {}, 744 | }, 745 | { 746 | id: 'true-1', 747 | name: 'True Branch', 748 | type: 'n8n-nodes-base.set', 749 | typeVersion: 3, 750 | position: [450, 200] as [number, number], 751 | parameters: {}, 752 | }, 753 | { 754 | id: 'false-1', 755 | name: 'False Branch', 756 | type: 'n8n-nodes-base.set', 757 | typeVersion: 3, 758 | position: [450, 400] as [number, number], 759 | parameters: {}, 760 | }, 761 | ], 762 | connections: { 763 | 'IF Node': { 764 | main: [ 765 | [{ node: 'True Branch', type: 'main', index: 0 }], 766 | [{ node: 'False Branch', type: 'main', index: 0 }], 767 | ], 768 | }, 769 | }, 770 | }; 771 | 772 | const errors = validateWorkflowStructure(workflow); 773 | expect(errors).toEqual([]); 774 | }); 775 | 776 | it('should validate invalid connections structure', () => { 777 | const workflow = { 778 | name: 'Invalid Connections', 779 | nodes: [ 780 | { 781 | id: 'node-1', 782 | name: 'Node 1', 783 | type: 'n8n-nodes-base.set', 784 | typeVersion: 3, 785 | position: [250, 300] as [number, number], 786 | parameters: {}, 787 | }, 788 | { 789 | id: 'node-2', 790 | name: 'Node 2', 791 | type: 'n8n-nodes-base.set', 792 | typeVersion: 3, 793 | position: [550, 300] as [number, number], 794 | parameters: {}, 795 | } 796 | ], 797 | connections: { 798 | 'Node 1': 'invalid', // Should be an object 799 | } as any, 800 | }; 801 | 802 | const errors = validateWorkflowStructure(workflow); 803 | expect(errors.some(e => e.includes('Invalid connections'))).toBe(true); 804 | }); 805 | }); 806 | 807 | describe('hasWebhookTrigger', () => { 808 | it('should return true for workflow with webhook node', () => { 809 | const workflow = new WorkflowBuilder() 810 | .addWebhookNode() 811 | .build() as Workflow; 812 | 813 | expect(hasWebhookTrigger(workflow)).toBe(true); 814 | }); 815 | 816 | it('should return true for workflow with webhookTrigger node', () => { 817 | const workflow = { 818 | name: 'Test', 819 | nodes: [{ 820 | id: 'webhook-1', 821 | name: 'Webhook Trigger', 822 | type: 'n8n-nodes-base.webhookTrigger', 823 | typeVersion: 1, 824 | position: [250, 300] as [number, number], 825 | parameters: {}, 826 | }], 827 | connections: {}, 828 | } as Workflow; 829 | 830 | expect(hasWebhookTrigger(workflow)).toBe(true); 831 | }); 832 | 833 | it('should return false for workflow without webhook nodes', () => { 834 | const workflow = new WorkflowBuilder() 835 | .addSlackNode() 836 | .addHttpRequestNode() 837 | .build() as Workflow; 838 | 839 | expect(hasWebhookTrigger(workflow)).toBe(false); 840 | }); 841 | 842 | it('should return true even if webhook is not the first node', () => { 843 | const workflow = new WorkflowBuilder() 844 | .addSlackNode() 845 | .addWebhookNode() 846 | .addHttpRequestNode() 847 | .build() as Workflow; 848 | 849 | expect(hasWebhookTrigger(workflow)).toBe(true); 850 | }); 851 | }); 852 | 853 | describe('getWebhookUrl', () => { 854 | it('should return webhook path from webhook node', () => { 855 | const workflow = { 856 | name: 'Test', 857 | nodes: [{ 858 | id: 'webhook-1', 859 | name: 'Webhook', 860 | type: 'n8n-nodes-base.webhook', 861 | typeVersion: 2, 862 | position: [250, 300] as [number, number], 863 | parameters: { 864 | path: 'my-custom-webhook', 865 | }, 866 | }], 867 | connections: {}, 868 | } as Workflow; 869 | 870 | expect(getWebhookUrl(workflow)).toBe('my-custom-webhook'); 871 | }); 872 | 873 | it('should return webhook path from webhookTrigger node', () => { 874 | const workflow = { 875 | name: 'Test', 876 | nodes: [{ 877 | id: 'webhook-1', 878 | name: 'Webhook Trigger', 879 | type: 'n8n-nodes-base.webhookTrigger', 880 | typeVersion: 1, 881 | position: [250, 300] as [number, number], 882 | parameters: { 883 | path: 'trigger-webhook-path', 884 | }, 885 | }], 886 | connections: {}, 887 | } as Workflow; 888 | 889 | expect(getWebhookUrl(workflow)).toBe('trigger-webhook-path'); 890 | }); 891 | 892 | it('should return null if no webhook node exists', () => { 893 | const workflow = new WorkflowBuilder() 894 | .addSlackNode() 895 | .build() as Workflow; 896 | 897 | expect(getWebhookUrl(workflow)).toBe(null); 898 | }); 899 | 900 | it('should return null if webhook node has no parameters', () => { 901 | const workflow = { 902 | name: 'Test', 903 | nodes: [{ 904 | id: 'webhook-1', 905 | name: 'Webhook', 906 | type: 'n8n-nodes-base.webhook', 907 | typeVersion: 2, 908 | position: [250, 300] as [number, number], 909 | parameters: undefined as any, 910 | }], 911 | connections: {}, 912 | } as Workflow; 913 | 914 | expect(getWebhookUrl(workflow)).toBe(null); 915 | }); 916 | 917 | it('should return null if webhook node has no path parameter', () => { 918 | const workflow = { 919 | name: 'Test', 920 | nodes: [{ 921 | id: 'webhook-1', 922 | name: 'Webhook', 923 | type: 'n8n-nodes-base.webhook', 924 | typeVersion: 2, 925 | position: [250, 300] as [number, number], 926 | parameters: { 927 | method: 'POST', 928 | // No path parameter 929 | }, 930 | }], 931 | connections: {}, 932 | } as Workflow; 933 | 934 | expect(getWebhookUrl(workflow)).toBe(null); 935 | }); 936 | 937 | it('should return first webhook path when multiple webhooks exist', () => { 938 | const workflow = { 939 | name: 'Test', 940 | nodes: [ 941 | { 942 | id: 'webhook-1', 943 | name: 'Webhook 1', 944 | type: 'n8n-nodes-base.webhook', 945 | typeVersion: 2, 946 | position: [250, 300] as [number, number], 947 | parameters: { 948 | path: 'first-webhook', 949 | }, 950 | }, 951 | { 952 | id: 'webhook-2', 953 | name: 'Webhook 2', 954 | type: 'n8n-nodes-base.webhook', 955 | typeVersion: 2, 956 | position: [550, 300] as [number, number], 957 | parameters: { 958 | path: 'second-webhook', 959 | }, 960 | }, 961 | ], 962 | connections: {}, 963 | } as Workflow; 964 | 965 | expect(getWebhookUrl(workflow)).toBe('first-webhook'); 966 | }); 967 | }); 968 | 969 | describe('getWorkflowStructureExample', () => { 970 | it('should return a string containing example workflow structure', () => { 971 | const example = getWorkflowStructureExample(); 972 | 973 | expect(example).toContain('Minimal Workflow Example'); 974 | expect(example).toContain('Manual Trigger'); 975 | expect(example).toContain('Set Data'); 976 | expect(example).toContain('connections'); 977 | expect(example).toContain('IMPORTANT: In connections, use the node NAME'); 978 | }); 979 | 980 | it('should contain valid JSON structure in example', () => { 981 | const example = getWorkflowStructureExample(); 982 | // Extract the JSON part between the first { and last } 983 | const match = example.match(/\{[\s\S]*\}/); 984 | expect(match).toBeTruthy(); 985 | 986 | if (match) { 987 | // Should not throw when parsing 988 | expect(() => JSON.parse(match[0])).not.toThrow(); 989 | } 990 | }); 991 | }); 992 | 993 | describe('getWorkflowFixSuggestions', () => { 994 | it('should suggest fixes for empty connections', () => { 995 | const errors = ['Multi-node workflow has empty connections']; 996 | const suggestions = getWorkflowFixSuggestions(errors); 997 | 998 | expect(suggestions).toContain('Add connections between your nodes. Each node (except endpoints) should connect to another node.'); 999 | expect(suggestions).toContain('Connection format: connections: { "Source Node Name": { "main": [[{ "node": "Target Node Name", "type": "main", "index": 0 }]] } }'); 1000 | }); 1001 | 1002 | it('should suggest fixes for single-node workflows', () => { 1003 | const errors = ['Single-node workflows are only valid for webhooks']; 1004 | const suggestions = getWorkflowFixSuggestions(errors); 1005 | 1006 | expect(suggestions).toContain('Add at least one more node to process data. Common patterns: Trigger → Process → Output'); 1007 | expect(suggestions).toContain('Examples: Manual Trigger → Set, Webhook → HTTP Request, Schedule Trigger → Database Query'); 1008 | }); 1009 | 1010 | it('should suggest fixes for node ID usage instead of names', () => { 1011 | const errors = ["Connection uses node ID 'set-1' but must use node name 'Set Data' instead of node name"]; 1012 | const suggestions = getWorkflowFixSuggestions(errors); 1013 | 1014 | expect(suggestions.some(s => s.includes('Replace node IDs with node names'))).toBe(true); 1015 | expect(suggestions.some(s => s.includes('connections: { "set-1": {...} }'))).toBe(true); 1016 | }); 1017 | 1018 | it('should return empty array for no errors', () => { 1019 | const suggestions = getWorkflowFixSuggestions([]); 1020 | expect(suggestions).toEqual([]); 1021 | }); 1022 | 1023 | it('should handle multiple error types', () => { 1024 | const errors = [ 1025 | 'Multi-node workflow has empty connections', 1026 | 'Single-node workflows are only valid for webhooks', 1027 | "Connection uses node ID instead of node name", 1028 | ]; 1029 | const suggestions = getWorkflowFixSuggestions(errors); 1030 | 1031 | expect(suggestions.length).toBeGreaterThan(3); 1032 | expect(suggestions).toContain('Add connections between your nodes. Each node (except endpoints) should connect to another node.'); 1033 | expect(suggestions).toContain('Add at least one more node to process data. Common patterns: Trigger → Process → Output'); 1034 | expect(suggestions).toContain('Replace node IDs with node names in connections. The name is what appears in the node header.'); 1035 | }); 1036 | 1037 | it('should not duplicate suggestions for similar errors', () => { 1038 | const errors = [ 1039 | "Connection uses node ID 'id1' instead of node name", 1040 | "Connection uses node ID 'id2' instead of node name", 1041 | ]; 1042 | const suggestions = getWorkflowFixSuggestions(errors); 1043 | 1044 | // Should only have 2 suggestions for this error type 1045 | const idSuggestions = suggestions.filter(s => s.includes('Replace node IDs')); 1046 | expect(idSuggestions.length).toBe(1); 1047 | }); 1048 | }); 1049 | 1050 | describe('Edge Cases and Error Conditions', () => { 1051 | it('should handle workflow with null values gracefully', () => { 1052 | const workflow = { 1053 | name: 'Test', 1054 | nodes: null as any, 1055 | connections: null as any, 1056 | }; 1057 | 1058 | const errors = validateWorkflowStructure(workflow); 1059 | expect(errors).toContain('Workflow must have at least one node'); 1060 | expect(errors).toContain('Workflow connections are required'); 1061 | }); 1062 | 1063 | it('should handle undefined parameters in cleaning functions', () => { 1064 | const workflow = { 1065 | name: undefined as any, 1066 | nodes: undefined as any, 1067 | connections: undefined as any, 1068 | }; 1069 | 1070 | expect(() => cleanWorkflowForCreate(workflow)).not.toThrow(); 1071 | expect(() => cleanWorkflowForUpdate(workflow as any)).not.toThrow(); 1072 | }); 1073 | 1074 | it('should handle circular references in workflow structure', () => { 1075 | const node1: any = { 1076 | id: 'node-1', 1077 | name: 'Node 1', 1078 | type: 'n8n-nodes-base.set', 1079 | typeVersion: 3, 1080 | position: [250, 300], 1081 | parameters: {}, 1082 | }; 1083 | 1084 | // Create circular reference 1085 | node1.parameters.circular = node1; 1086 | 1087 | const workflow = { 1088 | name: 'Circular Ref', 1089 | nodes: [node1], 1090 | connections: {}, 1091 | }; 1092 | 1093 | // Should handle circular references without crashing 1094 | expect(() => validateWorkflowStructure(workflow)).not.toThrow(); 1095 | }); 1096 | 1097 | it('should validate very large position values', () => { 1098 | const node = { 1099 | id: 'node-1', 1100 | name: 'Test Node', 1101 | type: 'n8n-nodes-base.set', 1102 | typeVersion: 3, 1103 | position: [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER] as [number, number], 1104 | parameters: {}, 1105 | }; 1106 | 1107 | expect(() => validateWorkflowNode(node)).not.toThrow(); 1108 | }); 1109 | 1110 | it('should handle special characters in node names', () => { 1111 | const workflow = { 1112 | name: 'Special Chars', 1113 | nodes: [ 1114 | { 1115 | id: 'node-1', 1116 | name: 'Node with "quotes" & special <chars>', 1117 | type: 'n8n-nodes-base.set', 1118 | typeVersion: 3, 1119 | position: [250, 300] as [number, number], 1120 | parameters: {}, 1121 | }, 1122 | { 1123 | id: 'node-2', 1124 | name: 'Normal Node', 1125 | type: 'n8n-nodes-base.set', 1126 | typeVersion: 3, 1127 | position: [550, 300] as [number, number], 1128 | parameters: {}, 1129 | }, 1130 | ], 1131 | connections: { 1132 | 'Node with "quotes" & special <chars>': { 1133 | main: [[{ node: 'Normal Node', type: 'main', index: 0 }]], 1134 | }, 1135 | }, 1136 | }; 1137 | 1138 | const errors = validateWorkflowStructure(workflow); 1139 | expect(errors).toEqual([]); 1140 | }); 1141 | 1142 | it('should handle empty string values', () => { 1143 | const workflow = { 1144 | name: '', 1145 | nodes: [{ 1146 | id: '', 1147 | name: '', 1148 | type: '', 1149 | typeVersion: 1, 1150 | position: [0, 0] as [number, number], 1151 | parameters: {}, 1152 | }], 1153 | connections: {}, 1154 | }; 1155 | 1156 | const errors = validateWorkflowStructure(workflow); 1157 | expect(errors).toContain('Workflow name is required'); 1158 | // Empty string for type will be caught as invalid 1159 | expect(errors.some(e => e.includes('Invalid node at index 0') || e.includes('Node types must include package prefix'))).toBe(true); 1160 | }); 1161 | 1162 | it('should handle negative position values', () => { 1163 | const node = { 1164 | id: 'node-1', 1165 | name: 'Test Node', 1166 | type: 'n8n-nodes-base.set', 1167 | typeVersion: 3, 1168 | position: [-100, -200] as [number, number], 1169 | parameters: {}, 1170 | }; 1171 | 1172 | // Negative positions are valid 1173 | expect(() => validateWorkflowNode(node)).not.toThrow(); 1174 | }); 1175 | 1176 | it('should validate settings with additional unknown properties', () => { 1177 | const settings = { 1178 | executionOrder: 'v1' as const, 1179 | timezone: 'UTC', 1180 | unknownProperty: 'should be allowed', 1181 | anotherUnknown: { nested: 'object' }, 1182 | }; 1183 | 1184 | // Zod by default strips unknown properties 1185 | const result = validateWorkflowSettings(settings); 1186 | expect(result).toHaveProperty('executionOrder', 'v1'); 1187 | expect(result).toHaveProperty('timezone', 'UTC'); 1188 | expect(result).not.toHaveProperty('unknownProperty'); 1189 | expect(result).not.toHaveProperty('anotherUnknown'); 1190 | }); 1191 | }); 1192 | 1193 | describe('Integration Tests', () => { 1194 | it('should validate a complete real-world workflow', () => { 1195 | const workflow = new WorkflowBuilder('Production Workflow') 1196 | .addWebhookNode({ 1197 | id: 'webhook-1', 1198 | name: 'Order Webhook', 1199 | parameters: { 1200 | path: 'new-order', 1201 | method: 'POST', 1202 | }, 1203 | }) 1204 | .addIfNode({ 1205 | id: 'if-1', 1206 | name: 'Check Order Value', 1207 | parameters: { 1208 | conditions: { 1209 | options: { caseSensitive: true, leftValue: '', typeValidation: 'strict' }, 1210 | conditions: [{ 1211 | id: '1', 1212 | leftValue: '={{ $json.orderValue }}', 1213 | rightValue: '100', 1214 | operator: { type: 'number', operation: 'gte' }, 1215 | }], 1216 | combinator: 'and', 1217 | }, 1218 | }, 1219 | }) 1220 | .addSlackNode({ 1221 | id: 'slack-1', 1222 | name: 'Notify High Value', 1223 | parameters: { 1224 | channel: '#high-value-orders', 1225 | text: 'High value order received: ${{ $json.orderId }}', 1226 | }, 1227 | }) 1228 | .addHttpRequestNode({ 1229 | id: 'http-1', 1230 | name: 'Update Inventory', 1231 | parameters: { 1232 | method: 'POST', 1233 | url: 'https://api.inventory.com/update', 1234 | sendBody: true, 1235 | bodyParametersJson: '={{ $json }}', 1236 | }, 1237 | }) 1238 | .connect('Order Webhook', 'Check Order Value') 1239 | .connect('Check Order Value', 'Notify High Value', 0) // True output 1240 | .connect('Check Order Value', 'Update Inventory', 1) // False output 1241 | .setSettings({ 1242 | executionOrder: 'v1', 1243 | timezone: 'America/New_York', 1244 | saveDataErrorExecution: 'all', 1245 | saveDataSuccessExecution: 'none', 1246 | executionTimeout: 300, 1247 | }) 1248 | .build(); 1249 | 1250 | const errors = validateWorkflowStructure(workflow as any); 1251 | expect(errors).toEqual([]); 1252 | 1253 | // Validate individual components 1254 | workflow.nodes.forEach(node => { 1255 | expect(() => validateWorkflowNode(node)).not.toThrow(); 1256 | }); 1257 | expect(() => validateWorkflowConnections(workflow.connections)).not.toThrow(); 1258 | expect(() => validateWorkflowSettings(workflow.settings!)).not.toThrow(); 1259 | }); 1260 | 1261 | it('should clean and validate workflow for API operations', () => { 1262 | const originalWorkflow = { 1263 | id: 'wf-123', 1264 | name: 'API Test Workflow', 1265 | nodes: [ 1266 | { 1267 | id: 'manual-1', 1268 | name: 'Manual Trigger', 1269 | type: 'n8n-nodes-base.manualTrigger', 1270 | typeVersion: 1, 1271 | position: [250, 300] as [number, number], 1272 | parameters: {}, 1273 | }, 1274 | { 1275 | id: 'set-1', 1276 | name: 'Set Data', 1277 | type: 'n8n-nodes-base.set', 1278 | typeVersion: 3.4, 1279 | position: [450, 300] as [number, number], 1280 | parameters: { 1281 | mode: 'manual', 1282 | assignments: { 1283 | assignments: [{ 1284 | id: '1', 1285 | name: 'testKey', 1286 | value: 'testValue', 1287 | type: 'string', 1288 | }], 1289 | }, 1290 | }, 1291 | } 1292 | ], 1293 | connections: { 1294 | 'Manual Trigger': { 1295 | main: [[{ 1296 | node: 'Set Data', 1297 | type: 'main', 1298 | index: 0, 1299 | }]], 1300 | }, 1301 | }, 1302 | createdAt: '2023-01-01T00:00:00Z', 1303 | updatedAt: '2023-01-02T00:00:00Z', 1304 | versionId: 'v123', 1305 | active: true, 1306 | tags: ['test', 'api'], 1307 | meta: { instanceId: 'instance-123' }, 1308 | }; 1309 | 1310 | // Test create cleaning 1311 | const forCreate = cleanWorkflowForCreate(originalWorkflow); 1312 | expect(forCreate).not.toHaveProperty('id'); 1313 | expect(forCreate).not.toHaveProperty('createdAt'); 1314 | expect(forCreate).not.toHaveProperty('updatedAt'); 1315 | expect(forCreate).not.toHaveProperty('versionId'); 1316 | expect(forCreate).not.toHaveProperty('active'); 1317 | expect(forCreate).not.toHaveProperty('tags'); 1318 | expect(forCreate).not.toHaveProperty('meta'); 1319 | expect(forCreate).toHaveProperty('settings'); 1320 | expect(validateWorkflowStructure(forCreate)).toEqual([]); 1321 | 1322 | // Test update cleaning 1323 | const forUpdate = cleanWorkflowForUpdate(originalWorkflow as any); 1324 | expect(forUpdate).not.toHaveProperty('id'); 1325 | expect(forUpdate).not.toHaveProperty('createdAt'); 1326 | expect(forUpdate).not.toHaveProperty('updatedAt'); 1327 | expect(forUpdate).not.toHaveProperty('versionId'); 1328 | expect(forUpdate).not.toHaveProperty('active'); 1329 | expect(forUpdate).not.toHaveProperty('tags'); 1330 | expect(forUpdate).not.toHaveProperty('meta'); 1331 | expect(forUpdate.settings).toEqual({}); // Settings replaced with empty object for API compatibility 1332 | expect(validateWorkflowStructure(forUpdate)).toEqual([]); 1333 | }); 1334 | }); 1335 | }); ``` -------------------------------------------------------------------------------- /src/services/task-templates.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Task Templates Service 3 | * 4 | * @deprecated This module is deprecated as of v2.15.0 and will be removed in v2.16.0. 5 | * The get_node_for_task tool has been removed in favor of template-based configuration examples. 6 | * 7 | * Migration: 8 | * - Use `search_nodes({query: "webhook", includeExamples: true})` to find nodes with real template configs 9 | * - Use `get_node_essentials({nodeType: "nodes-base.webhook", includeExamples: true})` for top 3 examples 10 | * - New approach provides 2,646 real templates vs 31 hardcoded tasks 11 | * 12 | * Provides pre-configured node settings for common tasks. 13 | * This helps AI agents quickly configure nodes for specific use cases. 14 | */ 15 | 16 | export interface TaskTemplate { 17 | task: string; 18 | description: string; 19 | nodeType: string; 20 | configuration: Record<string, any>; 21 | userMustProvide: Array<{ 22 | property: string; 23 | description: string; 24 | example?: any; 25 | }>; 26 | optionalEnhancements?: Array<{ 27 | property: string; 28 | description: string; 29 | when?: string; 30 | }>; 31 | notes?: string[]; 32 | } 33 | 34 | export class TaskTemplates { 35 | private static templates: Record<string, TaskTemplate> = { 36 | // HTTP Request Tasks 37 | 'get_api_data': { 38 | task: 'get_api_data', 39 | description: 'Make a simple GET request to retrieve data from an API', 40 | nodeType: 'nodes-base.httpRequest', 41 | configuration: { 42 | method: 'GET', 43 | url: '', 44 | authentication: 'none', 45 | // Default error handling for API calls 46 | onError: 'continueRegularOutput', 47 | retryOnFail: true, 48 | maxTries: 3, 49 | waitBetweenTries: 1000, 50 | alwaysOutputData: true 51 | }, 52 | userMustProvide: [ 53 | { 54 | property: 'url', 55 | description: 'The API endpoint URL', 56 | example: 'https://api.example.com/users' 57 | } 58 | ], 59 | optionalEnhancements: [ 60 | { 61 | property: 'authentication', 62 | description: 'Add authentication if the API requires it', 63 | when: 'API requires authentication' 64 | }, 65 | { 66 | property: 'sendHeaders', 67 | description: 'Add custom headers if needed', 68 | when: 'API requires specific headers' 69 | }, 70 | { 71 | property: 'alwaysOutputData', 72 | description: 'Set to true to capture error responses', 73 | when: 'Need to debug API errors' 74 | } 75 | ] 76 | }, 77 | 78 | 'post_json_request': { 79 | task: 'post_json_request', 80 | description: 'Send JSON data to an API endpoint', 81 | nodeType: 'nodes-base.httpRequest', 82 | configuration: { 83 | method: 'POST', 84 | url: '', 85 | sendBody: true, 86 | contentType: 'json', 87 | specifyBody: 'json', 88 | jsonBody: '', 89 | // POST requests might modify data, so be careful with retries 90 | onError: 'continueRegularOutput', 91 | retryOnFail: true, 92 | maxTries: 2, 93 | waitBetweenTries: 1000, 94 | alwaysOutputData: true 95 | }, 96 | userMustProvide: [ 97 | { 98 | property: 'url', 99 | description: 'The API endpoint URL', 100 | example: 'https://api.example.com/users' 101 | }, 102 | { 103 | property: 'jsonBody', 104 | description: 'The JSON data to send', 105 | example: '{\n "name": "John Doe",\n "email": "[email protected]"\n}' 106 | } 107 | ], 108 | optionalEnhancements: [ 109 | { 110 | property: 'authentication', 111 | description: 'Add authentication if required' 112 | }, 113 | { 114 | property: 'onError', 115 | description: 'Set to "continueRegularOutput" for non-critical operations', 116 | when: 'Failure should not stop the workflow' 117 | } 118 | ], 119 | notes: [ 120 | 'Make sure jsonBody contains valid JSON', 121 | 'Content-Type header is automatically set to application/json', 122 | 'Be careful with retries on non-idempotent operations' 123 | ] 124 | }, 125 | 126 | 'call_api_with_auth': { 127 | task: 'call_api_with_auth', 128 | description: 'Make an authenticated API request', 129 | nodeType: 'nodes-base.httpRequest', 130 | configuration: { 131 | method: 'GET', 132 | url: '', 133 | authentication: 'genericCredentialType', 134 | genericAuthType: 'headerAuth', 135 | sendHeaders: true, 136 | // Authentication calls should handle auth failures gracefully 137 | onError: 'continueErrorOutput', 138 | retryOnFail: true, 139 | maxTries: 3, 140 | waitBetweenTries: 2000, 141 | alwaysOutputData: true, 142 | headerParameters: { 143 | parameters: [ 144 | { 145 | name: '', 146 | value: '' 147 | } 148 | ] 149 | } 150 | }, 151 | userMustProvide: [ 152 | { 153 | property: 'url', 154 | description: 'The API endpoint URL' 155 | }, 156 | { 157 | property: 'headerParameters.parameters[0].name', 158 | description: 'The header name for authentication', 159 | example: 'Authorization' 160 | }, 161 | { 162 | property: 'headerParameters.parameters[0].value', 163 | description: 'The authentication value', 164 | example: 'Bearer YOUR_API_KEY' 165 | } 166 | ], 167 | optionalEnhancements: [ 168 | { 169 | property: 'method', 170 | description: 'Change to POST/PUT/DELETE as needed' 171 | } 172 | ] 173 | }, 174 | 175 | // Webhook Tasks 176 | 'receive_webhook': { 177 | task: 'receive_webhook', 178 | description: 'Set up a webhook to receive data from external services', 179 | nodeType: 'nodes-base.webhook', 180 | configuration: { 181 | httpMethod: 'POST', 182 | path: 'webhook', 183 | responseMode: 'lastNode', 184 | responseData: 'allEntries', 185 | // Webhooks should always respond, even on error 186 | onError: 'continueRegularOutput', 187 | alwaysOutputData: true 188 | }, 189 | userMustProvide: [ 190 | { 191 | property: 'path', 192 | description: 'The webhook path (will be appended to your n8n URL)', 193 | example: 'github-webhook' 194 | } 195 | ], 196 | optionalEnhancements: [ 197 | { 198 | property: 'httpMethod', 199 | description: 'Change if the service sends GET/PUT/etc' 200 | }, 201 | { 202 | property: 'responseCode', 203 | description: 'Set custom response code (default 200)' 204 | } 205 | ], 206 | notes: [ 207 | 'The full webhook URL will be: https://your-n8n.com/webhook/[path]', 208 | 'Test URL will be different from production URL' 209 | ] 210 | }, 211 | 212 | 'webhook_with_response': { 213 | task: 'webhook_with_response', 214 | description: 'Receive webhook and send custom response', 215 | nodeType: 'nodes-base.webhook', 216 | configuration: { 217 | httpMethod: 'POST', 218 | path: 'webhook', 219 | responseMode: 'responseNode', 220 | responseData: 'firstEntryJson', 221 | responseCode: 200, 222 | // Ensure webhook always sends response 223 | onError: 'continueRegularOutput', 224 | alwaysOutputData: true 225 | }, 226 | userMustProvide: [ 227 | { 228 | property: 'path', 229 | description: 'The webhook path' 230 | } 231 | ], 232 | notes: [ 233 | 'Use with a Respond to Webhook node to send custom response', 234 | 'responseMode: responseNode requires a Respond to Webhook node' 235 | ] 236 | }, 237 | 238 | 'process_webhook_data': { 239 | task: 'process_webhook_data', 240 | description: 'Process incoming webhook data with Code node (shows correct data access)', 241 | nodeType: 'nodes-base.code', 242 | configuration: { 243 | language: 'javaScript', 244 | jsCode: `// ⚠️ CRITICAL: Webhook data is nested under 'body' property! 245 | // Connect this Code node after a Webhook node 246 | 247 | // Access webhook payload data - it's under .body, not directly under .json 248 | const webhookData = items[0].json.body; // ✅ CORRECT 249 | const headers = items[0].json.headers; // HTTP headers 250 | const query = items[0].json.query; // Query parameters 251 | 252 | // Common mistake to avoid: 253 | // const command = items[0].json.testCommand; // ❌ WRONG - will be undefined! 254 | // const command = items[0].json.body.testCommand; // ✅ CORRECT 255 | 256 | // Process the webhook data 257 | try { 258 | // Validate required fields 259 | if (!webhookData.command) { 260 | throw new Error('Missing required field: command'); 261 | } 262 | 263 | // Process based on command 264 | let result = {}; 265 | switch (webhookData.command) { 266 | case 'process': 267 | result = { 268 | status: 'processed', 269 | data: webhookData.data, 270 | processedAt: DateTime.now().toISO() 271 | }; 272 | break; 273 | 274 | case 'validate': 275 | result = { 276 | status: 'validated', 277 | isValid: true, 278 | validatedFields: Object.keys(webhookData.data || {}) 279 | }; 280 | break; 281 | 282 | default: 283 | result = { 284 | status: 'unknown_command', 285 | command: webhookData.command 286 | }; 287 | } 288 | 289 | // Return processed data 290 | return [{ 291 | json: { 292 | ...result, 293 | requestId: headers['x-request-id'] || crypto.randomUUID(), 294 | source: query.source || 'webhook', 295 | originalCommand: webhookData.command, 296 | metadata: { 297 | httpMethod: items[0].json.httpMethod, 298 | webhookPath: items[0].json.webhookPath, 299 | timestamp: DateTime.now().toISO() 300 | } 301 | } 302 | }]; 303 | 304 | } catch (error) { 305 | // Return error response 306 | return [{ 307 | json: { 308 | status: 'error', 309 | error: error.message, 310 | timestamp: DateTime.now().toISO() 311 | } 312 | }]; 313 | }`, 314 | onError: 'continueRegularOutput' 315 | }, 316 | userMustProvide: [], 317 | notes: [ 318 | '⚠️ WEBHOOK DATA IS AT items[0].json.body, NOT items[0].json', 319 | 'This is the most common webhook processing mistake', 320 | 'Headers are at items[0].json.headers', 321 | 'Query parameters are at items[0].json.query', 322 | 'Connect this Code node directly after a Webhook node' 323 | ] 324 | }, 325 | 326 | // Database Tasks 327 | 'query_postgres': { 328 | task: 'query_postgres', 329 | description: 'Query data from PostgreSQL database', 330 | nodeType: 'nodes-base.postgres', 331 | configuration: { 332 | operation: 'executeQuery', 333 | query: '', 334 | // Database reads can continue on error 335 | onError: 'continueRegularOutput', 336 | retryOnFail: true, 337 | maxTries: 3, 338 | waitBetweenTries: 1000 339 | }, 340 | userMustProvide: [ 341 | { 342 | property: 'query', 343 | description: 'The SQL query to execute', 344 | example: 'SELECT * FROM users WHERE active = true LIMIT 10' 345 | } 346 | ], 347 | optionalEnhancements: [ 348 | { 349 | property: 'additionalFields.queryParams', 350 | description: 'Use parameterized queries for security', 351 | when: 'Using dynamic values' 352 | } 353 | ], 354 | notes: [ 355 | 'Always use parameterized queries to prevent SQL injection', 356 | 'Configure PostgreSQL credentials in n8n' 357 | ] 358 | }, 359 | 360 | 'insert_postgres_data': { 361 | task: 'insert_postgres_data', 362 | description: 'Insert data into PostgreSQL table', 363 | nodeType: 'nodes-base.postgres', 364 | configuration: { 365 | operation: 'insert', 366 | table: '', 367 | columns: '', 368 | returnFields: '*', 369 | // Database writes should stop on error by default 370 | onError: 'stopWorkflow', 371 | retryOnFail: true, 372 | maxTries: 2, 373 | waitBetweenTries: 1000 374 | }, 375 | userMustProvide: [ 376 | { 377 | property: 'table', 378 | description: 'The table name', 379 | example: 'users' 380 | }, 381 | { 382 | property: 'columns', 383 | description: 'Comma-separated column names', 384 | example: 'name,email,created_at' 385 | } 386 | ], 387 | notes: [ 388 | 'Input data should match the column structure', 389 | 'Use expressions like {{ $json.fieldName }} to map data' 390 | ] 391 | }, 392 | 393 | // AI/LangChain Tasks 394 | 'chat_with_ai': { 395 | task: 'chat_with_ai', 396 | description: 'Send a message to an AI model and get response', 397 | nodeType: 'nodes-base.openAi', 398 | configuration: { 399 | resource: 'chat', 400 | operation: 'message', 401 | modelId: 'gpt-3.5-turbo', 402 | messages: { 403 | values: [ 404 | { 405 | role: 'user', 406 | content: '' 407 | } 408 | ] 409 | }, 410 | // AI calls should handle rate limits and API errors 411 | onError: 'continueRegularOutput', 412 | retryOnFail: true, 413 | maxTries: 3, 414 | waitBetweenTries: 5000, 415 | alwaysOutputData: true 416 | }, 417 | userMustProvide: [ 418 | { 419 | property: 'messages.values[0].content', 420 | description: 'The message to send to the AI', 421 | example: '{{ $json.userMessage }}' 422 | } 423 | ], 424 | optionalEnhancements: [ 425 | { 426 | property: 'modelId', 427 | description: 'Change to gpt-4 for better results' 428 | }, 429 | { 430 | property: 'options.temperature', 431 | description: 'Adjust creativity (0-1)' 432 | }, 433 | { 434 | property: 'options.maxTokens', 435 | description: 'Limit response length' 436 | } 437 | ] 438 | }, 439 | 440 | 'ai_agent_workflow': { 441 | task: 'ai_agent_workflow', 442 | description: 'Create an AI agent that can use tools', 443 | nodeType: 'nodes-langchain.agent', 444 | configuration: { 445 | text: '', 446 | outputType: 'output', 447 | systemMessage: 'You are a helpful assistant.' 448 | }, 449 | userMustProvide: [ 450 | { 451 | property: 'text', 452 | description: 'The input prompt for the agent', 453 | example: '{{ $json.query }}' 454 | } 455 | ], 456 | optionalEnhancements: [ 457 | { 458 | property: 'systemMessage', 459 | description: 'Customize the agent\'s behavior' 460 | } 461 | ], 462 | notes: [ 463 | 'Connect tool nodes to give the agent capabilities', 464 | 'Configure the AI model credentials' 465 | ] 466 | }, 467 | 468 | // Data Processing Tasks 469 | 'transform_data': { 470 | task: 'transform_data', 471 | description: 'Transform data structure using JavaScript', 472 | nodeType: 'nodes-base.code', 473 | configuration: { 474 | language: 'javaScript', 475 | jsCode: `// Transform each item 476 | const results = []; 477 | 478 | for (const item of items) { 479 | results.push({ 480 | json: { 481 | // Transform your data here 482 | id: item.json.id, 483 | processedAt: new Date().toISOString() 484 | } 485 | }); 486 | } 487 | 488 | return results;` 489 | }, 490 | userMustProvide: [], 491 | notes: [ 492 | 'Access input data via items array', 493 | 'Each item has a json property with the data', 494 | 'Return array of objects with json property' 495 | ] 496 | }, 497 | 498 | 'filter_data': { 499 | task: 'filter_data', 500 | description: 'Filter items based on conditions', 501 | nodeType: 'nodes-base.if', 502 | configuration: { 503 | conditions: { 504 | conditions: [ 505 | { 506 | leftValue: '', 507 | rightValue: '', 508 | operator: { 509 | type: 'string', 510 | operation: 'equals' 511 | } 512 | } 513 | ] 514 | } 515 | }, 516 | userMustProvide: [ 517 | { 518 | property: 'conditions.conditions[0].leftValue', 519 | description: 'The value to check', 520 | example: '{{ $json.status }}' 521 | }, 522 | { 523 | property: 'conditions.conditions[0].rightValue', 524 | description: 'The value to compare against', 525 | example: 'active' 526 | } 527 | ], 528 | notes: [ 529 | 'True output contains matching items', 530 | 'False output contains non-matching items' 531 | ] 532 | }, 533 | 534 | // Communication Tasks 535 | 'send_slack_message': { 536 | task: 'send_slack_message', 537 | description: 'Send a message to Slack channel', 538 | nodeType: 'nodes-base.slack', 539 | configuration: { 540 | resource: 'message', 541 | operation: 'post', 542 | channel: '', 543 | text: '', 544 | // Messaging can continue on error 545 | onError: 'continueRegularOutput', 546 | retryOnFail: true, 547 | maxTries: 2, 548 | waitBetweenTries: 2000 549 | }, 550 | userMustProvide: [ 551 | { 552 | property: 'channel', 553 | description: 'The Slack channel', 554 | example: '#general' 555 | }, 556 | { 557 | property: 'text', 558 | description: 'The message text', 559 | example: 'New order received: {{ $json.orderId }}' 560 | } 561 | ], 562 | optionalEnhancements: [ 563 | { 564 | property: 'attachments', 565 | description: 'Add rich message attachments' 566 | }, 567 | { 568 | property: 'blocks', 569 | description: 'Use Block Kit for advanced formatting' 570 | } 571 | ] 572 | }, 573 | 574 | 'send_email': { 575 | task: 'send_email', 576 | description: 'Send an email notification', 577 | nodeType: 'nodes-base.emailSend', 578 | configuration: { 579 | fromEmail: '', 580 | toEmail: '', 581 | subject: '', 582 | text: '', 583 | // Email sending should retry on transient failures 584 | onError: 'continueRegularOutput', 585 | retryOnFail: true, 586 | maxTries: 3, 587 | waitBetweenTries: 3000, 588 | alwaysOutputData: true 589 | }, 590 | userMustProvide: [ 591 | { 592 | property: 'fromEmail', 593 | description: 'Sender email address', 594 | example: '[email protected]' 595 | }, 596 | { 597 | property: 'toEmail', 598 | description: 'Recipient email address', 599 | example: '{{ $json.customerEmail }}' 600 | }, 601 | { 602 | property: 'subject', 603 | description: 'Email subject', 604 | example: 'Order Confirmation #{{ $json.orderId }}' 605 | }, 606 | { 607 | property: 'text', 608 | description: 'Email body (plain text)', 609 | example: 'Thank you for your order!' 610 | } 611 | ], 612 | optionalEnhancements: [ 613 | { 614 | property: 'html', 615 | description: 'Use HTML for rich formatting' 616 | }, 617 | { 618 | property: 'attachments', 619 | description: 'Attach files to the email' 620 | } 621 | ] 622 | }, 623 | 624 | // AI Tool Usage Tasks 625 | 'use_google_sheets_as_tool': { 626 | task: 'use_google_sheets_as_tool', 627 | description: 'Use Google Sheets as an AI tool for reading/writing data', 628 | nodeType: 'nodes-base.googleSheets', 629 | configuration: { 630 | operation: 'append', 631 | sheetId: '={{ $fromAI("sheetId", "The Google Sheets ID") }}', 632 | range: '={{ $fromAI("range", "The range to append to, e.g. A:Z") }}', 633 | dataMode: 'autoMap' 634 | }, 635 | userMustProvide: [ 636 | { 637 | property: 'Google Sheets credentials', 638 | description: 'Configure Google Sheets API credentials in n8n' 639 | }, 640 | { 641 | property: 'Tool name in AI Agent', 642 | description: 'Give it a descriptive name like "Log Results to Sheet"' 643 | }, 644 | { 645 | property: 'Tool description', 646 | description: 'Describe when and how the AI should use this tool' 647 | } 648 | ], 649 | notes: [ 650 | 'Connect this node to the ai_tool port of an AI Agent node', 651 | 'The AI can dynamically determine sheetId and range using $fromAI', 652 | 'Works great for logging AI analysis results or reading data for processing' 653 | ] 654 | }, 655 | 656 | 'use_slack_as_tool': { 657 | task: 'use_slack_as_tool', 658 | description: 'Use Slack as an AI tool for sending notifications', 659 | nodeType: 'nodes-base.slack', 660 | configuration: { 661 | resource: 'message', 662 | operation: 'post', 663 | channel: '={{ $fromAI("channel", "The Slack channel, e.g. #general") }}', 664 | text: '={{ $fromAI("message", "The message to send") }}', 665 | attachments: [] 666 | }, 667 | userMustProvide: [ 668 | { 669 | property: 'Slack credentials', 670 | description: 'Configure Slack OAuth2 credentials in n8n' 671 | }, 672 | { 673 | property: 'Tool configuration in AI Agent', 674 | description: 'Name it something like "Send Slack Notification"' 675 | } 676 | ], 677 | notes: [ 678 | 'Perfect for AI agents that need to notify teams', 679 | 'The AI determines channel and message content dynamically', 680 | 'Can be enhanced with blocks for rich formatting' 681 | ] 682 | }, 683 | 684 | 'multi_tool_ai_agent': { 685 | task: 'multi_tool_ai_agent', 686 | description: 'AI agent with multiple tools for complex automation', 687 | nodeType: 'nodes-langchain.agent', 688 | configuration: { 689 | text: '={{ $json.query }}', 690 | outputType: 'output', 691 | systemMessage: 'You are an intelligent assistant with access to multiple tools. Use them wisely to complete tasks.' 692 | }, 693 | userMustProvide: [ 694 | { 695 | property: 'AI model credentials', 696 | description: 'OpenAI, Anthropic, or other LLM credentials' 697 | }, 698 | { 699 | property: 'Multiple tool nodes', 700 | description: 'Connect various nodes to the ai_tool port' 701 | }, 702 | { 703 | property: 'Tool descriptions', 704 | description: 'Clear descriptions for each connected tool' 705 | } 706 | ], 707 | optionalEnhancements: [ 708 | { 709 | property: 'Memory', 710 | description: 'Add memory nodes for conversation context' 711 | }, 712 | { 713 | property: 'Custom tools', 714 | description: 'Create Code nodes as custom tools' 715 | } 716 | ], 717 | notes: [ 718 | 'Connect multiple nodes: HTTP Request, Slack, Google Sheets, etc.', 719 | 'Each tool should have a clear, specific purpose', 720 | 'Test each tool individually before combining', 721 | 'Set N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true for community nodes' 722 | ] 723 | }, 724 | 725 | // Error Handling Templates 726 | 'api_call_with_retry': { 727 | task: 'api_call_with_retry', 728 | description: 'Resilient API call with automatic retry on failure', 729 | nodeType: 'nodes-base.httpRequest', 730 | configuration: { 731 | method: 'GET', 732 | url: '', 733 | // Retry configuration for transient failures 734 | retryOnFail: true, 735 | maxTries: 5, 736 | waitBetweenTries: 2000, 737 | // Always capture response for debugging 738 | alwaysOutputData: true, 739 | // Add request tracking 740 | sendHeaders: true, 741 | headerParameters: { 742 | parameters: [ 743 | { 744 | name: 'X-Request-ID', 745 | value: '={{ $workflow.id }}-{{ $itemIndex }}' 746 | } 747 | ] 748 | } 749 | }, 750 | userMustProvide: [ 751 | { 752 | property: 'url', 753 | description: 'The API endpoint to call', 754 | example: 'https://api.example.com/resource/{{ $json.id }}' 755 | } 756 | ], 757 | optionalEnhancements: [ 758 | { 759 | property: 'authentication', 760 | description: 'Add API authentication' 761 | }, 762 | { 763 | property: 'onError', 764 | description: 'Change to "stopWorkflow" for critical API calls', 765 | when: 'This is a critical API call that must succeed' 766 | } 767 | ], 768 | notes: [ 769 | 'Retries help with rate limits and transient network issues', 770 | 'waitBetweenTries prevents hammering the API', 771 | 'alwaysOutputData captures error responses for debugging', 772 | 'Consider exponential backoff for production use' 773 | ] 774 | }, 775 | 776 | 'fault_tolerant_processing': { 777 | task: 'fault_tolerant_processing', 778 | description: 'Data processing that continues despite individual item failures', 779 | nodeType: 'nodes-base.code', 780 | configuration: { 781 | language: 'javaScript', 782 | jsCode: `// Process items with error handling 783 | const results = []; 784 | 785 | for (const item of items) { 786 | try { 787 | // Your processing logic here 788 | const processed = { 789 | ...item.json, 790 | processed: true, 791 | timestamp: new Date().toISOString() 792 | }; 793 | 794 | results.push({ json: processed }); 795 | } catch (error) { 796 | // Log error but continue processing 797 | console.error('Processing failed for item:', item.json.id, error); 798 | 799 | // Add error item to results 800 | results.push({ 801 | json: { 802 | ...item.json, 803 | error: error.message, 804 | processed: false 805 | } 806 | }); 807 | } 808 | } 809 | 810 | return results;`, 811 | // Continue workflow even if code fails entirely 812 | onError: 'continueRegularOutput', 813 | alwaysOutputData: true 814 | }, 815 | userMustProvide: [ 816 | { 817 | property: 'Processing logic', 818 | description: 'Replace the comment with your data transformation logic' 819 | } 820 | ], 821 | optionalEnhancements: [ 822 | { 823 | property: 'Error notification', 824 | description: 'Add IF node after to handle error items separately' 825 | } 826 | ], 827 | notes: [ 828 | 'Individual item failures won\'t stop processing of other items', 829 | 'Error items are marked and can be handled separately', 830 | 'continueOnFail ensures workflow continues even on total failure' 831 | ] 832 | }, 833 | 834 | 'webhook_with_error_handling': { 835 | task: 'webhook_with_error_handling', 836 | description: 'Webhook that gracefully handles processing errors', 837 | nodeType: 'nodes-base.webhook', 838 | configuration: { 839 | httpMethod: 'POST', 840 | path: 'resilient-webhook', 841 | responseMode: 'responseNode', 842 | responseData: 'firstEntryJson', 843 | // Always continue to ensure response is sent 844 | onError: 'continueRegularOutput', 845 | alwaysOutputData: true 846 | }, 847 | userMustProvide: [ 848 | { 849 | property: 'path', 850 | description: 'Unique webhook path', 851 | example: 'order-processor' 852 | }, 853 | { 854 | property: 'Respond to Webhook node', 855 | description: 'Add node to send appropriate success/error responses' 856 | } 857 | ], 858 | optionalEnhancements: [ 859 | { 860 | property: 'Validation', 861 | description: 'Add IF node to validate webhook payload' 862 | }, 863 | { 864 | property: 'Error logging', 865 | description: 'Add error handler node for failed requests' 866 | } 867 | ], 868 | notes: [ 869 | 'onError: continueRegularOutput ensures webhook always sends a response', 870 | 'Use Respond to Webhook node to send appropriate status codes', 871 | 'Log errors but don\'t expose internal errors to webhook callers', 872 | 'Consider rate limiting for public webhooks' 873 | ] 874 | }, 875 | 876 | // Modern Error Handling Patterns 877 | 'modern_error_handling_patterns': { 878 | task: 'modern_error_handling_patterns', 879 | description: 'Examples of modern error handling using onError property', 880 | nodeType: 'nodes-base.httpRequest', 881 | configuration: { 882 | method: 'GET', 883 | url: '', 884 | // Modern error handling approach 885 | onError: 'continueRegularOutput', // Options: continueRegularOutput, continueErrorOutput, stopWorkflow 886 | retryOnFail: true, 887 | maxTries: 3, 888 | waitBetweenTries: 2000, 889 | alwaysOutputData: true 890 | }, 891 | userMustProvide: [ 892 | { 893 | property: 'url', 894 | description: 'The API endpoint' 895 | }, 896 | { 897 | property: 'onError', 898 | description: 'Choose error handling strategy', 899 | example: 'continueRegularOutput' 900 | } 901 | ], 902 | notes: [ 903 | 'onError replaces the deprecated continueOnFail property', 904 | 'continueRegularOutput: Continue with normal output on error', 905 | 'continueErrorOutput: Route errors to error output for special handling', 906 | 'stopWorkflow: Stop the entire workflow on error', 907 | 'Combine with retryOnFail for resilient workflows' 908 | ] 909 | }, 910 | 911 | 'database_transaction_safety': { 912 | task: 'database_transaction_safety', 913 | description: 'Database operations with proper error handling', 914 | nodeType: 'nodes-base.postgres', 915 | configuration: { 916 | operation: 'executeQuery', 917 | query: 'BEGIN; INSERT INTO orders ...; COMMIT;', 918 | // For transactions, don\'t retry automatically 919 | onError: 'continueErrorOutput', 920 | retryOnFail: false, 921 | alwaysOutputData: true 922 | }, 923 | userMustProvide: [ 924 | { 925 | property: 'query', 926 | description: 'Your SQL query or transaction' 927 | } 928 | ], 929 | notes: [ 930 | 'Transactions should not be retried automatically', 931 | 'Use continueErrorOutput to handle errors separately', 932 | 'Consider implementing compensating transactions', 933 | 'Always log transaction failures for audit' 934 | ] 935 | }, 936 | 937 | 'ai_rate_limit_handling': { 938 | task: 'ai_rate_limit_handling', 939 | description: 'AI API calls with rate limit handling', 940 | nodeType: 'nodes-base.openAi', 941 | configuration: { 942 | resource: 'chat', 943 | operation: 'message', 944 | modelId: 'gpt-4', 945 | messages: { 946 | values: [ 947 | { 948 | role: 'user', 949 | content: '' 950 | } 951 | ] 952 | }, 953 | // Handle rate limits with exponential backoff 954 | onError: 'continueRegularOutput', 955 | retryOnFail: true, 956 | maxTries: 5, 957 | waitBetweenTries: 5000, 958 | alwaysOutputData: true 959 | }, 960 | userMustProvide: [ 961 | { 962 | property: 'messages.values[0].content', 963 | description: 'The prompt for the AI' 964 | } 965 | ], 966 | notes: [ 967 | 'AI APIs often have rate limits', 968 | 'Longer wait times help avoid hitting limits', 969 | 'Consider implementing exponential backoff in Code node', 970 | 'Monitor usage to stay within quotas' 971 | ] 972 | }, 973 | 974 | // Code Node Tasks 975 | 'custom_ai_tool': { 976 | task: 'custom_ai_tool', 977 | description: 'Create a custom tool for AI agents using Code node', 978 | nodeType: 'nodes-base.code', 979 | configuration: { 980 | language: 'javaScript', 981 | mode: 'runOnceForEachItem', 982 | jsCode: `// Custom AI Tool - Example: Text Analysis 983 | // This code will be called by AI agents with $json containing the input 984 | 985 | // Access the input from the AI agent 986 | const text = $json.text || ''; 987 | const operation = $json.operation || 'analyze'; 988 | 989 | // Perform the requested operation 990 | let result = {}; 991 | 992 | switch (operation) { 993 | case 'wordCount': 994 | result = { 995 | wordCount: text.split(/\\s+/).filter(word => word.length > 0).length, 996 | characterCount: text.length, 997 | lineCount: text.split('\\n').length 998 | }; 999 | break; 1000 | 1001 | case 'extract': 1002 | // Extract specific patterns (emails, URLs, etc.) 1003 | result = { 1004 | emails: text.match(/[\\w.-]+@[\\w.-]+\\.\\w+/g) || [], 1005 | urls: text.match(/https?:\\/\\/[^\\s]+/g) || [], 1006 | numbers: text.match(/\\b\\d+\\b/g) || [] 1007 | }; 1008 | break; 1009 | 1010 | default: 1011 | result = { 1012 | error: 'Unknown operation', 1013 | availableOperations: ['wordCount', 'extract'] 1014 | }; 1015 | } 1016 | 1017 | return [{ 1018 | json: { 1019 | ...result, 1020 | originalText: text, 1021 | operation: operation, 1022 | processedAt: DateTime.now().toISO() 1023 | } 1024 | }];`, 1025 | onError: 'continueRegularOutput' 1026 | }, 1027 | userMustProvide: [], 1028 | notes: [ 1029 | 'Connect this to AI Agent node\'s tool input', 1030 | 'AI will pass data in $json', 1031 | 'Use "Run Once for Each Item" mode for AI tools', 1032 | 'Return structured data the AI can understand' 1033 | ] 1034 | }, 1035 | 1036 | 'aggregate_data': { 1037 | task: 'aggregate_data', 1038 | description: 'Aggregate data from multiple items into summary statistics', 1039 | nodeType: 'nodes-base.code', 1040 | configuration: { 1041 | language: 'javaScript', 1042 | jsCode: `// Aggregate data from all items 1043 | const stats = { 1044 | count: 0, 1045 | sum: 0, 1046 | min: Infinity, 1047 | max: -Infinity, 1048 | values: [], 1049 | categories: {}, 1050 | errors: [] 1051 | }; 1052 | 1053 | // Process each item 1054 | for (const item of items) { 1055 | try { 1056 | const value = item.json.value || item.json.amount || 0; 1057 | const category = item.json.category || 'uncategorized'; 1058 | 1059 | stats.count++; 1060 | stats.sum += value; 1061 | stats.min = Math.min(stats.min, value); 1062 | stats.max = Math.max(stats.max, value); 1063 | stats.values.push(value); 1064 | 1065 | // Count by category 1066 | stats.categories[category] = (stats.categories[category] || 0) + 1; 1067 | 1068 | } catch (error) { 1069 | stats.errors.push({ 1070 | item: item.json, 1071 | error: error.message 1072 | }); 1073 | } 1074 | } 1075 | 1076 | // Calculate additional statistics 1077 | const average = stats.count > 0 ? stats.sum / stats.count : 0; 1078 | const sorted = [...stats.values].sort((a, b) => a - b); 1079 | const median = sorted.length > 0 1080 | ? sorted[Math.floor(sorted.length / 2)] 1081 | : 0; 1082 | 1083 | return [{ 1084 | json: { 1085 | totalItems: stats.count, 1086 | sum: stats.sum, 1087 | average: average, 1088 | median: median, 1089 | min: stats.min === Infinity ? 0 : stats.min, 1090 | max: stats.max === -Infinity ? 0 : stats.max, 1091 | categoryCounts: stats.categories, 1092 | errorCount: stats.errors.length, 1093 | errors: stats.errors, 1094 | processedAt: DateTime.now().toISO() 1095 | } 1096 | }];`, 1097 | onError: 'continueRegularOutput' 1098 | }, 1099 | userMustProvide: [], 1100 | notes: [ 1101 | 'Assumes items have "value" or "amount" field', 1102 | 'Groups by "category" field if present', 1103 | 'Returns single item with all statistics', 1104 | 'Handles errors gracefully' 1105 | ] 1106 | }, 1107 | 1108 | 'batch_process_with_api': { 1109 | task: 'batch_process_with_api', 1110 | description: 'Process items in batches with API calls', 1111 | nodeType: 'nodes-base.code', 1112 | configuration: { 1113 | language: 'javaScript', 1114 | jsCode: `// Batch process items with API calls 1115 | const BATCH_SIZE = 10; 1116 | const API_URL = 'https://api.example.com/batch-process'; // USER MUST UPDATE 1117 | const results = []; 1118 | 1119 | // Process items in batches 1120 | for (let i = 0; i < items.length; i += BATCH_SIZE) { 1121 | const batch = items.slice(i, i + BATCH_SIZE); 1122 | 1123 | try { 1124 | // Prepare batch data 1125 | const batchData = batch.map(item => ({ 1126 | id: item.json.id, 1127 | data: item.json 1128 | })); 1129 | 1130 | // Make API request for batch 1131 | const response = await $helpers.httpRequest({ 1132 | method: 'POST', 1133 | url: API_URL, 1134 | body: { 1135 | items: batchData 1136 | }, 1137 | headers: { 1138 | 'Content-Type': 'application/json' 1139 | } 1140 | }); 1141 | 1142 | // Add results 1143 | if (response.results && Array.isArray(response.results)) { 1144 | response.results.forEach((result, index) => { 1145 | results.push({ 1146 | json: { 1147 | ...batch[index].json, 1148 | ...result, 1149 | batchNumber: Math.floor(i / BATCH_SIZE) + 1, 1150 | processedAt: DateTime.now().toISO() 1151 | } 1152 | }); 1153 | }); 1154 | } 1155 | 1156 | // Add delay between batches to avoid rate limits 1157 | if (i + BATCH_SIZE < items.length) { 1158 | await new Promise(resolve => setTimeout(resolve, 1000)); 1159 | } 1160 | 1161 | } catch (error) { 1162 | // Add failed batch items with error 1163 | batch.forEach(item => { 1164 | results.push({ 1165 | json: { 1166 | ...item.json, 1167 | error: error.message, 1168 | status: 'failed', 1169 | batchNumber: Math.floor(i / BATCH_SIZE) + 1 1170 | } 1171 | }); 1172 | }); 1173 | } 1174 | } 1175 | 1176 | return results;`, 1177 | onError: 'continueRegularOutput', 1178 | retryOnFail: true, 1179 | maxTries: 2 1180 | }, 1181 | userMustProvide: [ 1182 | { 1183 | property: 'jsCode', 1184 | description: 'Update API_URL in the code', 1185 | example: 'https://your-api.com/batch' 1186 | } 1187 | ], 1188 | notes: [ 1189 | 'Processes items in batches of 10', 1190 | 'Includes delay between batches', 1191 | 'Handles batch failures gracefully', 1192 | 'Update API_URL and adjust BATCH_SIZE as needed' 1193 | ] 1194 | }, 1195 | 1196 | 'error_safe_transform': { 1197 | task: 'error_safe_transform', 1198 | description: 'Transform data with comprehensive error handling', 1199 | nodeType: 'nodes-base.code', 1200 | configuration: { 1201 | language: 'javaScript', 1202 | jsCode: `// Safe data transformation with validation 1203 | const results = []; 1204 | const errors = []; 1205 | 1206 | for (const item of items) { 1207 | try { 1208 | // Validate required fields 1209 | const required = ['id', 'name']; // USER SHOULD UPDATE 1210 | const missing = required.filter(field => !item.json[field]); 1211 | 1212 | if (missing.length > 0) { 1213 | throw new Error(\`Missing required fields: \${missing.join(', ')}\`); 1214 | } 1215 | 1216 | // Transform data with type checking 1217 | const transformed = { 1218 | // Ensure ID is string 1219 | id: String(item.json.id), 1220 | 1221 | // Clean and validate name 1222 | name: String(item.json.name).trim(), 1223 | 1224 | // Parse numbers safely 1225 | amount: parseFloat(item.json.amount) || 0, 1226 | 1227 | // Parse dates safely 1228 | date: item.json.date 1229 | ? DateTime.fromISO(item.json.date).isValid 1230 | ? DateTime.fromISO(item.json.date).toISO() 1231 | : null 1232 | : null, 1233 | 1234 | // Boolean conversion 1235 | isActive: Boolean(item.json.active || item.json.isActive), 1236 | 1237 | // Array handling 1238 | tags: Array.isArray(item.json.tags) 1239 | ? item.json.tags.filter(tag => typeof tag === 'string') 1240 | : [], 1241 | 1242 | // Nested object handling 1243 | metadata: typeof item.json.metadata === 'object' 1244 | ? item.json.metadata 1245 | : {}, 1246 | 1247 | // Add processing info 1248 | processedAt: DateTime.now().toISO(), 1249 | originalIndex: items.indexOf(item) 1250 | }; 1251 | 1252 | results.push({ 1253 | json: transformed 1254 | }); 1255 | 1256 | } catch (error) { 1257 | errors.push({ 1258 | json: { 1259 | error: error.message, 1260 | originalData: item.json, 1261 | index: items.indexOf(item), 1262 | status: 'failed' 1263 | } 1264 | }); 1265 | } 1266 | } 1267 | 1268 | // Add summary at the end 1269 | results.push({ 1270 | json: { 1271 | _summary: { 1272 | totalProcessed: results.length - errors.length, 1273 | totalErrors: errors.length, 1274 | successRate: ((results.length - errors.length) / items.length * 100).toFixed(2) + '%', 1275 | timestamp: DateTime.now().toISO() 1276 | } 1277 | } 1278 | }); 1279 | 1280 | // Include errors at the end 1281 | return [...results, ...errors];`, 1282 | onError: 'continueRegularOutput' 1283 | }, 1284 | userMustProvide: [ 1285 | { 1286 | property: 'jsCode', 1287 | description: 'Update required fields array', 1288 | example: "const required = ['id', 'email', 'name'];" 1289 | } 1290 | ], 1291 | notes: [ 1292 | 'Validates all data types', 1293 | 'Handles missing/invalid data gracefully', 1294 | 'Returns both successful and failed items', 1295 | 'Includes processing summary' 1296 | ] 1297 | }, 1298 | 1299 | 'async_data_processing': { 1300 | task: 'async_data_processing', 1301 | description: 'Process data with async operations and proper error handling', 1302 | nodeType: 'nodes-base.code', 1303 | configuration: { 1304 | language: 'javaScript', 1305 | jsCode: `// Async processing with concurrent limits 1306 | const CONCURRENT_LIMIT = 5; 1307 | const results = []; 1308 | 1309 | // Process items with concurrency control 1310 | async function processItem(item, index) { 1311 | try { 1312 | // Simulate async operation (replace with actual logic) 1313 | // Example: API call, database query, file operation 1314 | await new Promise(resolve => setTimeout(resolve, 100)); 1315 | 1316 | // Actual processing logic here 1317 | const processed = { 1318 | ...item.json, 1319 | processed: true, 1320 | index: index, 1321 | timestamp: DateTime.now().toISO() 1322 | }; 1323 | 1324 | // Example async operation - external API call 1325 | if (item.json.needsEnrichment) { 1326 | const enrichment = await $helpers.httpRequest({ 1327 | method: 'GET', 1328 | url: \`https://api.example.com/enrich/\${item.json.id}\` 1329 | }); 1330 | processed.enrichment = enrichment; 1331 | } 1332 | 1333 | return { json: processed }; 1334 | 1335 | } catch (error) { 1336 | return { 1337 | json: { 1338 | ...item.json, 1339 | error: error.message, 1340 | status: 'failed', 1341 | index: index 1342 | } 1343 | }; 1344 | } 1345 | } 1346 | 1347 | // Process in batches with concurrency limit 1348 | for (let i = 0; i < items.length; i += CONCURRENT_LIMIT) { 1349 | const batch = items.slice(i, i + CONCURRENT_LIMIT); 1350 | const batchPromises = batch.map((item, batchIndex) => 1351 | processItem(item, i + batchIndex) 1352 | ); 1353 | 1354 | const batchResults = await Promise.all(batchPromises); 1355 | results.push(...batchResults); 1356 | } 1357 | 1358 | return results;`, 1359 | onError: 'continueRegularOutput', 1360 | retryOnFail: true, 1361 | maxTries: 2 1362 | }, 1363 | userMustProvide: [], 1364 | notes: [ 1365 | 'Processes 5 items concurrently', 1366 | 'Prevents overwhelming external services', 1367 | 'Each item processed independently', 1368 | 'Errors don\'t affect other items' 1369 | ] 1370 | }, 1371 | 1372 | 'python_data_analysis': { 1373 | task: 'python_data_analysis', 1374 | description: 'Analyze data using Python with statistics', 1375 | nodeType: 'nodes-base.code', 1376 | configuration: { 1377 | language: 'python', 1378 | pythonCode: `# Python data analysis - use underscore prefix for built-in variables 1379 | import json 1380 | from datetime import datetime 1381 | import statistics 1382 | 1383 | # Collect data for analysis 1384 | values = [] 1385 | categories = {} 1386 | dates = [] 1387 | 1388 | # Use _input.all() to get items in Python 1389 | for item in _input.all(): 1390 | # Convert JsProxy to Python dict for safe access 1391 | item_data = item.json.to_py() 1392 | 1393 | # Extract numeric values 1394 | if 'value' in item_data or 'amount' in item_data: 1395 | value = item_data.get('value', item_data.get('amount', 0)) 1396 | if isinstance(value, (int, float)): 1397 | values.append(value) 1398 | 1399 | # Count categories 1400 | category = item_data.get('category', 'uncategorized') 1401 | categories[category] = categories.get(category, 0) + 1 1402 | 1403 | # Collect dates 1404 | if 'date' in item_data: 1405 | dates.append(item_data['date']) 1406 | 1407 | # Calculate statistics 1408 | result = { 1409 | 'itemCount': len(_input.all()), 1410 | 'values': { 1411 | 'count': len(values), 1412 | 'sum': sum(values) if values else 0, 1413 | 'mean': statistics.mean(values) if values else 0, 1414 | 'median': statistics.median(values) if values else 0, 1415 | 'min': min(values) if values else 0, 1416 | 'max': max(values) if values else 0, 1417 | 'stdev': statistics.stdev(values) if len(values) > 1 else 0 1418 | }, 1419 | 'categories': categories, 1420 | 'dateRange': { 1421 | 'earliest': min(dates) if dates else None, 1422 | 'latest': max(dates) if dates else None, 1423 | 'count': len(dates) 1424 | }, 1425 | 'analysis': { 1426 | 'hasNumericData': len(values) > 0, 1427 | 'hasCategoricalData': len(categories) > 0, 1428 | 'hasTemporalData': len(dates) > 0, 1429 | 'dataQuality': 'good' if len(values) > len(items) * 0.8 else 'partial' 1430 | }, 1431 | 'processedAt': datetime.now().isoformat() 1432 | } 1433 | 1434 | # Return single summary item 1435 | return [{'json': result}]`, 1436 | onError: 'continueRegularOutput' 1437 | }, 1438 | userMustProvide: [], 1439 | notes: [ 1440 | 'Uses Python statistics module', 1441 | 'Analyzes numeric, categorical, and date data', 1442 | 'Returns comprehensive summary', 1443 | 'Handles missing data gracefully' 1444 | ] 1445 | } 1446 | }; 1447 | 1448 | /** 1449 | * Get all available tasks 1450 | */ 1451 | static getAllTasks(): string[] { 1452 | return Object.keys(this.templates); 1453 | } 1454 | 1455 | /** 1456 | * Get tasks for a specific node type 1457 | */ 1458 | static getTasksForNode(nodeType: string): string[] { 1459 | return Object.entries(this.templates) 1460 | .filter(([_, template]) => template.nodeType === nodeType) 1461 | .map(([task, _]) => task); 1462 | } 1463 | 1464 | /** 1465 | * Get a specific task template 1466 | */ 1467 | static getTaskTemplate(task: string): TaskTemplate | undefined { 1468 | return this.templates[task]; 1469 | } 1470 | 1471 | /** 1472 | * Get a specific task template (alias for getTaskTemplate) 1473 | */ 1474 | static getTemplate(task: string): TaskTemplate | undefined { 1475 | return this.getTaskTemplate(task); 1476 | } 1477 | 1478 | /** 1479 | * Search for tasks by keyword 1480 | */ 1481 | static searchTasks(keyword: string): string[] { 1482 | const lower = keyword.toLowerCase(); 1483 | return Object.entries(this.templates) 1484 | .filter(([task, template]) => 1485 | task.toLowerCase().includes(lower) || 1486 | template.description.toLowerCase().includes(lower) || 1487 | template.nodeType.toLowerCase().includes(lower) 1488 | ) 1489 | .map(([task, _]) => task); 1490 | } 1491 | 1492 | /** 1493 | * Get task categories 1494 | */ 1495 | static getTaskCategories(): Record<string, string[]> { 1496 | return { 1497 | 'HTTP/API': ['get_api_data', 'post_json_request', 'call_api_with_auth', 'api_call_with_retry'], 1498 | 'Webhooks': ['receive_webhook', 'webhook_with_response', 'webhook_with_error_handling', 'process_webhook_data'], 1499 | 'Database': ['query_postgres', 'insert_postgres_data', 'database_transaction_safety'], 1500 | 'AI/LangChain': ['chat_with_ai', 'ai_agent_workflow', 'multi_tool_ai_agent', 'ai_rate_limit_handling'], 1501 | 'Data Processing': ['transform_data', 'filter_data', 'fault_tolerant_processing', 'process_webhook_data'], 1502 | 'Communication': ['send_slack_message', 'send_email'], 1503 | 'AI Tool Usage': ['use_google_sheets_as_tool', 'use_slack_as_tool', 'multi_tool_ai_agent'], 1504 | 'Error Handling': ['modern_error_handling_patterns', 'api_call_with_retry', 'fault_tolerant_processing', 'webhook_with_error_handling', 'database_transaction_safety', 'ai_rate_limit_handling'] 1505 | }; 1506 | } 1507 | } ```