原始内容
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
// 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)
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)
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)
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
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
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
// 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
// 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
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
// 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/jsonheader'ı ekle continueOnFaililecontinueErrorOutputfarklı davranır — hata yönetiminde ikincisini tercih et- Supabase'de
Prefer: return=representationinsert sonrası eklenen satırı döndürür - Apify dataset URL'inde
clean=trueboş/null kayıtları filtreler - n8n Cloud'da timeout 1 dakika, self-hosted'da ayarlanabilir