返回 Skills
czlonkowski/n8n-skills· MIT 内容可用

n8n-node-configuration

Operation-aware node configuration guidance. Use when configuring nodes, understanding property dependencies, determining required fields, choosing between get_node detail levels, or learning common configuration patterns by node type. Always use this skill when setting up node parameters — it explains which fields are required for each operation, how displayOptions control field visibility, and when to use patchNodeField for surgical edits vs full node updates.

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: n8n-node-configuration description: Operation-aware node configuration guidance. Use when configuring nodes, understanding property dependencies, determining required fields, choosing between get_node detail levels, or learning common configuration patterns by node type. Always use this skill when setting up node parameters — it explains which fields are required for each operation, how displayOptions control field visibility, and when to use patchNodeField for surgical edits vs full node updates.

n8n Node Configuration

Expert guidance for operation-aware node configuration with property dependencies.


Configuration Philosophy

Progressive disclosure: Start minimal, add complexity as needed

Configuration best practices:

  • get_node with detail: "standard" is the most used discovery pattern
  • 56 seconds average between configuration edits
  • Covers 95% of use cases with 1-2K tokens response

Key insight: Most configurations need only standard detail, not full schema!


Core Concepts

1. Operation-Aware Configuration

Not all fields are always required - it depends on operation!

Example: Slack node

// For operation='post'
{
  "resource": "message",
  "operation": "post",
  "channel": "#general",  // Required for post
  "text": "Hello!"        // Required for post
}

// For operation='update'
{
  "resource": "message",
  "operation": "update",
  "messageId": "123",     // Required for update (different!)
  "text": "Updated!"      // Required for update
  // channel NOT required for update
}

Key: Resource + operation determine which fields are required!

2. Property Dependencies

Fields appear/disappear based on other field values

Example: HTTP Request node

// When method='GET'
{
  "method": "GET",
  "url": "https://api.example.com"
  // sendBody not shown (GET doesn't have body)
}

// When method='POST'
{
  "method": "POST",
  "url": "https://api.example.com",
  "sendBody": true,       // Now visible!
  "body": {               // Required when sendBody=true
    "contentType": "json",
    "content": {...}
  }
}

Mechanism: displayOptions control field visibility

3. Progressive Discovery

Use the right detail level:

  1. get_node({detail: "standard"}) - DEFAULT

    • Quick overview (~1-2K tokens)
    • Required fields + common options
    • Use first - covers 95% of needs
  2. get_node({mode: "search_properties", propertyQuery: "..."}) (for finding specific fields)

    • Find properties by name
    • Use when looking for auth, body, headers, etc.
  3. get_node({detail: "full"}) (complete schema)

    • All properties (~3-8K tokens)
    • Use only when standard detail is insufficient

Configuration Workflow

Standard Process

  1. Identify node type and operation.
  2. Use get_node (standard detail is default).
  3. Configure required fields.
  4. Validate configuration.
  5. If a field is unclear → get_node({mode: "search_properties"}).
  6. Add optional fields as needed.
  7. Validate again.
  8. Deploy.

Example: Configuring HTTP Request

The validate-driven loop in practice: start minimal (method, url, authentication), then let each validate_node error surface the next required field (sendBody for POST → body when sendBody=true) until valid. Full step-by-step walkthrough in OPERATION_PATTERNS.md.


get_node Detail Levels

Standard Detail (DEFAULT - Use This!)

✅ Starting configuration

get_node({
  nodeType: "nodes-base.slack"
});
// detail="standard" is the default

Returns (~1-2K tokens):

  • Required fields
  • Common options
  • Operation list
  • Metadata

Use: 95% of configuration needs

Full Detail (Use Sparingly)

✅ When standard isn't enough

get_node({
  nodeType: "nodes-base.slack",
  detail: "full"
});

Returns (~3-8K tokens):

  • Complete schema
  • All properties
  • All nested options

Warning: Large response, use only when standard insufficient

Search Properties Mode

✅ Looking for specific field

get_node({
  nodeType: "nodes-base.httpRequest",
  mode: "search_properties",
  propertyQuery: "auth"
});

Use: Find authentication, headers, body fields, etc.

Decision Tree

  1. Starting a new node config → get_node (standard).
  2. Standard has what you need → configure with it. Otherwise continue.
  3. Looking for a specific field → search_properties mode. Otherwise continue.
  4. Still need more → get_node({detail: "full"}).

Property Dependencies Deep Dive

Fields have displayOptions visibility rules: show/hide blocks where multiple conditions are AND'd and multiple values are OR'd (e.g. body shows when sendBody=true AND method IN (POST, PUT, PATCH)). The three recurring patterns are the boolean toggle (sendBody → body), the operation switch (post vs update show different fields), and type selection (string vs boolean conditions). To find what controls a field, use get_node({mode: "search_properties", propertyQuery: "..."}) or get_node({detail: "full"}) — especially when validation flags a field you don't see.

Mechanism details, all four dependency patterns, complex flows, nested dependencies, and troubleshooting are in DEPENDENCIES.md (quick-reference recap under Quick Reference: displayOptions and Common Dependency Patterns).


Common Node Patterns

Pattern 1: Resource/Operation Nodes

Examples: Slack, Google Sheets, Airtable

Structure:

{
  "resource": "<entity>",      // What type of thing
  "operation": "<action>",     // What to do with it
  // ... operation-specific fields
}

How to configure:

  1. Choose resource
  2. Choose operation
  3. Use get_node to see operation-specific requirements
  4. Configure required fields

Pattern 2: HTTP-Based Nodes

Examples: HTTP Request, Webhook

Structure:

{
  "method": "<HTTP_METHOD>",
  "url": "<endpoint>",
  "authentication": "<type>",
  // ... method-specific fields
}

Dependencies:

  • POST/PUT/PATCH → sendBody available
  • sendBody=true → body required
  • authentication != "none" → credentials required

Critical: credentials block, node id, typeVersion

  • Never set a placeholder credential ID (e.g. "id": "REPLACE_ME") — n8n's UI renders a permanently disabled credential selector for unknown IDs. Omit the credentials block when the real ID is unknown; the user then gets a normal clickable dropdown.
  • Node id must be a UUID v4, not a readable slug — the frontend binds forms and the credential component to it.
  • Don't hardcode old typeVersion values — verify the current version with get_node (httpRequest is 4.4+).

Pattern 3: Database Nodes

Examples: Postgres, MySQL, MongoDB

Structure:

{
  "operation": "<query|insert|update|delete>",
  // ... operation-specific fields
}

Dependencies:

  • operation="executeQuery" → query required
  • operation="insert" → table + values required
  • operation="update" → table + values + where required

Critical: Write operations may return 0 items

  • INSERT, UPDATE, DELETE can produce 0 n8n output items, depending on the node and operation (raw query execution reliably returns 0 result rows; some database nodes return the affected rows)
  • Set alwaysOutputData: true on write-operation nodes to keep downstream chains alive
  • Downstream nodes should use $('UpstreamNode').all() instead of $input if they need data

Pattern 4: Conditional Logic Nodes

Examples: IF, Switch, Merge

Structure:

{
  "conditions": {
    "<type>": [
      {
        "operation": "<operator>",
        "value1": "...",
        "value2": "..."  // Only for binary operators
      }
    ]
  }
}

Dependencies:

  • Binary operators (equals, contains, etc.) → value1 + value2
  • Unary operators (isEmpty, isNotEmpty) → value1 only + singleValue: true

Operation-Specific Configuration

Required fields shift with resource + operation: Slack post needs channel+text, but update needs messageId+text (channel optional) and channel/create needs name. HTTP GET uses sendQuery+queryParameters; POST needs sendBody+body. IF binary operators (equals) need value1+value2; unary (isEmpty) need only value1 plus auto-added singleValue: true. Concrete minimal configs for each in OPERATION_PATTERNS.md.


Handling Conditional Requirements

Some fields are required only under certain conditions: HTTP body is required when sendBody=true AND method IN (POST, PUT, PATCH, DELETE); IF singleValue should be true when the operator is unary (isEmpty, isNotEmpty, true, false) — and auto-sanitization sets it for you. Discover conditional requirements by reading the validation error, searching the property (get_node({mode: "search_properties"})), or iterating from a minimal config. Worked discovery examples in DEPENDENCIES.md.


Node-Specific Configuration Notes

SplitInBatches v3

{
  "batchSize": 100,  // Number of items per batch
  "options": {}
}

Output wiring:

  • main[0] (done) → Connect to downstream processing (add Limit 1 first)
  • main[1] (each batch) → Connect to loop body, then loop back to SplitInBatches input

See the n8n Workflow Patterns skill for detailed loop and nested loop patterns.

Google Sheets Node

Per-item execution: Each input item triggers a separate API call. If you have 100 items and use a Google Sheets "Append Row" node, it makes 100 API calls. To write in bulk, aggregate items in a Code node first, then use a single HTTP Request with the Sheets API.

Formula columns: Never use append on sheets with formula columns — it overwrites formulas. Instead, use HTTP Request with Google Sheets API values.update (PUT) method and a googleApi credential.


Configuration Anti-Patterns

❌ Don't: Over-configure Upfront

Bad:

// Adding every possible field
{
  "method": "GET",
  "url": "...",
  "sendQuery": false,
  "sendHeaders": false,
  "sendBody": false,
  "timeout": 10000,
  "ignoreResponseCode": false,
  // ... 20 more optional fields
}

Good:

// Start minimal
{
  "method": "GET",
  "url": "...",
  "authentication": "none"
}
// Add fields only when needed

❌ Don't: Skip Validation

Bad:

// Configure and deploy without validating
const config = {...};
n8n_update_partial_workflow({...});  // YOLO

Good:

// Validate before deploying
const config = {...};
const result = validate_node({...});
if (result.valid) {
  n8n_update_partial_workflow({...});
}

❌ Don't: Ignore Operation Context

Bad:

// Same config for all Slack operations
{
  "resource": "message",
  "operation": "post",
  "channel": "#general",
  "text": "..."
}

// Then switching operation without updating config
{
  "resource": "message",
  "operation": "update",  // Changed
  "channel": "#general",  // Wrong field for update!
  "text": "..."
}

Good:

// Check requirements when changing operation
get_node({
  nodeType: "nodes-base.slack"
});
// See what update operation needs (messageId, not channel)

Surgical Field Edits with patchNodeField

When you need to edit a specific string inside a node field — rather than replacing the whole field — use patchNodeField in n8n_update_partial_workflow. This is especially useful for:

  • Editing code inside Code nodes without re-transmitting the full code block
  • Updating URLs or text in large HTML email templates
  • Fixing typos in JSON bodies or long text fields
// Instead of replacing the entire jsCode field:
n8n_update_partial_workflow({
  id: "wf-123",
  operations: [{
    type: "patchNodeField",
    nodeName: "Code",
    fieldPath: "parameters.jsCode",
    patches: [{find: "const limit = 10;", replace: "const limit = 50;"}]
  }]
})

patchNodeField is strict — it errors if the find string isn't found or matches multiple times (unless replaceAll: true). This prevents accidental silent failures during configuration updates. See the n8n MCP Tools Expert skill for full syntax and examples.


Best Practices

Do

  1. Start with get_node (standard detail)

    • ~1-2K tokens response
    • Covers 95% of configuration needs
    • Default detail level
  2. Validate iteratively

    • Configure → Validate → Fix → Repeat
    • Average 2-3 iterations is normal
    • Read validation errors carefully
  3. Use search_properties mode when stuck

    • If field seems missing, search for it
    • Understand what controls field visibility
    • get_node({mode: "search_properties", propertyQuery: "..."})
  4. Respect operation context

    • Different operations = different requirements
    • Always check get_node when changing operation
    • Don't assume configs are transferable
  5. Trust auto-sanitization

    • Operator structure fixed automatically
    • Don't manually add/remove singleValue
    • IF/Switch metadata added on save

❌ Don't

  1. Jump to detail="full" immediately

    • Try standard detail first
    • Only escalate if needed
    • Full schema is 3-8K tokens
  2. Configure blindly

    • Always validate before deploying
    • Understand why fields are required
    • Use search_properties for conditional fields
  3. Copy configs without understanding

    • Different operations need different fields
    • Validate after copying
    • Adjust for new context
  4. Manually fix auto-sanitization issues

    • Let auto-sanitization handle operator structure
    • Focus on business logic
    • Save and let system fix structure

Silent-Failure Gotchas by Node Family

Some misconfigurations pass validate_node and validate_workflow clean, run without error, and quietly do the wrong thing — get_node shows the fields exist but not what happens when you omit them. The high-frequency ones:

  • Switch — no options.fallbackOutput ⇒ unmatched items silently dropped.
  • MergenumberOfInputs defaults to 2 (extra sources drop); useDataOfInput is 1-indexed vs the 0-indexed connections.<src>.main[idx] slot (useDataOfInput: "N"main[N-1]).
  • Database{{ }} interpolation into parameters.query is SQL injection; use $1/$2 placeholders + options.queryReplacement.
  • Slack — Block Kit must be wrapped ={{ { "blocks": ... } }} or it posts as plain text.
  • Webhook / RespondresponseCode defaults to 200 even on error branches.
  • Schedule Trigger — timezone is workflow-level (Workflow Settings), not per-rule.

Full symptom/cause/fix detail (in JSON + n8n_update_partial_workflow terms) in NODE_FAMILY_GOTCHAS.md.


Detailed References

For comprehensive guides on specific topics:


Summary

Configuration Strategy:

  1. Start with get_node (standard detail is default)
  2. Configure required fields for operation
  3. Validate configuration
  4. Search properties if stuck
  5. Iterate until valid (avg 2-3 cycles)
  6. Deploy with confidence

Key Principles:

  • Operation-aware: Different operations = different requirements
  • Progressive disclosure: Start minimal, add as needed
  • Dependency-aware: Understand field visibility rules
  • Validation-driven: Let validation guide configuration

Related Skills:

  • n8n MCP Tools Expert - How to use discovery tools correctly
  • n8n Validation Expert - Interpret validation errors
  • n8n Expression Syntax - Configure expression fields
  • n8n Workflow Patterns - Apply patterns with proper configuration

附带文件

DEPENDENCIES.md
# Property Dependencies Guide

Deep dive into n8n property dependencies and displayOptions mechanism.

---

## What Are Property Dependencies?

**Definition**: Rules that control when fields are visible or required based on other field values.

**Mechanism**: `displayOptions` in node schema

**Purpose**:
- Show relevant fields only
- Hide irrelevant fields
- Simplify configuration UX
- Prevent invalid configurations

---

## displayOptions Structure

### Basic Format

```javascript
{
  "name": "fieldName",
  "type": "string",
  "displayOptions": {
    "show": {
      "otherField": ["value1", "value2"]
    }
  }
}
```

**Translation**: Show `fieldName` when `otherField` equals "value1" OR "value2"

### Show vs Hide

#### show (Most Common)

**Show field when condition matches**:
```javascript
{
  "name": "body",
  "displayOptions": {
    "show": {
      "sendBody": [true]
    }
  }
}
```

**Meaning**: Show `body` when `sendBody = true`

#### hide (Less Common)

**Hide field when condition matches**:
```javascript
{
  "name": "advanced",
  "displayOptions": {
    "hide": {
      "simpleMode": [true]
    }
  }
}
```

**Meaning**: Hide `advanced` when `simpleMode = true`

### Multiple Conditions (AND Logic)

```javascript
{
  "name": "body",
  "displayOptions": {
    "show": {
      "sendBody": [true],
      "method": ["POST", "PUT", "PATCH"]
    }
  }
}
```

**Meaning**: Show `body` when:
- `sendBody = true` AND
- `method IN (POST, PUT, PATCH)`

**All conditions must match** (AND logic)

### Multiple Values (OR Logic)

```javascript
{
  "name": "someField",
  "displayOptions": {
    "show": {
      "method": ["POST", "PUT", "PATCH"]
    }
  }
}
```

**Meaning**: Show `someField` when:
- `method = POST` OR
- `method = PUT` OR
- `method = PATCH`

**Any value matches** (OR logic)

---

## Common Dependency Patterns

### Pattern 1: Boolean Toggle

**Use case**: Optional feature flag

**Example**: HTTP Request sendBody
```javascript
// Field: sendBody (boolean)
{
  "name": "sendBody",
  "type": "boolean",
  "default": false
}

// Field: body (depends on sendBody)
{
  "name": "body",
  "displayOptions": {
    "show": {
      "sendBody": [true]
    }
  }
}
```

**Flow**:
1. User sees sendBody checkbox
2. When checked → body field appears
3. When unchecked → body field hides

### Pattern 2: Resource/Operation Cascade

**Use case**: Different operations show different fields

**Example**: Slack message operations
```javascript
// Operation: post
{
  "name": "channel",
  "displayOptions": {
    "show": {
      "resource": ["message"],
      "operation": ["post"]
    }
  }
}

// Operation: update
{
  "name": "messageId",
  "displayOptions": {
    "show": {
      "resource": ["message"],
      "operation": ["update"]
    }
  }
}
```

**Flow**:
1. User selects resource="message"
2. User selects operation="post" → sees channel
3. User changes to operation="update" → channel hides, messageId shows

### Pattern 3: Type-Specific Configuration

**Use case**: Different types need different fields

**Example**: IF node conditions
```javascript
// String operations
{
  "name": "value2",
  "displayOptions": {
    "show": {
      "conditions.string.0.operation": ["equals", "notEquals", "contains"]
    }
  }
}

// Unary operations (isEmpty) don't show value2
{
  "displayOptions": {
    "hide": {
      "conditions.string.0.operation": ["isEmpty", "isNotEmpty"]
    }
  }
}
```

### Pattern 4: Method-Specific Fields

**Use case**: HTTP methods have different options

**Example**: HTTP Request
```javascript
// Query parameters (all methods can have)
{
  "name": "queryParameters",
  "displayOptions": {
    "show": {
      "sendQuery": [true]
    }
  }
}

// Body (only certain methods)
{
  "name": "body",
  "displayOptions": {
    "show": {
      "sendBody": [true],
      "method": ["POST", "PUT", "PATCH", "DELETE"]
    }
  }
}
```

---

## Finding Property Dependencies

### Using get_node with search_properties Mode

```javascript
// Find properties related to "body"
get_node({
  nodeType: "nodes-base.httpRequest",
  mode: "search_properties",
  propertyQuery: "body"
});
```

### Using get_node with Full Detail

```javascript
// Get complete schema with displayOptions
get_node({
  nodeType: "nodes-base.httpRequest",
  detail: "full"
});
```

### When to Use

**✅ Use when**:
- Validation fails with "missing field" but you don't see that field
- A field appears/disappears unexpectedly
- You need to understand what controls field visibility
- Building dynamic configuration tools

**❌ Don't use when**:
- Simple configuration (use `get_node` with standard detail)
- Just starting configuration
- Field requirements are obvious

---

## Complex Dependency Examples

### Example 1: HTTP Request Complete Flow

**Scenario**: Configuring POST with JSON body

**Step 1**: Set method
```javascript
{
  "method": "POST"
  // → sendBody becomes visible
}
```

**Step 2**: Enable body
```javascript
{
  "method": "POST",
  "sendBody": true
  // → body field becomes visible AND required
}
```

**Step 3**: Configure body
```javascript
{
  "method": "POST",
  "sendBody": true,
  "body": {
    "contentType": "json"
    // → content field becomes visible AND required
  }
}
```

**Step 4**: Add content
```javascript
{
  "method": "POST",
  "sendBody": true,
  "body": {
    "contentType": "json",
    "content": {
      "name": "John",
      "email": "john@example.com"
    }
  }
}
// ✅ Valid!
```

**Dependency chain**:
```
method=POST
  → sendBody visible
    → sendBody=true
      → body visible + required
        → body.contentType=json
          → body.content visible + required
```

### Example 2: IF Node Operator Dependencies

**Scenario**: String comparison with different operators

**Binary operator** (equals):
```javascript
{
  "conditions": {
    "string": [
      {
        "operation": "equals"
        // → value1 required
        // → value2 required
        // → singleValue should NOT be set
      }
    ]
  }
}
```

**Unary operator** (isEmpty):
```javascript
{
  "conditions": {
    "string": [
      {
        "operation": "isEmpty"
        // → value1 required
        // → value2 should NOT be set
        // → singleValue should be true (auto-added)
      }
    ]
  }
}
```

**Dependency table**:

| Operator | value1 | value2 | singleValue |
|---|---|---|---|
| equals | Required | Required | false |
| notEquals | Required | Required | false |
| contains | Required | Required | false |
| isEmpty | Required | Hidden | true |
| isNotEmpty | Required | Hidden | true |

### Example 3: Slack Operation Matrix

**Scenario**: Different Slack operations show different fields

```javascript
// post message
{
  "resource": "message",
  "operation": "post"
  // Shows: channel (required), text (required), attachments, blocks
}

// update message
{
  "resource": "message",
  "operation": "update"
  // Shows: messageId (required), text (required), channel (optional)
}

// delete message
{
  "resource": "message",
  "operation": "delete"
  // Shows: messageId (required), channel (required)
  // Hides: text, attachments, blocks
}

// get message
{
  "resource": "message",
  "operation": "get"
  // Shows: messageId (required), channel (required)
  // Hides: text, attachments, blocks
}
```

**Field visibility matrix**:

| Field | post | update | delete | get |
|---|---|---|---|---|
| channel | Required | Optional | Required | Required |
| text | Required | Required | Hidden | Hidden |
| messageId | Hidden | Required | Required | Required |
| attachments | Optional | Optional | Hidden | Hidden |
| blocks | Optional | Optional | Hidden | Hidden |

---

## Nested Dependencies

### What Are They?

**Definition**: Dependencies within object properties

**Example**: HTTP Request body.contentType controls body.content structure

```javascript
{
  "body": {
    "contentType": "json",
    // → content expects JSON object
    "content": {
      "key": "value"
    }
  }
}

{
  "body": {
    "contentType": "form-data",
    // → content expects form fields array
    "content": [
      {
        "name": "field1",
        "value": "value1"
      }
    ]
  }
}
```

### How to Handle

**Strategy**: Configure parent first, then children

```javascript
// Step 1: Parent
{
  "body": {
    "contentType": "json"  // Set parent first
  }
}

// Step 2: Children (structure determined by parent)
{
  "body": {
    "contentType": "json",
    "content": {           // JSON object format
      "key": "value"
    }
  }
}
```

---

## Auto-Sanitization and Dependencies

### What Auto-Sanitization Fixes

**Operator structure issues** (IF/Switch nodes):

**Example**: singleValue property
```javascript
// You configure (missing singleValue)
{
  "type": "boolean",
  "operation": "isEmpty"
  // Missing singleValue
}

// Auto-sanitization adds it
{
  "type": "boolean",
  "operation": "isEmpty",
  "singleValue": true  // ✅ Added automatically
}
```

### What It Doesn't Fix

**Missing required fields**:
```javascript
// You configure (missing channel)
{
  "resource": "message",
  "operation": "post",
  "text": "Hello"
  // Missing required field: channel
}

// Auto-sanitization does NOT add
// You must add it yourself
{
  "resource": "message",
  "operation": "post",
  "channel": "#general",  // ← You must add
  "text": "Hello"
}
```

---

## Troubleshooting Dependencies

### Problem 1: "Field X is required but not visible"

**Error**:
```json
{
  "type": "missing_required",
  "property": "body",
  "message": "body is required"
}
```

**But you don't see body field in configuration!**

**Solution**:
```javascript
// Check field dependencies using search_properties
get_node({
  nodeType: "nodes-base.httpRequest",
  mode: "search_properties",
  propertyQuery: "body"
});

// Find that body shows when sendBody=true
// Add sendBody
{
  "method": "POST",
  "sendBody": true,  // ← Now body appears!
  "body": {...}
}
```

### Problem 2: "Field disappears when I change operation"

**Scenario**:
```javascript
// Working configuration
{
  "resource": "message",
  "operation": "post",
  "channel": "#general",
  "text": "Hello"
}

// Change operation
{
  "resource": "message",
  "operation": "update",  // Changed
  "channel": "#general",  // Still here
  "text": "Updated"       // Still here
  // Missing: messageId (required for update!)
}
```

**Validation error**: "messageId is required"

**Why**: Different operation = different required fields

**Solution**:
```javascript
// Check requirements for new operation
get_node({
  nodeType: "nodes-base.slack"
});

// Configure for update operation
{
  "resource": "message",
  "operation": "update",
  "messageId": "1234567890",  // Required for update
  "text": "Updated",
  "channel": "#general"       // Optional for update
}
```

### Problem 3: "Validation passes but field doesn't save"

**Scenario**: Field hidden by dependencies after validation

**Example**:
```javascript
// Configure
{
  "method": "GET",
  "sendBody": true,  // ❌ GET doesn't support body
  "body": {...}      // This will be stripped
}

// After save
{
  "method": "GET"
  // body removed because method=GET hides it
}
```

**Solution**: Respect dependencies from the start

```javascript
// Correct approach - check property dependencies
get_node({
  nodeType: "nodes-base.httpRequest",
  mode: "search_properties",
  propertyQuery: "body"
});

// See that body only shows for POST/PUT/PATCH/DELETE
// Use correct method
{
  "method": "POST",
  "sendBody": true,
  "body": {...}
}
```

---

## Advanced Patterns

### Pattern 1: Conditional Required with Fallback

**Example**: Channel can be string OR expression

```javascript
// Option 1: String
{
  "channel": "#general"
}

// Option 2: Expression
{
  "channel": "={{$json.channelName}}"
}

// Validation accepts both
```

### Pattern 2: Mutually Exclusive Fields

**Example**: Use either ID or name, not both

```javascript
// Use messageId
{
  "messageId": "1234567890"
  // name not needed
}

// OR use messageName
{
  "messageName": "thread-name"
  // messageId not needed
}

// Dependencies ensure only one is required
```

### Pattern 3: Progressive Complexity

**Example**: Simple mode vs advanced mode

```javascript
// Simple mode
{
  "mode": "simple",
  "text": "{{$json.message}}"
  // Advanced fields hidden
}

// Advanced mode
{
  "mode": "advanced",
  "attachments": [...],
  "blocks": [...],
  "metadata": {...}
  // Simple field hidden, advanced fields shown
}
```

---

## Best Practices

### ✅ Do

1. **Check dependencies when stuck**
   ```javascript
   get_node({nodeType: "...", mode: "search_properties", propertyQuery: "..."});
   ```

2. **Configure parent properties first**
   ```javascript
   // First: method, resource, operation
   // Then: dependent fields
   ```

3. **Validate after changing operation**
   ```javascript
   // Operation changed → requirements changed
   validate_node({nodeType: "...", config: {...}, profile: "runtime"});
   ```

4. **Read validation errors for dependency hints**
   ```
   Error: "body required when sendBody=true"
   → Hint: Set sendBody=true to enable body
   ```

### ❌ Don't

1. **Don't ignore dependency errors**
   ```javascript
   // Error: "body not visible" → Check displayOptions
   ```

2. **Don't hardcode all possible fields**
   ```javascript
   // Bad: Adding fields that will be hidden
   ```

3. **Don't assume operations are identical**
   ```javascript
   // Each operation has unique requirements
   ```

---

## Summary

**Key Concepts**:
- `displayOptions` control field visibility
- `show` = field appears when conditions match
- `hide` = field disappears when conditions match
- Multiple conditions = AND logic
- Multiple values = OR logic

**Common Patterns**:
1. Boolean toggle (sendBody → body)
2. Resource/operation cascade (different operations → different fields)
3. Type-specific config (string vs boolean conditions)
4. Method-specific fields (GET vs POST)

**Troubleshooting**:
- Field required but not visible → Check dependencies
- Field disappears after change → Operation changed requirements
- Field doesn't save → Hidden by dependencies

**Tools**:
- `get_node({mode: "search_properties"})` - Find property dependencies
- `get_node({detail: "full"})` - See complete schema with displayOptions
- `get_node` - See operation requirements (standard detail)
- Validation errors - Hints about dependencies

**Related Files**:
- **[SKILL.md](SKILL.md)** - Main configuration guide
- **[OPERATION_PATTERNS.md](OPERATION_PATTERNS.md)** - Common patterns by node type

---

## Quick Reference: displayOptions and Common Dependency Patterns

A condensed introduction to the displayOptions mechanism and the three most common dependency patterns, plus how to find them.

### displayOptions Mechanism

**Fields have visibility rules**:

```javascript
{
  "name": "body",
  "displayOptions": {
    "show": {
      "sendBody": [true],
      "method": ["POST", "PUT", "PATCH"]
    }
  }
}
```

**Translation**: "body" field shows when:
- sendBody = true AND
- method = POST, PUT, or PATCH

### Common Dependency Patterns

#### Pattern 1: Boolean Toggle

**Example**: HTTP Request sendBody
```javascript
// sendBody controls body visibility
{
  "sendBody": true   // → body field appears
}
```

#### Pattern 2: Operation Switch

**Example**: Slack resource/operation
```javascript
// Different operations → different fields
{
  "resource": "message",
  "operation": "post"
  // → Shows: channel, text, attachments, etc.
}

{
  "resource": "message",
  "operation": "update"
  // → Shows: messageId, text (different fields!)
}
```

#### Pattern 3: Type Selection

**Example**: IF node conditions
```javascript
{
  "type": "string",
  "operation": "contains"
  // → Shows: value1, value2
}

{
  "type": "boolean",
  "operation": "equals"
  // → Shows: value1, value2, different operators
}
```

### Finding Property Dependencies

**Use get_node with search_properties mode**:
```javascript
get_node({
  nodeType: "nodes-base.httpRequest",
  mode: "search_properties",
  propertyQuery: "body"
});

// Returns property paths matching "body" with descriptions
```

**Or use full detail for complete schema**:
```javascript
get_node({
  nodeType: "nodes-base.httpRequest",
  detail: "full"
});

// Returns complete schema with displayOptions rules
```

**Use this when**: Validation fails and you don't understand why field is missing/required

---

## Handling Conditional Requirements

How to discover and satisfy fields that are required only under certain conditions.

### Example: HTTP Request Body

**Scenario**: body field required, but only sometimes

**Rule**:
```
body is required when:
  - sendBody = true AND
  - method IN (POST, PUT, PATCH, DELETE)
```

**How to discover**:
```javascript
// Option 1: Read validation error
validate_node({...});
// Error: "body required when sendBody=true"

// Option 2: Search for the property
get_node({
  nodeType: "nodes-base.httpRequest",
  mode: "search_properties",
  propertyQuery: "body"
});
// Shows: body property with displayOptions rules

// Option 3: Try minimal config and iterate
// Start without body, validation will tell you if needed
```

### Example: IF Node singleValue

**Scenario**: singleValue property appears for unary operators

**Rule**:
```
singleValue should be true when:
  - operation IN (isEmpty, isNotEmpty, true, false)
```

**Good news**: Auto-sanitization fixes this!

**Manual check**:
```javascript
get_node({
  nodeType: "nodes-base.if",
  detail: "full"
});
// Shows complete schema with operator-specific rules
```
NODE_FAMILY_GOTCHAS.md
# Node Family Gotchas

Silent-failure traps grouped by node family. These don't show up in `validate_node` or `validate_workflow` — the workflow validates clean, runs without error, and quietly does the wrong thing. `get_node` shows you the fields exist; it doesn't tell you what happens when you leave them off. This file covers the consequence.

Each entry: **symptom** (what you see at runtime), **cause** (why), **fix** (in n8n-mcp / JSON terms).

## Contents

- [Switch — dropped items on the unmatched path](#switch)
- [Merge — wrong input count and the 1-vs-0 index trap](#merge)
- [Database (Postgres / MySQL / Supabase) — SQL injection, transactions, no-rows](#database)
- [Slack — Block Kit, threads, operation values](#slack)
- [Webhook / Respond to Webhook — response codes and modes](#webhook--respond-to-webhook)
- [Schedule Trigger — timezone, cron fields, missed runs](#schedule-trigger)

---

## Switch

**Symptom:** items that match none of the rules vanish. No error, no warning — the workflow just loses data on the unmatched path.

**Cause:** without a fallback output, the Switch has nowhere to send unmatched items, so it discards them.

**Fix:** set `options.fallbackOutput: "extra"` and give it a name with `options.renameFallbackOutput`. While you're there, name every rule output too — unnamed `0 / 1 / 2` outputs are unreadable a month later, and a failure on "output 2" tells the operator nothing.

```json
{
  "parameters": {
    "mode": "rules",
    "rules": {
      "values": [
        { "outputKey": "Paid", "renameOutput": true, "conditions": { "...": "..." } },
        { "outputKey": "Refunded", "renameOutput": true, "conditions": { "...": "..." } }
      ]
    },
    "options": {
      "fallbackOutput": "extra",
      "renameFallbackOutput": "Unexpected"
    }
  }
}
```

Apply surgically with `patchNodeField` on `parameters.options.fallbackOutput`, or with `updateNode` for the full `options` object. After wiring, confirm the fallback branch goes somewhere real (a log, an alert, a NoOp) — an enabled fallback that connects to nothing drops items just the same.

---

## Merge

Two traps, both silent. They live on different Merge modes — `numberOfInputs` on Append/Combine, `useDataOfInput` on Choose Branch — so in practice you hit one or the other, not both.

### Trap 1: input count defaults to 2

**Symptom:** you wire 3+ sources into a Merge, the canvas shows three wires going in, the workflow validates and runs — but only the first two sources' items appear downstream. The third silently drops.

**Cause:** `numberOfInputs` defaults to `2`. The third wire connects to an input slot that doesn't exist on the node.

**Fix:** set `numberOfInputs` to match your wire count.

```json
{ "parameters": { "mode": "append", "numberOfInputs": 3 } }
```

Verify with `get_node` for the merge node on the user's n8n version — the field name has shifted across versions. After building, pull the workflow with `n8n_get_workflow` and confirm `parameters.numberOfInputs` matches the number of source entries in the `connections` object feeding it.

### Trap 2: `useDataOfInput` is 1-indexed, connections are 0-indexed

**Symptom:** the Merge passes through the wrong source. Downstream gets real data with real field names — just from the wrong upstream branch. Looks identical to a working flow; the shape is right, the contents are wrong.

**Cause:** `parameters.useDataOfInput` matches the UI labels (Input 1, Input 2, Input 3 — **1-indexed**), but the wiring position in `connections.<source>.main[idx]` is **0-indexed** like every other array. Off by one.

**Fix — the translation rule:**

> `useDataOfInput: "N"` is fed by the connection at `main[N-1]`.

| `useDataOfInput` | Connection slot |
|---|---|
| `"1"` | `connections.<source>.main[0]` |
| `"2"` | `connections.<source>.main[1]` |
| `"3"` | `connections.<source>.main[2]` |

When you add the connection via `n8n_update_partial_workflow`, the `addConnection` operation targets a specific input index. To pass through Input 2, the source whose data you want must land on the connection at `main[1]`. After wiring, **verify with `n8n_get_workflow`**: read the `connections` object and confirm the source you intend to pass through actually sits at `main[N-1]`. This is the only reliable check — it won't surface in validation.

---

## Database

Covers Postgres, MySQL, and Supabase (when used via the Postgres node against the same database). The exact field set differs per node and version — `get_node` is canonical. This is the security and behavior layer it doesn't show.

### Never interpolate user input into SQL

**Symptom:** the query works in testing, then a value containing a quote or `;` produces a SQL error — or worse, executes injected SQL. `$json.email = "x'; DROP TABLE users; --"` is game over.

**Cause:** n8n substitutes `{{ ... }}` expressions into the query text **before** the database driver binds parameters. Anything inside `{{ }}` becomes part of the SQL itself, not a bound value.

**Fix:** use `$1, $2, ...` placeholders in the query and pass values through `options.queryReplacement`. The values flow through the driver's parameter binding and never touch the SQL text. (The n8n MySQL node also uses `$1, $2` + `queryReplacement`, not MySQL's native `?` — the node normalizes to the driver.)

```json
{
  "parameters": {
    "operation": "executeQuery",
    "query": "SELECT * FROM users WHERE email = $1",
    "options": {
      "queryReplacement": "={{ $json.email }}"
    }
  }
}
```

`queryReplacement` takes a comma-separated list — each piece becomes one parameter: `={{ $json.email }},={{ $json.id }}` → `$1, $2`. The `=` prefix is just n8n's expression-mode marker. Treat any DB node with a `{{ ... }}` expression inside `parameters.query` as a critical injection finding.

### Transactions are bounded to one node

**Symptom:** two separate DB nodes, the second fails, and the first's write is already committed — no rollback.

**Cause:** there is no cross-node transaction in n8n. Atomicity is bounded to a single `executeQuery` invocation.

**Fix:** for atomic multi-step writes, put all the statements in one Postgres/MySQL `executeQuery` node and set `options.queryBatching: "transaction"` explicitly — don't rely on the default, which has shifted across node versions (single-query and independent batching are the other modes; confirm the current set and default with `get_node`). Everything that node runs in that execution goes through one BEGIN/COMMIT; any failure rolls it all back. Pre-compute lookups and derived values upstream so the transactional node receives ready-to-write data.

```json
{
  "parameters": {
    "operation": "executeQuery",
    "query": "INSERT INTO orders (customer_id, total) VALUES ($1, $2)",
    "options": {
      "queryBatching": "transaction",
      "queryReplacement": "={{ $json.customerId }},={{ $json.total }}"
    }
  }
}
```

Supabase's REST layer has no transactions — drop to the Postgres node connected directly to the same database when you need atomicity.

### "No rows" produces no items

**Symptom:** a `select` / `executeQuery` that matches nothing returns zero items, and the downstream node simply doesn't run — looks like the branch was skipped.

**Cause:** zero matched rows = zero n8n output items, and most nodes treat "no input items" as "nothing to do."

**Fix:** set `alwaysOutputData: true` on the DB node so a single empty item flows through, then branch on the result with an IF. (This is the same gotcha as write operations — INSERT/UPDATE/DELETE often return 0 items too; `alwaysOutputData: true` keeps the chain alive.)

---

## Slack

The exact param shapes shift across versions — `get_node` for `nodes-base.slack` is canonical. These are the traps it won't warn you about.

### Block Kit must be wrapped, or it posts as plain text

**Symptom:** you pass a Block Kit array, the request succeeds, but the message arrives as plain text (or empty). No node error, no validation warning.

**Cause:** the node accepts a bare array silently and drops the rich content. Slack's `chat.postMessage` expects `{ "blocks": [...] }` — an object with a `blocks` key — and the node forwards your value as-is.

**Fix:** wrap the array in an object, in expression mode so the node receives a real object (not a stringified one). Reference the source by node name, not `$json`:

```
={{ { "blocks": $('Build Message').item.json.blocks } }}
```

Don't stringify-then-reparse hybrids (`{{ ... .toJsonString() }}` glued into a string) — they work on some versions but break on escaping and large payloads. Hand the node the structure directly.

### Thread replies need `thread_ts`

**Symptom:** a "reply" posts as a new top-level channel message instead of in the thread.

**Cause:** without `thread_ts` (the timestamp of the message being replied to), Slack has no thread to attach to.

**Fix:** set `thread_ts` to the parent message's `ts`. Use `get_node` to find where the field sits on the current version — it moved out of `otherOptions` where older docs put it. Add `reply_broadcast: true` if the reply should also show in the main channel.

### Operation display name ≠ internal value

**Symptom:** you set `operation: "send"` (matching the UI's "Send a message") and validation rejects it.

**Cause:** the display label and the stored value diverge. "Send a message" is `operation: "post"`, not `"send"`.

**Fix:** read the real operation values from `get_node` for `nodes-base.slack` rather than guessing from the UI label. This display-vs-value mismatch recurs across resource nodes (e.g. "Get Many" → `getAll` on Gmail/Supabase).

---

## Webhook / Respond to Webhook

Entry and exit of request/response API workflows. `get_node` is canonical for field shapes; this is the runtime behavior it doesn't show.

### Response code defaults to 200 — even on error branches

**Symptom:** an error branch returns HTTP 200 with an error body. The caller's HTTP client sees success while the body says failure — the worst of both worlds, because the caller's error handling never fires.

**Cause:** `responseCode` defaults to `200` on every Respond to Webhook node, including the ones you wired to error paths.

**Fix:** set `responseCode` explicitly on every Respond branch — 4xx for caller errors (400 validation, 401/403 auth, 409 conflict, 429 rate limit), 5xx for server errors. A workflow can have multiple Respond nodes, one per response shape; n8n returns whichever fires first.

### Use `responseMode: "responseNode"` for real request/response APIs

**Symptom:** the caller gets an immediate 200 and never sees the workflow's actual output, even though the workflow computes a response.

**Cause:** the Webhook trigger's `responseMode` defaults to `onReceived` (acknowledge immediately, run async). The caller can't see downstream results.

**Fix:** set `parameters.responseMode: "responseNode"` on the Webhook trigger and control the response with explicit Respond to Webhook nodes. (`lastNode` returns the last node's output synchronously — fine for simple cases; `responseNode` is the flexible choice for multi-status APIs.)

### `respondWith: "json"` takes the object, not a stringified string

**Symptom:** the response body comes back double-encoded — escaped quotes, a JSON string wrapped in another JSON string.

**Cause:** the `responseBody` field accepts both an object and a string. If you pass `JSON.stringify(obj)`, n8n serializes that string again.

**Fix:** pass the object directly in expression mode and let the node serialize it once:

```
={{ { "status": "ok", "id": $('Create Record').item.json.id } }}
```

---

## Schedule Trigger

`get_node` for `nodes-base.scheduleTrigger` shows the rule structure. These are the behaviors outside the type def.

### Timezone is workflow-level, not per-rule

**Symptom:** a job that should fire at 9am local drifts after a DST change or an instance move.

**Cause:** the Schedule Trigger uses the **workflow's** timezone (Workflow Settings → Timezone). There is no `timezone` field inside a rule. Without an explicit workflow timezone, it follows the host's clock.

**Fix:** set the workflow timezone explicitly for any schedule that must run at a specific local time. The per-rule config has no timezone to set — don't look for one.

### Cron accepts 5 or 6 fields

**Symptom:** confusion over whether a cron expression needs a seconds field — the UI hint shows 6 fields, the placeholder shows 5.

**Cause:** n8n's cron supports both 5-field (`Minute Hour DoM Month DoW`) and 6-field (`Second Minute Hour DoM Month DoW`) formats. Both are valid.

**Fix:** use whichever you intend; just be consistent. For simple recurrences ("every Monday 9am"), the interval modes (`field: "weeks"` etc.) are clearer and less error-prone than cron.

### Restarts can miss runs — design for idempotency

**Symptom:** an instance restart or downtime window overlapping a scheduled time, and that run never happens.

**Cause:** schedules fire against the instance's clock. If the instance is down at fire time, the run is simply skipped — there's no catch-up queue.

**Fix:** for business-critical schedules, make the workflow idempotent (running it twice produces the same result) and, where it matters, detect missed runs at workflow start by comparing the last successful run to the expected cadence and catching up.
OPERATION_PATTERNS.md
# Operation Patterns Guide

Common node configuration patterns organized by node type and operation.

---

## Overview

**Purpose**: Quick reference for common node configurations

**Coverage**: Top 20 most-used nodes from 525 available

**Pattern format**:
- Minimal valid configuration
- Common options
- Real-world examples
- Gotchas and tips

---

## HTTP & API Nodes

### HTTP Request (nodes-base.httpRequest)

Most versatile node for HTTP operations

#### GET Request

**Minimal**:
```javascript
{
  "method": "GET",
  "url": "https://api.example.com/users",
  "authentication": "none"
}
```

**With query parameters**:
```javascript
{
  "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**:
```javascript
{
  "method": "GET",
  "url": "https://api.example.com/users",
  "authentication": "predefinedCredentialType",
  "nodeCredentialType": "httpHeaderAuth"
}
```

#### POST with JSON

**Minimal**:
```javascript
{
  "method": "POST",
  "url": "https://api.example.com/users",
  "authentication": "none",
  "sendBody": true,
  "body": {
    "contentType": "json",
    "content": {
      "name": "John Doe",
      "email": "john@example.com"
    }
  }
}
```

**With expressions**:
```javascript
{
  "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!

#### PUT/PATCH Request

**Pattern**: Same as POST, but method changes
```javascript
{
  "method": "PUT",  // or "PATCH"
  "url": "https://api.example.com/users/123",
  "authentication": "none",
  "sendBody": true,
  "body": {
    "contentType": "json",
    "content": {
      "name": "Updated Name"
    }
  }
}
```

#### DELETE Request

**Minimal** (no body):
```javascript
{
  "method": "DELETE",
  "url": "https://api.example.com/users/123",
  "authentication": "none"
}
```

**With body** (some APIs allow):
```javascript
{
  "method": "DELETE",
  "url": "https://api.example.com/users",
  "authentication": "none",
  "sendBody": true,
  "body": {
    "contentType": "json",
    "content": {
      "ids": ["123", "456"]
    }
  }
}
```

---

### Webhook (nodes-base.webhook)

Most common trigger - 813 searches!

#### Basic Webhook

**Minimal**:
```javascript
{
  "path": "my-webhook",
  "httpMethod": "POST",
  "responseMode": "onReceived"
}
```

**Gotcha**: Webhook data is under `$json.body`, not `$json`!

```javascript
// ❌ Wrong
{
  "text": "={{$json.email}}"
}

// ✅ Correct
{
  "text": "={{$json.body.email}}"
}
```

#### Webhook with Authentication

**Header auth**:
```javascript
{
  "path": "secure-webhook",
  "httpMethod": "POST",
  "responseMode": "onReceived",
  "authentication": "headerAuth",
  "options": {
    "responseCode": 200,
    "responseData": "{\n  \"success\": true\n}"
  }
}
```

#### Webhook Returning Data

**Custom response**:
```javascript
{
  "path": "my-webhook",
  "httpMethod": "POST",
  "responseMode": "lastNode",  // Return data from last node
  "options": {
    "responseCode": 201,
    "responseHeaders": {
      "entries": [
        {
          "name": "Content-Type",
          "value": "application/json"
        }
      ]
    }
  }
}
```

---

## Communication Nodes

### Slack (nodes-base.slack)

Popular choice for AI agent workflows

#### Post Message

**Minimal**:
```javascript
{
  "resource": "message",
  "operation": "post",
  "channel": "#general",
  "text": "Hello from n8n!"
}
```

**With dynamic content**:
```javascript
{
  "resource": "message",
  "operation": "post",
  "channel": "={{$json.channel}}",
  "text": "New user: {{$json.name}} ({{$json.email}})"
}
```

**With attachments**:
```javascript
{
  "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!

#### Update Message

**Minimal**:
```javascript
{
  "resource": "message",
  "operation": "update",
  "messageId": "1234567890.123456",  // From previous message post
  "text": "Updated message content"
}
```

**Note**: `messageId` required, `channel` optional (can be inferred)

#### Create Channel

**Minimal**:
```javascript
{
  "resource": "channel",
  "operation": "create",
  "name": "new-project-channel",  // Lowercase, no spaces
  "isPrivate": false
}
```

**Gotcha**: Channel name must be lowercase, no spaces, 1-80 chars!

---

### Gmail (nodes-base.gmail)

Popular for email automation

#### Send Email

**Minimal**:
```javascript
{
  "resource": "message",
  "operation": "send",
  "to": "user@example.com",
  "subject": "Hello from n8n",
  "message": "This is the email body"
}
```

**With dynamic content**:
```javascript
{
  "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": "admin@example.com",
    "replyTo": "support@example.com"
  }
}
```

#### Get Email

**Minimal**:
```javascript
{
  "resource": "message",
  "operation": "getAll",
  "returnAll": false,
  "limit": 10
}
```

**With filters**:
```javascript
{
  "resource": "message",
  "operation": "getAll",
  "returnAll": false,
  "limit": 50,
  "filters": {
    "q": "is:unread from:important@example.com",
    "labelIds": ["INBOX"]
  }
}
```

---

## Database Nodes

### Postgres (nodes-base.postgres)

Database operations - 456 templates

#### Execute Query

**Minimal** (SELECT):
```javascript
{
  "operation": "executeQuery",
  "query": "SELECT * FROM users WHERE active = true LIMIT 100"
}
```

**With parameters** (SQL injection prevention):
```javascript
{
  "operation": "executeQuery",
  "query": "SELECT * FROM users WHERE email = $1 AND active = $2",
  "additionalFields": {
    "mode": "list",
    "queryParameters": "user@example.com,true"
  }
}
```

**Gotcha**: ALWAYS use parameterized queries for user input!

```javascript
// ❌ 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}}"
  }
}
```

#### Insert

**Minimal**:
```javascript
{
  "operation": "insert",
  "table": "users",
  "columns": "name,email,created_at",
  "additionalFields": {
    "mode": "list",
    "queryParameters": "John Doe,john@example.com,NOW()"
  }
}
```

**With expressions**:
```javascript
{
  "operation": "insert",
  "table": "users",
  "columns": "name,email,metadata",
  "additionalFields": {
    "mode": "list",
    "queryParameters": "={{$json.name}},={{$json.email}},{{JSON.stringify($json)}}"
  }
}
```

#### Update

**Minimal**:
```javascript
{
  "operation": "update",
  "table": "users",
  "updateKey": "id",
  "columns": "name,email",
  "additionalFields": {
    "mode": "list",
    "queryParameters": "={{$json.id}},Updated Name,newemail@example.com"
  }
}
```

---

## Storage Nodes

### Data Table (nodes-base.dataTable)

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`, `like` operator, `returnAll`, `allConditions` AND-of-multiple-filters, `isTrue` unary boolean condition, `upsert` with `matchingColumns` (no duplicates), `defineBelow` resourceMapper writing values, `deleteRows` (the reserved-word workaround) returning affected rows, and `dataTableId` resourceLocator in `mode: "name"`. All 6 assertions passed.

**Node shape**:
- `type`: `n8n-nodes-base.dataTable`
- `typeVersion`: `1.1` (also `1`)
- `resource`: `"row"` or `"table"`
- Row `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 matches

**Table selection** — always a resourceLocator parameter named `dataTableId`:
```javascript
"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`:
```javascript
"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):
```javascript
"matchType": "anyCondition",   // or "allConditions"
"filters": {
  "conditions": [
    { "keyName": "user_email", "condition": "eq",        "keyValue": "a@b.com" },
    { "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.

#### Minimal Insert
```javascript
{
  "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 All Rows
`Get` requires at least one condition — a bare "return everything" isn't allowed. Trick: filter on the always-populated system `id` column with `isNotEmpty`.
```javascript
{
  "resource": "row",
  "operation": "get",
  "dataTableId": { "__rl": true, "mode": "name", "value": "email_buffer" },
  "matchType": "anyCondition",
  "filters": {
    "conditions": [ { "keyName": "id", "condition": "isNotEmpty" } ]
  },
  "returnAll": true,
  "options": {}
}
```

#### Delete All Rows
Same `id isNotEmpty` trick — a delete without conditions throws `At least one condition is required`.
```javascript
{
  "resource": "row",
  "operation": "deleteRows",
  "dataTableId": { "__rl": true, "mode": "name", "value": "email_buffer" },
  "matchType": "anyCondition",
  "filters": {
    "conditions": [ { "keyName": "id", "condition": "isNotEmpty" } ]
  },
  "options": {}
}
```

#### Upsert by Natural Key
```javascript
{
  "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**:
- Scope is **per project** — Data Tables are not shared across n8n projects. Move a workflow to another project and its Data Table references break.
- Filter operator `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.
- Expression values in `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".
- Race condition in buffer/flush patterns: rows added between `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.
- **Zero-match halts the chain.** When `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.
- **DT operations execute once per input item.** A `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.
- DT nodes do not natively offer a "run once for all items" mode like the Code node — the collapse-node pattern is currently the only clean workaround.

---

## Data Transformation Nodes

### Set (nodes-base.set)

Most used transformation - 68% of workflows!

#### Set Fixed Values

**Minimal**:
```javascript
{
  "mode": "manual",
  "duplicateItem": false,
  "assignments": {
    "assignments": [
      {
        "name": "status",
        "value": "active",
        "type": "string"
      },
      {
        "name": "count",
        "value": 100,
        "type": "number"
      }
    ]
  }
}
```

#### Set from Input Data

**Mapping data**:
```javascript
{
  "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!

```javascript
// ❌ 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
}
```

---

### Code (nodes-base.code)

JavaScript execution - 42% of workflows

#### Simple Transformation

**Minimal**:
```javascript
{
  "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**:
```javascript
{
  "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 `{{...}}`!

```javascript
// ❌ Wrong - expressions don't work in Code nodes
{
  "jsCode": "const name = '={{$json.name}}';"
}

// ✅ Correct - direct access
{
  "jsCode": "const name = $input.item.json.name;"
}
```

---

## Conditional Nodes

### IF (nodes-base.if)

Conditional logic - 38% of workflows

#### String Comparison

**Equals** (binary):
```javascript
{
  "conditions": {
    "string": [
      {
        "value1": "={{$json.status}}",
        "operation": "equals",
        "value2": "active"
      }
    ]
  }
}
```

**Contains** (binary):
```javascript
{
  "conditions": {
    "string": [
      {
        "value1": "={{$json.email}}",
        "operation": "contains",
        "value2": "@example.com"
      }
    ]
  }
}
```

**isEmpty** (unary):
```javascript
{
  "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!

#### Number Comparison

**Greater than**:
```javascript
{
  "conditions": {
    "number": [
      {
        "value1": "={{$json.age}}",
        "operation": "larger",
        "value2": 18
      }
    ]
  }
}
```

#### Boolean Comparison

**Is true**:
```javascript
{
  "conditions": {
    "boolean": [
      {
        "value1": "={{$json.isActive}}",
        "operation": "true"
        // Unary - no value2
      }
    ]
  }
}
```

#### Multiple Conditions (AND)

**All must match**:
```javascript
{
  "conditions": {
    "string": [
      {
        "value1": "={{$json.status}}",
        "operation": "equals",
        "value2": "active"
      }
    ],
    "number": [
      {
        "value1": "={{$json.age}}",
        "operation": "larger",
        "value2": 18
      }
    ]
  },
  "combineOperation": "all"  // AND logic
}
```

#### Multiple Conditions (OR)

**Any can match**:
```javascript
{
  "conditions": {
    "string": [
      {
        "value1": "={{$json.status}}",
        "operation": "equals",
        "value2": "active"
      },
      {
        "value1": "={{$json.status}}",
        "operation": "equals",
        "value2": "pending"
      }
    ]
  },
  "combineOperation": "any"  // OR logic
}
```

---

### Switch (nodes-base.switch)

Multi-way routing - 18% of workflows

#### Basic Switch

**Minimal**:
```javascript
{
  "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 Nodes

### OpenAI (nodes-langchain.openAi)

AI operations - 234 templates

#### Chat Completion

**Minimal**:
```javascript
{
  "resource": "chat",
  "operation": "complete",
  "messages": {
    "values": [
      {
        "role": "user",
        "content": "={{$json.prompt}}"
      }
    ]
  }
}
```

**With system prompt**:
```javascript
{
  "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
  }
}
```

---

## Schedule Nodes

### Schedule Trigger (nodes-base.scheduleTrigger)

Time-based workflows - 28% have schedule triggers

#### Daily at Specific Time

**Minimal**:
```javascript
{
  "rule": {
    "interval": [
      {
        "field": "hours",
        "hoursInterval": 24
      }
    ],
    "hour": 9,
    "minute": 0,
    "timezone": "America/New_York"
  }
}
```

**Gotcha**: Always set timezone explicitly!

```javascript
// ❌ Bad - uses server timezone
{
  "rule": {
    "interval": [...]
  }
}

// ✅ Good - explicit timezone
{
  "rule": {
    "interval": [...],
    "timezone": "America/New_York"
  }
}
```

#### Every N Minutes

**Minimal**:
```javascript
{
  "rule": {
    "interval": [
      {
        "field": "minutes",
        "minutesInterval": 15
      }
    ]
  }
}
```

#### Cron Expression

**Advanced scheduling**:
```javascript
{
  "mode": "cron",
  "cronExpression": "0 */2 * * *",  // Every 2 hours
  "timezone": "America/New_York"
}
```

---

## Summary

**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**:
1. Use patterns as starting point
2. Adapt to your use case
3. Validate configuration
4. Iterate based on errors
5. Deploy when valid

**Related Files**:
- **[SKILL.md](SKILL.md)** - Configuration workflow and philosophy
- **[DEPENDENCIES.md](DEPENDENCIES.md)** - Property dependency rules

---

## Worked Example: Configuring HTTP Request Step by Step

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
```javascript
// Goal: POST JSON to API
```

**Step 2**: Get node info
```javascript
const info = get_node({
  nodeType: "nodes-base.httpRequest"
});

// Returns: method, url, sendBody, body, authentication required/optional
```

**Step 3**: Minimal config
```javascript
{
  "method": "POST",
  "url": "https://api.example.com/create",
  "authentication": "none"
}
```

**Step 4**: Validate
```javascript
validate_node({
  nodeType: "nodes-base.httpRequest",
  config,
  profile: "runtime"
});
// → Error: "sendBody required for POST"
```

**Step 5**: Add required field
```javascript
{
  "method": "POST",
  "url": "https://api.example.com/create",
  "authentication": "none",
  "sendBody": true
}
```

**Step 6**: Validate again
```javascript
validate_node({...});
// → Error: "body required when sendBody=true"
```

**Step 7**: Complete configuration
```javascript
{
  "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
```javascript
validate_node({...});
// → Valid! ✅
```

---

## Operation-Specific Configuration Examples

Concrete minimal configs showing how required fields differ by resource + operation.

### Slack Node Examples

#### Post Message
```javascript
{
  "resource": "message",
  "operation": "post",
  "channel": "#general",      // Required
  "text": "Hello!",           // Required
  "attachments": [],          // Optional
  "blocks": []                // Optional
}
```

#### Update Message
```javascript
{
  "resource": "message",
  "operation": "update",
  "messageId": "1234567890",  // Required (different from post!)
  "text": "Updated!",         // Required
  "channel": "#general"       // Optional (can be inferred)
}
```

#### Create Channel
```javascript
{
  "resource": "channel",
  "operation": "create",
  "name": "new-channel",      // Required
  "isPrivate": false          // Optional
  // Note: text NOT required for this operation
}
```

### HTTP Request Node Examples

#### GET Request
```javascript
{
  "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"
      }
    ]
  }
}
```

#### POST with JSON
```javascript
{
  "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": "john@example.com"
    }
  }
}
```

### IF Node Examples

#### String Comparison (Binary)
```javascript
{
  "conditions": {
    "string": [
      {
        "value1": "={{$json.status}}",
        "operation": "equals",
        "value2": "active"              // Binary: needs value2
      }
    ]
  }
}
```

#### Empty Check (Unary)
```javascript
{
  "conditions": {
    "string": [
      {
        "value1": "={{$json.email}}",
        "operation": "isEmpty",
        // No value2 - unary operator
        "singleValue": true             // Auto-added by sanitization
      }
    ]
  }
}
```
README.md
# n8n Node Configuration

Expert guidance for operation-aware node configuration with property dependencies.

## Overview

**Skill Name**: n8n Node Configuration
**Priority**: Medium
**Purpose**: Teach operation-aware configuration with progressive discovery and dependency awareness

## The Problem This Solves

Node configuration patterns:

- get_node is the primary discovery tool (18s avg from search → standard detail)
- 91.7% success rate with standard detail configuration
- 56 seconds average between configuration edits

**Key insight**: Most configurations only need standard detail, not full schema!

## What This Skill Teaches

### Core Concepts

1. **Operation-Aware Configuration**
   - Resource + operation determine required fields
   - Different operations = different requirements
   - Always check requirements when changing operation

2. **Property Dependencies**
   - Fields appear/disappear based on other field values
   - displayOptions control visibility
   - Conditional required fields
   - Understanding dependency chains

3. **Progressive Discovery**
   - Start with get_node standard detail (91.7% success)
   - Escalate to get_node({mode: "search_properties"}) if needed
   - Use get_node({detail: "full"}) only when necessary
   - Right tool for right job

4. **Configuration Workflow**
   - Identify → Discover → Configure → Validate → Iterate
   - Average 2-3 validation cycles
   - Read errors for dependency hints
   - 56 seconds between edits average

5. **Common Patterns**
   - Resource/operation nodes (Slack, Sheets)
   - HTTP-based nodes (HTTP Request, Webhook)
   - Database nodes (Postgres, MySQL)
   - Conditional logic nodes (IF, Switch)

## File Structure

```
n8n-node-configuration/
├── SKILL.md
│   Main configuration guide
│   - Configuration philosophy (progressive disclosure)
│   - Core concepts (operation-aware, dependencies)
│   - Configuration workflow (8-step process)
│   - get_node vs get_node({detail: "full"})
│   - Property dependencies deep dive
│   - Common node patterns (4 categories)
│   - Operation-specific examples
│   - Conditional requirements
│   - Anti-patterns and best practices
│
├── DEPENDENCIES.md
│   Property dependencies reference
│   - displayOptions mechanism
│   - show vs hide rules
│   - Multiple conditions (AND logic)
│   - Multiple values (OR logic)
│   - 4 common dependency patterns
│   - Using get_node({mode: "search_properties"})
│   - Complex dependency examples
│   - Nested dependencies
│   - Auto-sanitization interaction
│   - Troubleshooting guide
│   - Advanced patterns
│
├── OPERATION_PATTERNS.md
│   Common configurations by node type
│   - HTTP Request (GET/POST/PUT/DELETE)
│   - Webhook (basic/auth/response)
│   - Slack (post/update/create)
│   - Gmail (send/get)
│   - Postgres (query/insert/update)
│   - Set (values/mapping)
│   - Code (per-item/all-items)
│   - IF (string/number/boolean)
│   - Switch (rules/fallback)
│   - OpenAI (chat completion)
│   - Schedule (daily/interval/cron)
│   - Gotchas and tips for each
│
└── README.md (this file)
    Skill metadata and statistics
```

**Total**: 4 files + 4 evaluations

## Usage Statistics

Configuration metrics:

| Metric | Value | Insight |
|---|---|---|
| get_node | Primary tool | Most popular discovery pattern |
| Success rate (standard) | 91.7% | Standard detail sufficient for most |
| Avg time search→get_node | 18 seconds | Fast discovery workflow |
| Avg time between edits | 56 seconds | Iterative configuration |

## Tool Usage Pattern

**Most common discovery pattern**:
```
search_nodes → get_node (18s average)
```

**Configuration cycle**:
```
get_node → configure → validate → iterate (56s avg per edit)
```

## Key Insights

### 1. Progressive Disclosure Works

**91.7% success rate** with get_node (standard detail) proves most configurations don't need full schema.

**Strategy**:
1. Start with standard detail
2. Escalate to search_properties mode if stuck
3. Use full schema only when necessary

### 2. Operations Determine Requirements

**Same node, different operation = different requirements**

Example: Slack message
- `operation="post"` → needs channel + text
- `operation="update"` → needs messageId + text (different!)

### 3. Dependencies Control Visibility

**Fields appear/disappear based on other values**

Example: HTTP Request
- `method="GET"` → body hidden
- `method="POST"` + `sendBody=true` → body required

### 4. Configuration is Iterative

**Average 56 seconds between edits** shows configuration is iterative, not one-shot.

**Normal workflow**:
1. Configure minimal
2. Validate → error
3. Add missing field
4. Validate → error
5. Adjust value
6. Validate → valid ✅

### 5. Common Gotchas Exist

**Top 5 gotchas** from patterns:
1. Webhook data under `$json.body` (not `$json`)
2. POST needs `sendBody: true`
3. Slack channel format (`#name`)
4. SQL parameterized queries (injection prevention)
5. Timezone must be explicit (schedule nodes)

## Usage Examples

### Example 1: Basic Configuration Flow

```javascript
// Step 1: Get standard info
const info = get_node({
  nodeType: "nodes-base.slack"
});

// Step 2: Configure for operation
{
  "resource": "message",
  "operation": "post",
  "channel": "#general",
  "text": "Hello!"
}

// Step 3: Validate
validate_node({...});
// ✅ Valid!
```

### Example 2: Handling Dependencies

```javascript
// Step 1: Configure HTTP POST
{
  "method": "POST",
  "url": "https://api.example.com/create"
}

// Step 2: Validate → Error: "sendBody required"
// Step 3: Check dependencies
get_node({mode: "search_properties"})({
  nodeType: "nodes-base.httpRequest"
});
// Shows: body visible when sendBody=true

// Step 4: Fix
{
  "method": "POST",
  "url": "https://api.example.com/create",
  "sendBody": true,
  "body": {
    "contentType": "json",
    "content": {...}
  }
}
// ✅ Valid!
```

### Example 3: Operation Change

```javascript
// Initial config (post operation)
{
  "resource": "message",
  "operation": "post",
  "channel": "#general",
  "text": "Hello"
}

// Change operation
{
  "resource": "message",
  "operation": "update",  // Changed!
  // Need to check new requirements
}

// Get standard info for update operation
get_node({nodeType: "nodes-base.slack"});
// Shows: messageId required, channel optional

// Correct config
{
  "resource": "message",
  "operation": "update",
  "messageId": "1234567890.123456",
  "text": "Updated"
}
```

## When This Skill Activates

**Trigger phrases**:
- "how to configure"
- "what fields are required"
- "property dependencies"
- "get_node vs get_node({detail: "full"})"
- "operation-specific"
- "field not visible"

**Common scenarios**:
- Configuring new nodes
- Understanding required fields
- Field appears/disappears unexpectedly
- Choosing between discovery tools
- Switching operations
- Learning common patterns

## Integration with Other Skills

### Works With:
- **n8n MCP Tools Expert** - How to call discovery tools correctly
- **n8n Validation Expert** - Interpret missing_required errors
- **n8n Expression Syntax** - Configure expression fields
- **n8n Workflow Patterns** - Apply patterns with proper node config

### Complementary:
- Use MCP Tools Expert to learn tool selection
- Use Validation Expert to fix configuration errors
- Use Expression Syntax for dynamic field values
- Use Workflow Patterns to understand node relationships

## Testing

**Evaluations**: 4 test scenarios

1. **eval-001-property-dependencies.json**
   - Tests understanding of displayOptions
   - Guides to get_node({mode: "search_properties"})
   - Explains conditional requirements

2. **eval-002-operation-specific-config.json**
   - Tests operation-aware configuration
   - Identifies resource + operation pattern
   - References OPERATION_PATTERNS.md

3. **eval-003-conditional-fields.json**
   - Tests unary vs binary operators
   - Explains singleValue dependency
   - Mentions auto-sanitization

4. **eval-004-standard-vs-full.json**
   - Tests tool selection knowledge
   - Explains progressive disclosure
   - Provides success rate statistics

## Success Metrics

**Before this skill**:
- Using get_node({detail: "full"}) for everything (slow, overwhelming)
- Not understanding property dependencies
- Confused when fields appear/disappear
- Not aware of operation-specific requirements
- Trial and error configuration

**After this skill**:
- Start with get_node standard detail (91.7% success)
- Understand displayOptions mechanism
- Predict field visibility based on dependencies
- Check requirements when changing operations
- Systematic configuration approach
- Know common patterns by node type

## Coverage

**Node types covered**: Top 20 most-used nodes

| Category | Nodes | Coverage |
|---|---|---|
| HTTP/API | HTTP Request, Webhook | Complete |
| Communication | Slack, Gmail | Common operations |
| Database | Postgres, MySQL | CRUD operations |
| Transform | Set, Code | All modes |
| Conditional | IF, Switch | All operator types |
| AI | OpenAI | Chat completion |
| Schedule | Schedule Trigger | All modes |

## Related Documentation

- **n8n-mcp MCP Server**: Provides discovery tools
- **n8n Node API**: get_node, get_node({mode: "search_properties"}), get_node({detail: "full"})
- **n8n Schema**: displayOptions mechanism, property definitions

## Version History

- **v1.0** (2025-10-20): Initial implementation
  - SKILL.md with configuration workflow
  - DEPENDENCIES.md with displayOptions deep dive
  - OPERATION_PATTERNS.md with 20+ node patterns
  - 4 evaluation scenarios

## Author

Conceived by Romuald Członkowski - [www.aiadvisors.pl/en](https://www.aiadvisors.pl/en)

Part of the n8n-skills meta-skill collection.