Skip to content

Conversation

@standujar
Copy link
Collaborator

@standujar standujar commented Feb 5, 2026

Summary

  • Modify existing workflows: Load deployed workflows for editing, then use CREATE_N8N_WORKFLOW to make changes and redeploy (preserves workflow ID → UPDATE instead of CREATE)
  • Output schema validation: Validate $json.field expressions against upstream node output schemas, with type-aware auto-correction
  • Trigger schema capture: Capture and validate trigger output schemas (Gmail, GitHub, Google Calendar, etc.)
  • Draft lifecycle hardening: Prevent multi-step agent from auto-confirming drafts in the same turn
  • Compound expression support: Detect $json refs inside ||, ternary, and concatenation expressions
  • Simplify trigger output: Force simple: true on triggers that support it (Gmail, Notion, etc.) for predictable output

Key changes

Schema validation & correction

  • validateOutputReferences() checks every $json.field expression against the source node's output schema
  • Regex updated to match $json.field individually (not just {{ $json.field }}) — catches {{ $json.a || $json.b }}
  • Correction prompt includes field types (snippet (string) vs payload (object)) so LLM picks the right field
  • normalizeTriggerSimpleParam() forces simple: true on triggers that support it — ensures our captured schema matches runtime output
  • loadTriggerOutputSchema() returns null when simple: false (raw mode output differs from captured schema)

Trigger schema infrastructure

  • scripts/capture-trigger-schemas.ts — capture trigger output schemas from n8n executions
  • scripts/crawl-trigger-schemas.ts — discover all triggers and their configurations
  • .github/workflows/schema-update.yml — weekly cron: regenerate schemas, compare with latest npm release, auto-bump version if changed

Draft lifecycle

  • Removed intent and modification action parameters — multi-step agents can no longer bypass classifyDraftIntent()
  • Added originMessageId to WorkflowDraft — if the action is called again with the same message that created the draft, re-show preview instead of classifying
  • Parameter diff shown in preview after modification

New files

  • src/utils/outputSchema.ts — schema loading, field path extraction, expression parsing, type-aware field paths
  • src/prompts/fieldCorrection.ts — type-aware LLM correction prompt
  • src/actions/modifyExistingWorkflow.ts — load existing deployed workflows for editing
  • scripts/capture-trigger-schemas.ts, scripts/crawl-trigger-schemas.ts, scripts/create-credentials.ts
  • .github/workflows/schema-update.yml

Modified files

  • src/actions/createWorkflow.ts — removed explicit intent, added originMessageId guard
  • src/utils/workflow.tsvalidateOutputReferences(), normalizeTriggerSimpleParam(), isTriggerNode() helper
  • src/utils/generation.tscorrectFieldReferences(), removed duplicate intent log
  • src/services/n8n-workflow-service.ts — wired normalizeTriggerSimpleParam + validateOutputReferences
  • .github/workflows/npm-deploy.yml — uses crawl + trigger capture steps

Test plan

  • 215 tests pass (unit + integration + e2e)
  • Build compiles without errors
  • Manual test: Gmail trigger corrections apply correctly (subject → Subject, from → From)
  • Manual test: compound expressions {{ $json.textHtml || $json.textPlain }} detected and corrected
  • Manual test: originMessageId guard prevents same-turn auto-confirm
  • Manual test: user "Va y, tu peux creer" correctly classified as confirm on next turn
  • /deslop and /larp clean

Summary by CodeRabbit

  • New Features

    • Added ability to modify existing deployed workflows with change previews
    • Automatic detection and correction of invalid field references in expressions
    • Parameter name validation and correction capabilities
  • Bug Fixes

    • Improved workflow validation with upstream-aware schema checking
    • Enhanced parameter handling with option alignment and expression prefix enforcement
    • Better trigger detection and normalization
  • Tests

    • Comprehensive test coverage for output schemas and workflow modifications

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR introduces a workflow modification system with schema-driven field validation and auto-correction. It adds new scripts for generating node and trigger schema catalogs, expands validation utilities for output references and parameter correction, and integrates these capabilities into the workflow generation pipeline. Includes comprehensive test coverage and GitHub Actions workflows for automated schema updates.

Changes

Cohort / File(s) Summary
Script Infrastructure
scripts/crawl.ts, scripts/crawl-nodes.ts, scripts/crawl-schemas.ts, scripts/capture-trigger-schemas.ts, scripts/crawl-trigger-schemas.ts, scripts/tsconfig.json, scripts/langchain-output-schemas.json
New master crawler script orchestrating node catalog, schema index, and trigger schema generation. Enhanced crawl-nodes with LangChain integration. Added crawl-schemas for building schemaIndex.json. New capture-trigger-schemas for runtime schema extraction via N8N API. Added crawl-trigger-schemas using OpenAPI specs and trigger code analysis. TypeScript config for scripts directory. LangChain output schema definitions.
Workflow Modification Feature
src/actions/createWorkflow.ts, src/actions/modifyExistingWorkflow.ts, src/actions/index.ts, src/index.ts
New modifyExistingWorkflowAction for loading and editing deployed workflows with draft caching and workflow matching. Enhanced createWorkflowAction with parameters property, originMessageId tracking, and change detection in previews. Updated action exports and plugin registration.
Schema and Validation Utilities
src/utils/outputSchema.ts, src/utils/workflow.ts, src/utils/generation.ts, src/utils/catalog.ts
New comprehensive outputSchema module with field path enumeration, schema loading, expression parsing, and validation. Enhanced workflow.ts with output reference validation, parameter correction, unknown parameter detection, and expression prefix enforcement. Extended generation.ts with field/parameter correction functions and output schema context building. Enhanced catalog.ts with token-based search scoring, node simplification for LLM usage, and improved node definition matching.
Prompt and Configuration
src/prompts/fieldCorrection.ts, src/prompts/parameterCorrection.ts, src/prompts/workflowGeneration.ts, src/prompts/actionResponse.ts, src/prompts/index.ts
New field and parameter correction prompts guiding LLM-based field reference and parameter name fixes. Enhanced workflowGeneration.ts with stricter rules for parameter names and fixedCollection handling. Updated actionResponse.ts to document changed parameters in previews. Extended prompts exports.
Type System and Exports
src/types/index.ts, src/utils/index.ts, src/utils/credentialResolver.ts
Updated NodeDefinition to allow version as array and inputs/outputs as string variants. Added WorkflowDraft.originMessageId, SchemaContent, ExpressionRef, and OutputRefValidation types. Extended utils exports for schema and validation functions. Updated credentialResolver to accept tagName parameter for credential naming.
Service Layer
src/services/n8n-workflow-service.ts
Added getWorkflow method for retrieving deployed workflows. Enhanced deployment with tag-based naming, create-vs-update logic, and parameter validation/correction steps including trigger normalization, option correction, unknown parameter detection, and output reference validation.
Unit Tests
__tests__/unit/credentialResolver.test.ts, __tests__/unit/outputSchema.test.ts, __tests__/unit/workflow.test.ts, __tests__/unit/catalog.test.ts, __tests__/unit/generation.test.ts, __tests__/fixtures/workflows.ts
New comprehensive outputSchema test suite covering schema loading, field extraction, expression parsing, and validation. Extended workflow tests for output validation, parameter correction, unknown detection, and expression prefixing. Enhanced catalog tests for search, filtering, and node simplification. Added generation tests for output schema context injection. New trigger node fixtures.
Integration Tests
__tests__/integration/actions/createWorkflow.test.ts
Extended modify intent tests validating preview updates with changed parameters, draft caching with originMessageId, and anti-loop behavior preventing redundant processing of identical messages.
GitHub Actions and CI/CD
.github/workflows/npm-deploy.yml, .github/workflows/schema-update.yml, .github/workflows/ci.yml, .gitignore, package.json
Updated npm-deploy to run new crawl script with API credentials. Added schema-update workflow for weekly scheduled schema regeneration with version bumping and change detection. Updated CI workflow to use crawl instead of crawl-nodes. Enhanced .gitignore for generated schema files. Updated package.json with crawl, crawl:nodes, and crawl:schemas scripts.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ModifyAction as Modify Action
    participant N8nService as N8N Workflow Service
    participant N8nAPI as N8N API
    participant Cache as Draft Cache

    User->>ModifyAction: Request to modify workflow
    ModifyAction->>Cache: Check for existing draft
    Cache-->>ModifyAction: Draft found or none
    alt Draft exists and not expired
        ModifyAction-->>User: Prompt to finish/cancel current draft
    else No draft
        ModifyAction->>N8nService: List user workflows
        N8nService->>N8nAPI: GET /workflows
        N8nAPI-->>N8nService: Workflows list
        N8nService-->>ModifyAction: Deployed workflows
        
        ModifyAction->>ModifyAction: Match user description to workflow
        alt Match confidence: high/medium
            ModifyAction->>N8nAPI: GET /workflows/:id
            N8nAPI-->>ModifyAction: Workflow details
            ModifyAction->>Cache: Store draft with originMessageId
            ModifyAction-->>User: Return preview with workflow details
        else Match confidence: low
            ModifyAction-->>User: Request confirmation
        else No match
            ModifyAction-->>User: Return workflow list for disambiguation
        end
    end
Loading
sequenceDiagram
    participant Generator as Workflow Generator
    participant Validator as Validation Engine
    participant Corrector as Correction Service
    participant LLM as Language Model
    participant Catalog as Node Catalog

    Generator->>Generator: Generate initial workflow
    Generator->>Validator: validateWorkflow()
    Validator->>Validator: Check structure, connections
    Validator-->>Generator: Validation results
    
    Generator->>Validator: validateOutputReferences()
    Validator->>Catalog: Load upstream output schemas
    Catalog-->>Validator: Field definitions
    Validator->>Validator: Check $json expressions match fields
    Validator-->>Generator: Invalid references list
    
    alt Invalid references exist
        Generator->>Corrector: correctFieldReferences()
        Corrector->>LLM: Request field path corrections
        LLM-->>Corrector: Corrected expressions
        Corrector->>Generator: Updated workflow
    end
    
    Generator->>Validator: detectUnknownParameters()
    Validator->>Catalog: Get property definitions
    Catalog-->>Validator: Known properties
    Validator-->>Generator: Unknown parameters list
    
    alt Unknown parameters exist
        Generator->>Corrector: correctParameterNames()
        Corrector->>LLM: Request parameter mapping
        LLM-->>Corrector: Corrected names
        Corrector->>Generator: Updated workflow
    end
    
    Generator->>Validator: ensureExpressionPrefix()
    Validator->>Validator: Prefix $json expressions with "="
    Validator-->>Generator: Prefixed workflow
    
    Generator-->>Generator: Return validated, corrected workflow
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hopping through schemas with glee,
We modify workflows so spree,
Fields corrected with care,
Parameters everywhere,
Validations set all data free! 🌿✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.74% 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 clearly and concisely summarizes the main changes: adding a modify workflow action and output schema validation, which are the primary objectives of this changeset.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/workflow-modify-and-schema-validation

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.

@claude

This comment was marked as outdated.

@standujar standujar force-pushed the feat/workflow-modify-and-schema-validation branch from 5e6c29c to ee307ef Compare February 5, 2026 17:34
@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@standujar standujar force-pushed the feat/workflow-modify-and-schema-validation branch from ee307ef to 7057f70 Compare February 5, 2026 17:35
@claude

This comment was marked as outdated.

Copy link

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

🤖 Fix all issues with AI agents
In `@src/actions/modifyExistingWorkflow.ts`:
- Around line 127-138: modifyExistingWorkflow skips using expired WorkflowDrafts
but doesn't remove them from cache, allowing stale entries to accumulate; fix by
deleting the expired cache entry. In the modifyExistingWorkflow flow where you
call runtime.getCache<WorkflowDraft>(cacheKey) and detect an expired draft via
DRAFT_TTL_MS, add await runtime.deleteCache(cacheKey) before
returning/continuing (mirroring createWorkflow/activateWorkflow behavior);
ensure you keep the existing callback handling and return { success: false }
after deletion to avoid changing control flow.

In `@src/utils/generation.ts`:
- Around line 468-482: The replaceInObject traversal currently uses
String.replace which only replaces the first occurrence; update the
string-replacement logic in replaceInObject (for obj[key] and array element
value[i]) to replace all occurrences of original by replacement (e.g., use
value.replaceAll(original, replacement) or value.replace(new
RegExp(escapeForRegExp(original), 'g'), replacement) or
value.split(original).join(replacement)), ensuring you handle possible
special-regex characters when choosing the RegExp approach.

In `@src/utils/outputSchema.ts`:
- Around line 234-278: The function fieldExistsInSchema can exit the loop after
consuming an array index (the i++ in the array branch) and then fall through to
the final `return false`; to fix it, inside the `else if (prop.type === 'array'
&& prop.items)` branch (in fieldExistsInSchema) after incrementing `i` and
setting `current = prop.items`, add a check that if `i === path.length - 1`
return true (since the numeric index was the last segment), ensuring the
function returns true when the final segment is reached via an array items
traversal.
- Around line 1-6: Change the legacy JSON import assertion on the schemaIndex
import to the modern `with` form: locate the import statement that brings in
schemaIndex (import schemaIndex from '../data/schemaIndex.json' assert { type:
'json' };) in src/utils/outputSchema.ts and replace the `assert { type: 'json'
}` usage with the standardized `with { type: 'json' }` syntax so the module
imports correctly under TypeScript 5.3+ / Node 22+.

In `@src/utils/workflow.ts`:
- Around line 301-347: The current validateOutputReferences only checks the
first upstream node; change it to validate each expression against all
applicable upstreams: for each node in workflow.nodes get expressions via
parseExpressions and upstreamNames from buildUpstreamMap, then for each
expression iterate upstreamNames (or the explicitly referenced node if the
expression targets $('NodeName')—handle that case first) and for each upstream
load the schema via loadOutputSchema and test with fieldExistsInSchema; treat
the expression as valid if any upstream schema contains the field, and only push
an invalid ref when none of the upstream schemas match, providing
availableFields as the union (or concatenation) of getAllFieldPaths results from
all attempted upstream schemas; update references to sourceNode/sourceNodeName
accordingly.
🧹 Nitpick comments (7)
scripts/crawl-schemas.ts (2)

15-19: Consider aligning SchemaContent with the runtime type definition.

The SchemaContent interface here is missing the items property that exists in src/types/index.ts:352-357. While this script generates the schema index and may not need the full type, keeping them consistent prevents potential mismatches.

♻️ Suggested alignment
 interface SchemaContent {
   type: string;
   properties?: Record<string, unknown>;
+  items?: SchemaContent;
   [key: string]: unknown;
 }

93-100: Consider logging skipped files for debugging purposes.

The empty catch blocks silently swallow errors, which is fine for production robustness but can make debugging difficult when schema files are unexpectedly missing from the index.

♻️ Optional: Add debug logging
           try {
             const schemaPath = path.join(resourcePath, opFile.name);
             const content = await readFile(schemaPath, 'utf-8');
             const schema = JSON.parse(content) as SchemaContent;
             schemas[resource][operation] = schema;
           } catch {
-            // Skip invalid schema files
+            // Skip invalid schema files (uncomment for debugging)
+            // console.warn(`Skipping invalid schema: ${path.join(resourcePath, opFile.name)}`);
           }
src/utils/credentialResolver.ts (1)

111-116: Consider sanitizing tagName for credential name.

The credential name ${credType}_${tagName} could contain problematic characters if tagName includes spaces, special characters, or is excessively long. Depending on n8n's credential name constraints, this could cause API errors.

♻️ Optional: Sanitize tagName before use
-        const credName = `${credType}_${tagName}`;
+        const sanitizedTag = tagName.replace(/[^a-zA-Z0-9_:-]/g, '_').slice(0, 50);
+        const credName = `${credType}_${sanitizedTag}`;
src/actions/createWorkflow.ts (1)

88-112: Parameter diff only captures changed/added parameters, not removed ones.

The current implementation detects parameters that changed or were added in afterNode, but won't report parameters that existed in beforeNode but were removed in afterNode. This may be intentional for preview purposes, but worth noting.

Additionally, JSON.stringify comparison is order-sensitive for object properties, which could produce false positives if the LLM reorders keys without changing values.

♻️ Optional: Use deep equality and track removed params
 function diffNodeParams(
   before: N8nWorkflow,
   after: N8nWorkflow
 ): Record<string, Record<string, unknown>> {
   const changes: Record<string, Record<string, unknown>> = {};

   for (const afterNode of after.nodes) {
     const beforeNode = before.nodes.find((n) => n.name === afterNode.name);
     const afterParams = (afterNode.parameters || {}) as Record<string, unknown>;
     const beforeParams = (beforeNode?.parameters || {}) as Record<string, unknown>;

     const nodeChanges: Record<string, unknown> = {};
+    const allKeys = new Set([...Object.keys(afterParams), ...Object.keys(beforeParams)]);
-    for (const [key, value] of Object.entries(afterParams)) {
-      if (JSON.stringify(value) !== JSON.stringify(beforeParams[key])) {
-        nodeChanges[key] = value;
-      }
-    }
+    for (const key of allKeys) {
+      const beforeVal = beforeParams[key];
+      const afterVal = afterParams[key];
+      if (JSON.stringify(beforeVal) !== JSON.stringify(afterVal)) {
+        nodeChanges[key] = afterVal; // undefined if removed
+      }
+    }

     if (Object.keys(nodeChanges).length > 0) {
       changes[afterNode.name] = nodeChanges;
     }
   }

   return changes;
 }
__tests__/integration/actions/createWorkflow.test.ts (1)

521-554: Duplicate createDraftInCache helper function.

This function is identical to the one defined at lines 187-220. Consider extracting it to a shared location within the test file or to test fixtures to avoid duplication.

♻️ Move shared helper to top of describe block or fixtures
+// Move createDraftInCache to top-level of the describe block, outside nested describes
 describe('CREATE_N8N_WORKFLOW action', () => {
+  function createDraftInCache(): WorkflowDraft {
+    return {
+      workflow: {
+        name: 'Stripe Gmail Summary',
+        // ... rest of implementation
+      },
+      prompt: 'Send Stripe summaries via Gmail',
+      userId: 'user-001',
+      createdAt: Date.now(),
+    };
+  }
+
   describe('handler - existing draft', () => {
-    function createDraftInCache(): WorkflowDraft { ... }
     // ... tests
   });

   describe('handler - explicit intent via _options', () => {
-    function createDraftInCache(): WorkflowDraft { ... }
     // ... tests
   });
 });
src/services/n8n-workflow-service.ts (1)

243-250: Consider extracting the duplicated validation/correction pattern.

The same validation and auto-correction logic is repeated in both generateWorkflowDraft and modifyWorkflowDraft. This could be extracted into a private helper method to reduce duplication.

♻️ Optional refactor to extract helper
+  private async validateAndCorrectReferences(workflow: N8nWorkflow): Promise<N8nWorkflow> {
+    const invalidRefs = validateOutputReferences(workflow);
+    if (invalidRefs.length > 0) {
+      logger.debug(
+        { src: 'plugin:n8n-workflow:service:main' },
+        `Found ${invalidRefs.length} invalid field reference(s), auto-correcting...`
+      );
+      return correctFieldReferences(this.runtime, workflow, invalidRefs);
+    }
+    return workflow;
+  }

Then in both methods:

-    const invalidRefs = validateOutputReferences(workflow);
-    if (invalidRefs.length > 0) {
-      logger.debug(
-        { src: 'plugin:n8n-workflow:service:main' },
-        `Found ${invalidRefs.length} invalid field reference(s), auto-correcting...`
-      );
-      workflow = await correctFieldReferences(this.runtime, workflow, invalidRefs);
-    }
+    workflow = await this.validateAndCorrectReferences(workflow);

Also applies to: 310-317

src/utils/outputSchema.ts (1)

86-115: Consider adding a depth limit to prevent stack overflow on deeply nested schemas.

getAllFieldPaths recursively traverses nested objects and arrays without a depth limit. While typical n8n schemas are shallow, malformed or adversarial input could cause stack overflow.

♻️ Optional: Add depth protection
-export function getAllFieldPaths(schema: SchemaContent, prefix = ''): string[] {
+export function getAllFieldPaths(schema: SchemaContent, prefix = '', maxDepth = 10): string[] {
   const paths: string[] = [];
   const properties = schema.properties;
 
-  if (!properties) {
+  if (!properties || maxDepth <= 0) {
     return paths;
   }
 
   for (const [key, value] of Object.entries(properties)) {
     const currentPath = prefix ? `${prefix}.${key}` : key;
     paths.push(currentPath);
 
     if (typeof value === 'object' && value !== null) {
       const propSchema = value as SchemaContent;
 
       if (propSchema.type === 'object' && propSchema.properties) {
-        paths.push(...getAllFieldPaths(propSchema, currentPath));
+        paths.push(...getAllFieldPaths(propSchema, currentPath, maxDepth - 1));
       }
 
       if (propSchema.type === 'array' && propSchema.items) {
         const items = propSchema.items as SchemaContent;
         if (items.type === 'object' && items.properties) {
-          paths.push(...getAllFieldPaths(items, `${currentPath}[0]`));
+          paths.push(...getAllFieldPaths(items, `${currentPath}[0]`, maxDepth - 1));
         }
       }
     }
   }
 
   return paths;
 }

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as duplicate.

@claude

This comment was marked as abuse.

Copy link

@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

🤖 Fix all issues with AI agents
In `@src/actions/modifyExistingWorkflow.ts`:
- Around line 122-142: The code assumes message.entityId exists before calling
service.listWorkflows, which lets listWorkflows(undefined) return unfiltered
results; add a guard that validates message.entityId / userId is present and
return an error (via callback and { success: false }) before checking cache or
calling service.listWorkflows. Specifically, in modifyExistingWorkflow.ts check
message.entityId (used as userId) at the top of the try block and short-circuit
with a callback error and return when it's falsy, ensuring
runtime.getCache(cacheKey) and service.listWorkflows(userId) only run with a
valid userId; apply the same pattern to the other files mentioned.

In `@src/services/n8n-workflow-service.ts`:
- Around line 243-250: After calling correctFieldReferences, re-run
validateOutputReferences against the updated workflow and handle any remaining
invalidRefs: call validateOutputReferences(workflow) again, and if the returned
list is non-empty, surface the issue (e.g., logger.warn or throw a clear error
requiring clarification) rather than proceeding; update the block around
validateOutputReferences / correctFieldReferences (referencing
validateOutputReferences, correctFieldReferences, this.runtime, and workflow) to
perform this second validation and decide whether to warn or halt when residual
invalid references remain.
🧹 Nitpick comments (1)
__tests__/integration/actions/createWorkflow.test.ts (1)

521-554: Duplicate helper function createDraftInCache — consider extracting to shared scope.

This helper is identical to the one defined at lines 187-220 in the "handler - existing draft" describe block. Consider hoisting it to the top-level describe scope to eliminate duplication.

♻️ Suggested refactor

Move createDraftInCache to the top-level describe('CREATE_N8N_WORKFLOW action', ...) block so both test suites can share it:

 describe('CREATE_N8N_WORKFLOW action', () => {
+  function createDraftInCache(): WorkflowDraft {
+    return {
+      workflow: {
+        name: 'Stripe Gmail Summary',
+        // ... rest of implementation
+      },
+      prompt: 'Send Stripe summaries via Gmail',
+      userId: 'user-001',
+      createdAt: Date.now(),
+    };
+  }
+
   describe('validate', () => {
     // ...
   });

   describe('handler - existing draft', () => {
-    function createDraftInCache(): WorkflowDraft { ... }
     // tests using createDraftInCache()
   });

   describe('handler - explicit intent via _options', () => {
-    function createDraftInCache(): WorkflowDraft { ... }
     // tests using createDraftInCache()
   });

- Add MODIFY_EXISTING_N8N_WORKFLOW action to load deployed workflows for editing
- Add output schema validation to check $json expressions against upstream node schemas
- Add auto-correction for invalid field references using LLM
- Add explicit intent support via _options for multi-step agent control
- Include parameter diff in preview response after workflow modification
- Preserve workflow ID through modification flow for updates (not creates)
- Add crawl-schemas script to extract output schemas from n8n-nodes-base
@standujar standujar force-pushed the feat/workflow-modify-and-schema-validation branch from 7057f70 to e7bddc0 Compare February 5, 2026 17:48
@claude

This comment was marked as spam.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

…e hardening

- Add trigger output schema capture and validation (Gmail, GitHub, Google Calendar, etc.)
- Fix parseExpressions regex to detect $json refs in compound expressions (||, ternary)
- Include field types in correction prompt so LLM picks string over object
- Force simple=true on trigger nodes that support it (normalizeTriggerSimpleParam)
- Skip schema validation when simple=false (raw mode differs from captured schema)
- Remove explicit intent parameter to prevent multi-step agent auto-confirm
- Add originMessageId guard to block same-turn draft confirmation
- Remove duplicate intent classification log
- Add schema-update CI workflow (weekly cron, auto-bump on schema change)
- Update npm-deploy workflow with crawl + trigger capture steps
Copy link

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

Caution

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

⚠️ Outside diff range comments (1)
src/actions/createWorkflow.ts (1)

396-437: ⚠️ Potential issue | 🟡 Minor

Modified draft doesn't carry originMessageId — intentional?

On line 408, the modified draft is created without originMessageId. This means if the framework auto-chains another CREATE_N8N_WORKFLOW call in the same turn after a modify, the same-turn guard (line 316) won't fire. The generateAndPreview path (line 562) does set it. Consider setting originMessageId: message.id here for consistency.

Proposed fix
             const modifiedDraft: WorkflowDraft = {
               workflow: modifiedWorkflow,
               prompt: existingDraft.prompt,
               userId,
               createdAt: Date.now(),
+              originMessageId: message.id,
             };
🤖 Fix all issues with AI agents
In @.github/workflows/npm-deploy.yml:
- Around line 76-80: The "Capture trigger schemas" workflow step currently can
fail the whole publish pipeline if N8N_HOST or secrets are misconfigured and it
also uses an unrecognized flag; update the step named "Capture trigger schemas"
to add continue-on-error: true so schema capture failures don't block
publishing, and remove (or correct) the unsupported "--from-existing" argument
passed to the script invocation ("bun run scripts/capture-trigger-schemas.ts")
so the step calls the script with only supported flags.

In @.github/workflows/schema-update.yml:
- Around line 22-23: The CI flag --from-existing is not parsed by
capture-trigger-schemas.ts so it is ignored; update the CLI parsing (where
--trigger, --timeout, --keep, and --create-only are handled) to accept a
--from-existing boolean (e.g., set a fromExisting variable in parseArgs or the
existing argument-parsing block) and then propagate that flag into the main flow
(functions like captureTriggerSchemas / createAndActivateWorkflows or whichever
routines perform creation/activation) so that when fromExisting is true the code
only reads existing workflows and explicitly skips any creation or activation
steps.

In `@scripts/capture-trigger-schemas.ts`:
- Around line 135-143: The KNOWN_CREDENTIALS constant currently contains
hardcoded credential IDs; remove the hardcoded map and load these IDs from
environment variables instead (either a single JSON string env like
KNOWN_CREDENTIALS_JSON that you JSON.parse and validate, or individual env vars
per key such as KNOWN_CREDENTIALS_gmailOAuth2, _googleCalendarOAuth2Api, etc.).
Update scripts/capture-trigger-schemas.ts to read process.env for these values,
validate required keys exist (throw or log a clear error if missing), and
fallback to an empty object only for non-critical keys; ensure no secret values
remain in source control and document the expected env var names in the project
README or .env.example.
- Around line 305-310: The script's main() argument parsing misses the
--from-existing flag, so add parsing for it (e.g., const fromExisting =
args.includes('--from-existing')) alongside existing flags (filterTrigger,
timeoutSec, keepWorkflows, createOnly) and then use that boolean to
short-circuit the create/activate/wait flow in the trigger capture logic (the
block that currently creates, activates, and waits for triggers between lines
84-132), making the script only capture schemas from existing workflows when
fromExisting is true; update any downstream conditionals that currently check
createOnly/keepWorkflows to also respect fromExisting where appropriate.

In `@scripts/crawl-trigger-schemas.ts`:
- Around line 136-146: Check response.ok before calling response.json() and only
parse and write to disk when the HTTP response is successful: after fetching
(the response variable) if !response.ok throw or handle the error (do not call
response.json or update cachedGuruIndex), and ensure fs.writeFileSync(cachePath,
...) and the console.log that reports cached APIs run only when cachedGuruIndex
was successfully populated; update the logic around cachedGuruIndex, response,
CACHE_DIR and cachePath so failed responses are not parsed or written to the
cache and return or propagate an error instead.

In `@scripts/create-credentials.ts`:
- Line 1: The shebang in scripts/create-credentials.ts currently uses
npx/ts-node which conflicts with the project's Bun runtime; either replace the
top-line shebang with one that invokes Bun (#!/usr/bin/env bun) if you intend
the script to be directly executable, or remove the shebang entirely if the
script will be run via bun run (e.g., package.json scripts); update the file's
first line accordingly so the runtime matches how create-credentials.ts is
executed.
- Around line 168-180: The current credential creation pushes a Twitter entry
into configs with empty-string fallbacks for TWITTER_API_SECRET and
TWITTER_ACCESS_TOKEN_SECRET (and similarly uses '' for GITHUB_USER), which
creates invalid creds; update the guard that builds the twitter credential (the
block that pushes the object with name '[Auto] Twitter OAuth1' and type
'twitterOAuth1Api') to require all four env vars (TWITTER_API_KEY,
TWITTER_API_SECRET, TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET) before
pushing, or skip creation and emit a warning via logger when any required secret
is missing; likewise remove the empty-string fallback for GITHUB_USER and only
create the GitHub credential when the required env var(s) are present, logging a
clear warning if partial env data is provided.

In `@src/actions/modifyExistingWorkflow.ts`:
- Around line 222-229: The call to formatActionResponse(runtime,
'WORKFLOW_LOADED', ...) uses an undefined response type —
ACTION_RESPONSE_SYSTEM_PROMPT only contains PREVIEW, CLARIFICATION,
DEPLOY_SUCCESS, AUTH_REQUIRED, CANCELLED, EMPTY_PROMPT, UNSUPPORTED_INTEGRATION,
and ERROR — so either change the second argument in modifyExistingWorkflow (the
formatActionResponse call) to an existing type like 'PREVIEW' or add a new
'WORKFLOW_LOADED' entry to the ACTION_RESPONSE_SYSTEM_PROMPT definition in
src/prompts/actionResponse.ts; update the prompt text for WORKFLOW_LOADED if
adding it so it matches the intended output format and keep references to
formatActionResponse and ACTION_RESPONSE_SYSTEM_PROMPT consistent.
- Around line 198-205: The draft created in modifyExistingWorkflow.ts is missing
the WorkflowDraft.originMessageId field so the same-turn auto-confirm guard
(which checks existingDraft.originMessageId === message.id in createWorkflow.ts)
can be bypassed; fix by adding originMessageId: message.id (or the appropriate
incoming message identifier variable available in this scope) to the draft
object before calling runtime.setCache(cacheKey, draft) so the guard will
correctly detect same-turn drafts.

In `@src/utils/generation.ts`:
- Around line 409-433: The LLM output (variable cleaned) is used blindly and may
not be a valid expression; add a lightweight validation after computing cleaned
(in the async map over invalidRefs that produces corrections) that checks
cleaned against expected patterns (eg. matches /^\$json\.[\w\.]+$/ or
/^\$\(.+\)$/ or other allowed prefixes like "$json." or "$("), and only return {
original, corrected, nodeName } when it passes; otherwise logger.warn with
ref.expression and cleaned and return null so invalid LLM responses are not
applied. Ensure the validation logic and logging occur in the same try block
right after the cleaned assignment (referencing cleaned, ref.expression, and
nodeName).
🧹 Nitpick comments (9)
src/utils/credentialResolver.ts (1)

69-78: Consider using an options object to reduce positional parameter sprawl.

resolveOneCredential now has 8 positional parameters, making call sites fragile and harder to read. An options/config object would improve clarity and make future additions less error-prone.

♻️ Example refactor
-async function resolveOneCredential(
-  credType: string,
-  userId: string,
-  config: N8nPluginConfig,
-  credStore: N8nCredentialStoreApi | null,
-  credProvider: CredentialProvider | null,
-  apiClient: N8nApiClient | null,
-  missingConnections: MissingConnection[],
-  tagName: string
-): Promise<string | null> {
+interface ResolveOneCredentialOpts {
+  credType: string;
+  userId: string;
+  config: N8nPluginConfig;
+  credStore: N8nCredentialStoreApi | null;
+  credProvider: CredentialProvider | null;
+  apiClient: N8nApiClient | null;
+  missingConnections: MissingConnection[];
+  tagName: string;
+}
+
+async function resolveOneCredential(opts: ResolveOneCredentialOpts): Promise<string | null> {
+  const { credType, userId, config, credStore, credProvider, apiClient, missingConnections, tagName } = opts;
scripts/create-credentials.ts (1)

53-61: JSON.parse on potentially malformed response text will throw an opaque error.

If the n8n API returns a non-204 success status with malformed JSON, JSON.parse(text) on line 60 will throw a SyntaxError without context about which endpoint failed. A try-catch with the endpoint info would make debugging easier.

Proposed improvement
   const text = await response.text();
-  return text ? JSON.parse(text) : undefined;
+  if (!text) return undefined as T;
+  try {
+    return JSON.parse(text) as T;
+  } catch {
+    throw new Error(`n8n API ${method} ${endpoint}: invalid JSON response: ${text.slice(0, 200)}`);
+  }
scripts/crawl-trigger-schemas.ts (2)

415-432: Dead code: the n8n === 'me' guard on Line 427 is unreachable.

Line 425 already handles spec.startsWith('{') with continue, so spec.startsWith('{') on Line 427 is always false. The branch is harmless but misleading.

♻️ Remove or consolidate
     // Template params match anything
     if (spec.startsWith('{') || n8n.startsWith('{')) continue;
-    // "me" matches "{userId}" equivalent
-    if (n8n === 'me' && spec.startsWith('{')) continue;
     if (n8n !== spec) return false;

751-771: Unhandled rejection from crawlTriggers() will produce a cryptic error.

Add a .catch() handler (like the companion script capture-trigger-schemas.ts does) for clearer failure output.

♻️ Suggested fix
-crawlTriggers().then((result) => {
+crawlTriggers().then((result) => {
   // ... existing code ...
-});
+}).catch((error) => {
+  console.error('Fatal:', error);
+  process.exit(1);
+});
.github/workflows/schema-update.yml (1)

30-34: Verify the tarball extraction path convention for scoped npm packages.

The glob elizaos-plugin-n8n-workflow-*.tgz assumes npm's scoped-package tarball naming. If the package scope or name ever changes, this will silently break (no file found → compare step skips everything → no update triggered). Consider failing explicitly if no tarball is found.

🛡️ Suggested guard
       mkdir -p /tmp/latest
       bunx npm pack `@elizaos/plugin-n8n-workflow`@latest --pack-destination /tmp
-      tar xzf /tmp/elizaos-plugin-n8n-workflow-*.tgz -C /tmp/latest
+      TARBALL=$(ls /tmp/elizaos-plugin-n8n-workflow-*.tgz 2>/dev/null | head -1)
+      if [ -z "$TARBALL" ]; then
+        echo "::warning::No tarball found — first publish?"
+      else
+        tar xzf "$TARBALL" -C /tmp/latest
+      fi
src/utils/workflow.ts (1)

294-307: normalizeTriggerSimpleParam silently overrides explicit simple: false.

If a user intentionally configured full (non-simplified) trigger output, this function forces it back to true. This is likely intended to align with schema validation (which only covers simplified output), but it would be worth a log when overriding an explicit value.

♻️ Suggested improvement
     if (hasSimple) {
+      if (node.parameters?.simple === false) {
+        // Log when overriding an explicit user choice
+        // (schema validation requires simplified output)
+      }
       node.parameters = { ...node.parameters, simple: true };
     }
.github/workflows/npm-deploy.yml (1)

98-98: Migrate actions/create-release@v1 to an actively maintained alternative.

This action was archived on March 4, 2021 and is now read-only. It will not receive bug fixes, security patches, or updates. Use softprops/action-gh-release@v2 or the gh release create CLI command instead.

__tests__/unit/workflow.test.ts (1)

414-440: Consider adding a test for a node with all required parameters satisfied.

Currently only the negative case (missing required param) and the skip-unknown case are tested. A positive test confirming zero warnings for a fully specified node would strengthen the suite and guard against regressions.

src/actions/createWorkflow.ts (1)

88-112: diffNodeParams only detects changed/added params — removed nodes and removed params are invisible.

If a modification removes a node or deletes a parameter, it won't appear in the diff. For preview purposes this is acceptable since the full node list is shown, but it could confuse users who expect the diff to be exhaustive.

Optional: also surface removed params
 function diffNodeParams(
   before: N8nWorkflow,
   after: N8nWorkflow
 ): Record<string, Record<string, unknown>> {
   const changes: Record<string, Record<string, unknown>> = {};

   for (const afterNode of after.nodes) {
     const beforeNode = before.nodes.find((n) => n.name === afterNode.name);
     const afterParams = (afterNode.parameters || {}) as Record<string, unknown>;
     const beforeParams = (beforeNode?.parameters || {}) as Record<string, unknown>;

     const nodeChanges: Record<string, unknown> = {};
     for (const [key, value] of Object.entries(afterParams)) {
       if (JSON.stringify(value) !== JSON.stringify(beforeParams[key])) {
         nodeChanges[key] = value;
       }
     }
+    // Detect removed params
+    for (const key of Object.keys(beforeParams)) {
+      if (!(key in afterParams)) {
+        nodeChanges[key] = null; // signals removal
+      }
+    }

     if (Object.keys(nodeChanges).length > 0) {
       changes[afterNode.name] = nodeChanges;
     }
   }

   return changes;
 }

Comment on lines 22 to 23
- name: Capture trigger schemas
run: bun run scripts/capture-trigger-schemas.ts --from-existing
Copy link

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

🏁 Script executed:

#!/bin/bash
# Check if --from-existing is parsed anywhere in the capture script
rg -n 'from-existing' scripts/capture-trigger-schemas.ts

Repository: elizaos-plugins/plugin-n8n-workflow

Length of output: 61


🏁 Script executed:

#!/bin/bash
# Look at the argument parsing logic around lines 306-310
sed -n '300,320p' scripts/capture-trigger-schemas.ts

Repository: elizaos-plugins/plugin-n8n-workflow

Length of output: 931


🏁 Script executed:

#!/bin/bash
# Search for --from-existing across the entire codebase
rg -r 'from-existing' --type ts --type js

Repository: elizaos-plugins/plugin-n8n-workflow

Length of output: 475


🏁 Script executed:

#!/bin/bash
# Check the workflow file mentioned in the review
grep -n 'from-existing' .github/workflows/schema-update.yml

Repository: elizaos-plugins/plugin-n8n-workflow

Length of output: 153


🏁 Script executed:

#!/bin/bash
# Search for this flag in all workflow files
grep -rn 'from-existing' .github/workflows/

Repository: elizaos-plugins/plugin-n8n-workflow

Length of output: 297


🏁 Script executed:

#!/bin/bash
# Check the full main() function to understand the behavior without this flag
sed -n '300,400p' scripts/capture-trigger-schemas.ts | head -60

Repository: elizaos-plugins/plugin-n8n-workflow

Length of output: 2371


--from-existing flag is not recognized by capture-trigger-schemas.ts.

The script only parses --trigger=, --timeout=, --keep, and --create-only (lines 307-310). The --from-existing flag, which is passed in both .github/workflows/schema-update.yml (line 23) and .github/workflows/npm-deploy.yml (line 77), is silently ignored. The script will create and activate new workflows instead of only reading from existing ones, contrary to the intended behavior in CI.

🤖 Prompt for AI Agents
In @.github/workflows/schema-update.yml around lines 22 - 23, The CI flag
--from-existing is not parsed by capture-trigger-schemas.ts so it is ignored;
update the CLI parsing (where --trigger, --timeout, --keep, and --create-only
are handled) to accept a --from-existing boolean (e.g., set a fromExisting
variable in parseArgs or the existing argument-parsing block) and then propagate
that flag into the main flow (functions like captureTriggerSchemas /
createAndActivateWorkflows or whichever routines perform creation/activation) so
that when fromExisting is true the code only reads existing workflows and
explicitly skips any creation or activation steps.

Comment on lines +135 to +143
const KNOWN_CREDENTIALS: Record<string, string> = {
gmailOAuth2: 'hjSOdmEwxXzQpj3V',
googleCalendarOAuth2Api: 'yHjZgYhYPxTOFwdr',
googleDriveOAuth2Api: 'LoKYrCVJOpU8PhAM',
googleSheetsTriggerOAuth2Api: 'UcHkFKN0Khm7z1O3',
googleBusinessProfileOAuth2Api: 'lo6OS1caDfS84dgR',
githubOAuth2Api: 'T3JHHfCQmwbQgtEr',
linearOAuth2Api: 'jADHC6LxJ8ax7JMc',
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hardcoded credential IDs committed to source control.

Gitleaks rightly flags these as secrets. Even though they are n8n-internal IDs (not raw API keys), they reference real credential entries on a specific instance and expose infrastructure details. Move them to environment variables or a .env file excluded from version control.

🛡️ Suggested approach
-const KNOWN_CREDENTIALS: Record<string, string> = {
-  gmailOAuth2: 'hjSOdmEwxXzQpj3V',
-  googleCalendarOAuth2Api: 'yHjZgYhYPxTOFwdr',
-  ...
-};
+// Load from env: N8N_CRED_MAP='gmailOAuth2=abc123,googleCalendarOAuth2Api=def456'
+const KNOWN_CREDENTIALS: Record<string, string> = {};
+const credMapEnv = process.env.N8N_CRED_MAP ?? '';
+for (const pair of credMapEnv.split(',').filter(Boolean)) {
+  const [key, val] = pair.split('=');
+  if (key && val) KNOWN_CREDENTIALS[key.trim()] = val.trim();
+}
📝 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
const KNOWN_CREDENTIALS: Record<string, string> = {
gmailOAuth2: 'hjSOdmEwxXzQpj3V',
googleCalendarOAuth2Api: 'yHjZgYhYPxTOFwdr',
googleDriveOAuth2Api: 'LoKYrCVJOpU8PhAM',
googleSheetsTriggerOAuth2Api: 'UcHkFKN0Khm7z1O3',
googleBusinessProfileOAuth2Api: 'lo6OS1caDfS84dgR',
githubOAuth2Api: 'T3JHHfCQmwbQgtEr',
linearOAuth2Api: 'jADHC6LxJ8ax7JMc',
};
// Load from env: N8N_CRED_MAP='gmailOAuth2=abc123,googleCalendarOAuth2Api=def456'
const KNOWN_CREDENTIALS: Record<string, string> = {};
const credMapEnv = process.env.N8N_CRED_MAP ?? '';
for (const pair of credMapEnv.split(',').filter(Boolean)) {
const [key, val] = pair.split('=');
if (key && val) KNOWN_CREDENTIALS[key.trim()] = val.trim();
}
🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 136-136: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 138-138: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 139-139: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 140-140: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 141-141: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 142-142: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In `@scripts/capture-trigger-schemas.ts` around lines 135 - 143, The
KNOWN_CREDENTIALS constant currently contains hardcoded credential IDs; remove
the hardcoded map and load these IDs from environment variables instead (either
a single JSON string env like KNOWN_CREDENTIALS_JSON that you JSON.parse and
validate, or individual env vars per key such as KNOWN_CREDENTIALS_gmailOAuth2,
_googleCalendarOAuth2Api, etc.). Update scripts/capture-trigger-schemas.ts to
read process.env for these values, validate required keys exist (throw or log a
clear error if missing), and fallback to an empty object only for non-critical
keys; ensure no secret values remain in source control and document the expected
env var names in the project README or .env.example.

Comment on lines +305 to +310
async function main() {
const args = process.argv.slice(2);
const filterTrigger = args.find((a) => a.startsWith('--trigger='))?.split('=')[1];
const timeoutSec = parseInt(args.find((a) => a.startsWith('--timeout='))?.split('=')[1] ?? '30');
const keepWorkflows = args.includes('--keep');
const createOnly = args.includes('--create-only');
Copy link

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

🏁 Script executed:

# Check if --from-existing is used in CI workflows
rg -n 'from-existing' .github/workflows/ || echo "No matches found in workflows"

Repository: elizaos-plugins/plugin-n8n-workflow

Length of output: 297


🏁 Script executed:

# Check the full argument parsing in the script
cat -n scripts/capture-trigger-schemas.ts | head -350 | tail -100

Repository: elizaos-plugins/plugin-n8n-workflow

Length of output: 4453


🏁 Script executed:

# Also check if --from-existing is referenced anywhere else in the script
rg -n 'from' scripts/capture-trigger-schemas.ts

Repository: elizaos-plugins/plugin-n8n-workflow

Length of output: 592


🏁 Script executed:

# Search for camelCase or snake_case variants
rg -n 'fromExisting|from_existing|FromExisting' scripts/capture-trigger-schemas.ts

Repository: elizaos-plugins/plugin-n8n-workflow

Length of output: 61


🏁 Script executed:

# Check the full main function to see flow and understand what should happen with the flag
sed -n '305,450p' scripts/capture-trigger-schemas.ts | cat -n

Repository: elizaos-plugins/plugin-n8n-workflow

Length of output: 6361


--from-existing flag is used in CI workflows but not parsed.

Both .github/workflows/schema-update.yml:23 and .github/workflows/npm-deploy.yml:77 invoke this script with --from-existing, but the argument parsing on lines 305-310 doesn't handle it. The script will always proceed to create, activate, and wait for trigger workflows (lines 84-132) instead of only capturing schemas from existing workflows as intended.

🤖 Prompt for AI Agents
In `@scripts/capture-trigger-schemas.ts` around lines 305 - 310, The script's
main() argument parsing misses the --from-existing flag, so add parsing for it
(e.g., const fromExisting = args.includes('--from-existing')) alongside existing
flags (filterTrigger, timeoutSec, keepWorkflows, createOnly) and then use that
boolean to short-circuit the create/activate/wait flow in the trigger capture
logic (the block that currently creates, activates, and waits for triggers
between lines 84-132), making the script only capture schemas from existing
workflows when fromExisting is true; update any downstream conditionals that
currently check createOnly/keepWorkflows to also respect fromExisting where
appropriate.

Comment on lines +198 to +205
// Create draft from the existing workflow
const draft: WorkflowDraft = {
workflow: fullWorkflow,
prompt: `Modify existing workflow: ${fullWorkflow.name}`,
userId,
createdAt: Date.now(),
};
await runtime.setCache(cacheKey, draft);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Draft is missing originMessageId — same-turn auto-confirm guard won't apply.

createWorkflow.ts (line 316) checks existingDraft.originMessageId === message.id to prevent auto-confirm on the same turn. Drafts created here lack originMessageId, so if an agent chains MODIFY_EXISTING_N8N_WORKFLOWCREATE_N8N_WORKFLOW in one turn, the guard is bypassed and the draft could be auto-confirmed.

Proposed fix
       const draft: WorkflowDraft = {
         workflow: fullWorkflow,
         prompt: `Modify existing workflow: ${fullWorkflow.name}`,
         userId,
         createdAt: Date.now(),
+        originMessageId: message.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
// Create draft from the existing workflow
const draft: WorkflowDraft = {
workflow: fullWorkflow,
prompt: `Modify existing workflow: ${fullWorkflow.name}`,
userId,
createdAt: Date.now(),
};
await runtime.setCache(cacheKey, draft);
// Create draft from the existing workflow
const draft: WorkflowDraft = {
workflow: fullWorkflow,
prompt: `Modify existing workflow: ${fullWorkflow.name}`,
userId,
createdAt: Date.now(),
originMessageId: message.id,
};
await runtime.setCache(cacheKey, draft);
🤖 Prompt for AI Agents
In `@src/actions/modifyExistingWorkflow.ts` around lines 198 - 205, The draft
created in modifyExistingWorkflow.ts is missing the
WorkflowDraft.originMessageId field so the same-turn auto-confirm guard (which
checks existingDraft.originMessageId === message.id in createWorkflow.ts) can be
bypassed; fix by adding originMessageId: message.id (or the appropriate incoming
message identifier variable available in this scope) to the draft object before
calling runtime.setCache(cacheKey, draft) so the guard will correctly detect
same-turn drafts.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude
Copy link

claude bot commented Feb 10, 2026

PR Review Summary

I've completed a comprehensive review of this PR. Overall, the implementation is solid with good security practices. I've identified several issues ranging from critical security concerns to code quality improvements.

Critical Issues 🔴

  1. Hardcoded credential IDs in capture-trigger-schemas.ts expose sensitive data
  2. ReDoS vulnerability in regex patterns with unbounded quantifiers

High Priority Issues 🟡

  1. Missing input validation on workflow IDs (path traversal risk)
  2. Race condition in draft cache TTL checking
  3. Unsafe string replacement could cause unintended changes

Code Quality Issues 🔵

  1. TypeScript type safety could be improved in several areas
  2. Missing null checks in schema validation
  3. Inconsistent error handling patterns

See inline comments for specific fixes and recommendations.

googleBusinessProfileOAuth2Api: 'lo6OS1caDfS84dgR',
githubOAuth2Api: 'T3JHHfCQmwbQgtEr',
linearOAuth2Api: 'jADHC6LxJ8ax7JMc',
};
Copy link

Choose a reason for hiding this comment

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

🔴 CRITICAL SECURITY ISSUE: Hardcoded Credential IDs

These hardcoded credential IDs should never be committed to source control as they could provide unauthorized access to your n8n instance.

Recommended fixes:

  1. Move these to environment variables or a config file excluded from git
  2. Add *credentials*.json to .gitignore
  3. Document the credential setup process in README
Suggested change
};
// Load from environment or secure config
const KNOWN_CREDENTIALS: Record<string, string> = process.env.N8N_CREDENTIALS
? JSON.parse(process.env.N8N_CREDENTIALS)
: {};

Consider rotating these credentials since they're now public in the PR.

const refs: ExpressionRef[] = [];

// Match every $json.field reference, even inside compound expressions like {{ $json.a || $json.b }}
const simplePattern = /\$json\.([a-zA-Z0-9_.\[\]'"-]{1,200})/g;
Copy link

Choose a reason for hiding this comment

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

🔴 CRITICAL: ReDoS (Regular Expression Denial of Service) Vulnerability

The regex patterns use unbounded quantifiers {1,200} with complex character classes, which can cause catastrophic backtracking on malicious input.

Attack scenario:

const malicious = "$json." + "a".repeat(1000) + "!";
// Could cause regex engine to hang

Recommended fix:

Suggested change
const simplePattern = /\$json\.([a-zA-Z0-9_.\[\]'"-]{1,200})/g;
// Use atomic groups or possessive quantifiers, limit complexity
const simplePattern = /\$json\.([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+|\[\d+\])*)/g;
const namedNodePattern = /\$\(['"]([^'"]{1,100})['"]\)\.item\.json\.([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+|\[\d+\])*)/g;

Additionally, add input length validation before regex matching:

if (str.length > 10000) return [];  // Reject suspiciously long inputs


// Medium/High confidence — load the workflow
const workflowId = matchResult.matchedWorkflowId;
const fullWorkflow = await service.getWorkflow(workflowId);
Copy link

Choose a reason for hiding this comment

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

🟡 SECURITY: Missing Input Validation on Workflow ID

The workflow ID from matchedWorkflowId is passed directly to the API without validation. While n8n likely validates this, defense-in-depth suggests validating format here.

Potential issues:

  • Path traversal if IDs can contain ../
  • Injection if IDs can contain special characters

Recommended fix:

Suggested change
const fullWorkflow = await service.getWorkflow(workflowId);
const workflowId = matchResult.matchedWorkflowId;
// Validate workflow ID format (assuming n8n uses alphanumeric IDs)
if (!/^[a-zA-Z0-9-]+$/.test(workflowId)) {
logger.error({ src: 'plugin:n8n-workflow:action:modify-existing' },
`Invalid workflow ID format: ${workflowId}`);
if (callback) {
await callback({ text: 'Invalid workflow identifier', success: false });
}
return { success: false };
}
const fullWorkflow = await service.getWorkflow(workflowId);


// Check for existing draft — if one exists, user should use CREATE_N8N_WORKFLOW
const existingDraft = await runtime.getCache<WorkflowDraft>(cacheKey);
if (existingDraft && Date.now() - existingDraft.createdAt < DRAFT_TTL_MS) {
Copy link

Choose a reason for hiding this comment

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

🟡 BUG: Race Condition in TTL Check

There's a TOCTOU (Time-of-check Time-of-use) race condition. If the draft expires between the check and the response, the user gets inconsistent behavior.

Issue:

if (existingDraft && Date.now() - existingDraft.createdAt < DRAFT_TTL_MS) {
  // Draft could expire here before callback executes
  await callback({ text: '...' });
}

Recommended fix:

Suggested change
if (existingDraft && Date.now() - existingDraft.createdAt < DRAFT_TTL_MS) {
const existingDraft = await runtime.getCache<WorkflowDraft>(cacheKey);
const draftAge = existingDraft ? Date.now() - existingDraft.createdAt : null;
if (existingDraft && draftAge !== null && draftAge < DRAFT_TTL_MS) {
const remainingMinutes = Math.floor((DRAFT_TTL_MS - draftAge) / 60000);
if (callback) {
await callback({
text: `You already have a workflow draft in progress (expires in ${remainingMinutes}min). ` +
'Please confirm, modify, or cancel that draft first before loading another workflow.',
success: false,
});
}
return { success: false };
}

const value = obj[key];

if (typeof value === 'string' && value.includes(original)) {
obj[key] = value.replaceAll(original, replacement);
Copy link

Choose a reason for hiding this comment

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

🟡 BUG: Unsafe String Replacement Could Cause Unintended Changes

Using replaceAll() with plain strings can cause unintended replacements if the original expression appears elsewhere in the workflow.

Example issue:

// If correcting "$json.name" → "$json.fullName"
// This would incorrectly replace:
"Please enter your name"  "Please enter your fullName"

Recommended fix:

Suggested change
obj[key] = value.replaceAll(original, replacement);
if (typeof value === 'string' && value.includes(original)) {
// Only replace if it's actually the expression, not just substring match
// Use a more precise regex-based replacement
const escapedOriginal = original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(escapedOriginal, 'g');
obj[key] = value.replace(regex, replacement);
}

Better yet, track the exact parameter path during validation and only replace in that specific location rather than doing global replacements.

return false;
}

const prop = properties[segment] as SchemaContent | undefined;
Copy link

Choose a reason for hiding this comment

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

🔵 Code Quality: Missing Type Safety

The type assertion as SchemaContent bypasses TypeScript's type checking. This could lead to runtime errors if the schema structure is malformed.

Recommended fix:

Suggested change
const prop = properties[segment] as SchemaContent | undefined;
const prop = properties[segment];
// Type guard instead of assertion
if (!prop || typeof prop !== 'object' || !('type' in prop)) {
return false;
}
const propSchema = prop as SchemaContent;
if (!propSchema) {
return false;
}


function getSourceSchema(sourceName: string) {
if (schemaCache.has(sourceName)) {
return schemaCache.get(sourceName)!;
Copy link

Choose a reason for hiding this comment

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

🔵 Code Quality: Potential Null Dereference

If schemaCache.get(sourceName) returns undefined, the ! assertion will cause the function to return null without proper type safety.

Recommended fix:

Suggested change
return schemaCache.get(sourceName)!;
function getSourceSchema(sourceName: string) {
if (schemaCache.has(sourceName)) {
return schemaCache.get(sourceName) ?? null;
}

This makes the null handling explicit and type-safe.

/**
* Parses "from.value[0].address" or "headers['content-type']" into path segments.
*/
function parseFieldPath(field: string): string[] {
Copy link

Choose a reason for hiding this comment

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

🔵 Code Quality: Complex Parsing Logic Without Bounds

The manual character-by-character parsing could be vulnerable to edge cases and doesn't have clear bounds checking.

Suggestions:

  1. Add a maximum path length check at the start
  2. Consider using a battle-tested parser library for complex paths
  3. Add unit tests for malformed inputs like:
    • Unclosed brackets: field[unclosed
    • Nested quotes: field['nested\\'quote']
    • Very deep nesting: a.b.c.d.e.f.g... (200+ levels)
function parseFieldPath(field: string): string[] {
  if (field.length > 500) {
    throw new Error('Field path exceeds maximum length');
  }
  // ... existing parsing logic
}

};
}

function diffNodeParams(
Copy link

Choose a reason for hiding this comment

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

🔵 Code Quality: Inconsistent Parameter Comparison

Using JSON.stringify for object comparison has performance implications and can give false positives/negatives due to property ordering.

Issues:

  • {a:1, b:2} vs {b:2, a:1} would be considered different
  • Performance overhead for large parameter objects
  • Doesn't handle undefined values correctly

Recommended approach:

function deepEqual(a: unknown, b: unknown): boolean {
  if (a === b) return true;
  if (typeof a !== typeof b) return false;
  if (typeof a !== 'object' || a === null || b === null) return false;
  
  const keysA = Object.keys(a);
  const keysB = Object.keys(b as object);
  if (keysA.length !== keysB.length) return false;
  
  return keysA.every(key => deepEqual((a as any)[key], (b as any)[key]));
}

Or use a library like lodash.isEqual for this.


const workflow = await generateWorkflow(
let workflow = await generateWorkflow(
this.runtime,
Copy link

Choose a reason for hiding this comment

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

🔵 Code Quality: Unhandled Promise Rejection

The generateWorkflow function call isn't wrapped in try-catch. If it throws, the error might not be properly logged or handled according to the service's error handling pattern.

While there is an outer try-catch at line 122, it's better to have explicit error handling for critical operations to provide better context.

Suggested improvement:

    let workflow;
    try {
      workflow = await generateWorkflow(
        this.runtime,
        prompt,
        relevantNodes.map((r) => r.node)
      );
    } catch (error) {
      logger.error(
        { src: 'plugin:n8n-workflow:service:main' },
        `Workflow generation failed: ${error instanceof Error ? error.message : String(error)}`
      );
      throw error;  // Re-throw after logging with context
    }

@claude
Copy link

claude bot commented Feb 10, 2026

Additional Observations

Positive Aspects ✅

  1. Good validation pipeline - The multi-layer validation (schema → parameters → output refs) is well-designed
  2. Proper error logging - Consistent use of structured logging throughout
  3. Type safety - Generally good TypeScript usage with proper interfaces
  4. Test coverage - PR mentions 215 tests passing which is excellent
  5. Security-conscious - Using environment variables for API keys in most places

Recommendations for Future

  1. Add rate limiting for LLM calls in correction functions to prevent abuse
  2. Consider caching for schema lookups - the same schemas are loaded repeatedly
  3. Add telemetry for tracking which corrections are most common (helps improve the LLM prompts)
  4. Document security model - Create SECURITY.md explaining:
    • Credential handling
    • Input validation boundaries
    • Trust boundaries between user input and n8n API

Test Coverage Needed

Please ensure tests cover:

  • Malformed regex inputs (ReDoS scenarios)
  • Very long field paths and expressions
  • Race conditions in draft lifecycle
  • Workflow ID validation edge cases
  • Schema with circular references (if possible in n8n)

@standujar standujar merged commit 02808ce into main Feb 10, 2026
7 checks passed
@standujar standujar deleted the feat/workflow-modify-and-schema-validation branch February 10, 2026 16:40
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