data/skills/n8n-node-configuration/OPERATION_PATTERNS.md
Common node configuration patterns organized by node type and operation.
Purpose: Quick reference for common node configurations
Coverage: Top 20 most-used nodes from 525 available
Pattern format:
Most versatile node for HTTP operations
Minimal:
{
"method": "GET",
"url": "https://api.example.com/users",
"authentication": "none"
}
With query parameters:
{
"method": "GET",
"url": "https://api.example.com/users",
"authentication": "none",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "limit",
"value": "100"
},
{
"name": "offset",
"value": "={{$json.offset}}"
}
]
}
}
With authentication:
{
"method": "GET",
"url": "https://api.example.com/users",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "httpHeaderAuth"
}
Minimal:
{
"method": "POST",
"url": "https://api.example.com/users",
"authentication": "none",
"sendBody": true,
"body": {
"contentType": "json",
"content": {
"name": "John Doe",
"email": "[email protected]"
}
}
}
With expressions:
{
"method": "POST",
"url": "https://api.example.com/users",
"authentication": "none",
"sendBody": true,
"body": {
"contentType": "json",
"content": {
"name": "={{$json.name}}",
"email": "={{$json.email}}",
"metadata": {
"source": "n8n",
"timestamp": "={{$now.toISO()}}"
}
}
}
}
Gotcha: Remember sendBody: true for POST/PUT/PATCH!
Pattern: Same as POST, but method changes
{
"method": "PUT", // or "PATCH"
"url": "https://api.example.com/users/123",
"authentication": "none",
"sendBody": true,
"body": {
"contentType": "json",
"content": {
"name": "Updated Name"
}
}
}
Minimal (no body):
{
"method": "DELETE",
"url": "https://api.example.com/users/123",
"authentication": "none"
}
With body (some APIs allow):
{
"method": "DELETE",
"url": "https://api.example.com/users",
"authentication": "none",
"sendBody": true,
"body": {
"contentType": "json",
"content": {
"ids": ["123", "456"]
}
}
}
Most common trigger - 813 searches!
Minimal:
{
"path": "my-webhook",
"httpMethod": "POST",
"responseMode": "onReceived"
}
Gotcha: Webhook data is under $json.body, not $json!
// ❌ Wrong
{
"text": "={{$json.email}}"
}
// ✅ Correct
{
"text": "={{$json.body.email}}"
}
Header auth:
{
"path": "secure-webhook",
"httpMethod": "POST",
"responseMode": "onReceived",
"authentication": "headerAuth",
"options": {
"responseCode": 200,
"responseData": "{\n \"success\": true\n}"
}
}
Custom response:
{
"path": "my-webhook",
"httpMethod": "POST",
"responseMode": "lastNode", // Return data from last node
"options": {
"responseCode": 201,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
}
Popular choice for AI agent workflows
Minimal:
{
"resource": "message",
"operation": "post",
"channel": "#general",
"text": "Hello from n8n!"
}
With dynamic content:
{
"resource": "message",
"operation": "post",
"channel": "={{$json.channel}}",
"text": "New user: {{$json.name}} ({{$json.email}})"
}
With attachments:
{
"resource": "message",
"operation": "post",
"channel": "#alerts",
"text": "Error Alert",
"attachments": [
{
"color": "#ff0000",
"fields": [
{
"title": "Error Type",
"value": "={{$json.errorType}}"
},
{
"title": "Timestamp",
"value": "={{$now.toLocaleString()}}"
}
]
}
]
}
Gotcha: Channel must start with # for public channels or be a channel ID!
Minimal:
{
"resource": "message",
"operation": "update",
"messageId": "1234567890.123456", // From previous message post
"text": "Updated message content"
}
Note: messageId required, channel optional (can be inferred)
Minimal:
{
"resource": "channel",
"operation": "create",
"name": "new-project-channel", // Lowercase, no spaces
"isPrivate": false
}
Gotcha: Channel name must be lowercase, no spaces, 1-80 chars!
Popular for email automation
Minimal:
{
"resource": "message",
"operation": "send",
"to": "[email protected]",
"subject": "Hello from n8n",
"message": "This is the email body"
}
With dynamic content:
{
"resource": "message",
"operation": "send",
"to": "={{$json.email}}",
"subject": "Order Confirmation #{{$json.orderId}}",
"message": "Dear {{$json.name}},\n\nYour order has been confirmed.\n\nThank you!",
"options": {
"ccList": "[email protected]",
"replyTo": "[email protected]"
}
}
Minimal:
{
"resource": "message",
"operation": "getAll",
"returnAll": false,
"limit": 10
}
With filters:
{
"resource": "message",
"operation": "getAll",
"returnAll": false,
"limit": 50,
"filters": {
"q": "is:unread from:[email protected]",
"labelIds": ["INBOX"]
}
}
Database operations - 456 templates
Minimal (SELECT):
{
"operation": "executeQuery",
"query": "SELECT * FROM users WHERE active = true LIMIT 100"
}
With parameters (SQL injection prevention):
{
"operation": "executeQuery",
"query": "SELECT * FROM users WHERE email = $1 AND active = $2",
"additionalFields": {
"mode": "list",
"queryParameters": "[email protected],true"
}
}
Gotcha: ALWAYS use parameterized queries for user input!
// ❌ BAD - SQL injection risk!
{
"query": "SELECT * FROM users WHERE email = '{{$json.email}}'"
}
// ✅ GOOD - Parameterized
{
"query": "SELECT * FROM users WHERE email = $1",
"additionalFields": {
"mode": "list",
"queryParameters": "={{$json.email}}"
}
}
Minimal:
{
"operation": "insert",
"table": "users",
"columns": "name,email,created_at",
"additionalFields": {
"mode": "list",
"queryParameters": "John Doe,[email protected],NOW()"
}
}
With expressions:
{
"operation": "insert",
"table": "users",
"columns": "name,email,metadata",
"additionalFields": {
"mode": "list",
"queryParameters": "={{$json.name}},={{$json.email}},{{JSON.stringify($json)}}"
}
}
Minimal:
{
"operation": "update",
"table": "users",
"updateKey": "id",
"columns": "name,email",
"additionalFields": {
"mode": "list",
"queryParameters": "={{$json.id}},Updated Name,[email protected]"
}
}
Persistent, structured per-project key-value storage — an in-n8n alternative to external SQL for small state like buffers, de-dup sets, counters, or lookup caches. Do not confuse with the MCP tool n8n_manage_datatable — that tool manages tables from outside n8n (create/list/delete tables and rows from Claude). The nodes-base.dataTable node below is what you drop into a workflow to read/write rows during execution.
Verified end-to-end against live n8n on 2026-04-08 with a 15-node assertion harness exercising every claim below: insert returning rows with system
id,likeoperator,returnAll,allConditionsAND-of-multiple-filters,isTrueunary boolean condition,upsertwithmatchingColumns(no duplicates),defineBelowresourceMapper writing values,deleteRows(the reserved-word workaround) returning affected rows, anddataTableIdresourceLocator inmode: "name". All 6 assertions passed.
Node shape:
type: n8n-nodes-base.dataTabletypeVersion: 1.1 (also 1)resource: "row" or "table"operation values — note the reserved-word workaround on delete:
"insert" — Insert row"get" — Get row(s)"update" — Update row(s) matching conditions"upsert" — Update if match, else insert"deleteRows" — Delete row(s) matching conditions (not "delete" — delete is a JS reserved word, the node uses deleteRows)"rowExists" — Pass through input if at least one match"rowNotExists" — Pass through input if zero matchesTable selection — always a resourceLocator parameter named dataTableId:
"dataTableId": {
"__rl": true,
"mode": "list", // or "name" or "id"
"value": "dt_xyz123" // or the name when mode=name
}
Row mapping (insert/update/upsert) — resourceMapper parameter named columns:
"columns": {
"mappingMode": "defineBelow", // or "autoMapInputData"
"value": {
"user_email": "={{ $json.email }}",
"score": "={{ $json.score }}",
"active": true
},
"matchingColumns": [], // filled for update/upsert match keys
"schema": [], // n8n re-loads at runtime; safe to leave empty
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
In autoMapInputData mode, incoming item field names must match column names exactly and value is ignored.
Filtering (get/update/upsert/deleteRows/rowExists/rowNotExists):
"matchType": "anyCondition", // or "allConditions"
"filters": {
"conditions": [
{ "keyName": "user_email", "condition": "eq", "keyValue": "[email protected]" },
{ "keyName": "score", "condition": "gte", "keyValue": 10 },
{ "keyName": "archived", "condition": "isNotEmpty" }
]
}
Supported condition values: eq, neq, like, ilike, gt, gte, lt, lte, isEmpty, isNotEmpty, isTrue, isFalse. The last four are unary — omit keyValue.
Get options: returnAll: true bypasses the default 50-row limit. options can include ordering.
Insert option: options.optimizeBulk: true skips returning inserted rows for ~5x bulk throughput. Do not use when downstream nodes need the inserted row ids.
Mutating ops (update/upsert/deleteRows) accept options.dryRun: true — returns the rows that would be affected with before/after states, without writing.
{
"resource": "row",
"operation": "insert",
"dataTableId": { "__rl": true, "mode": "name", "value": "email_buffer" },
"columns": {
"mappingMode": "defineBelow",
"value": {
"from_name": "={{ $json.from_name }}",
"subject": "={{ $json.subject }}"
},
"matchingColumns": [],
"schema": []
},
"options": {}
}
Get requires at least one condition — a bare "return everything" isn't allowed. Trick: filter on the always-populated system id column with isNotEmpty.
{
"resource": "row",
"operation": "get",
"dataTableId": { "__rl": true, "mode": "name", "value": "email_buffer" },
"matchType": "anyCondition",
"filters": {
"conditions": [ { "keyName": "id", "condition": "isNotEmpty" } ]
},
"returnAll": true,
"options": {}
}
Same id isNotEmpty trick — a delete without conditions throws At least one condition is required.
{
"resource": "row",
"operation": "deleteRows",
"dataTableId": { "__rl": true, "mode": "name", "value": "email_buffer" },
"matchType": "anyCondition",
"filters": {
"conditions": [ { "keyName": "id", "condition": "isNotEmpty" } ]
},
"options": {}
}
{
"resource": "row",
"operation": "upsert",
"dataTableId": { "__rl": true, "mode": "name", "value": "user_scores" },
"matchType": "allConditions",
"filters": {
"conditions": [ { "keyName": "user_email", "condition": "eq", "keyValue": "={{ $json.email }}" } ]
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"user_email": "={{ $json.email }}",
"score": "={{ $json.score }}"
},
"matchingColumns": ["user_email"],
"schema": []
},
"options": {}
}
System columns: every table auto-has id plus created/updated timestamps — you don't declare these and can't write to them. They're usable in filters.
When to reach for Data Table vs alternatives:
| Need | Use |
|---|---|
| Small per-workflow scratch state, single workflow, not durable across workflow edits | $getWorkflowStaticData('global') inside a Code node |
| Persistent structured state, queryable by column, survives workflow rename/edit/deactivation, shared across multiple workflows in the same project | Data Table node |
| Large datasets (>>10k rows), complex joins, transactions, FKs, indexes | External Postgres/MySQL |
| Unstructured key-value cache with TTL | Redis |
Gotchas:
eq on a column that doesn't exist in the table returns a validation error at execution, not at import — always verify column names match the live table.columns.value are evaluated per input item. If the upstream node emits N items, Insert runs N times unless you explicitly use optimizeBulk.deleteRows is the operation value, not delete. Using delete will import but fail at execution with "unknown operation".Get and deleteRows will be wiped without being read. For at-least-once semantics, delete by specific row ids returned from Get instead of by a broad filter.get, deleteRows, update, or upsert matches 0 rows, the node emits 0 output items and n8n stops the downstream branch silently — no error, just nothing happens. This bites cleanup steps in idempotent test/setup workflows where the table starts empty. Fix: set node-level "alwaysOutputData": true (sibling of parameters/type, NOT inside parameters) on any DT node that may legitimately match nothing. The node will then emit a single empty item and the chain continues.Get node fed 3 input items will run 3 separate queries and concatenate the results — usually not what you want. Insert a "collapse" Code node (return [{ json: {} }];) between any multi-item-emitting node and a downstream DT op that should run exactly once.Most used transformation - 68% of workflows!
Minimal:
{
"mode": "manual",
"duplicateItem": false,
"assignments": {
"assignments": [
{
"name": "status",
"value": "active",
"type": "string"
},
{
"name": "count",
"value": 100,
"type": "number"
}
]
}
}
Mapping data:
{
"mode": "manual",
"duplicateItem": false,
"assignments": {
"assignments": [
{
"name": "fullName",
"value": "={{$json.firstName}} {{$json.lastName}}",
"type": "string"
},
{
"name": "email",
"value": "={{$json.email.toLowerCase()}}",
"type": "string"
},
{
"name": "timestamp",
"value": "={{$now.toISO()}}",
"type": "string"
}
]
}
}
Gotcha: Use correct type for each field!
// ❌ Wrong type
{
"name": "age",
"value": "25", // String
"type": "string" // Will be string "25"
}
// ✅ Correct type
{
"name": "age",
"value": 25, // Number
"type": "number" // Will be number 25
}
JavaScript execution - 42% of workflows
Minimal:
{
"mode": "runOnceForAllItems",
"jsCode": "return $input.all().map(item => ({\n json: {\n name: item.json.name.toUpperCase(),\n email: item.json.email\n }\n}));"
}
Per-item processing:
{
"mode": "runOnceForEachItem",
"jsCode": "// Process each item\nconst data = $input.item.json;\n\nreturn {\n json: {\n fullName: `${data.firstName} ${data.lastName}`,\n email: data.email.toLowerCase(),\n timestamp: new Date().toISOString()\n }\n};"
}
Gotcha: In Code nodes, use $input.item.json or $input.all(), NOT {{...}}!
// ❌ Wrong - expressions don't work in Code nodes
{
"jsCode": "const name = '={{$json.name}}';"
}
// ✅ Correct - direct access
{
"jsCode": "const name = $input.item.json.name;"
}
Conditional logic - 38% of workflows
Equals (binary):
{
"conditions": {
"string": [
{
"value1": "={{$json.status}}",
"operation": "equals",
"value2": "active"
}
]
}
}
Contains (binary):
{
"conditions": {
"string": [
{
"value1": "={{$json.email}}",
"operation": "contains",
"value2": "@example.com"
}
]
}
}
isEmpty (unary):
{
"conditions": {
"string": [
{
"value1": "={{$json.email}}",
"operation": "isEmpty"
// No value2 - unary operator
// singleValue: true added by auto-sanitization
}
]
}
}
Gotcha: Unary operators (isEmpty, isNotEmpty) don't need value2!
Greater than:
{
"conditions": {
"number": [
{
"value1": "={{$json.age}}",
"operation": "larger",
"value2": 18
}
]
}
}
Is true:
{
"conditions": {
"boolean": [
{
"value1": "={{$json.isActive}}",
"operation": "true"
// Unary - no value2
}
]
}
}
All must match:
{
"conditions": {
"string": [
{
"value1": "={{$json.status}}",
"operation": "equals",
"value2": "active"
}
],
"number": [
{
"value1": "={{$json.age}}",
"operation": "larger",
"value2": 18
}
]
},
"combineOperation": "all" // AND logic
}
Any can match:
{
"conditions": {
"string": [
{
"value1": "={{$json.status}}",
"operation": "equals",
"value2": "active"
},
{
"value1": "={{$json.status}}",
"operation": "equals",
"value2": "pending"
}
]
},
"combineOperation": "any" // OR logic
}
Multi-way routing - 18% of workflows
Minimal:
{
"mode": "rules",
"rules": {
"rules": [
{
"conditions": {
"string": [
{
"value1": "={{$json.status}}",
"operation": "equals",
"value2": "active"
}
]
}
},
{
"conditions": {
"string": [
{
"value1": "={{$json.status}}",
"operation": "equals",
"value2": "pending"
}
]
}
}
]
},
"fallbackOutput": "extra" // Catch-all for non-matching
}
Gotcha: Number of rules must match number of outputs!
AI operations - 234 templates
Minimal:
{
"resource": "chat",
"operation": "complete",
"messages": {
"values": [
{
"role": "user",
"content": "={{$json.prompt}}"
}
]
}
}
With system prompt:
{
"resource": "chat",
"operation": "complete",
"messages": {
"values": [
{
"role": "system",
"content": "You are a helpful assistant specialized in customer support."
},
{
"role": "user",
"content": "={{$json.userMessage}}"
}
]
},
"options": {
"temperature": 0.7,
"maxTokens": 500
}
}
Time-based workflows - 28% have schedule triggers
Minimal:
{
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 24
}
],
"hour": 9,
"minute": 0,
"timezone": "America/New_York"
}
}
Gotcha: Always set timezone explicitly!
// ❌ Bad - uses server timezone
{
"rule": {
"interval": [...]
}
}
// ✅ Good - explicit timezone
{
"rule": {
"interval": [...],
"timezone": "America/New_York"
}
}
Minimal:
{
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 15
}
]
}
}
Advanced scheduling:
{
"mode": "cron",
"cronExpression": "0 */2 * * *", // Every 2 hours
"timezone": "America/New_York"
}
Key Patterns by Category:
| Category | Most Common | Key Gotcha |
|---|---|---|
| HTTP/API | GET, POST JSON | Remember sendBody: true |
| Webhooks | POST receiver | Data under $json.body |
| Communication | Slack post | Channel format (#name) |
| Database | SELECT with params | Use parameterized queries |
| Transform | Set assignments | Correct type per field |
| Conditional | IF string equals | Unary vs binary operators |
| AI | OpenAI chat | System + user messages |
| Schedule | Daily at time | Set timezone explicitly |
Configuration Approach:
Related Files:
A full validate-driven walkthrough of building a POST JSON request from minimal config, letting validation surface each required field.
Step 1: Identify what you need
// Goal: POST JSON to API
Step 2: Get node info
const info = get_node({
nodeType: "nodes-base.httpRequest"
});
// Returns: method, url, sendBody, body, authentication required/optional
Step 3: Minimal config
{
"method": "POST",
"url": "https://api.example.com/create",
"authentication": "none"
}
Step 4: Validate
validate_node({
nodeType: "nodes-base.httpRequest",
config,
profile: "runtime"
});
// → Error: "sendBody required for POST"
Step 5: Add required field
{
"method": "POST",
"url": "https://api.example.com/create",
"authentication": "none",
"sendBody": true
}
Step 6: Validate again
validate_node({...});
// → Error: "body required when sendBody=true"
Step 7: Complete configuration
{
"method": "POST",
"url": "https://api.example.com/create",
"authentication": "none",
"sendBody": true,
"body": {
"contentType": "json",
"content": {
"name": "={{$json.name}}",
"email": "={{$json.email}}"
}
}
}
Step 8: Final validation
validate_node({...});
// → Valid! ✅
Concrete minimal configs showing how required fields differ by resource + operation.
{
"resource": "message",
"operation": "post",
"channel": "#general", // Required
"text": "Hello!", // Required
"attachments": [], // Optional
"blocks": [] // Optional
}
{
"resource": "message",
"operation": "update",
"messageId": "1234567890", // Required (different from post!)
"text": "Updated!", // Required
"channel": "#general" // Optional (can be inferred)
}
{
"resource": "channel",
"operation": "create",
"name": "new-channel", // Required
"isPrivate": false // Optional
// Note: text NOT required for this operation
}
{
"method": "GET",
"url": "https://api.example.com/users",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "httpHeaderAuth",
"sendQuery": true, // Optional
"queryParameters": { // Shows when sendQuery=true
"parameters": [
{
"name": "limit",
"value": "100"
}
]
}
}
{
"method": "POST",
"url": "https://api.example.com/users",
"authentication": "none",
"sendBody": true, // Required for POST
"body": { // Required when sendBody=true
"contentType": "json",
"content": {
"name": "John Doe",
"email": "[email protected]"
}
}
}
{
"conditions": {
"string": [
{
"value1": "={{$json.status}}",
"operation": "equals",
"value2": "active" // Binary: needs value2
}
]
}
}
{
"conditions": {
"string": [
{
"value1": "={{$json.email}}",
"operation": "isEmpty",
// No value2 - unary operator
"singleValue": true // Auto-added by sanitization
}
]
}
}