Skip to content

fix: Replace non-existent /execute endpoint with webhook-based execution in N8nWorkflowTool#23

Merged
MervinPraison merged 2 commits intomainfrom
claude/issue-22-20260416-1904
Apr 16, 2026
Merged

fix: Replace non-existent /execute endpoint with webhook-based execution in N8nWorkflowTool#23
MervinPraison merged 2 commits intomainfrom
claude/issue-22-20260416-1904

Conversation

@MervinPraison
Copy link
Copy Markdown
Owner

@MervinPraison MervinPraison commented Apr 16, 2026

Automated PR from Claude's work on issue #22.

Branch: claude/issue-22-20260416-1904

Fixes the N8nWorkflowTool.run() method that was calling a non-existent /api/v1/workflows/{id}/execute endpoint.

Closes #22

Summary by CodeRabbit

  • Updates

    • Workflow execution now triggers via the workflow's webhook and returns immediately (no polling).
    • Workflows are auto-activated when required and webhook requests use simplified headers.
    • wait_for_completion is now ignored; responses favor JSON when available.
  • Bug Fixes

    • Clear error reported when a workflow has no webhook trigger.
  • Tests

    • Tests updated/added to cover webhook-based execution, activation, timeouts, and missing-trigger cases.

…ion in N8nWorkflowTool

- Replace POST /api/v1/workflows/{id}/execute with webhook trigger execution
- Add _get_webhook_url method to fetch workflow and find webhook trigger nodes
- Automatically activate workflows if inactive
- Add proper error handling for missing webhook triggers
- Update unit tests to mock workflow fetch + webhook execution pattern
- Remove obsolete _wait_for_execution method

Fixes #22

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 16, 2026 19:19
Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MervinPraison has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 42682a24-869e-484d-a2f5-7b0f13c64274

📥 Commits

Reviewing files that changed from the base of the PR and between d4ec322 and 29d6808.

📒 Files selected for processing (1)
  • tests/test_n8n_integration.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_n8n_integration.py

📝 Walkthrough

Walkthrough

The execution path in N8nWorkflowTool was changed from calling a non-existent API execute endpoint to webhook-based execution: run() now fetches the workflow, locates the webhook trigger node, activates the workflow if needed, and POSTs to the constructed webhook URL, returning the webhook response (or error dicts) immediately.

Changes

Cohort / File(s) Summary
Webhook-based Execution Implementation
praisonai_tools/n8n/n8n_workflow.py
Replaced API-based execute call with webhook invocation. Added _get_webhook_url(workflow_id) to fetch workflow, find n8n-nodes-base.webhook, build /webhook/{path}, and activate workflow if inactive. Removed polling/wait logic; simplified headers and response parsing (JSON fallback to text).
Webhook Execution Tests
tests/test_n8n_integration.py
Updated tests to mock GET workflow + POST webhook flows. Adjusted success assertions to webhook response fields, moved error/timeout simulations to workflow GET, added test_n8n_workflow_no_webhook_trigger for missing webhook node.

Sequence Diagram

sequenceDiagram
    participant Tool as N8nWorkflowTool
    participant API as N8n API
    participant Webhook as N8n Webhook

    Tool->>API: GET /api/v1/workflows/{id}
    activate API
    API-->>Tool: workflow definition (nodes, active)
    deactivate API

    alt webhook node found
        Tool->>Tool: extract webhook path
        alt workflow inactive
            Tool->>API: PATCH /api/v1/workflows/{id} (active=true)
            activate API
            API-->>Tool: activated
            deactivate API
        end
        Tool->>Webhook: POST /webhook/{path} (json payload)
        activate Webhook
        Webhook-->>Tool: execution result (JSON or text)
        deactivate Webhook
    else no webhook trigger
        Tool-->>Tool: return error dict ("Workflow has no Webhook trigger node")
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I hopped through nodes to find the way,
A webhook path to save the day.
No more 405s, just a cheerful POST,
Activate, send, and celebrate the host!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main change: replacing the non-existent /execute endpoint with webhook-based execution in N8nWorkflowTool.
Linked Issues check ✅ Passed The PR implements Option A from issue #22: modifying run() to fetch workflow, locate webhook trigger, activate if needed, and POST to webhook URL with error handling for missing webhook nodes.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing webhook-based execution in N8nWorkflowTool and updating tests to match the new implementation pattern.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-22-20260416-1904

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request transitions n8n workflow execution from the direct API execution endpoint to a webhook-based approach. The logic now involves fetching the workflow definition to identify a webhook trigger node, ensuring the workflow is active, and then invoking the constructed webhook URL. Feedback focuses on optimizing performance by caching the webhook URL, increasing robustness by supporting configurable HTTP methods, ensuring consistent timeout usage, and preventing URL formatting issues with path slashes.

if self.api_key:
headers["X-N8N-API-KEY"] = self.api_key
# Fetch workflow to find webhook trigger
webhook_url = self._get_webhook_url(workflow_id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The _get_webhook_url method is called on every execution of run. This performs an additional API request to fetch the workflow definition every time, which is inefficient and adds latency. Consider caching the resolved webhook URL to improve performance for repeated executions.

# Execute workflow via webhook (no API key needed for webhooks)
try:
with httpx.Client(timeout=self.timeout) as client:
response = client.post(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The tool hardcodes the POST method for the webhook call. While POST is the most common method for webhooks, n8n allows configuring the Webhook node to respond to other methods (GET, PUT, etc.). If a workflow is configured with a different method, this call will fail with a 405 error. It would be more robust to use the method defined in the workflow's webhook node parameters.

headers["X-N8N-API-KEY"] = self.api_key

try:
with httpx.Client(timeout=30.0) as client:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The timeout for fetching the workflow definition is hardcoded to 30.0 seconds. It should use self.timeout to remain consistent with the tool's configuration.

Suggested change
with httpx.Client(timeout=30.0) as client:
with httpx.Client(timeout=self.timeout) as client:


# Construct webhook URL
# Use /webhook-test/ for test mode, /webhook/ for production
webhook_url = f"{self.n8n_url}/webhook/{path}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the webhook path starts with a forward slash, this string formatting will result in a double slash in the URL (e.g., .../webhook//path). Stripping the leading slash ensures a valid URL is constructed.

Suggested change
webhook_url = f"{self.n8n_url}/webhook/{path}"
webhook_url = f"{self.n8n_url}/webhook/{path.lstrip('/')}"

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the n8n integration so workflow execution no longer calls a non-existent Public API /execute endpoint, and instead triggers workflows via their Webhook node.

Changes:

  • Reworked N8nWorkflowTool.run() to fetch workflow metadata, resolve a Webhook trigger path, and execute via /webhook/{path}.
  • Added _get_webhook_url() helper to locate the webhook trigger and auto-activate inactive workflows.
  • Updated unit tests to mock workflow fetch + webhook execution, and added a “no webhook trigger” test.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
praisonai_tools/n8n/n8n_workflow.py Switches execution from /api/v1/workflows/{id}/execute to webhook-triggered invocation and adds webhook URL resolution helper.
tests/test_n8n_integration.py Updates mocks/assertions for webhook-based execution and adds coverage for workflows without a webhook trigger.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 82 to 87
with httpx.Client(timeout=self.timeout) as client:
response = client.post(
f"{self.n8n_url}/api/v1/workflows/{workflow_id}/execute",
json={"data": input_data or {}},
headers=headers,
webhook_url,
json=input_data or {},
headers={"Content-Type": "application/json"},
)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The webhook node’s parameters.httpMethod is read in tests and is a configurable part of n8n’s Webhook trigger, but run() always calls client.post(...). Workflows configured with a GET/PUT webhook will fail to execute. Consider having _get_webhook_url() return both the webhook URL and HTTP method (or accept method as an override) and use client.request(method, ...) instead of hard-coding POST.

Copilot uses AI. Check for mistakes.
headers["X-N8N-API-KEY"] = self.api_key

try:
with httpx.Client(timeout=30.0) as client:
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_webhook_url() uses a hard-coded httpx.Client(timeout=30.0) for workflow fetch/activation, while run() uses self.timeout for the webhook call. This makes the timeout constructor argument apply only to the webhook request, not the initial workflow lookup/activation, which is surprising and can cause much longer hangs than configured. Consider using timeout=self.timeout here (or introducing a clearly named separate API-timeout setting).

Suggested change
with httpx.Client(timeout=30.0) as client:
with httpx.Client(timeout=self.timeout) as client:

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +169
# Ensure workflow is active
if not workflow.get("active"):
activate_response = client.patch(
f"{self.n8n_url}/api/v1/workflows/{workflow_id}",
json={"active": True},
headers=headers,
)
activate_response.raise_for_status()
logger.info(f"Activated workflow {workflow_id}")

# Construct webhook URL
# Use /webhook-test/ for test mode, /webhook/ for production
webhook_url = f"{self.n8n_url}/webhook/{path}"
return webhook_url

except httpx.HTTPStatusError as e:
logger.error(f"n8n API error fetching workflow {workflow_id}: {e.response.status_code} - {e.response.text}")
return {"error": f"HTTP {e.response.status_code}: {e.response.text}"}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The except httpx.HTTPStatusError branch logs "API error fetching workflow" for any HTTPStatusError in the whole _get_webhook_url() block, including failures from the activation PATCH. This makes logs misleading when activation fails (e.g., missing permissions). Consider separating the GET and PATCH into their own try/except blocks (or improving the log message) so it’s clear which request failed.

Copilot uses AI. Check for mistakes.
Comment thread tests/test_n8n_integration.py Outdated
tool = N8nWorkflowTool(timeout=5.0)
result = tool.run(workflow_id="test-workflow")

assert "timed out after 5.0 seconds" in result["error"]
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_n8n_workflow_timeout_error expects a formatted message including the tool timeout, but tool.run() now delegates the workflow fetch to _get_webhook_url(), which catches httpx.TimeoutException under the generic Exception handler and returns {"error": "Request timed out"}. This assertion will fail; either update the test to match the current error string, or (preferably) add a dedicated except httpx.TimeoutException in _get_webhook_url() that returns a consistent timeout message that includes self.timeout (and consider using timeout=self.timeout for the fetch client as well).

Suggested change
assert "timed out after 5.0 seconds" in result["error"]
assert "Request timed out" in result["error"]

Copilot uses AI. Check for mistakes.
@MervinPraison
Copy link
Copy Markdown
Owner Author

Hold on merge: validated locally and CI confirms TestN8nWorkflowTool::test_n8n_workflow_timeout_error fails on Python 3.10, 3.11, and 3.12:

AssertionError: assert 'timed out after 5.0 seconds' in 'Request timed out'

The implementation in n8n_workflow.py:171 produces the error message 'Request timed out' but the test asserts the substring 'timed out after 5.0 seconds'. Either:

  • Update the test assertion to match the actual error message (simpler), or
  • Update _get_webhook_url to include the timeout value in the error message (more informative)

All other 12 tests pass. Please fix and re-push.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
tests/test_n8n_integration.py (1)

171-182: ⚠️ Potential issue | 🟠 Major

This test will fail against the current implementation.

mock_client.get.side_effect = TimeoutException(...) is raised inside _get_webhook_url, which only has except httpx.HTTPStatusError and a generic except Exception. The timeout falls through to the generic handler and returns {"error": "Request timed out"}, so "timed out after 5.0 seconds" in result["error"] is false. The fix belongs in _get_webhook_url (see the separate comment on that file) — flagging here so you know this test is the one that will catch it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_n8n_integration.py` around lines 171 - 182, The test fails because
a timeout raised inside _get_webhook_url is currently caught only by the generic
Exception handler and returns a generic message; update _get_webhook_url in
class N8nWorkflowTool to explicitly catch httpx.TimeoutException (or
httpx.ReadTimeout/TimeoutError as appropriate) before the broad except
Exception, and return an error string that includes the configured timeout
(e.g., "timed out after {self.timeout} seconds"); make sure the specific timeout
handler uses self.timeout (or the timeout param passed) in the message and
preserves other existing error-return behavior for non-timeout exceptions.
🧹 Nitpick comments (3)
praisonai_tools/n8n/n8n_workflow.py (2)

136-144: Consider path normalization and multi-webhook workflows.

Two nits worth addressing while touching this logic:

  • If path is stored with a leading / (n8n generally strips it, but user-edited JSON may not), f"{self.n8n_url}/webhook/{path}" produces a double slash. A path.lstrip('/') is cheap insurance.
  • Only the first n8n-nodes-base.webhook node is returned. For workflows with multiple webhook triggers, callers currently have no way to pick one. Acceptable for this PR, but worth a docstring note or a future webhook_name parameter (Option B from issue #22).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@praisonai_tools/n8n/n8n_workflow.py` around lines 136 - 144, The code that
finds the webhook node (iterating over nodes from workflow.get("nodes", []))
should normalize the webhook path before building the URL: when you extract the
node's "path" value (from the found webhook_node) call path = path.lstrip('/')
so f"{self.n8n_url}/webhook/{path}" cannot produce a double slash; update the
code that builds the webhook URL to use the stripped path. Also add a brief
docstring note on the function (or above the webhook_node selection) indicating
that only the first n8n-nodes-base.webhook node is currently selected and that
multi-webhook workflows aren’t supported yet (suggest adding an optional
webhook_name parameter in future to select among multiple webhook nodes).

162-164: Stale comment about /webhook-test/.

The comment mentions choosing between /webhook-test/ and /webhook/, but the code unconditionally uses /webhook/. Either drop the comment or expose a test_mode/parameter flag so production vs. test webhook URLs can be selected (test URLs are only active while the editor is listening, so defaulting to /webhook/ is correct — the comment just misleads readers).

✂️ Suggested fix
-                # Construct webhook URL
-                # Use /webhook-test/ for test mode, /webhook/ for production
-                webhook_url = f"{self.n8n_url}/webhook/{path}"
+                # Construct production webhook URL
+                webhook_url = f"{self.n8n_url}/webhook/{path}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@praisonai_tools/n8n/n8n_workflow.py` around lines 162 - 164, The comment
about choosing between "/webhook-test/" and "/webhook/" is stale; either remove
the misleading comment or add an explicit flag to select test vs production
webhook path. Update the code around the webhook_url construction (where
webhook_url = f"{self.n8n_url}/webhook/{path}") to accept a parameter or
attribute like test_mode (default False) and set the prefix to "/webhook-test/"
when test_mode is True and "/webhook/" otherwise, or simply delete the obsolete
comment if you want to always use "/webhook/"; ensure you reference
self.n8n_url, path and the webhook_url assignment when applying the change.
tests/test_n8n_integration.py (1)

91-212: Missing coverage for the activation path.

Every test here sets "active": True on the mocked workflow, so the client.patch(.../workflows/{id}, json={"active": True}) branch in _get_webhook_url is never exercised. That's the exact branch I flagged as likely using the wrong endpoint in praisonai_tools/n8n/n8n_workflow.py — once that activation call is corrected, please add a test that returns "active": False on the fetch, asserts the correct activation HTTP call (method + URL), and then verifies the webhook POST still happens. Same goes for a webhook trigger with httpMethod != "POST" to prevent regressions there.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_n8n_integration.py` around lines 91 - 212, Add a new test that
exercises the activation branch in _get_webhook_url by mocking the workflow
fetch to return "active": False, then assert that the client.patch call was made
to activate the workflow (verify method is patch and the URL matches the
workflows activation endpoint used by N8nWorkflowTool/_get_webhook_url), then
ensure the webhook POST still occurs and returns the expected result; also add a
second test variant where the webhook node's parameters.httpMethod is not "POST"
and assert the code either converts/uses the correct method or fails as expected
(verifying mock_client.post/appropriate method call and the webhook URL
resolution in _get_webhook_url).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@praisonai_tools/n8n/n8n_workflow.py`:
- Around line 115-172: In _get_webhook_url, timeouts are being swallowed and the
client uses a hardcoded 30s; change the httpx.Client timeout to use self.timeout
and add an explicit except httpx.TimeoutException handler (alongside the
existing httpx.HTTPStatusError and generic Exception handlers) that logs the
timeout and returns the same message format used in run() (e.g. "Workflow
webhook execution timed out after {self.timeout} seconds"); modify the
try/except in the _get_webhook_url method and reference self.timeout and
httpx.TimeoutException to implement this fix.
- Around line 152-160: Replace the PATCH call that tries to set {"active": True}
with a POST to the dedicated activate endpoint: use
client.post(f"{self.n8n_url}/api/v1/workflows/{workflow_id}/activate",
headers=headers) (instead of client.patch), call raise_for_status on that
response, and keep the logger.info(f"Activated workflow {workflow_id}") and the
surrounding active-check logic; update the variable currently named
activate_response to hold the POST response and remove any payload/json since
the activate endpoint requires no body.
- Around line 81-88: The webhook HTTP method is being ignored because run()
always calls client.post(); update _get_webhook_url() to read httpMethod from
webhook_node.parameters (default to "GET" per n8n), return the method along with
the webhook_url, then change run() to dispatch the request based on that method
(use client.get(), client.post(), client.put(), client.delete(), client.patch(),
client.head(), client.options() as appropriate) and validate the method before
making the call—if the method is unsupported return a clear error instead of
attempting the request.

---

Duplicate comments:
In `@tests/test_n8n_integration.py`:
- Around line 171-182: The test fails because a timeout raised inside
_get_webhook_url is currently caught only by the generic Exception handler and
returns a generic message; update _get_webhook_url in class N8nWorkflowTool to
explicitly catch httpx.TimeoutException (or httpx.ReadTimeout/TimeoutError as
appropriate) before the broad except Exception, and return an error string that
includes the configured timeout (e.g., "timed out after {self.timeout}
seconds"); make sure the specific timeout handler uses self.timeout (or the
timeout param passed) in the message and preserves other existing error-return
behavior for non-timeout exceptions.

---

Nitpick comments:
In `@praisonai_tools/n8n/n8n_workflow.py`:
- Around line 136-144: The code that finds the webhook node (iterating over
nodes from workflow.get("nodes", [])) should normalize the webhook path before
building the URL: when you extract the node's "path" value (from the found
webhook_node) call path = path.lstrip('/') so f"{self.n8n_url}/webhook/{path}"
cannot produce a double slash; update the code that builds the webhook URL to
use the stripped path. Also add a brief docstring note on the function (or above
the webhook_node selection) indicating that only the first
n8n-nodes-base.webhook node is currently selected and that multi-webhook
workflows aren’t supported yet (suggest adding an optional webhook_name
parameter in future to select among multiple webhook nodes).
- Around line 162-164: The comment about choosing between "/webhook-test/" and
"/webhook/" is stale; either remove the misleading comment or add an explicit
flag to select test vs production webhook path. Update the code around the
webhook_url construction (where webhook_url = f"{self.n8n_url}/webhook/{path}")
to accept a parameter or attribute like test_mode (default False) and set the
prefix to "/webhook-test/" when test_mode is True and "/webhook/" otherwise, or
simply delete the obsolete comment if you want to always use "/webhook/"; ensure
you reference self.n8n_url, path and the webhook_url assignment when applying
the change.

In `@tests/test_n8n_integration.py`:
- Around line 91-212: Add a new test that exercises the activation branch in
_get_webhook_url by mocking the workflow fetch to return "active": False, then
assert that the client.patch call was made to activate the workflow (verify
method is patch and the URL matches the workflows activation endpoint used by
N8nWorkflowTool/_get_webhook_url), then ensure the webhook POST still occurs and
returns the expected result; also add a second test variant where the webhook
node's parameters.httpMethod is not "POST" and assert the code either
converts/uses the correct method or fails as expected (verifying
mock_client.post/appropriate method call and the webhook URL resolution in
_get_webhook_url).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ea083b2b-fc9f-4ad5-af5d-308bf6cbeabd

📥 Commits

Reviewing files that changed from the base of the PR and between 4ad5166 and d4ec322.

📒 Files selected for processing (2)
  • praisonai_tools/n8n/n8n_workflow.py
  • tests/test_n8n_integration.py

Comment on lines 81 to 88
try:
with httpx.Client(timeout=self.timeout) as client:
response = client.post(
f"{self.n8n_url}/api/v1/workflows/{workflow_id}/execute",
json={"data": input_data or {}},
headers=headers,
webhook_url,
json=input_data or {},
headers={"Content-Type": "application/json"},
)
response.raise_for_status()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

n8n Webhook trigger node default httpMethod parameter values

💡 Result:

The n8n Webhook trigger node has an HTTP Method parameter that is configurable. There is no single fixed "default" value explicitly documented in the official docs, but multiple authoritative sources indicate it defaults to GET when using a single method. When "Allow Multiple HTTP Methods" is disabled (default behavior), the node accepts a single HTTP method, and documentation examples consistently use GET (e.g., curl examples with --request GET, references to selecting GET). When enabled, it defaults to accepting both GET and POST. In the source code, the runtime falls back to 'GET' via (parameters?.httpMethod ?? 'GET') when matching webhooks, confirming GET as the effective default for unset parameters.

Citations:


🏁 Script executed:

# First, locate and read the relevant file
find . -name "n8n_workflow.py" -type f

Repository: MervinPraison/PraisonAI-Tools

Length of output: 110


🏁 Script executed:

# Read the file to examine lines around 81-88 and 146-150
cat -n ./praisonai_tools/n8n/n8n_workflow.py | head -160

Repository: MervinPraison/PraisonAI-Tools

Length of output: 7331


🏁 Script executed:

# Continue reading the file from line 160 onwards to see the full webhook URL construction
cat -n ./praisonai_tools/n8n/n8n_workflow.py | tail -n +160

Repository: MervinPraison/PraisonAI-Tools

Length of output: 5173


Fix hardcoded POST method — webhook HTTP method is ignored.

The code extracts the webhook path at lines 146–150 but ignores the httpMethod parameter stored in the webhook node (e.g., GET, PUT, DELETE). The run() method hardcodes client.post() at line 83, causing any workflow with a non-POST webhook trigger to fail with 404 or 405. Per n8n documentation, the default is GET when not explicitly set, making this a common failure case.

Extract httpMethod from webhook_node.parameters in _get_webhook_url(), pass it back to run(), and dispatch the request using the appropriate method (client.get(), client.post(), client.put(), client.delete(), etc.). If the configured method is unsupported, return a clear error before attempting the request.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@praisonai_tools/n8n/n8n_workflow.py` around lines 81 - 88, The webhook HTTP
method is being ignored because run() always calls client.post(); update
_get_webhook_url() to read httpMethod from webhook_node.parameters (default to
"GET" per n8n), return the method along with the webhook_url, then change run()
to dispatch the request based on that method (use client.get(), client.post(),
client.put(), client.delete(), client.patch(), client.head(), client.options()
as appropriate) and validate the method before making the call—if the method is
unsupported return a clear error instead of attempting the request.

Comment on lines +115 to +172
try:
import httpx
except ImportError:
return {"error": "httpx not installed"}

waited = 0
while waited < max_wait:
try:
# Prepare headers for API calls
headers = {"Content-Type": "application/json"}
if self.api_key:
headers["X-N8N-API-KEY"] = self.api_key

try:
with httpx.Client(timeout=30.0) as client:
# Fetch workflow definition
response = client.get(
f"{self.n8n_url}/api/v1/executions/{execution_id}",
f"{self.n8n_url}/api/v1/workflows/{workflow_id}",
headers=headers,
)
response.raise_for_status()
workflow = response.json()

execution = response.json()
status = execution.get("status")
# Find webhook trigger node
nodes = workflow.get("nodes", [])
webhook_node = None
for node in nodes:
if node.get("type") == "n8n-nodes-base.webhook":
webhook_node = node
break

if status in ["success", "error", "canceled"]:
return execution
if not webhook_node:
return {"error": "Workflow has no Webhook trigger node. Add a Webhook trigger to enable external execution."}

time.sleep(poll_interval)
waited += poll_interval
# Extract webhook path
parameters = webhook_node.get("parameters", {})
path = parameters.get("path", "")
if not path:
return {"error": "Webhook trigger node has no path configured"}

except Exception as e:
logger.error(f"Error polling execution {execution_id}: {e}")
return {"error": f"Error polling execution: {e}"}

return {"error": f"Execution {execution_id} did not complete within {max_wait} seconds"}
# Ensure workflow is active
if not workflow.get("active"):
activate_response = client.patch(
f"{self.n8n_url}/api/v1/workflows/{workflow_id}",
json={"active": True},
headers=headers,
)
activate_response.raise_for_status()
logger.info(f"Activated workflow {workflow_id}")

# Construct webhook URL
# Use /webhook-test/ for test mode, /webhook/ for production
webhook_url = f"{self.n8n_url}/webhook/{path}"
return webhook_url

except httpx.HTTPStatusError as e:
logger.error(f"n8n API error fetching workflow {workflow_id}: {e.response.status_code} - {e.response.text}")
return {"error": f"HTTP {e.response.status_code}: {e.response.text}"}
except Exception as e:
logger.error(f"Error fetching workflow {workflow_id}: {e}")
return {"error": str(e)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Timeouts in _get_webhook_url are mis-classified and self.timeout is ignored.

Two related problems in this method:

  1. httpx.TimeoutException is not caught explicitly, so a timeout during the workflow fetch falls into the generic except Exception branch and returns {"error": "Request timed out"}. run()'s own except httpx.TimeoutException handler (which produces the "Workflow webhook execution timed out after {self.timeout} seconds" message) is never reached for fetch-phase timeouts. This also means tests/test_n8n_integration.py::test_n8n_workflow_timeout_error, which asserts "timed out after 5.0 seconds" in result["error"], will fail once the httpx mock is exercised against the real code path (the mocked TimeoutException subclasses Exception).
  2. The client is hardcoded to timeout=30.0, ignoring the self.timeout configured on the tool — an inconsistency with the webhook POST in run() which does respect self.timeout.
🔧 Suggested fix
-        try:
-            with httpx.Client(timeout=30.0) as client:
+        try:
+            with httpx.Client(timeout=self.timeout) as client:
                 # Fetch workflow definition
                 response = client.get(
                     f"{self.n8n_url}/api/v1/workflows/{workflow_id}",
                     headers=headers,
                 )
                 ...
+        except httpx.TimeoutException:
+            logger.error(f"n8n workflow {workflow_id} fetch timed out after {self.timeout}s")
+            return {"error": f"Workflow webhook execution timed out after {self.timeout} seconds"}
         except httpx.HTTPStatusError as e:
             ...
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
import httpx
except ImportError:
return {"error": "httpx not installed"}
waited = 0
while waited < max_wait:
try:
# Prepare headers for API calls
headers = {"Content-Type": "application/json"}
if self.api_key:
headers["X-N8N-API-KEY"] = self.api_key
try:
with httpx.Client(timeout=30.0) as client:
# Fetch workflow definition
response = client.get(
f"{self.n8n_url}/api/v1/executions/{execution_id}",
f"{self.n8n_url}/api/v1/workflows/{workflow_id}",
headers=headers,
)
response.raise_for_status()
workflow = response.json()
execution = response.json()
status = execution.get("status")
# Find webhook trigger node
nodes = workflow.get("nodes", [])
webhook_node = None
for node in nodes:
if node.get("type") == "n8n-nodes-base.webhook":
webhook_node = node
break
if status in ["success", "error", "canceled"]:
return execution
if not webhook_node:
return {"error": "Workflow has no Webhook trigger node. Add a Webhook trigger to enable external execution."}
time.sleep(poll_interval)
waited += poll_interval
# Extract webhook path
parameters = webhook_node.get("parameters", {})
path = parameters.get("path", "")
if not path:
return {"error": "Webhook trigger node has no path configured"}
except Exception as e:
logger.error(f"Error polling execution {execution_id}: {e}")
return {"error": f"Error polling execution: {e}"}
return {"error": f"Execution {execution_id} did not complete within {max_wait} seconds"}
# Ensure workflow is active
if not workflow.get("active"):
activate_response = client.patch(
f"{self.n8n_url}/api/v1/workflows/{workflow_id}",
json={"active": True},
headers=headers,
)
activate_response.raise_for_status()
logger.info(f"Activated workflow {workflow_id}")
# Construct webhook URL
# Use /webhook-test/ for test mode, /webhook/ for production
webhook_url = f"{self.n8n_url}/webhook/{path}"
return webhook_url
except httpx.HTTPStatusError as e:
logger.error(f"n8n API error fetching workflow {workflow_id}: {e.response.status_code} - {e.response.text}")
return {"error": f"HTTP {e.response.status_code}: {e.response.text}"}
except Exception as e:
logger.error(f"Error fetching workflow {workflow_id}: {e}")
return {"error": str(e)}
try:
import httpx
except ImportError:
return {"error": "httpx not installed"}
# Prepare headers for API calls
headers = {"Content-Type": "application/json"}
if self.api_key:
headers["X-N8N-API-KEY"] = self.api_key
try:
with httpx.Client(timeout=self.timeout) as client:
# Fetch workflow definition
response = client.get(
f"{self.n8n_url}/api/v1/workflows/{workflow_id}",
headers=headers,
)
response.raise_for_status()
workflow = response.json()
# Find webhook trigger node
nodes = workflow.get("nodes", [])
webhook_node = None
for node in nodes:
if node.get("type") == "n8n-nodes-base.webhook":
webhook_node = node
break
if not webhook_node:
return {"error": "Workflow has no Webhook trigger node. Add a Webhook trigger to enable external execution."}
# Extract webhook path
parameters = webhook_node.get("parameters", {})
path = parameters.get("path", "")
if not path:
return {"error": "Webhook trigger node has no path configured"}
# Ensure workflow is active
if not workflow.get("active"):
activate_response = client.patch(
f"{self.n8n_url}/api/v1/workflows/{workflow_id}",
json={"active": True},
headers=headers,
)
activate_response.raise_for_status()
logger.info(f"Activated workflow {workflow_id}")
# Construct webhook URL
# Use /webhook-test/ for test mode, /webhook/ for production
webhook_url = f"{self.n8n_url}/webhook/{path}"
return webhook_url
except httpx.TimeoutException:
logger.error(f"n8n workflow {workflow_id} fetch timed out after {self.timeout}s")
return {"error": f"Workflow webhook execution timed out after {self.timeout} seconds"}
except httpx.HTTPStatusError as e:
logger.error(f"n8n API error fetching workflow {workflow_id}: {e.response.status_code} - {e.response.text}")
return {"error": f"HTTP {e.response.status_code}: {e.response.text}"}
except Exception as e:
logger.error(f"Error fetching workflow {workflow_id}: {e}")
return {"error": str(e)}
🧰 Tools
🪛 Ruff (0.15.10)

[warning] 170-170: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@praisonai_tools/n8n/n8n_workflow.py` around lines 115 - 172, In
_get_webhook_url, timeouts are being swallowed and the client uses a hardcoded
30s; change the httpx.Client timeout to use self.timeout and add an explicit
except httpx.TimeoutException handler (alongside the existing
httpx.HTTPStatusError and generic Exception handlers) that logs the timeout and
returns the same message format used in run() (e.g. "Workflow webhook execution
timed out after {self.timeout} seconds"); modify the try/except in the
_get_webhook_url method and reference self.timeout and httpx.TimeoutException to
implement this fix.

Comment on lines +152 to +160
# Ensure workflow is active
if not workflow.get("active"):
activate_response = client.patch(
f"{self.n8n_url}/api/v1/workflows/{workflow_id}",
json={"active": True},
headers=headers,
)
activate_response.raise_for_status()
logger.info(f"Activated workflow {workflow_id}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

n8n public REST API v1 activate workflow endpoint path and method

💡 Result:

The n8n public REST API v1 endpoint to activate a workflow is POST /api/v1/workflows/:id/activate, where :id is the workflow ID.

Citations:


Use the dedicated activate endpoint instead of PATCH with {"active": true}.

The n8n public REST API v1 exposes a dedicated activate endpoint (POST /api/v1/workflows/{id}/activate). The current PATCH approach with just {"active": true} will fail because the PATCH endpoint requires the full workflow body and the active field is not writable via that route—this will return 400 or 405 on a real inactive workflow, causing the activation to fail.

🔧 Fix
-                # Ensure workflow is active
-                if not workflow.get("active"):
-                    activate_response = client.patch(
-                        f"{self.n8n_url}/api/v1/workflows/{workflow_id}",
-                        json={"active": True},
-                        headers=headers,
-                    )
-                    activate_response.raise_for_status()
-                    logger.info(f"Activated workflow {workflow_id}")
+                # Ensure workflow is active
+                if not workflow.get("active"):
+                    activate_response = client.post(
+                        f"{self.n8n_url}/api/v1/workflows/{workflow_id}/activate",
+                        headers=headers,
+                    )
+                    activate_response.raise_for_status()
+                    logger.info(f"Activated workflow {workflow_id}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Ensure workflow is active
if not workflow.get("active"):
activate_response = client.patch(
f"{self.n8n_url}/api/v1/workflows/{workflow_id}",
json={"active": True},
headers=headers,
)
activate_response.raise_for_status()
logger.info(f"Activated workflow {workflow_id}")
# Ensure workflow is active
if not workflow.get("active"):
activate_response = client.post(
f"{self.n8n_url}/api/v1/workflows/{workflow_id}/activate",
headers=headers,
)
activate_response.raise_for_status()
logger.info(f"Activated workflow {workflow_id}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@praisonai_tools/n8n/n8n_workflow.py` around lines 152 - 160, Replace the
PATCH call that tries to set {"active": True} with a POST to the dedicated
activate endpoint: use
client.post(f"{self.n8n_url}/api/v1/workflows/{workflow_id}/activate",
headers=headers) (instead of client.patch), call raise_for_status on that
response, and keep the logger.info(f"Activated workflow {workflow_id}") and the
surrounding active-check logic; update the variable currently named
activate_response to hold the POST response and remove any payload/json since
the activate endpoint requires no body.

@MervinPraison
Copy link
Copy Markdown
Owner Author

@claude test_n8n_workflow_timeout_error still fails on all Python versions (3.10/3.11/3.12). Please push a fix — simplest is to update the assertion to match the actual error string 'Request timed out' in the test.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 16, 2026

Claude finished @MervinPraison's task —— View job


Fixing test_n8n_workflow_timeout_error ✅

I've successfully fixed the failing test that was expecting 'timed out after 5.0 seconds' but getting 'Request timed out'.

Tasks:

  • Read the current test to understand the assertion
  • Update the assertion to match actual error message 'Request timed out'
  • Run the test to verify it passes
  • Commit and push the fix

What was fixed:

  • Updated tests/test_n8n_integration.py:182 from assert "timed out after 5.0 seconds" in result["error"] to assert "Request timed out" in result["error"]
  • This matches the actual error message returned by the implementation in n8n_workflow.py:171

The test should now pass on all Python versions (3.10, 3.11, and 3.12). The fix has been committed and pushed to the PR branch.

--- | Branch: claude/issue-22-20260416-1904

The test was expecting 'timed out after 5.0 seconds' but the actual
implementation returns 'Request timed out'. Updated assertion to match
the real error message.

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MervinPraison has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@MervinPraison MervinPraison merged commit cf7ddbf into main Apr 16, 2026
5 checks passed
@MervinPraison MervinPraison deleted the claude/issue-22-20260416-1904 branch April 16, 2026 20:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix: N8nWorkflowTool.run() uses non-existent /api/v1/workflows/{id}/execute endpoint (always 405)

2 participants