Skip to content

fix: prevent multi-step confabulation + ensure AI nodes in results#19

Open
standujar wants to merge 3 commits intomainfrom
fix/multi-step-loop-and-ai-nodes
Open

fix: prevent multi-step confabulation + ensure AI nodes in results#19
standujar wants to merge 3 commits intomainfrom
fix/multi-step-loop-and-ai-nodes

Conversation

@standujar
Copy link
Copy Markdown
Collaborator

@standujar standujar commented Feb 20, 2026

Summary

  • Bug 1 — Multi-step loop confabulation: Callbacks now carry data: { awaitingUserInput: true } so the cloud multi-step loop breaks correctly after preview/clarification/auth-required states. Removes the dead originMessageId guard.
  • Bug 2 — AI nodes missing from search: supplementAINodes() ensures OpenAI and AI Transform nodes appear in search results when AI-intent keywords are detected, even when service keywords (gmail, trigger) dominate the top 15 scores.
  • Bug 1 variant in modifyExistingWorkflow: Same missing awaitingUserInput signal patched in the modify-existing action callback.
  • Tests updated: 8 new awaitingUserInput tests, 2 new AI node scoring tests, removed obsolete originMessageId tests.
  • Version: 1.2.2 → 1.2.3

Changed files

File What changed
src/actions/createWorkflow.ts 7 callbacks patched with awaitingUserInput, originMessageId guard removed
src/actions/modifyExistingWorkflow.ts Callback patched with awaitingUserInput
src/services/n8n-workflow-service.ts supplementAINodes() + 17 AI-intent keywords
src/types/index.ts originMessageId removed from WorkflowDraft
__tests__/integration/actions/createWorkflow.test.ts 8 new tests, 1 removed, 1 updated
__tests__/unit/catalog.test.ts 2 new tests documenting AI node scoring gap
package.json Version bump 1.2.2 → 1.2.3

Test plan

  • All 313 tests pass (bun test)
  • Build clean (bun run build)
  • Deploy to staging and verify multi-step agent conversation (10+ LLM calls) completes without confabulation
  • Verify AI-intent prompts ("summarize my emails") include OpenAI node in generated workflows

Summary by CodeRabbit

  • New Features

    • AI-related nodes now auto-supplement search results when AI keywords are detected.
    • Delete workflow now requires a two-step confirmation (prompt then confirm).
  • Improvements

    • Callbacks and previews consistently signal "awaiting user input" across generation, modification, auth, deploy, and error flows.
    • Deployment responses include credential-related warnings when integrations are missing.
    • Workflow listings omit last-run details and simplify output.
  • Tests

    • Expanded integration and unit tests for awaiting-user-input flows and AI node search.
  • Chores

    • Package version bumped to 1.2.3.

…rch results

Bug 1: Callbacks now carry data: { awaitingUserInput: true } so the cloud
multi-step loop breaks correctly after preview/clarification/auth-required
states. Removes the originMessageId guard (dead code once callbacks signal
correctly).

Bug 2: supplementAINodes() ensures OpenAI and AI Transform nodes appear in
search results when AI-intent keywords are detected, even when service
keywords (gmail, trigger) dominate the scores.

Also patches modifyExistingWorkflow callback (same bug class as Bug 1).
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 20, 2026

Walkthrough

Removes originMessageId from drafts, adds data: { awaitingUserInput: true } to many action callbacks, supplements node search results with AI nodes for AI-related keywords, centralizes draft TTL, introduces two-step pending deletion flow, and updates numerous tests and HTTP status/response behaviors.

Changes

Cohort / File(s) Summary
Action handlers & draft flow
src/actions/createWorkflow.ts, src/actions/modifyExistingWorkflow.ts, src/actions/activateWorkflow.ts
Removed originMessageId and the same-origin draft guard; dropped messageId from generate/preview signatures; import shared DRAFT_TTL_MS; callbacks now frequently include data: { awaitingUserInput: true }.
Deletion flow
src/actions/deleteWorkflow.ts
Added pending-deletion confirmation flow with TTL-backed PendingDeletion cache, first-step confirmation prompt (awaitingUserInput) and second-step actual deletion on confirmation.
Node search / workflow service
src/services/n8n-workflow-service.ts
Added private supplementAINodes to detect AI-related keywords and append OpenAI/AI Transform nodes to search results; enhanced deployWorkflow error handling to create workflow on 404.
Types & constants
src/types/index.ts, src/utils/constants.ts
Removed originMessageId?: string from WorkflowDraft; added exported DRAFT_TTL_MS (30 * 60 * 1000) and switched local TTLs to use it.
Providers & status
src/providers/pendingDraft.ts, src/providers/workflowStatus.ts
Pending draft provider now imports shared TTL; workflowStatus removed per-workflow last-execution fetch and "Last run" line.
Routes & utils
src/routes/workflows.ts, src/utils/credentialResolver.ts, src/utils/workflow.ts
create/update workflow routes return 424 on missing credentials and include warnings array; credentialResolver now omits non-HTTPS authUrl values; trigger/orphan detection logic tightened to use isTriggerNode only.
Tests
__tests__/integration/actions/createWorkflow.test.ts, __tests__/unit/catalog.test.ts, __tests__/e2e/lifecycle.test.ts, __tests__/integration/actions/lifecycleActions.test.ts, __tests__/integration/providers/providers.test.ts, __tests__/unit/routes/workflows.test.ts
Extensive test updates: removed originMessageId/anti-loop assertions; added many assertions for awaitingUserInput in callback data; added AI node scoring tests; adapted deletion tests to two-step confirmation; adjusted user IDs and expected HTTP status for missing integrations.
Package metadata
package.json
Version bumped from 1.2.2 to 1.2.3.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Handler as Action Handler
    participant Generator as Workflow Generator
    participant Search as Node Search Service
    participant Cache as Draft/Deletion Cache
    participant Callback as Callback Handler

    User->>Handler: Request create/modify workflow (with keywords)
    Handler->>Generator: generateAndPreview(...)
    Generator->>Search: searchNodes(keywords)
    Search->>Search: supplementAINodes(keywords) 
    Search-->>Generator: combinedNodes
    Generator->>Cache: persist draft (DRAFT_TTL_MS)
    Generator->>Callback: callback(text, data: { awaitingUserInput: true })
    Callback-->>User: Prompt for confirmation/input
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through drafts and left no trace,
Awaiting your input in every place,
AI sprouts where keywords bloom,
Two-step deletes keep danger from doom,
A tiny rabbit cheers: "Ready to race!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the two main bugs being addressed: preventing multi-step confabulation (via awaitingUserInput callbacks) and ensuring AI nodes appear in search results (via supplementAINodes). It is concise, specific, and directly reflects the core changes.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/multi-step-loop-and-ai-nodes

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

@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: 1

🧹 Nitpick comments (1)
src/services/n8n-workflow-service.ts (1)

151-189: Consider hoisting AI_INTENT_KEYWORDS to a module-level constant.

The Set is re-created on every invocation. Since the values are static, moving it outside the method avoids redundant allocations and makes the keyword list easier to locate and maintain.

+const AI_INTENT_KEYWORDS = new Set([
+  'summarize', 'summary', 'translate', 'translation', 'classify',
+  'categorize', 'extract', 'analyze', 'analysis', 'sentiment',
+  'rewrite', 'detect', 'ai', 'llm', 'gpt', 'openai',
+]);
+
 // Inside the class:
-  private supplementAINodes(...) {
-    const AI_INTENT_KEYWORDS = new Set([...]);
+  private supplementAINodes(
+    mainResults: NodeSearchResult[],
+    keywords: string[]
+  ): NodeSearchResult[] {
     const hasAIIntent = keywords.some((kw) => AI_INTENT_KEYWORDS.has(kw.toLowerCase()));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/n8n-workflow-service.ts` around lines 151 - 189, Hoist the
AI_INTENT_KEYWORDS Set out of supplementAINodes to a module-level constant
(e.g., define const AI_INTENT_KEYWORDS = new Set([...]) at the top of the file)
and remove the local declaration inside the supplementAINodes method; keep the
same lowercase keyword values and preserve the call that checks
keywords.some((kw) => AI_INTENT_KEYWORDS.has(kw.toLowerCase())). Update any
imports/exports only if you need to reuse the constant elsewhere, and ensure
supplementAINodes now references the top-level AI_INTENT_KEYWORDS symbol.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/services/n8n-workflow-service.ts`:
- Around line 174-175: The early return after computing hasAIIntent violates the
`curly` rule; update the conditional to use braces around the return (e.g.,
change the single-line `if (!hasAIIntent) return mainResults;` to a braced
block) in the function where `const hasAIIntent = keywords.some((kw) =>
AI_INTENT_KEYWORDS.has(kw.toLowerCase()));` is declared so the `if` uses `{ ...
}` around the `return mainResults;`.

---

Nitpick comments:
In `@src/services/n8n-workflow-service.ts`:
- Around line 151-189: Hoist the AI_INTENT_KEYWORDS Set out of supplementAINodes
to a module-level constant (e.g., define const AI_INTENT_KEYWORDS = new
Set([...]) at the top of the file) and remove the local declaration inside the
supplementAINodes method; keep the same lowercase keyword values and preserve
the call that checks keywords.some((kw) =>
AI_INTENT_KEYWORDS.has(kw.toLowerCase())). Update any imports/exports only if
you need to reuse the constant elsewhere, and ensure supplementAINodes now
references the top-level AI_INTENT_KEYWORDS symbol.

Comment thread src/services/n8n-workflow-service.ts Outdated
@claude

This comment was marked as spam.

@claude

This comment was marked as outdated.

Comment thread src/services/n8n-workflow-service.ts
Comment thread src/services/n8n-workflow-service.ts
Comment thread src/actions/createWorkflow.ts
Comment thread src/actions/createWorkflow.ts
Comment thread src/types/index.ts
Comment thread __tests__/integration/actions/createWorkflow.test.ts
Comment thread __tests__/unit/catalog.test.ts
standujar added a commit to elizaOS/cloud that referenced this pull request Feb 20, 2026
Picks up two bug fixes from elizaos-plugins/plugin-n8n-workflow#19:

- Multi-step loop confabulation: callbacks now carry
  data: { awaitingUserInput: true } so the cloud multi-step loop
  breaks correctly after preview/clarification/auth-required states.
- AI nodes missing from search: supplementAINodes() ensures OpenAI
  and AI Transform nodes appear when AI-intent keywords are detected.
…y test

- Remove over-broad keywords (extract, analyze, detect, classify, categorize)
  that would inject AI nodes for non-AI prompts like "extract attachments"
- Add braces around early return to fix ESLint curly rule (CI failure)
- Add comment clarifying show_preview test uses intentionally invalid intent
- Remove historical comment about originMessageId
@claude

This comment was marked as outdated.

Comment thread src/services/n8n-workflow-service.ts Outdated
mainResults: NodeSearchResult[],
keywords: string[]
): NodeSearchResult[] {
const AI_INTENT_KEYWORDS = new Set([
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI_INTENT_KEYWORDS is a Set allocated on every call to supplementAINodes. Because this method is invoked for both generateWorkflowDraft and modifyWorkflowDraft, it allocates and GC-collects a new Set on every user turn. Move it to module (or class-static) scope:

Suggested change
const AI_INTENT_KEYWORDS = new Set([
private static readonly AI_INTENT_KEYWORDS = new Set([
'summarize',
'summary',
'translate',
'translation',
'sentiment',
'rewrite',
'ai',
'llm',
'gpt',
'openai',
]);
private supplementAINodes(
mainResults: NodeSearchResult[],
keywords: string[]
): NodeSearchResult[] {
const AI_INTENT_KEYWORDS = N8nWorkflowService.AI_INTENT_KEYWORDS;

Comment thread src/services/n8n-workflow-service.ts Outdated
});
if (callback) {
await callback({ text, success: true });
await callback({ text, success: true, data: { awaitingUserInput: true } });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Behaviour question: AUTH_REQUIRED now signals awaitingUserInput: true

With this change, the AUTH_REQUIRED callback (unresolved credentials) now carries data: { awaitingUserInput: true }. This is correct for the multi-step loop — the agent waits for the user to connect their services and re-confirm.

However, the draft is not deleted after the credentials check returns missing connections. If the user never connects the services, the draft remains cached for 30 minutes and a second "yes" attempt will re-run deployWorkflow again. This is probably the intended behaviour (retry without regenerating), but it is worth a comment explaining why the cache key is deliberately left intact here.

Comment thread src/actions/createWorkflow.ts
Comment thread src/types/index.ts
… shared constants, security hardening

- Narrow broad catch to 404 only when updating workflows (prevents duplicate creation)
- Add confirm-before-delete flow with cache-based pending deletion and 5min TTL
- Extract DRAFT_TTL_MS to shared constant (was duplicated in 4 files)
- Return HTTP 424 instead of 200 for missing credentials in REST routes
- Remove N+1 API calls in workflowStatus provider (no per-workflow execution fetch)
- Validate authUrl starts with https:// before embedding in Markdown
- Fix overly broad trigger detection (use node.type only, not node.name)
- Hoist AI_INTENT_KEYWORDS to module level (avoid re-creating Set per call)
- Clarify draft retention after AUTH_REQUIRED (intentional retry without regeneration)
- Update tests to match new confirm-before-delete and 424 status behavior
@claude
Copy link
Copy Markdown

claude bot commented Feb 20, 2026

Review: fix/multi-step-loop-and-ai-nodes

Overall Assessment

The core fixes are sound — propagating awaitingUserInput through all callback paths is a clean and consistent improvement over the message-ID guard, and the 404-specific catch in deployWorkflow is a clear regression fix. The DRAFT_TTL_MS extraction and the HTTP 424 status correction are nice clean-ups.

Several issues need attention before merge, ranging from a potential runtime error to fragile UX in the new delete-confirmation flow.


Issues

🔴 High — Delete confirmation may never fire

The delete action's examples array only shows delete-request messages ("Delete the old payment workflow", etc.). There are no examples with confirmation-type messages ("yes", "confirm", "do it"). ElizaOS uses these examples for action routing — without them, there's a real risk the LLM router will not select DELETE_N8N_WORKFLOW when the user replies "yes" to the confirmation prompt, leaving the pending deletion in cache until the 5-minute TTL expires.

There's also no provider (analogous to pendingDraftProvider) injecting pending-deletion context into the prompt, so the LLM has no explicit signal that a delete confirmation is expected.

Suggested mitigations:

  • Add a pendingDeletion provider similar to pendingDraftProvider that injects pending-deletion context into the system prompt.
  • Add at least one two-turn example covering the confirmation step to the examples array.

🟡 Medium — Strict regex silently cancels on natural confirmations

/^(yes|confirm|ok|do it|go ahead|oui|y)$/i — the ^ and $ anchors mean input like "yes please", "yes!", "Yes, go ahead with it" or "Yes.", all cancel the deletion silently instead of confirming it. Users will lose their deletion request without knowing why. Consider a looser match (e.g. test if the text contains one of the confirm words) or an LLM-based intent classifier consistent with the rest of the codebase.

🟡 Medium — AI_INTENT_KEYWORDS is too narrow and hardcodes vendor names

The set has 10 keywords and includes vendor names (gpt, openai) but misses common AI intent words: analyze, classify, extract, generate, chat, embedding, claude, anthropic, gemini. More importantly, supplementAINodes always injects OpenAI and AI Transform nodes regardless of what AI provider the user intends; a user asking for a Claude-based workflow won't get Claude nodes supplemented.

🟡 Medium — DELETE_CONFIRM_TTL_MS not extracted to constants

This PR moves DRAFT_TTL_MS to src/utils/constants.ts for consistency, but the new DELETE_CONFIRM_TTL_MS remains a module-level magic number in deleteWorkflow.ts. It should follow the same pattern.

🟡 Medium — workflowStatus provider information regression without capability update

Removing the per-workflow execution fetch is a good performance trade-off, but the provider's name and capabilityDescription still imply richer workflow status info than what is now returned. The removed test assertion (expect(result.text).toContain('success')) confirms execution status context is gone — downstream agents relying on that context will silently get less data.

🟠 Low — authUrl validation only checks scheme

result.authUrl.startsWith('https://') prevents plain-HTTP redirect URLs, which is a good call. But it still allows private-network HTTPS URLs (https://169.254.x.x, https://internal.corp) if a credential provider is compromised. At minimum, add a try { new URL(authUrl) } catch guard so a malformed string doesn't propagate — and consider documenting the deliberate scope of this check.

🟠 Low — Trigger-node detection regression risk

Removing the node.name.toLowerCase().includes('start') heuristic is a clean-up, but the LLM occasionally emits a generic node with name: 'Start' and a non-trigger type. With the old code this was silently accepted; now it will produce a "Workflow has no trigger node" validation warning (potentially escalating to a generation error if the LLM fix loop doesn't handle it). Worth tracking in staging.


Positives

  • The awaitingUserInput propagation across all callback paths is consistent and well-tested (8 new integration tests covering every branch).
  • 404-specific catch in deployWorkflow correctly limits error swallowing to the one case that warrants a fallback.
  • HTTP 424 for missing integrations is semantically correct and the test was updated.
  • DRAFT_TTL_MS centralisation prevents future drift across 4+ files.
  • The delete-confirmation UX pattern (ask → confirm → act) is the right approach; it just needs routing support to be reliable.

const pending = await runtime.getCache<PendingDeletion>(cacheKey);
if (pending && Date.now() - pending.createdAt < DELETE_CONFIRM_TTL_MS) {
const userText = (message.content?.text || '').toLowerCase().trim();
const isConfirm = /^(yes|confirm|ok|do it|go ahead|oui|y)$/i.test(userText);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The ^...$ anchors make this stricter than expected. Natural confirmation phrases like "yes please", "yes!", "Yes, do it.", or "yes go ahead" all fail the match and silently cancel the deletion instead.

Consider a contains-style check or an LLM intent classifier (consistent with classifyDraftIntent elsewhere):

Suggested change
const isConfirm = /^(yes|confirm|ok|do it|go ahead|oui|y)$/i.test(userText);
const isConfirm = /\b(yes|confirm|ok|do it|go ahead|oui)\b/i.test(userText) || userText === 'y';

Or, if strictness is intentional (to avoid false positives), at least tell the user why the deletion was cancelled so they can retype an explicit confirmation.

return { success: true };
}

// Not a confirm — cancel the pending deletion
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When the input is not a confirmation keyword, the pending deletion is cancelled and the user is told "Deletion cancelled." — but they may have simply made a typo or asked a follow-up question. Consider distinguishing:

  • Explicit cancel words (no, cancel, stop, never mind) → cancel + inform
  • Unrecognised input → re-prompt ("Please reply 'yes' to confirm or 'no' to cancel"), keep the pending in cache

As written, any message that doesn't exactly match the confirm regex clears the cache, so the user has to restart the whole deletion flow.

import { matchWorkflow } from '../utils/generation';
import { buildConversationContext } from '../utils/context';

const DELETE_CONFIRM_TTL_MS = 5 * 60 * 1000;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR centralises DRAFT_TTL_MS into src/utils/constants.ts, but DELETE_CONFIRM_TTL_MS remains a local magic number. For consistency and to make it easy to tune from one place, move it to the same constants file.

Suggested change
const DELETE_CONFIRM_TTL_MS = 5 * 60 * 1000;
import { DELETE_CONFIRM_TTL_MS } from '../utils/constants';


try {
const userId = message.entityId;
const cacheKey = `workflow_delete_pending:${userId}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LLM routing gap: The confirmation loop relies on the ElizaOS orchestrator re-invoking DELETE_N8N_WORKFLOW when the user replies "yes". But the action's examples array only shows initial delete requests — there are no two-turn confirmation examples, and no provider (analogous to pendingDraftProvider) injects pending-deletion context into the system prompt.

Without this context, the LLM may route "yes" to a different action (e.g. CREATE_N8N_WORKFLOW if there's also a pending draft) or to no action at all, leaving the pending deletion in cache until the 5-minute TTL silently expires.

Consider adding:

  1. A pendingDeletionProvider that tells the LLM a deletion is awaiting confirmation.
  2. A two-turn example in the examples array to improve action routing.

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.

🧹 Nitpick comments (2)
src/services/n8n-workflow-service.ts (1)

155-166: Move AI_INTENT_KEYWORDS to a module-level constant.

The Set is reconstructed on every invocation of supplementAINodes. Hoisting it to module scope eliminates the redundant allocation.

♻️ Proposed refactor
+const AI_INTENT_KEYWORDS = new Set([
+  'summarize',
+  'summary',
+  'translate',
+  'translation',
+  'sentiment',
+  'rewrite',
+  'ai',
+  'llm',
+  'gpt',
+  'openai',
+]);

 private supplementAINodes(
   mainResults: NodeSearchResult[],
   keywords: string[]
 ): NodeSearchResult[] {
-  const AI_INTENT_KEYWORDS = new Set([
-    'summarize',
-    'summary',
-    'translate',
-    'translation',
-    'sentiment',
-    'rewrite',
-    'ai',
-    'llm',
-    'gpt',
-    'openai',
-  ]);
-
   const hasAIIntent = keywords.some((kw) => AI_INTENT_KEYWORDS.has(kw.toLowerCase()));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/n8n-workflow-service.ts` around lines 155 - 166, The Set
constant AI_INTENT_KEYWORDS is being recreated inside supplementAINodes on every
call; move its declaration to module scope (top-level) so it is constructed once
and reused. Locate AI_INTENT_KEYWORDS and the function supplementAINodes in
src/services/n8n-workflow-service.ts, hoist the new Set declaration out of the
function to module-level scope, and update supplementAINodes to reference the
hoisted AI_INTENT_KEYWORDS without changing its name or semantics.
__tests__/integration/actions/createWorkflow.test.ts (1)

894-913: Consider scoping the negative assertions to awaitingUserInput only.

Lines 912 and 933 assert expect(lastResult?.data).toBeUndefined(), which will break if any future change adds an unrelated field to data on successful deploy or cancel (e.g., { deployedId: 'wf-001' }). Scoping to the specific field tested by this suite is more resilient.

♻️ Proposed refactor
-      expect(lastResult?.data).toBeUndefined();
+      expect((lastResult?.data as Record<string, unknown>)?.awaitingUserInput).toBeUndefined();

Apply the same change at line 933.

Also applies to: 915-934

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

In `@__tests__/integration/actions/createWorkflow.test.ts` around lines 894 - 913,
The assertion currently checks the entire payload with
expect(lastResult?.data).toBeUndefined(), which is brittle; change it to only
assert the awaitingUserInput field is absent by replacing that assertion with
expect(lastResult?.data?.awaitingUserInput).toBeUndefined() (and make the same
change in the sibling test around the cancel case), keeping the rest of the test
that invokes createWorkflowAction.handler, getLastCallbackResult(callback) and
uses the same mock callback unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/services/n8n-workflow-service.ts`:
- Around line 169-171: The early-return now correctly uses braces around the if
statement (if (!hasAIIntent) { return mainResults; }), but the review contains a
leftover duplicate comment marker; remove the redundant
"[duplicate_comment]"/duplicate review note from the PR/comments and ensure the
code around hasAIIntent and mainResults in src/services/n8n-workflow-service.ts
remains unchanged and passes linting.

---

Nitpick comments:
In `@__tests__/integration/actions/createWorkflow.test.ts`:
- Around line 894-913: The assertion currently checks the entire payload with
expect(lastResult?.data).toBeUndefined(), which is brittle; change it to only
assert the awaitingUserInput field is absent by replacing that assertion with
expect(lastResult?.data?.awaitingUserInput).toBeUndefined() (and make the same
change in the sibling test around the cancel case), keeping the rest of the test
that invokes createWorkflowAction.handler, getLastCallbackResult(callback) and
uses the same mock callback unchanged.

In `@src/services/n8n-workflow-service.ts`:
- Around line 155-166: The Set constant AI_INTENT_KEYWORDS is being recreated
inside supplementAINodes on every call; move its declaration to module scope
(top-level) so it is constructed once and reused. Locate AI_INTENT_KEYWORDS and
the function supplementAINodes in src/services/n8n-workflow-service.ts, hoist
the new Set declaration out of the function to module-level scope, and update
supplementAINodes to reference the hoisted AI_INTENT_KEYWORDS without changing
its name or semantics.

'ai',
'llm',
'gpt',
'openai',
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The keyword set is narrow and vendor-specific. Common AI intent words that are missing: analyze, classify, extract, generate, chat, embedding, claude, anthropic, gemini, mistral. A user asking "summarize Slack messages with Claude" would match (summarize is present), but "analyze my emails with Anthropic" would not.

Also, several of these — gpt, openai — are vendor names rather than intents, which means the supplement only helps users who specifically name OpenAI, not users who want AI capability in general.

return mainResults;
}

const aiResults = searchNodes(['openai', 'ai transform'], 5);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This hardcodes openai and ai transform as the supplemented nodes. Any user asking for Claude, Gemini, Mistral, or another AI provider gets OpenAI nodes injected instead of the provider they want. If the intent is to ensure some AI node is present, this may produce misleading workflow previews.

Consider either:

  • Supplementing a broader set of LangChain/AI nodes (all @n8n/n8n-nodes-langchain.*)
  • Or resolving the specific AI provider from the keywords before searching


if (result?.status === 'needs_auth') {
missingConnections.push({ credType, authUrl: result.authUrl });
const authUrl = result.authUrl.startsWith('https://') ? result.authUrl : undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good security improvement — prevents non-HTTPS redirect URLs from propagating to the UI. A couple of follow-up hardening suggestions:

  1. The CredentialProviderResult type declares authUrl: string (not optional), but external providers could misbehave and return undefined. Wrapping in typeof result.authUrl === 'string' && costs nothing and prevents a potential TypeError if a provider doesn't honour the contract.

  2. The scheme check doesn't prevent SSRF-style private-network URLs (https://169.254.169.254, https://internal.corp). If the plugin is deployed server-side, consider documenting this scope limitation or adding a hostname allowlist.

Comment thread src/utils/workflow.ts
const hasTrigger = workflow.nodes.some(
(node) => isTriggerNode(node.type) || node.name.toLowerCase().includes('start')
);
const hasTrigger = workflow.nodes.some((node) => isTriggerNode(node.type));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Removing the node.name.toLowerCase().includes('start') heuristic is the right correctness call. Worth noting: the LLM occasionally emits a generic node with name: "Start" and a non-trigger type (e.g. n8n-nodes-base.noOp). The old code silently ignored this; the new code will produce a "Workflow has no trigger node" validation warning for those workflows.

This is a net improvement in strictness, but it may surface as a new class of generation warnings in staging. Keeping an eye on it during the staging test checklist is worthwhile.

}

status += '\n';
status += ` Nodes: ${workflow.nodes?.length || 0}\n\n`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Removing the per-workflow execution fetch is a good performance improvement (was N+1 API calls per status request). However, the provider's name (n8n_workflow_status) and the removed test assertion (expect(result.text).toContain('success')) confirm that execution status is silently no longer available to downstream agents.

If any agent prompt or capability description mentions "last execution status", it should be updated to reflect this. Consider adding a note in the provider's JSDoc or a separate workflowExecutionsProvider for callers that need execution history.

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
__tests__/integration/providers/providers.test.ts (2)

268-334: ⚠️ Potential issue | 🔴 Critical

pendingDraftProvider tests broken by cache key / entityId mismatch — pipeline failure.

The createMockMessage calls were updated to use entityId: 'custom-user-0000-0000-000000000001', but the four cache setup objects still use the key 'workflow_draft:user-001'. pendingDraftProvider constructs cacheKey = \workflow_draft:${message.entityId}`and therefore looks up'workflow_draft:custom-user-0000-0000-000000000001'`, which is absent.

Impact per test:

Test Line Result
returns draft info when draft exists 286 FAILS (CI-confirmed)
returns empty for expired draft 310–311 passes for wrong reason (key not found, not TTL expiry)
includes node names in text 332–333 FAILS
scoped to user — no draft for other user 354 passes for wrong reason
🐛 Proposed fix — update all four cache-key strings to match the new entityId
-  'workflow_draft:user-001': {
+  'workflow_draft:custom-user-0000-0000-000000000001': {
     workflow: draftWorkflow,
     prompt: 'Send gmail to telegram',
-    userId: 'user-001',
+    userId: 'custom-user-0000-0000-000000000001',
     createdAt: Date.now(),
   },

Apply the same replacement to the cache blocks in the returns empty for expired draft test (lines ~294-300), the includes node names in text test (lines ~316-322), and the scoped to user — no draft for other user test (lines ~338-344).

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

In `@__tests__/integration/providers/providers.test.ts` around lines 268 - 334,
The tests fail because createMockMessage now uses entityId
'custom-user-0000-0000-000000000001' but the cache entries still use the old key
'workflow_draft:user-001', so pendingDraftProvider.get (which builds cacheKey =
`workflow_draft:${message.entityId}`) can't find the drafts; update each cache
object used in the tests (the four occurrences in the tests including in the
"returns draft info when draft exists", "returns empty for expired draft",
"includes node names in text", and "scoped to user — no draft for other user"
blocks) to use the matching key
'workflow_draft:custom-user-0000-0000-000000000001' so pendingDraftProvider.get
locates the intended draft entries when calling createMockMessage with that
entityId.

154-207: ⚠️ Potential issue | 🟡 Minor

getWorkflowExecutions mocks are dead code after the N+1 removal — stale tests.

workflowStatus.ts no longer calls getWorkflowExecutions (N+1 queries removed). Both tests affected:

  • includes workflow status and execution info (line 154): the getWorkflowExecutions mock is never invoked; test name is now misleading.
  • handles execution fetch error per workflow (line 188): test intent ("Should still return workflow info even if executions fail") no longer applies.

Both tests still pass by coincidence because the workflow listing path is unchanged, but they are verifying non-existent behavior.

♻️ Suggested cleanup

For includes workflow status and execution info:

-    getWorkflowExecutions: mock(() =>
-      Promise.resolve([
-        createExecution({
-          status: 'success',
-          startedAt: '2025-01-15T10:30:00.000Z',
-        }),
-      ])
-    ),

For handles execution fetch error per workflow, consider removing the entire test (it tests removed behavior) or renaming it to reflect what it actually verifies today (graceful handling of listWorkflows not being a concern, since it doesn't throw in this path).

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

In `@__tests__/integration/providers/providers.test.ts` around lines 154 - 207,
The tests reference getWorkflowExecutions which is no longer used by
workflowStatusProvider.get; update tests accordingly by removing the dead
getWorkflowExecutions mocks and either rename or delete the affected tests: for
the "includes workflow status and execution info" test remove the
execution-related setup and rename to reflect it only verifies listWorkflows
output (e.g., "includes workflow status"), and for "handles execution fetch
error per workflow" either delete the test entirely or change it to assert
graceful behavior of workflowStatusProvider.get when listWorkflows returns
data/errors; locate usages in the tests named 'includes workflow status and
execution info' and 'handles execution fetch error per workflow' to make these
edits.
🧹 Nitpick comments (5)
src/actions/deleteWorkflow.ts (2)

15-15: Consider moving DELETE_CONFIRM_TTL_MS to src/utils/constants.ts for consistency.

The PR centralises DRAFT_TTL_MS in constants.ts; the new DELETE_CONFIRM_TTL_MS follows the same TTL-constant pattern and is a natural companion there.

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

In `@src/actions/deleteWorkflow.ts` at line 15, Move the DELETE_CONFIRM_TTL_MS
constant out of deleteWorkflow and into the shared constants module where
DRAFT_TTL_MS lives (constants.ts), then replace the local definition in
deleteWorkflow.ts with an import of DELETE_CONFIRM_TTL_MS from that constants
module; ensure the exported name matches existing usages and update the import
statements in deleteWorkflow.ts to reference the centralized constant.

113-113: Redundant i flag — userText is already lowercased.

userText is computed via .toLowerCase().trim() on the line above, so the /i flag on the confirmation regex is unnecessary.

-const isConfirm = /^(yes|confirm|ok|do it|go ahead|oui|y)$/i.test(userText);
+const isConfirm = /^(yes|confirm|ok|do it|go ahead|oui|y)$/.test(userText);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/deleteWorkflow.ts` at line 113, The regex used to set isConfirm
redundantly includes the case-insensitive flag while userText is already set via
.toLowerCase().trim(); update the pattern in deleteWorkflow.ts by removing the
/i flag (change /^(yes|confirm|ok|do it|go ahead|oui|y)$/i to
/^(yes|confirm|ok|do it|go ahead|oui|y)$/) so isConfirm uses the lowercase
userText correctly; ensure this change targets the isConfirm assignment where
userText is defined.
__tests__/integration/actions/lifecycleActions.test.ts (1)

289-303: Test name "delete success" is misleading — it tests only the confirmation prompt, not actual deletion.

The handler is called once (step 1 only), so lastResult is the confirmation prompt callback (success: true). No deletion actually occurs in this test. Consider renaming to delete confirmation prompt returns success: true in callback to reduce confusion.

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

In `@__tests__/integration/actions/lifecycleActions.test.ts` around lines 289 -
303, The test named "delete success returns success: true in callback" is
misleading because it only exercises the confirmation prompt (calling
deleteWorkflowAction.handler once) and asserts the prompt callback result;
rename the test to something explicit like "delete confirmation prompt returns
success: true in callback" and update the test description string in the test
block (the test(...) invocation) so it accurately references the confirmation
prompt; keep the existing call to deleteWorkflowAction.handler, the
createMockMessage with text 'Delete Stripe', createStateWithWorkflows, and
assertions as-is.
src/services/n8n-workflow-service.ts (1)

165-186: supplementAINodes implementation is clean and correct.

Dedup via existingNames Set prevents duplicates, and the method is appropriately scoped as private. The debug logging is helpful for diagnosing supplementation in production.

One minor observation: the search terms ['openai', 'ai transform'] are hardcoded. If additional AI nodes are added to the catalog later, they won't be picked up here. Consider co-locating these with AI_INTENT_KEYWORDS or extracting them to a constant for discoverability.

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

In `@src/services/n8n-workflow-service.ts` around lines 165 - 186, The hardcoded
search terms in supplementAINodes (the array passed to searchNodes(['openai',
'ai transform'], 5)) should be extracted into a named constant co-located with
AI_INTENT_KEYWORDS (e.g., AI_NODE_SEARCH_TERMS) so future AI node names are
discoverable and maintainable; replace the inline array in supplementAINodes
with that constant and ensure the new constant is exported or defined near
AI_INTENT_KEYWORDS so it’s easy to update when new AI nodes are added.
src/routes/workflows.ts (1)

72-80: HTTP 424 for missing credentials is semantically correct.

This is a breaking change for any API consumers that previously relied on a 200 response with success: false and reason: 'missing_integrations'. If external clients consume this API, consider documenting the status code change in release notes.

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

In `@src/routes/workflows.ts` around lines 72 - 80, The handler currently returns
HTTP 424 for missing credentials (res.status(424).json(...)) which breaks
clients expecting a 200 with success:false; change the response back to 200
while preserving the payload shape (keep success:false,
reason:'missing_integrations', missingIntegrations and warnings) so existing
consumers continue to work, and add an inline comment near this block (in
workflows.ts around the result.missingCredentials check) indicating the semantic
vs. compatibility tradeoff and a TODO to document the status-code change in
release notes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@__tests__/integration/actions/lifecycleActions.test.ts`:
- Around line 191-216: The test for the two-step delete flow is missing an
assertion that the first call signals awaiting user input; after calling
deleteWorkflowAction.handler the test should assert that the returned payload
(result1) includes data.awaitingUserInput === true (and/or that the callback was
invoked with a message containing data.awaitingUserInput: true) so the
multi-step loop can continue; update the Step 1 assertions in the test that uses
deleteWorkflowAction.handler, createMockMessage, createStateWithWorkflows and
the callback, and keep the existing checks that service.deleteWorkflow was not
called.

In `@src/actions/deleteWorkflow.ts`:
- Around line 105-142: The pending-deletion entry is not removed when it exists
but is expired; update the logic around
runtime.getCache<PendingDeletion>(cacheKey) and the DELETE_CONFIRM_TTL_MS check
so that if pending exists but Date.now() - pending.createdAt >=
DELETE_CONFIRM_TTL_MS you call runtime.deleteCache(cacheKey) to clear the stale
entry before proceeding to the "start a new deletion flow" branch; preserve
existing behavior for the confirmed and non-confirmed branches (using
service.deleteWorkflow, callback, and logging) and ensure you reference the same
cacheKey/pending variables so no duplicate cache entries remain.

---

Outside diff comments:
In `@__tests__/integration/providers/providers.test.ts`:
- Around line 268-334: The tests fail because createMockMessage now uses
entityId 'custom-user-0000-0000-000000000001' but the cache entries still use
the old key 'workflow_draft:user-001', so pendingDraftProvider.get (which builds
cacheKey = `workflow_draft:${message.entityId}`) can't find the drafts; update
each cache object used in the tests (the four occurrences in the tests including
in the "returns draft info when draft exists", "returns empty for expired
draft", "includes node names in text", and "scoped to user — no draft for other
user" blocks) to use the matching key
'workflow_draft:custom-user-0000-0000-000000000001' so pendingDraftProvider.get
locates the intended draft entries when calling createMockMessage with that
entityId.
- Around line 154-207: The tests reference getWorkflowExecutions which is no
longer used by workflowStatusProvider.get; update tests accordingly by removing
the dead getWorkflowExecutions mocks and either rename or delete the affected
tests: for the "includes workflow status and execution info" test remove the
execution-related setup and rename to reflect it only verifies listWorkflows
output (e.g., "includes workflow status"), and for "handles execution fetch
error per workflow" either delete the test entirely or change it to assert
graceful behavior of workflowStatusProvider.get when listWorkflows returns
data/errors; locate usages in the tests named 'includes workflow status and
execution info' and 'handles execution fetch error per workflow' to make these
edits.

---

Duplicate comments:
In `@src/services/n8n-workflow-service.ts`:
- Around line 170-172: The curly-brace lint issue has already been fixed for the
early return around hasAIIntent; ensure the if block using hasAIIntent and
returning mainResults remains wrapped in braces (if (!hasAIIntent) { return
mainResults; }) and remove any stale duplicate review/comment markers; verify
the code paths in the function containing hasAIIntent and mainResults remain
logically unchanged after this change.

---

Nitpick comments:
In `@__tests__/integration/actions/lifecycleActions.test.ts`:
- Around line 289-303: The test named "delete success returns success: true in
callback" is misleading because it only exercises the confirmation prompt
(calling deleteWorkflowAction.handler once) and asserts the prompt callback
result; rename the test to something explicit like "delete confirmation prompt
returns success: true in callback" and update the test description string in the
test block (the test(...) invocation) so it accurately references the
confirmation prompt; keep the existing call to deleteWorkflowAction.handler, the
createMockMessage with text 'Delete Stripe', createStateWithWorkflows, and
assertions as-is.

In `@src/actions/deleteWorkflow.ts`:
- Line 15: Move the DELETE_CONFIRM_TTL_MS constant out of deleteWorkflow and
into the shared constants module where DRAFT_TTL_MS lives (constants.ts), then
replace the local definition in deleteWorkflow.ts with an import of
DELETE_CONFIRM_TTL_MS from that constants module; ensure the exported name
matches existing usages and update the import statements in deleteWorkflow.ts to
reference the centralized constant.
- Line 113: The regex used to set isConfirm redundantly includes the
case-insensitive flag while userText is already set via .toLowerCase().trim();
update the pattern in deleteWorkflow.ts by removing the /i flag (change
/^(yes|confirm|ok|do it|go ahead|oui|y)$/i to /^(yes|confirm|ok|do it|go
ahead|oui|y)$/) so isConfirm uses the lowercase userText correctly; ensure this
change targets the isConfirm assignment where userText is defined.

In `@src/routes/workflows.ts`:
- Around line 72-80: The handler currently returns HTTP 424 for missing
credentials (res.status(424).json(...)) which breaks clients expecting a 200
with success:false; change the response back to 200 while preserving the payload
shape (keep success:false, reason:'missing_integrations', missingIntegrations
and warnings) so existing consumers continue to work, and add an inline comment
near this block (in workflows.ts around the result.missingCredentials check)
indicating the semantic vs. compatibility tradeoff and a TODO to document the
status-code change in release notes.

In `@src/services/n8n-workflow-service.ts`:
- Around line 165-186: The hardcoded search terms in supplementAINodes (the
array passed to searchNodes(['openai', 'ai transform'], 5)) should be extracted
into a named constant co-located with AI_INTENT_KEYWORDS (e.g.,
AI_NODE_SEARCH_TERMS) so future AI node names are discoverable and maintainable;
replace the inline array in supplementAINodes with that constant and ensure the
new constant is exported or defined near AI_INTENT_KEYWORDS so it’s easy to
update when new AI nodes are added.

Comment on lines +191 to 216
test('deletes matched workflow after confirmation', async () => {
const { runtime, service } = createRuntimeWithMatchingWorkflow();
const message = createMockMessage({
content: { text: 'Delete the Stripe workflow' },
});
const callback = createMockCallback();

const result = await deleteWorkflowAction.handler(
// Step 1: request deletion → gets confirmation prompt
const result1 = await deleteWorkflowAction.handler(
runtime,
message,
createMockMessage({ content: { text: 'Delete the Stripe workflow' } }),
createStateWithWorkflows(),
{},
callback
);
expect(result1.success).toBe(true);
expect(service.deleteWorkflow).not.toHaveBeenCalled();

expect(result.success).toBe(true);
// Step 2: confirm → actually deletes
const result2 = await deleteWorkflowAction.handler(
runtime,
createMockMessage({ content: { text: 'yes' } }),
createStateWithWorkflows(),
{},
callback
);
expect(result2.success).toBe(true);
expect(service.deleteWorkflow).toHaveBeenCalledWith('wf-001');
});
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 | 🟡 Minor

Step 1 of two-step delete is missing the core awaitingUserInput assertion.

The test verifies result1.success === true and service.deleteWorkflow not called, but the primary fix this PR ships (Bug 1 variant) is that the callback/return payload includes data: { awaitingUserInput: true }. That signal is what breaks the multi-step loop, and it's not asserted here.

💚 Suggested additions to step 1 assertions
   expect(result1.success).toBe(true);
   expect(service.deleteWorkflow).not.toHaveBeenCalled();
+  expect(result1.data).toEqual({ awaitingUserInput: true });
+  const promptCallback = (callback as any).mock.calls[0][0];
+  expect(promptCallback.data).toEqual({ awaitingUserInput: true });
📝 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
test('deletes matched workflow after confirmation', async () => {
const { runtime, service } = createRuntimeWithMatchingWorkflow();
const message = createMockMessage({
content: { text: 'Delete the Stripe workflow' },
});
const callback = createMockCallback();
const result = await deleteWorkflowAction.handler(
// Step 1: request deletion → gets confirmation prompt
const result1 = await deleteWorkflowAction.handler(
runtime,
message,
createMockMessage({ content: { text: 'Delete the Stripe workflow' } }),
createStateWithWorkflows(),
{},
callback
);
expect(result1.success).toBe(true);
expect(service.deleteWorkflow).not.toHaveBeenCalled();
expect(result.success).toBe(true);
// Step 2: confirm → actually deletes
const result2 = await deleteWorkflowAction.handler(
runtime,
createMockMessage({ content: { text: 'yes' } }),
createStateWithWorkflows(),
{},
callback
);
expect(result2.success).toBe(true);
expect(service.deleteWorkflow).toHaveBeenCalledWith('wf-001');
});
test('deletes matched workflow after confirmation', async () => {
const { runtime, service } = createRuntimeWithMatchingWorkflow();
const callback = createMockCallback();
// Step 1: request deletion → gets confirmation prompt
const result1 = await deleteWorkflowAction.handler(
runtime,
createMockMessage({ content: { text: 'Delete the Stripe workflow' } }),
createStateWithWorkflows(),
{},
callback
);
expect(result1.success).toBe(true);
expect(service.deleteWorkflow).not.toHaveBeenCalled();
expect(result1.data).toEqual({ awaitingUserInput: true });
const promptCallback = (callback as any).mock.calls[0][0];
expect(promptCallback.data).toEqual({ awaitingUserInput: true });
// Step 2: confirm → actually deletes
const result2 = await deleteWorkflowAction.handler(
runtime,
createMockMessage({ content: { text: 'yes' } }),
createStateWithWorkflows(),
{},
callback
);
expect(result2.success).toBe(true);
expect(service.deleteWorkflow).toHaveBeenCalledWith('wf-001');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/integration/actions/lifecycleActions.test.ts` around lines 191 -
216, The test for the two-step delete flow is missing an assertion that the
first call signals awaiting user input; after calling
deleteWorkflowAction.handler the test should assert that the returned payload
(result1) includes data.awaitingUserInput === true (and/or that the callback was
invoked with a message containing data.awaitingUserInput: true) so the
multi-step loop can continue; update the Step 1 assertions in the test that uses
deleteWorkflowAction.handler, createMockMessage, createStateWithWorkflows and
the callback, and keep the existing checks that service.deleteWorkflow was not
called.

Comment on lines 105 to +142
try {
const userId = message.entityId;
const cacheKey = `workflow_delete_pending:${userId}`;

// Check for pending confirmation
const pending = await runtime.getCache<PendingDeletion>(cacheKey);
if (pending && Date.now() - pending.createdAt < DELETE_CONFIRM_TTL_MS) {
const userText = (message.content?.text || '').toLowerCase().trim();
const isConfirm = /^(yes|confirm|ok|do it|go ahead|oui|y)$/i.test(userText);

if (isConfirm) {
await service.deleteWorkflow(pending.workflowId);
await runtime.deleteCache(cacheKey);

logger.info(
{ src: 'plugin:n8n-workflow:action:delete' },
`Deleted workflow ${pending.workflowId} after confirmation`
);

if (callback) {
await callback({
text: `Workflow "${pending.workflowName}" deleted permanently.`,
success: true,
});
}
return { success: true };
}

// Not a confirm — cancel the pending deletion
await runtime.deleteCache(cacheKey);
if (callback) {
await callback({
text: 'Deletion cancelled.',
success: true,
});
}
return { success: true };
}
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 | 🟡 Minor

Stale expired pending-deletion cache entry not cleaned up before falling through.

When pending exists but the TTL has expired (Date.now() - pending.createdAt >= DELETE_CONFIRM_TTL_MS), the condition on line 111 is false and the code falls into the "No pending — start a new deletion flow" branch without deleting the stale cache entry. The entry self-heals the next time a new deletion request stores a fresh entry, but in the interim a "yes" message lands in matchWorkflow (which tries to match it as a workflow name) and the user gets a confusing "Could not identify which workflow to delete" response.

🛡️ Proposed fix — expire stale cache entry explicitly
 const pending = await runtime.getCache<PendingDeletion>(cacheKey);
 if (pending && Date.now() - pending.createdAt < DELETE_CONFIRM_TTL_MS) {
   // ... handle confirmation / cancellation ...
 }

+// If pending existed but TTL expired, clean it up before starting fresh
+if (pending) {
+  await runtime.deleteCache(cacheKey);
+}

 // No pending — start a new deletion flow
 const workflows = await service.listWorkflows(userId);
📝 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 {
const userId = message.entityId;
const cacheKey = `workflow_delete_pending:${userId}`;
// Check for pending confirmation
const pending = await runtime.getCache<PendingDeletion>(cacheKey);
if (pending && Date.now() - pending.createdAt < DELETE_CONFIRM_TTL_MS) {
const userText = (message.content?.text || '').toLowerCase().trim();
const isConfirm = /^(yes|confirm|ok|do it|go ahead|oui|y)$/i.test(userText);
if (isConfirm) {
await service.deleteWorkflow(pending.workflowId);
await runtime.deleteCache(cacheKey);
logger.info(
{ src: 'plugin:n8n-workflow:action:delete' },
`Deleted workflow ${pending.workflowId} after confirmation`
);
if (callback) {
await callback({
text: `Workflow "${pending.workflowName}" deleted permanently.`,
success: true,
});
}
return { success: true };
}
// Not a confirm — cancel the pending deletion
await runtime.deleteCache(cacheKey);
if (callback) {
await callback({
text: 'Deletion cancelled.',
success: true,
});
}
return { success: true };
}
try {
const userId = message.entityId;
const cacheKey = `workflow_delete_pending:${userId}`;
// Check for pending confirmation
const pending = await runtime.getCache<PendingDeletion>(cacheKey);
if (pending && Date.now() - pending.createdAt < DELETE_CONFIRM_TTL_MS) {
const userText = (message.content?.text || '').toLowerCase().trim();
const isConfirm = /^(yes|confirm|ok|do it|go ahead|oui|y)$/i.test(userText);
if (isConfirm) {
await service.deleteWorkflow(pending.workflowId);
await runtime.deleteCache(cacheKey);
logger.info(
{ src: 'plugin:n8n-workflow:action:delete' },
`Deleted workflow ${pending.workflowId} after confirmation`
);
if (callback) {
await callback({
text: `Workflow "${pending.workflowName}" deleted permanently.`,
success: true,
});
}
return { success: true };
}
// Not a confirm — cancel the pending deletion
await runtime.deleteCache(cacheKey);
if (callback) {
await callback({
text: 'Deletion cancelled.',
success: true,
});
}
return { success: true };
}
// If pending existed but TTL expired, clean it up before starting fresh
if (pending) {
await runtime.deleteCache(cacheKey);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/deleteWorkflow.ts` around lines 105 - 142, The pending-deletion
entry is not removed when it exists but is expired; update the logic around
runtime.getCache<PendingDeletion>(cacheKey) and the DELETE_CONFIRM_TTL_MS check
so that if pending exists but Date.now() - pending.createdAt >=
DELETE_CONFIRM_TTL_MS you call runtime.deleteCache(cacheKey) to clear the stale
entry before proceeding to the "start a new deletion flow" branch; preserve
existing behavior for the confirmed and non-confirmed branches (using
service.deleteWorkflow, callback, and logging) and ensure you reference the same
cacheKey/pending variables so no duplicate cache entries remain.

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.

1 participant