Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run crawl-nodes
- run: bun run crawl
env:
N8N_API_KEY: ${{ secrets.N8N_API_KEY }}
N8N_HOST: ${{ secrets.N8N_HOST }}
- run: bun run test:unit

test-integration:
Expand All @@ -32,7 +35,10 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run crawl-nodes
- run: bun run crawl
env:
N8N_API_KEY: ${{ secrets.N8N_API_KEY }}
N8N_HOST: ${{ secrets.N8N_HOST }}
- run: bun run test:integration

test-e2e:
Expand All @@ -42,7 +48,10 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run crawl-nodes
- run: bun run crawl
env:
N8N_API_KEY: ${{ secrets.N8N_API_KEY }}
N8N_HOST: ${{ secrets.N8N_HOST }}
- run: bun run test:e2e

build:
Expand All @@ -53,5 +62,8 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run crawl-nodes
- run: bun run crawl
env:
N8N_API_KEY: ${{ secrets.N8N_API_KEY }}
N8N_HOST: ${{ secrets.N8N_HOST }}
- run: bun run build
7 changes: 5 additions & 2 deletions .github/workflows/npm-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,11 @@ jobs:
- name: Install dependencies
run: bun install

- name: Generate node catalog
run: bun run crawl-nodes
- name: Generate node catalog and schemas
run: bun run crawl
env:
N8N_API_KEY: ${{ secrets.N8N_API_KEY }}
N8N_HOST: ${{ secrets.N8N_HOST }}

- name: Build package
run: bun run build
Expand Down
79 changes: 79 additions & 0 deletions .github/workflows/schema-update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Schema Update

on:
schedule:
- cron: '0 3 * * 1' # Monday 3AM UTC
workflow_dispatch:

jobs:
check:
runs-on: ubuntu-latest
permissions:
contents: write
actions: write
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install

- name: Generate schemas
run: bun run crawl
env:
N8N_API_KEY: ${{ secrets.N8N_API_KEY }}
N8N_HOST: ${{ secrets.N8N_HOST }}

- run: bun run build

- name: Download latest release
run: |
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
Comment on lines +27 to +31
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

First-release scenario will fail silently.

If @elizaos/plugin-n8n-workflow hasn't been published to npm yet, bunx npm pack @elizaos/plugin-n8n-workflow@latest will fail, aborting the entire job. Consider adding continue-on-error: true to this step and treating a missing release as "everything changed":

♻️ Suggested fix
       - name: Download latest release
+        id: download
+        continue-on-error: true
         run: |
           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

Then in the compare step, treat a download failure as "changed":

       - name: Compare with latest release
         id: compare
         run: |
+          if [ "${{ steps.download.outcome }}" = "failure" ]; then
+            echo "No previous release found — treating as changed"
+            echo "changed=true" >> $GITHUB_OUTPUT
+            exit 0
+          fi
           CHANGED=false
🤖 Prompt for AI Agents
In @.github/workflows/schema-update.yml around lines 27 - 31, Add
continue-on-error: true to the "Download latest release" step that runs bunx npm
pack `@elizaos/plugin-n8n-workflow`@latest so the job doesn't abort if the package
isn't published; detect the failure by checking the presence of the extracted
/tmp/latest folder or by setting a step output (e.g., download_succeeded) based
on the pack/tar exit status, then update the subsequent compare step to treat
download_succeeded=false (or missing /tmp/latest) as "changed" so the workflow
proceeds as a first-release case.


- name: Compare with latest release
id: compare
run: |
CHANGED=false

for file in defaultNodes.json schemaIndex.json triggerSchemaIndex.json; do
NEW="dist/data/$file"
OLD="/tmp/latest/package/dist/data/$file"

if [ ! -f "$NEW" ]; then
continue
fi
Comment on lines +42 to +44
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing local artifact skipped silently — may mask build failures.

If dist/data/$file doesn't exist (e.g., the crawl or build step failed partially), the loop continues without marking anything as changed. This means a broken build could produce an empty diff and no version bump, silently swallowing the problem. Consider logging a warning or failing if an expected file is absent after a successful build.

🤖 Prompt for AI Agents
In @.github/workflows/schema-update.yml around lines 42 - 44, The loop that
skips missing artifacts currently does `if [ ! -f "$NEW" ]; then continue; fi`
which silently hides build failures; change this to emit a clear failure or
warning by checking the same `$NEW` variable inside the loop and either (a)
`echo` a descriptive warning with the file name and set a non-success flag
(e.g., `MISSING=true`) so the workflow can fail after the loop, or (b)
immediately `echo "ERROR: missing expected artifact $NEW"` and `exit 1`; update
the surrounding logic to use the `MISSING` flag if chosen so the job fails when
any expected `dist/data/$file` is absent.


if [ ! -f "$OLD" ]; then
echo "$file: NEW file (not in latest release)"
CHANGED=true
continue
fi

if ! diff <(jq -S . "$NEW") <(jq -S . "$OLD") > /dev/null 2>&1; then
echo "$file: CHANGED"
CHANGED=true
else
echo "$file: unchanged"
fi
done

echo "changed=$CHANGED" >> $GITHUB_OUTPUT

- name: Bump version
if: steps.compare.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
npm version patch --no-git-tag-version
VERSION=$(jq -r .version package.json)
git add package.json
git commit -m "chore: bump to v${VERSION} (schema update)"
git push
Copy link

Choose a reason for hiding this comment

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

Security concern: This workflow automatically pushes version bumps to main without creating a PR for review.

Risk: Schema changes could introduce breaking changes that bypass code review.

Recommendation: Instead of auto-pushing, create a PR:

- name: Create PR for schema update
  if: steps.compare.outputs.changed == 'true'
  run: |
    git checkout -b schema-update-$(date +%Y%m%d)
    git add package.json dist/data/
    git commit -m "chore: update schemas (automated)"
    git push origin schema-update-$(date +%Y%m%d)
    gh pr create --title "chore: update schemas" --body "Automated schema update from scheduled job"

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Trigger publish
if: steps.compare.outputs.changed == 'true'
run: gh workflow run npm-deploy.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ coverage/
.temp/
tmp/
temp/
# Generated node catalog (run `bun run crawl-nodes` to regenerate)
# Generated data files (run `bun run crawl` to regenerate)
src/data/defaultNodes.json
src/data/schemaIndex.json
src/data/triggerSchemaIndex.json

# Script cache
.cache/

116 changes: 106 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ User Prompt: "Send me Stripe payment summaries every Monday via Gmail"
│ Max: 5 keywords │
└──────────────────┬───────────────────────┘
┌──────────────────────────────────────────┐
┌────────────────────�����────────────────────┐
│ 2. searchNodes (local catalog) │
│ 457 embedded n8n node definitions │
│ Keyword scoring: │
Expand All @@ -471,6 +471,7 @@ User Prompt: "Send me Stripe payment summaries every Monday via Gmail"
┌──────────────────────────────────────────┐
│ 3. generateWorkflow (TEXT_LARGE) │
│ Input: user prompt + node defs │
│ + output schemas (from index) │
│ Config: temperature 0, JSON mode │
│ Output: complete n8n workflow JSON │
│ Includes: _meta.assumptions, │
Expand All @@ -479,7 +480,36 @@ User Prompt: "Send me Stripe payment summaries every Monday via Gmail"
└──────────────────┬───────────────────────┘
┌──────────────────────────────────────────┐
│ 4. validateWorkflow │
│ 4. normalizeTriggerSimpleParam │
│ Set simple=true on trigger nodes │
└──────────────────┬───────────────────────┘
┌──────────────────────────────────────────┐
│ 5. correctOptionParameters │
│ Fix node types, versions, resources, │
│ and operations against the catalog │
└──────────────────┬───────────────────────┘
┌──────────────────────────────────────────┐
│ 6. detectUnknownParameters │
│ + correctParameterNames (LLM) │
│ Fix param names not in catalog │
└──────────────────┬───────────────────────┘
┌──────────────────────────────────────────┐
│ 7. validateOutputReferences │
│ + correctFieldReferences (LLM) │
│ Validate $json expressions against │
│ output schemas, fix invalid paths │
└──────────────────┬───────────────────────┘
┌──────────────────────────────────────────┐
│ 8. ensureExpressionPrefix │
│ Wrap {{ }} with ={{ }} for n8n │
└───────��──────────┬───────────────────────┘
┌──────────────────────────────────────────┐
│ 9. validateWorkflow │
│ - nodes array exists, non-empty │
│ - connections object valid │
│ - required fields on each node │
Expand All @@ -492,7 +522,7 @@ User Prompt: "Send me Stripe payment summaries every Monday via Gmail"
└──────────────────┬───────────────────────┘
┌──────────────────────────────────────────┐
5. injectCatalogClarifications
10. injectCatalogClarifications │
│ Check each node against catalog: │
│ - validateNodeParameters │
│ → missing required params? │
Expand All @@ -502,7 +532,7 @@ User Prompt: "Send me Stripe payment summaries every Monday via Gmail"
└──────────────────┬───────────────────────┘
┌──────────────────────────────────────────┐
6. positionNodes
11. positionNodes │
│ BFS layout from trigger nodes: │
│ - Triggers at x=250 │
│ - Each level: x += 250 │
Expand Down Expand Up @@ -847,13 +877,19 @@ src/
│ ├── keywordExtraction.ts # System prompt for keyword extraction
│ ├── workflowMatching.ts # System prompt for semantic matching
│ ├── draftIntent.ts # System prompt for intent classification
│ ├── parameterCorrection.ts # System prompt for parameter name correction
│ └── actionResponse.ts # System prompt for response formatting
├── schemas/
│ ├── keywordExtraction.ts # JSON schema for keyword output
│ ├── workflowMatching.ts # JSON schema for matching output
│ └── draftIntent.ts # JSON schema for intent output
├── types/
│ └── index.ts # All TypeScript interfaces and types
├── data/ # Generated data (run `bun run crawl`)
│ ├── defaultNodes.json # 457 n8n node definitions (types, params, versions)
│ ├── schemaIndex.json # Output schemas per node/resource/operation
│ ├── triggerSchemaIndex.json # Trigger output schemas (captured from n8n)
│ └── langchain-output-schemas.json # Manual overrides for langchain nodes
├── db/
│ └── schema.ts # Drizzle ORM schema (PostgreSQL)
└── utils/
Expand All @@ -862,19 +898,79 @@ src/
├── context.ts # Conversation context builder + user tag naming
├── credentialResolver.ts # 4-step credential resolution chain
├── generation.ts # LLM utilities (extract, generate, match, classify, format)
└── workflow.ts # Validation, positioning, auto-fix
├── outputSchema.ts # Output schema validation + expression parsing
└── workflow.ts # Validation, positioning, corrections, auto-fix

scripts/
├── crawl.ts # Master crawl (runs all 3 steps below)
├── crawl-nodes.ts # Step 1: Extract node defs from n8n-nodes-base
├── crawl-schemas.ts # Step 2: Extract output schemas from __schema__/ dirs
└── capture-trigger-schemas.ts # Step 3: Capture trigger schemas from live n8n
```

---

## Data Generation

The plugin relies on three generated data files in `src/data/`. These are **not committed to git** — they are regenerated by `bun run crawl`.

### `bun run crawl`

Runs three steps:

| Step | Script | Generates | Requires |
|------|--------|-----------|----------|
| 1/3 | `crawl-nodes.ts` | `defaultNodes.json` — 457 node definitions with parameters, versions, credentials | n8n-nodes-base package |
| 2/3 | `crawl-schemas.ts` | `schemaIndex.json` — output schemas per node/resource/operation | n8n-nodes-base `__schema__/` dirs + langchain overrides |
| 3/3 | `capture-trigger-schemas.ts` | `triggerSchemaIndex.json` — trigger output schemas | `N8N_HOST` + `N8N_API_KEY` env vars |

**Step 3 is skipped** if `N8N_HOST` / `N8N_API_KEY` are not set (a warning is printed).

### Output Schema System

The LLM receives output schemas during workflow generation to prevent hallucinated field paths:

- **n8n-nodes-base nodes**: Schemas are crawled from `__schema__/` directories in the npm package
- **@n8n/n8n-nodes-langchain nodes**: No `__schema__/` dirs exist — schemas are defined in `src/data/langchain-output-schemas.json` and merged at crawl time
- **Trigger nodes**: Schemas are captured from real n8n executions via `capture-trigger-schemas.ts`

The generation prompt includes output schemas for all relevant nodes, so the LLM uses correct field paths like `$json.output[0].content[0].text` instead of inventing wrong ones like `$json.choices[0].message.content`.

As a safety net, `validateOutputReferences` checks all `$json` expressions against schemas post-generation and `correctFieldReferences` (LLM) fixes any remaining invalid paths.

---

## Development

### Prerequisites

| Requirement | Purpose |
|-------------|---------|
| [Bun](https://bun.sh) | Runtime and package manager |
| `N8N_HOST` | n8n instance URL (for trigger schema capture) |
| `N8N_API_KEY` | n8n API key (for trigger schema capture) |

### Setup

```bash
bun install # Install dependencies
N8N_HOST=https://... N8N_API_KEY=... bun run crawl # Generate data files
bun run build # Compile TypeScript
```

### Commands

```bash
bun install # install dependencies
bun run build # compile TypeScript
bun test # run tests (162 tests)
bun run lint # lint
bun run format # format
bun run crawl # Generate all data files (nodes + schemas + triggers)
bun run crawl:nodes # Generate only defaultNodes.json
bun run crawl:schemas # Generate only schemaIndex.json
bun run build # Compile TypeScript
bun test # Run all tests (~260 tests)
bun run test:unit # Run unit tests only
bun run test:integration # Run integration tests only
bun run test:e2e # Run e2e tests only
bun run lint # Lint
bun run format # Format
```

## License
Expand Down
22 changes: 22 additions & 0 deletions __tests__/fixtures/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,28 @@ export function createSlackNode(overrides?: Partial<N8nNode>): N8nNode {
};
}

export function createGmailTriggerNode(overrides?: Partial<N8nNode>): N8nNode {
return {
name: 'Gmail Trigger',
type: 'n8n-nodes-base.gmailTrigger',
typeVersion: 1,
position: [250, 300],
parameters: {},
...overrides,
};
}

export function createGithubTriggerNode(overrides?: Partial<N8nNode>): N8nNode {
return {
name: 'GitHub Trigger',
type: 'n8n-nodes-base.githubTrigger',
typeVersion: 1,
position: [250, 300],
parameters: {},
...overrides,
};
}

// ============================================================================
// WORKFLOWS
// ============================================================================
Expand Down
Loading
Loading