mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 10:31:15 +00:00
feat: AI Workflow Builder backend (no-changelog) (#14837)
This commit is contained in:
466
packages/@n8n/ai-workflow-builder/src/chains/nodes-composer.ts
Normal file
466
packages/@n8n/ai-workflow-builder/src/chains/nodes-composer.ts
Normal file
@@ -0,0 +1,466 @@
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import type { AIMessageChunk } from '@langchain/core/messages';
|
||||
import { SystemMessage } from '@langchain/core/messages';
|
||||
import { ChatPromptTemplate, HumanMessagePromptTemplate } from '@langchain/core/prompts';
|
||||
import { DynamicStructuredTool } from '@langchain/core/tools';
|
||||
import { OperationalError } from 'n8n-workflow';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Using SystemMessage directly instead of escapeSingleCurlyBrackets to avoid
|
||||
// issues with double curly braces in n8n expressions
|
||||
const systemPrompt = new SystemMessage(`You are an expert n8n workflow architect who creates complete node configurations for complex workflows.
|
||||
|
||||
## Your Task
|
||||
Generate fully-formed n8n node configurations with properly structured parameters for each selected node.
|
||||
|
||||
## Reference Information
|
||||
You will receive:
|
||||
1. The original user workflow request
|
||||
2. A list of selected n8n nodes with their descriptions and parameters
|
||||
|
||||
## Node Configuration Guidelines
|
||||
1. CREATE PROPER STRUCTURE: Include all required fields (parameters, name, type)
|
||||
2. USE DESCRIPTIVE NAMES: Each node name should clearly describe its function
|
||||
3. POPULATE KEY PARAMETERS: Set values for essential parameters based on node type
|
||||
4. MAINTAIN LOGICAL FLOW: Node parameters should enable proper data flow
|
||||
5. FOLLOW NODE PATTERNS: Use the correct structure for each node type
|
||||
6. ADD DOCUMENTATION: Include at least one sticky note, explaining the workflow. Include additional sticky notes for complex parts of the workflow.
|
||||
|
||||
## CRITICAL: Correctly Formatting n8n Expressions
|
||||
When using expressions to reference data from other nodes:
|
||||
- ALWAYS use the format: \`={{ $('Node Name').item.json.field }}\`
|
||||
- NEVER omit the equals sign before the double curly braces
|
||||
- ALWAYS use DOUBLE curly braces, never single
|
||||
- NEVER use emojis or special characters inside expressions as they will break the expression
|
||||
- INCORRECT: \`{ $('Node Name').item.json.field }\` (missing =, single braces)
|
||||
- INCORRECT: \`{{ $('Node Name').item.json.field }}\` (missing =)
|
||||
- INCORRECT: \`={{ $('👍 Node').item.json.field }}\` (contains emoji)
|
||||
- CORRECT: \`={{ $('Previous Node').item.json.field }}\`
|
||||
|
||||
This format is essential for n8n to properly process the expression.
|
||||
|
||||
## IF Node Configuration (CRITICAL)
|
||||
The IF node allows conditional branching based on comparing values. It has two outputs:
|
||||
- Output 0: TRUE branch (when conditions are met)
|
||||
- Output 1: FALSE branch (when conditions are NOT met)
|
||||
|
||||
### Key Points for IF Node:
|
||||
1. MATCH OPERATOR TYPE TO DATA TYPE - Use the correct operator type that matches your data:
|
||||
- For string values: use "type": "string" with operations like "equals", "contains", "exists"
|
||||
- For number values: use "type": "number" with operations like "equals", "gt", "lt"
|
||||
- For boolean values: use "type": "boolean" with operations like "equals", "true", "false"
|
||||
- For arrays: use "type": "array" with operations like "empty", "contains"
|
||||
- For objects: use "type": "object" with operations like "exists", "empty"
|
||||
- For dates: use "type": "dateTime" with operations like "before", "after"
|
||||
|
||||
2. USE SINGLE VALUE OPERATORS CORRECTLY:
|
||||
- Some operators like "exists", "notExists", "empty" don't need a right value
|
||||
- For these operators, include "singleValue": true in the operator object
|
||||
- Example: Checking if a string exists: "operator": { "type": "string", "operation": "exists", "singleValue": true }
|
||||
|
||||
3. USE CORRECT DATA TYPES FOR RIGHT VALUES:
|
||||
- Number comparisons: use actual numbers (without quotes) like 5, not "5"
|
||||
- Boolean comparisons: use true or false (without quotes), not "true" or "false"
|
||||
- String comparisons: use quoted strings like "text"
|
||||
- When using expressions for the right value, include the proper format: "={{ expression }}"
|
||||
|
||||
### IF Node Examples
|
||||
#### Example 1: Check if a number is greater than 5
|
||||
\`\`\`json
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": false,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "={{ $('Previous Node').item.json.amount }}",
|
||||
"rightValue": 5,
|
||||
"operator": {
|
||||
"type": "number",
|
||||
"operation": "gt"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {
|
||||
"ignoreCase": true,
|
||||
"looseTypeValidation": true
|
||||
}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
#### Example 2: Check if a string exists
|
||||
\`\`\`json
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": false,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "={{ $('Previous Node').item.json.email }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "exists",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {
|
||||
"ignoreCase": true,
|
||||
"looseTypeValidation": true
|
||||
}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
#### Example 3: Check if a boolean is true
|
||||
\`\`\`json
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": false,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "={{ $('Previous Node').item.json.isActive }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {
|
||||
"ignoreCase": true,
|
||||
"looseTypeValidation": true
|
||||
}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
#### Example 4: Compare string value
|
||||
\`\`\`json
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": false,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "={{ $('Previous Node').item.json.status }}",
|
||||
"rightValue": "active",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {
|
||||
"ignoreCase": true,
|
||||
"looseTypeValidation": true
|
||||
}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
#### Example 5: Compare boolean value
|
||||
\`\`\`json
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": false,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "={{ $('Previous Node').item.json.isVerified }}",
|
||||
"rightValue": true,
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {
|
||||
"ignoreCase": true,
|
||||
"looseTypeValidation": true
|
||||
}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Common Operator Types and Operations
|
||||
|
||||
#### String Operators:
|
||||
- "exists", "notExists", "empty", "notEmpty" (use with "singleValue": true)
|
||||
- "equals", "notEquals", "contains", "notContains", "startsWith", "endsWith", "regex"
|
||||
|
||||
#### Number Operators:
|
||||
- "exists", "notExists" (use with "singleValue": true)
|
||||
- "equals", "notEquals", "gt" (greater than), "lt" (less than), "gte" (greater than or equal), "lte" (less than or equal)
|
||||
|
||||
#### Boolean Operators:
|
||||
- "exists", "notExists" (use with "singleValue": true)
|
||||
- "true", "false" (use with "singleValue": true)
|
||||
- "equals", "notEquals"
|
||||
|
||||
#### Array Operators:
|
||||
- "exists", "notExists", "empty", "notEmpty" (use with "singleValue": true)
|
||||
- "contains", "notContains", "lengthEquals", "lengthNotEquals"
|
||||
|
||||
## Other Important Node Structures
|
||||
|
||||
### Set Node Structure
|
||||
\`\`\`json
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "unique-id-1",
|
||||
"name": "property_name_1",
|
||||
"value": "property_value_1",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### HTTP Request Node Structures
|
||||
|
||||
#### GET Request
|
||||
\`\`\`json
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://example.com",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "header-name",
|
||||
"value": "header-value"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
#### POST Request
|
||||
\`\`\`json
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://example.com",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "header-name",
|
||||
"value": "header-value"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "field-name",
|
||||
"value": "field-value"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Sticky Note Structure
|
||||
\`\`\`json
|
||||
{
|
||||
"parameters": {
|
||||
"content": "Note content here"
|
||||
},
|
||||
"name": "Descriptive Name",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"notes": true
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Expression Examples
|
||||
1. Reference a field from another node:
|
||||
\`\`\`
|
||||
"value": "={{ $('Previous Node').item.json.fieldName }}"
|
||||
\`\`\`
|
||||
|
||||
2. Use an expression with string concatenation:
|
||||
\`\`\`
|
||||
"value": "={{ 'Hello ' + $('User Input').item.json.name }}"
|
||||
\`\`\`
|
||||
|
||||
3. Access an array item:
|
||||
\`\`\`
|
||||
"value": "={{ $('Data Node').item.json.items[0].id }}"
|
||||
\`\`\`
|
||||
|
||||
4. IMPORTANT: How to properly format text fields with expressions
|
||||
|
||||
### PREFERRED METHOD: Embedding expressions directly within text
|
||||
\`\`\`
|
||||
"text": "=ALERT: It is currently raining in {{ $('Weather Node').item.json.city }}! Temperature: {{ $('Weather Node').item.json.main.temp }}°C"
|
||||
\`\`\`
|
||||
|
||||
### Alternative method: Using string concatenation (use only when needed for complex operations)
|
||||
\`\`\`
|
||||
"text": "={{ 'ALERT: It is currently raining in ' + $('Weather Node').item.json.city + '! Temperature: ' + $('Weather Node').item.json.temp + '°C' }}"
|
||||
\`\`\`
|
||||
|
||||
## CRITICAL: Formatting Text Fields with Expressions
|
||||
|
||||
### KEY RULES FOR THE PREFERRED METHOD (Embedding expressions in text):
|
||||
- Start the string with just "=" (not "={{")
|
||||
- Place each expression inside {{ }} without the = prefix
|
||||
- MOST READABLE and RECOMMENDED approach
|
||||
- Example: "text": "=Status: {{ $('Node').item.json.status }} at {{ $('Node').item.json.time }}"
|
||||
|
||||
### KEY RULES FOR THE ALTERNATIVE METHOD (String concatenation):
|
||||
- Only use when you need complex operations not possible with embedded expressions
|
||||
- Enclose the entire text in a single expression with "={{ }}"
|
||||
- Put all static text in quotes and connect with + operators
|
||||
- Example: "text": "={{ 'Status: ' + $('Node').item.json.status + ' at ' + $('Node').item.json.time }}"
|
||||
|
||||
### EXAMPLES OF PREFERRED USAGE:
|
||||
|
||||
1. Slack message (PREFERRED):
|
||||
\`\`\`json
|
||||
"text": "=ALERT: It is currently raining in {{ $('Weather Node').item.json.city }}! Temperature: {{ $('Weather Node').item.json.main.temp }}°C"
|
||||
\`\`\`
|
||||
|
||||
2. Email subject (PREFERRED):
|
||||
\`\`\`json
|
||||
"subject": "=Order #{{ $('Order Node').item.json.orderId }} Status Update"
|
||||
\`\`\`
|
||||
|
||||
3. Image prompt (PREFERRED):
|
||||
\`\`\`json
|
||||
"prompt": "=Create an image of {{ $('Location Node').item.json.city }} during {{ $('Weather Node').item.json.weather[0].description }}"
|
||||
\`\`\`
|
||||
|
||||
4. Slack message with multiple data points (PREFERRED):
|
||||
\`\`\`json
|
||||
"text": "=Customer {{ $('Customer Data').item.json.name }} has placed order #{{ $('Order Data').item.json.id }} for {{ $('Order Data').item.json.amount }}€"
|
||||
\`\`\`
|
||||
|
||||
5. HTTP request URL (PREFERRED):
|
||||
\`\`\`json
|
||||
"url": "=https://api.example.com/users/{{ $('User Data').item.json.id }}/orders?status={{ $('Filter').item.json.status }}"
|
||||
\`\`\`
|
||||
|
||||
### COMMON MISTAKES TO AVOID:
|
||||
- INCORRECT: "text": "ALERT: Temperature is {{ $('Weather Node').item.json.temp }}°C" (missing = prefix)
|
||||
- INCORRECT: "text": "={{ $('Weather Node').item.json.temp }}" (using expression for dynamic part only)
|
||||
- INCORRECT: "text": "={{ $('⚠️ Weather').item.json.temp }}" (emoji in node name)
|
||||
- INCORRECT: "text": "={{ 'ALERT' }} {{ $('Weather').item.json.city }}" (mixing methods)
|
||||
|
||||
## Output Format
|
||||
Return valid JSON that can be consumed by the n8n platform. Your response must match the tool's required schema.`);
|
||||
|
||||
const humanTemplate = `
|
||||
<user_workflow_prompt>
|
||||
{user_workflow_prompt}
|
||||
</user_workflow_prompt>
|
||||
<selected_n8n_nodes>
|
||||
{nodes}
|
||||
</selected_n8n_nodes>
|
||||
`;
|
||||
|
||||
export const nodesComposerPrompt = ChatPromptTemplate.fromMessages([
|
||||
systemPrompt,
|
||||
HumanMessagePromptTemplate.fromTemplate(humanTemplate),
|
||||
]);
|
||||
|
||||
const nodeConfigSchema = z.object({
|
||||
nodes: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
parameters: z
|
||||
.record(z.string(), z.any())
|
||||
.describe(
|
||||
"The node's configuration parameters. Must include all required parameters for the node type to function properly. For expressions referencing other nodes, use the format: \"={{ $('Node Name').item.json.field }}\"",
|
||||
)
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
message: 'Parameters cannot be empty',
|
||||
}),
|
||||
type: z
|
||||
.string()
|
||||
.describe('The full node type identifier (e.g., "n8n-nodes-base.httpRequest")'),
|
||||
name: z
|
||||
.string()
|
||||
.describe(
|
||||
'A descriptive name for the node that clearly indicates its purpose in the workflow',
|
||||
),
|
||||
})
|
||||
.describe('A complete n8n node configuration'),
|
||||
)
|
||||
.describe('Array of all nodes for the workflow with their complete configurations'),
|
||||
});
|
||||
|
||||
const generateNodeConfigTool = new DynamicStructuredTool({
|
||||
name: 'generate_n8n_nodes',
|
||||
description:
|
||||
'Generate fully configured n8n nodes with appropriate parameters based on the workflow requirements and selected node types.',
|
||||
schema: nodeConfigSchema,
|
||||
func: async (input) => {
|
||||
return { nodes: input.nodes };
|
||||
},
|
||||
});
|
||||
|
||||
export const nodesComposerChain = (llm: BaseChatModel) => {
|
||||
if (!llm.bindTools) {
|
||||
throw new OperationalError("LLM doesn't support binding tools");
|
||||
}
|
||||
|
||||
return nodesComposerPrompt
|
||||
.pipe(
|
||||
llm.bindTools([generateNodeConfigTool], {
|
||||
tool_choice: generateNodeConfigTool.name,
|
||||
}),
|
||||
)
|
||||
.pipe((x: AIMessageChunk) => {
|
||||
const toolCall = x.tool_calls?.[0];
|
||||
return (toolCall?.args as z.infer<typeof nodeConfigSchema>).nodes;
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user