---
slug: "n8n-skill-claude"
source_type: "skill_md"
source_url: "https://cdn.jsdelivr.net/gh/aliilhangis/n8n-skill-claude@main/SKILL.md"
repo: "https://github.com/aliilhangis/n8n-skill-claude"
source_file: "SKILL.md"
branch: "main"
---
# n8n Workflow Automation Skill
**by @thealiilhan**

Bu skill, n8n ile production-ready otomasyon workflow'ları oluşturmak için gereken tüm pattern'leri, node kurallarını ve entegrasyon detaylarını içerir.

---

## TEMEL EXPRESSION SYNTAX

```
{{ $json.fieldName }}                          → mevcut node çıktısı
{{ $json['field name'] }}                      → boşluklu alan adı
{{ $('NodeAdı').item.json.fieldName }}          → başka node'dan veri
{{ $('NodeAdı').all()[0].json.fieldName }}      → başka node'dan ilk item
{{ $node["NodeAdı"].json.fieldName }}           → alternatif syntax
{{ JSON.stringify($json.array) }}              → array'i JSON string'e çevir
{{ $now.toISO() }}                             → şu anki timestamp (Luxon)
{{ $today.toFormat('yyyy-MM-dd') }}            → tarih formatı
{{ $json.list.length }}                        → array uzunluğu
{{ $runIndex }}                                → SplitInBatches loop index
```

**Kritik:** `$('NodeAdı')` büyük/küçük harf duyarlıdır. Türkçe karakter dahil, node adı değişirse expression da güncellenmeli.

---

## CODE NODE — JAVASCRIPT

```javascript
// Tüm input item'larını al
const items = $input.all();

// Tek item
const item = $input.first();

// Başka node'dan veri al
const config = $('Config').item.json;
const allRows = $('Apify Sonuçları').all();

// Temel output formatı — her zaman { json: {} } döndür
return items.map(item => ({
  json: {
    fieldA: item.json.something,
    fieldB: 'statik değer',
    processedAt: new Date().toISOString()
  }
}));

// Filtreleme
const filtered = items.filter(item => item.json.rating <= 2);
return filtered.map(item => ({ json: item.json }));

// Birden fazla item üret (split/expand)
const result = [];
for (const item of items) {
  for (const row of item.json.rows || []) {
    result.push({ json: row });
  }
}
return result;

// Boş kontrol — hiç output vermemek için
if (!items.length) return [];
```

### HTTP Request (Code Node içinden)
```javascript
const response = await this.helpers.httpRequest({
  method: 'POST',
  url: 'https://api.example.com/endpoint',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer TOKEN'
  },
  body: { key: 'value' },
  json: true  // body'yi otomatik stringify, response'u parse et
});
return [{ json: response }];
```

### JSON Parse (güvenli)
```javascript
let parsed;
try {
  parsed = JSON.parse(rawText);
} catch (e) {
  const cleaned = rawText
    .replace(/```json\n?/g, '')
    .replace(/```\n?/g, '')
    .trim();
  parsed = JSON.parse(cleaned);
}
```

---

## SUPABASE ENTEGRASYONU

### HTTP Request Node ile (önerilen)
```
Method: POST
URL: https://PROJE_ID.supabase.co/rest/v1/tablo_adi
Headers:
  apikey: SERVICE_ROLE_KEY
  Authorization: Bearer SERVICE_ROLE_KEY
  Content-Type: application/json
  Prefer: return=representation              → insert sonrası veriyi döndür
  Prefer: resolution=merge-duplicates        → upsert (üsttekiyle birlikte)
Body: {{ $json }}
```

### Code Node içinden (batch insert)
```javascript
const rows = $input.all().map(item => ({
  field1: item.json.value1,
  field2: item.json.value2
}));

const response = await this.helpers.httpRequest({
  method: 'POST',
  url: `https://${PROJECT_ID}.supabase.co/rest/v1/${TABLE}`,
  headers: {
    'apikey': SERVICE_ROLE_KEY,
    'Authorization': `Bearer ${SERVICE_ROLE_KEY}`,
    'Content-Type': 'application/json',
    'Prefer': 'return=minimal'
  },
  body: rows,
  json: true
});
return [{ json: { inserted: rows.length } }];
```

### SELECT / Filtreleme
```
GET https://PROJE.supabase.co/rest/v1/tablo?select=*&status=eq.active&limit=100
GET https://PROJE.supabase.co/rest/v1/tablo?select=id,name&created_at=gte.2024-01-01
GET https://PROJE.supabase.co/rest/v1/tablo?select=*&order=created_at.desc&limit=50
```

**Kritik:** RLS açıksa anon key çalışmaz — mutlaka **service role key** kullan.

---

## APIFY ASYNC POLLING PATTERN

Apify actor'ları asenkron çalışır. Run başlatılır, bitene kadar status poll edilir, sonra dataset çekilir.

```
Run Başlat → Run ID Kaydet → Wait → Status Kontrol → Poll State Güncelle
                                                              ↓
                                          SUCCEEDED? → YES → Dataset Çek
                                                     → NO  → Max Poll Aşıldı?
                                                               → YES → Hata log
                                                               → NO  → Wait'e dön (loop)
```

### Adım 1 — Run Başlat (HTTP Request)
```
POST https://api.apify.com/v2/acts/ACTOR_ID/runs?token=APIFY_TOKEN
Content-Type: application/json
Body: { ...actor input parametreleri }
```

### Adım 2 — Run ID Kaydet (Set Node)
```
runId     = {{ $json.data.id }}
pollCount = 0
maxPolls  = 20
```

### Adım 3 — Wait Node
```
15–30 saniye (actor ağırlığına göre ayarla)
NOT: Wait node bir webhookId alır. Node'u silip yeniden eklersen ID değişir, loop kırılır.
```

### Adım 4 — Status Kontrol (HTTP Request)
```
GET https://api.apify.com/v2/acts/ACTOR_ID/runs/{{ $('Run ID Kaydet').item.json.runId }}?token=APIFY_TOKEN
```

### Adım 5 — Poll State Güncelle (Set Node)
```
runId     = {{ $('Run ID Kaydet').item.json.runId }}
pollCount = {{ $('Run ID Kaydet').item.json.pollCount + 1 }}
maxPolls  = {{ $('Run ID Kaydet').item.json.maxPolls }}
status    = {{ $json.data.status }}
```

### Adım 6 — IF: SUCCEEDED mi?
```
leftValue:  {{ $json.status }}
operation:  equals
rightValue: SUCCEEDED
TRUE  → Dataset Çek
FALSE → Max Poll Aşıldı mı? (pollCount >= maxPolls)
```

### Adım 7 — Dataset Çek (HTTP Request)
```
GET https://api.apify.com/v2/actor-runs/{{ $('Run ID Kaydet').item.json.runId }}/dataset/items
Params: token=APIFY_TOKEN, format=json, clean=true
```

### Alternatif: run-sync (kısa actor'lar için)
```
POST https://api.apify.com/v2/acts/ACTOR_ID/run-sync-get-dataset-items?token=TOKEN&waitForFinish=120
```
2 dakika üzeri işlerde timeout'a düşer → o zaman async pattern kullan.

---

## SOSYAL MEDYA SCRAPING

### Instagram (RapidAPI)
```
# Profil
GET https://instagram-social-api.p.rapidapi.com/v1/info?username_or_id_or_url=USERNAME

# Postlar
GET https://instagram-social-api.p.rapidapi.com/v1/posts?username_or_id_or_url=USERNAME

Headers: X-RapidAPI-Key, X-RapidAPI-Host: instagram-social-api.p.rapidapi.com

# Veri yapısı:
# response.data.items[] → postlar
# item.caption.text → caption
# item.caption.hashtags[] → hashtagler
# item.location.name → lokasyon
# item.like_count, item.comment_count
```

### TikTok (RapidAPI — keyword arama)
```
GET https://tiktok-scraper7.p.rapidapi.com/feed/search
Params:
  keywords: arama terimi
  region: tr
  count: 10
  cursor: 0
  publish_time: 0 (tümü) | 1 (gün) | 7 (hafta) | 30 (ay)
  sort_type: 0 (alaka) | 1 (beğeni)
Headers: X-Rapidapi-Key, X-Rapidapi-Host: tiktok-scraper7.p.rapidapi.com
```

### Twitter/X (Apify — run-sync)
```
POST https://api.apify.com/v2/acts/61RPP7dywgiy0JPD0/run-sync-get-dataset-items?token=TOKEN
Body:
{
  "searchTerms": ["arama terimi"],
  "maxItems": 100,
  "sort": "Latest",
  "tweetLanguage": "tr",
  "onlyVerifiedUsers": false,
  "onlyTwitterBlue": false
}
Timeout: 300000ms

# Veri yapısı:
# item.id, item.text, item.createdAt, item.url
# item.author.userName, item.author.name, item.author.followers
# item.author.isVerified, item.author.isBlueVerified, item.author.location
# item.likeCount, item.retweetCount, item.replyCount, item.viewCount
```

### Trustpilot (Apify — async)
```
Actor ID: 6q70QEFc2Zk0ObldU
Body:
{
  "companyUrls": ["domain.com"],
  "maxReviewsPerCompany": 10,
  "sort": "recency",
  "verified": false,
  "withReplies": false,
  "includeCompanyInfo": true
}
→ Async polling pattern zorunlu (2+ dakika sürer)
```

### YouTube (RapidAPI)
```
GET https://youtube-search-and-download.p.rapidapi.com/search
Params: query, type=v, sort=r (relevance) | u (upload date)
Headers: X-RapidAPI-Key, X-RapidAPI-Host: youtube-search-and-download.p.rapidapi.com
```

### Meta Ads Library / Google Ads Transparency
```
→ Apify actor ile çek, async pattern uygula
→ Veri: advertiser, ad copy, start date, platforms, media
```

---

## VERİ DEPOLAMA

### Google Sheets — Append
```
Node: Google Sheets → Operation: Append
documentId: Sheet URL veya ID
sheetName: Sayfa adı
Columns mappingMode: autoMapInputData

# Kritik: JSON key adları = kolon başlığı (büyük/küçük harf dahil eşleşmeli)
return items.map(item => ({
  json: {
    'Tweet ID': item.json.id,
    'Kullanıcı': item.json.author?.userName || '',
    'Beğeni': item.json.likeCount || 0,
    'Tarih': item.json.createdAt,
    'Çekilme Zamanı': new Date().toISOString()
  }
}));
```

### Google Sheets — Upsert
```
Node: Google Sheets → Operation: appendOrUpdate
matchingColumns: ['Tweet ID']  → bu kolona göre güncelle, yoksa ekle
```

### Supabase — Upsert (duplicate önleme)
```
Headers'a ekle: Prefer: resolution=merge-duplicates
+ on_conflict parametresi için URL'e ekle: ?on_conflict=unique_kolon
```

---

## EMAIL

### Brevo ile HTML Mail
```
POST https://api.brevo.com/v3/smtp/email
Headers: api-key: BREVO_KEY, Content-Type: application/json
Body:
{
  "sender": { "name": "Bot Adı", "email": "gonderici@domain.com" },
  "to": [{ "email": "{{ $json.recipientEmail }}", "name": "{{ $json.recipientName }}" }],
  "cc": [{ "email": "{{ $json.ccEmail }}" }],
  "subject": "{{ $json.subject }}",
  "htmlContent": "{{ $json.htmlContent }}"
}
Timeout: 30000ms
```

### HTML Mail Şablonu
```javascript
const htmlContent = `
<!DOCTYPE html><html>
<body style="background:#f3f4f6; padding:40px 10px; margin:0;">
  <div style="max-width:650px; margin:0 auto; background:#fff; border-radius:12px;
              box-shadow:0 4px 20px rgba(0,0,0,0.08); border:1px solid #e5e7eb; overflow:hidden;">
    <div style="background:linear-gradient(135deg,#4A90E2 0%,#6366f1 100%);
                padding:35px 20px; text-align:center; color:white;">
      <h1 style="margin:0; font-size:24px; font-family:sans-serif;">${title}</h1>
      <p style="margin-top:8px; opacity:0.9; font-size:14px; font-family:sans-serif;">${date}</p>
    </div>
    <div style="padding:30px 40px;">${body}</div>
    <div style="background:#f9fafb; padding:20px; text-align:center; border-top:1px solid #e5e7eb;">
      <p style="margin:0; color:#9ca3af; font-size:12px; font-family:sans-serif;">Otomatik rapor</p>
    </div>
  </div>
</body></html>`;
```

### Gmail (n8n Native)
```
Node: Gmail → Operation: Send
To: {{ $json.email }}
Subject: {{ $json.subject }}
Message: {{ $json.htmlContent }}
Message Type: HTML
```

---

## AI ENTEGRASYONU

### OpenAI (HTTP Request Node)
```
POST https://api.openai.com/v1/chat/completions
Headers: Authorization: Bearer sk-..., Content-Type: application/json
Body:
{
  "model": "gpt-4o-mini",
  "messages": [
    { "role": "system", "content": "Sen bir uzman asistansın." },
    { "role": "user", "content": {{ JSON.stringify($json.prompt) }} }
  ],
  "response_format": { "type": "json_object" },
  "temperature": 0.7,
  "max_tokens": 2000
}

# Yanıtı oku: {{ $json.choices[0].message.content }}
```

### n8n AI Node (LangChain)
```
Node: @n8n/n8n-nodes-langchain.openAi
modelId: gpt-4o-mini
responses → content: {{ $json.prompt }}

# Çıktıyı oku: {{ $json.output[0].content[0].text }}
```

### Prompt İçinde n8n Değişkeni
```javascript
// String içinde
`Şu kullanıcıyı analiz et: ${$('Config').item.json.username}`

// Expression içinde — tırnak sorununu önlemek için JSON.stringify kullan
{{ JSON.stringify($json.data) }}

// Sistem promptu + veri birleştirme
const prompt = `Veriyi işle:\n${JSON.stringify(items.map(i => i.json))}`;
```

---

## WORKFLOW MİMARİSİ

### Config Node Pattern
Her workflow'un başına bir Set node koy, tüm değişkenleri burada tut:
```
Node: Set (adı: Config)
  companyUrls   = "domain1.com,domain2.com"
  maxResults    = 5
  targetEmail   = "hedef@email.com"
  slackWebhook  = "https://hooks.slack.com/..."
```
Kullanımı: `{{ $('Config').item.json.maxResults }}`

### Multi-Platform Paralel Akış
```
Trigger → Config
              ↓
    ┌─────────┼──────────┬──────────┐
  Platform1 Platform2 Platform3 Platform4
    ↓           ↓          ↓          ↓
  Format     Format     Format     Format
    ↓           ↓          ↓          ↓
  Hedef      Hedef      Hedef      Hedef
```
Her dal bağımsız çalışır, birinin hatası diğerini etkilemez.

### Split → Process → Merge
```javascript
// Büyük array'i item'lara böl
return bigArray.map(row => ({ json: row }));

// Sonra SplitInBatches node ile batch'le (rate limit varsa)
// Batchsize: 10, her batch arasında Wait: 1sn

// Merge node ile sonunda birleştir
// Mode: Append All Items
```

### Website Monitor Pattern
```
Schedule Trigger (12 saatte bir)
    → URL Listesi (Code Node)  [array → her URL ayrı item]
    → HTTP Request (onError: continueErrorOutput)
        ├── [Başarı output] → IF status == "up" → log
        └── [Hata output]  → Email / Slack alert
```

---

## HATA YÖNETİMİ

### HTTP Request Node
```
Settings → On Error: continueErrorOutput
  → output 1: başarılı yanıt
  → output 2: hata ($json.error.message)

Settings → Retry On Fail: true, Max Tries: 2, Wait: 1000ms
Settings → Timeout: 300000 (scraping işlemlerinde)
```

### Code Node — Try/Catch
```javascript
try {
  const result = JSON.parse($json.responseText);
  return [{ json: { success: true, data: result } }];
} catch (error) {
  return [{ json: { success: false, error: error.message, raw: $json.responseText } }];
}
```

### Hata Email/Slack Bildirimi
```javascript
// Hata branch'inde:
return [{
  json: {
    subject: `⚠️ Workflow Hatası - ${new Date().toLocaleDateString('tr-TR')}`,
    body: `Node: ${$json.node || 'bilinmiyor'}\nHata: ${$json.error?.message || 'bilinmiyor'}`,
    recipient: 'ali@domain.com'
  }
}];
```

---

## TRIGGERLAR

```
# Zamanlama
Schedule Trigger
  field: minutes (minutesInterval: 30)
  field: hours   (hoursInterval: 6)
  field: days    (hour: 9, minute: 0)  → her sabah 09:00
  field: weeks   → haftalık

# Manuel test
Manual Trigger

# Webhook (dışarıdan tetikleme)
Webhook Trigger
  HTTP Method: POST
  URL: https://n8n-instance/webhook/UUID
  Authentication: Header Auth

# Form
n8n Form Trigger → form alanlarını tanımla
  URL: https://n8n-instance/form/UUID
```

---

## CREDENTIAL TIPLERI

| Servis | n8n Credential Tipi | Detay |
|---|---|---|
| Apify | httpQueryAuth | `?token=TOKEN` query param |
| Apify | httpHeaderAuth | `Authorization: Bearer TOKEN` |
| RapidAPI | httpHeaderAuth | `X-RapidAPI-Key` header |
| Supabase | httpHeaderAuth | `apikey` + `Authorization: Bearer` |
| OpenAI | httpHeaderAuth | `Authorization: Bearer sk-...` |
| Brevo | httpHeaderAuth | `api-key` header |
| Google Sheets | googleSheetsOAuth2Api | OAuth2 |
| Gmail | gmailOAuth2 | OAuth2 |
| GitHub | githubApi | Personal access token |
| Slack | slackApi | Bot token |

---

## SIK YAPILAN HATALAR

```
1. $('NodeAdı') — node adı değişti, expression güncellenmedi → kırmızı hata
2. Supabase anon key ile RLS bypass → 401/403, service role key kullan
3. Apify run-sync timeout → 2dk+ işlerde async pattern'e geç
4. Code node'da return eksik veya yanlış format → null/undefined output
5. Google Sheets autoMapInputData — key adı kolon başlığıyla eşleşmiyor → boş satır
6. Wait node loop'ta silinip yeniden eklendi → webhookId değişir, loop kırılır
7. Array'i expression'a direkt koymak → [object Object], JSON.stringify kullan
8. OpenAI prompt'unda çift tırnak içinde çift tırnak → expression bozulur
9. Supabase batch insert'te rows boş array → 400 hatası, önce kontrol et
10. Schedule Trigger timezone — n8n instance UTC'de, Türkiye UTC+3
11. continueOnFail ≠ continueErrorOutput — ikincisi hata branch'i açar, birincisi açmaz
12. SplitInBatches loop'ta $('önceki node') — loop index'ine göre değişir, dikkatli kullan
```

---

## ÖNEMLİ NOTLAR

- Code node çıktısı her zaman `[{ json: {...} }]` formatında olmalı
- n8n expression'larında Luxon (tarih), lodash ve temel JS kullanılabilir
- HTTP Request node'da JSON body gönderirken `Content-Type: application/json` header'ı ekle
- `continueOnFail` ile `continueErrorOutput` farklı davranır — hata yönetiminde ikincisini tercih et
- Supabase'de `Prefer: return=representation` insert sonrası eklenen satırı döndürür
- Apify dataset URL'inde `clean=true` boş/null kayıtları filtreler
- n8n Cloud'da timeout 1 dakika, self-hosted'da ayarlanabilir
