{"repo": "aliilhangis/n8n-skill-claude", "files": {"SKILL.md": {"799f08c269e10e2fd17da8c4c4990bff25b1bf80": {"t": 1785674437.7606294, "n": 16340, "h": "d5367186455971c1", "c": "# n8n Workflow Automation Skill\n**by @thealiilhan**\n\nBu 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.\n\n---\n\n## TEMEL EXPRESSION SYNTAX\n\n```\n{{ $json.fieldName }} → mevcut node çıktısı\n{{ $json['field name'] }} → boşluklu alan adı\n{{ $('NodeAdı').item.json.fieldName }} → başka node'dan veri\n{{ $('NodeAdı').all()[0].json.fieldName }} → başka node'dan ilk item\n{{ $node[\"NodeAdı\"].json.fieldName }} → alternatif syntax\n{{ JSON.stringify($json.array) }} → array'i JSON string'e çevir\n{{ $now.toISO() }} → şu anki timestamp (Luxon)\n{{ $today.toFormat('yyyy-MM-dd') }} → tarih formatı\n{{ $json.list.length }} → array uzunluğu\n{{ $runIndex }} → SplitInBatches loop index\n```\n\n**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.\n\n---\n\n## CODE NODE — JAVASCRIPT\n\n```javascript\n// Tüm input item'larını al\nconst items = $input.all();\n\n// Tek item\nconst item = $input.first();\n\n// Başka node'dan veri al\nconst config = $('Config').item.json;\nconst allRows = $('Apify Sonuçları').all();\n\n// Temel output formatı — her zaman { json: {} } döndür\nreturn items.map(item => ({\n json: {\n fieldA: item.json.something,\n fieldB: 'statik değer',\n processedAt: new Date().toISOString()\n }\n}));\n\n// Filtreleme\nconst filtered = items.filter(item => item.json.rating <= 2);\nreturn filtered.map(item => ({ json: item.json }));\n\n// Birden fazla item üret (split/expand)\nconst result = [];\nfor (const item of items) {\n for (const row of item.json.rows || []) {\n result.push({ json: row });\n }\n}\nreturn result;\n\n// Boş kontrol — hiç output vermemek için\nif (!items.length) return [];\n```\n\n### HTTP Request (Code Node içinden)\n```javascript\nconst response = await this.helpers.httpRequest({\n method: 'POST',\n url: 'https://api.example.com/endpoint',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer TOKEN'\n },\n body: { key: 'value' },\n json: true // body'yi otomatik stringify, response'u parse et\n});\nreturn [{ json: response }];\n```\n\n### JSON Parse (güvenli)\n```javascript\nlet parsed;\ntry {\n parsed = JSON.parse(rawText);\n} catch (e) {\n const cleaned = rawText\n .replace(/```json\\n?/g, '')\n .replace(/```\\n?/g, '')\n .trim();\n parsed = JSON.parse(cleaned);\n}\n```\n\n---\n\n## SUPABASE ENTEGRASYONU\n\n### HTTP Request Node ile (önerilen)\n```\nMethod: POST\nURL: https://PROJE_ID.supabase.co/rest/v1/tablo_adi\nHeaders:\n apikey: SERVICE_ROLE_KEY\n Authorization: Bearer SERVICE_ROLE_KEY\n Content-Type: application/json\n Prefer: return=representation → insert sonrası veriyi döndür\n Prefer: resolution=merge-duplicates → upsert (üsttekiyle birlikte)\nBody: {{ $json }}\n```\n\n### Code Node içinden (batch insert)\n```javascript\nconst rows = $input.all().map(item => ({\n field1: item.json.value1,\n field2: item.json.value2\n}));\n\nconst response = await this.helpers.httpRequest({\n method: 'POST',\n url: `https://${PROJECT_ID}.supabase.co/rest/v1/${TABLE}`,\n headers: {\n 'apikey': SERVICE_ROLE_KEY,\n 'Authorization': `Bearer ${SERVICE_ROLE_KEY}`,\n 'Content-Type': 'application/json',\n 'Prefer': 'return=minimal'\n },\n body: rows,\n json: true\n});\nreturn [{ json: { inserted: rows.length } }];\n```\n\n### SELECT / Filtreleme\n```\nGET https://PROJE.supabase.co/rest/v1/tablo?select=*&status=eq.active&limit=100\nGET https://PROJE.supabase.co/rest/v1/tablo?select=id,name&created_at=gte.2024-01-01\nGET https://PROJE.supabase.co/rest/v1/tablo?select=*&order=created_at.desc&limit=50\n```\n\n**Kritik:** RLS açıksa anon key çalışmaz — mutlaka **service role key** kullan.\n\n---\n\n## APIFY ASYNC POLLING PATTERN\n\nApify actor'ları asenkron çalışır. Run başlatılır, bitene kadar status poll edilir, sonra dataset çekilir.\n\n```\nRun Başlat → Run ID Kaydet → Wait → Status Kontrol → Poll State Güncelle\n ↓\n SUCCEEDED? → YES → Dataset Çek\n → NO → Max Poll Aşıldı?\n → YES → Hata log\n → NO → Wait'e dön (loop)\n```\n\n### Adım 1 — Run Başlat (HTTP Request)\n```\nPOST https://api.apify.com/v2/acts/ACTOR_ID/runs?token=APIFY_TOKEN\nContent-Type: application/json\nBody: { ...actor input parametreleri }\n```\n\n### Adım 2 — Run ID Kaydet (Set Node)\n```\nrunId = {{ $json.data.id }}\npollCount = 0\nmaxPolls = 20\n```\n\n### Adım 3 — Wait Node\n```\n15–30 saniye (actor ağırlığına göre ayarla)\nNOT: Wait node bir webhookId alır. Node'u silip yeniden eklersen ID değişir, loop kırılır.\n```\n\n### Adım 4 — Status Kontrol (HTTP Request)\n```\nGET https://api.apify.com/v2/acts/ACTOR_ID/runs/{{ $('Run ID Kaydet').item.json.runId }}?token=APIFY_TOKEN\n```\n\n### Adım 5 — Poll State Güncelle (Set Node)\n```\nrunId = {{ $('Run ID Kaydet').item.json.runId }}\npollCount = {{ $('Run ID Kaydet').item.json.pollCount + 1 }}\nmaxPolls = {{ $('Run ID Kaydet').item.json.maxPolls }}\nstatus = {{ $json.data.status }}\n```\n\n### Adım 6 — IF: SUCCEEDED mi?\n```\nleftValue: {{ $json.status }}\noperation: equals\nrightValue: SUCCEEDED\nTRUE → Dataset Çek\nFALSE → Max Poll Aşıldı mı? (pollCount >= maxPolls)\n```\n\n### Adım 7 — Dataset Çek (HTTP Request)\n```\nGET https://api.apify.com/v2/actor-runs/{{ $('Run ID Kaydet').item.json.runId }}/dataset/items\nParams: token=APIFY_TOKEN, format=json, clean=true\n```\n\n### Alternatif: run-sync (kısa actor'lar için)\n```\nPOST https://api.apify.com/v2/acts/ACTOR_ID/run-sync-get-dataset-items?token=TOKEN&waitForFinish=120\n```\n2 dakika üzeri işlerde timeout'a düşer → o zaman async pattern kullan.\n\n---\n\n## SOSYAL MEDYA SCRAPING\n\n### Instagram (RapidAPI)\n```\n# Profil\nGET https://instagram-social-api.p.rapidapi.com/v1/info?username_or_id_or_url=USERNAME\n\n# Postlar\nGET https://instagram-social-api.p.rapidapi.com/v1/posts?username_or_id_or_url=USERNAME\n\nHeaders: X-RapidAPI-Key, X-RapidAPI-Host: instagram-social-api.p.rapidapi.com\n\n# Veri yapısı:\n# response.data.items[] → postlar\n# item.caption.text → caption\n# item.caption.hashtags[] → hashtagler\n# item.location.name → lokasyon\n# item.like_count, item.comment_count\n```\n\n### TikTok (RapidAPI — keyword arama)\n```\nGET https://tiktok-scraper7.p.rapidapi.com/feed/search\nParams:\n keywords: arama terimi\n region: tr\n count: 10\n cursor: 0\n publish_time: 0 (tümü) | 1 (gün) | 7 (hafta) | 30 (ay)\n sort_type: 0 (alaka) | 1 (beğeni)\nHeaders: X-Rapidapi-Key, X-Rapidapi-Host: tiktok-scraper7.p.rapidapi.com\n```\n\n### Twitter/X (Apify — run-sync)\n```\nPOST https://api.apify.com/v2/acts/61RPP7dywgiy0JPD0/run-sync-get-dataset-items?token=TOKEN\nBody:\n{\n \"searchTerms\": [\"arama terimi\"],\n \"maxItems\": 100,\n \"sort\": \"Latest\",\n \"tweetLanguage\": \"tr\",\n \"onlyVerifiedUsers\": false,\n \"onlyTwitterBlue\": false\n}\nTimeout: 300000ms\n\n# Veri yapısı:\n# item.id, item.text, item.createdAt, item.url\n# item.author.userName, item.author.name, item.author.followers\n# item.author.isVerified, item.author.isBlueVerified, item.author.location\n# item.likeCount, item.retweetCount, item.replyCount, item.viewCount\n```\n\n### Trustpilot (Apify — async)\n```\nActor ID: 6q70QEFc2Zk0ObldU\nBody:\n{\n \"companyUrls\": [\"domain.com\"],\n \"maxReviewsPerCompany\": 10,\n \"sort\": \"recency\",\n \"verified\": false,\n \"withReplies\": false,\n \"includeCompanyInfo\": true\n}\n→ Async polling pattern zorunlu (2+ dakika sürer)\n```\n\n### YouTube (RapidAPI)\n```\nGET https://youtube-search-and-download.p.rapidapi.com/search\nParams: query, type=v, sort=r (relevance) | u (upload date)\nHeaders: X-RapidAPI-Key, X-RapidAPI-Host: youtube-search-and-download.p.rapidapi.com\n```\n\n### Meta Ads Library / Google Ads Transparency\n```\n→ Apify actor ile çek, async pattern uygula\n→ Veri: advertiser, ad copy, start date, platforms, media\n```\n\n---\n\n## VERİ DEPOLAMA\n\n### Google Sheets — Append\n```\nNode: Google Sheets → Operation: Append\ndocumentId: Sheet URL veya ID\nsheetName: Sayfa adı\nColumns mappingMode: autoMapInputData\n\n# Kritik: JSON key adları = kolon başlığı (büyük/küçük harf dahil eşleşmeli)\nreturn items.map(item => ({\n json: {\n 'Tweet ID': item.json.id,\n 'Kullanıcı': item.json.author?.userName || '',\n 'Beğeni': item.json.likeCount || 0,\n 'Tarih': item.json.createdAt,\n 'Çekilme Zamanı': new Date().toISOString()\n }\n}));\n```\n\n### Google Sheets — Upsert\n```\nNode: Google Sheets → Operation: appendOrUpdate\nmatchingColumns: ['Tweet ID'] → bu kolona göre güncelle, yoksa ekle\n```\n\n### Supabase — Upsert (duplicate önleme)\n```\nHeaders'a ekle: Prefer: resolution=merge-duplicates\n+ on_conflict parametresi için URL'e ekle: ?on_conflict=unique_kolon\n```\n\n---\n\n## EMAIL\n\n### Brevo ile HTML Mail\n```\nPOST https://api.brevo.com/v3/smtp/email\nHeaders: api-key: BREVO_KEY, Content-Type: application/json\nBody:\n{\n \"sender\": { \"name\": \"Bot Adı\", \"email\": \"gonderici@domain.com\" },\n \"to\": [{ \"email\": \"{{ $json.recipientEmail }}\", \"name\": \"{{ $json.recipientName }}\" }],\n \"cc\": [{ \"email\": \"{{ $json.ccEmail }}\" }],\n \"subject\": \"{{ $json.subject }}\",\n \"htmlContent\": \"{{ $json.htmlContent }}\"\n}\nTimeout: 30000ms\n```\n\n### HTML Mail Şablonu\n```javascript\nconst htmlContent = `\n\n\n
\n
\n

${title}

\n

${date}

\n
\n
${body}
\n
\n

Otomatik rapor

\n
\n
\n`;\n```\n\n### Gmail (n8n Native)\n```\nNode: Gmail → Operation: Send\nTo: {{ $json.email }}\nSubject: {{ $json.subject }}\nMessage: {{ $json.htmlContent }}\nMessage Type: HTML\n```\n\n---\n\n## AI ENTEGRASYONU\n\n### OpenAI (HTTP Request Node)\n```\nPOST https://api.openai.com/v1/chat/completions\nHeaders: Authorization: Bearer sk-..., Content-Type: application/json\nBody:\n{\n \"model\": \"gpt-4o-mini\",\n \"messages\": [\n { \"role\": \"system\", \"content\": \"Sen bir uzman asistansın.\" },\n { \"role\": \"user\", \"content\": {{ JSON.stringify($json.prompt) }} }\n ],\n \"response_format\": { \"type\": \"json_object\" },\n \"temperature\": 0.7,\n \"max_tokens\": 2000\n}\n\n# Yanıtı oku: {{ $json.choices[0].message.content }}\n```\n\n### n8n AI Node (LangChain)\n```\nNode: @n8n/n8n-nodes-langchain.openAi\nmodelId: gpt-4o-mini\nresponses → content: {{ $json.prompt }}\n\n# Çıktıyı oku: {{ $json.output[0].content[0].text }}\n```\n\n### Prompt İçinde n8n Değişkeni\n```javascript\n// String içinde\n`Şu kullanıcıyı analiz et: ${$('Config').item.json.username}`\n\n// Expression içinde — tırnak sorununu önlemek için JSON.stringify kullan\n{{ JSON.stringify($json.data) }}\n\n// Sistem promptu + veri birleştirme\nconst prompt = `Veriyi işle:\\n${JSON.stringify(items.map(i => i.json))}`;\n```\n\n---\n\n## WORKFLOW MİMARİSİ\n\n### Config Node Pattern\nHer workflow'un başına bir Set node koy, tüm değişkenleri burada tut:\n```\nNode: Set (adı: Config)\n companyUrls = \"domain1.com,domain2.com\"\n maxResults = 5\n targetEmail = \"hedef@email.com\"\n slackWebhook = \"https://hooks.slack.com/...\"\n```\nKullanımı: `{{ $('Config').item.json.maxResults }}`\n\n### Multi-Platform Paralel Akış\n```\nTrigger → Config\n ↓\n ┌─────────┼──────────┬──────────┐\n Platform1 Platform2 Platform3 Platform4\n ↓ ↓ ↓ ↓\n Format Format Format Format\n ↓ ↓ ↓ ↓\n Hedef Hedef Hedef Hedef\n```\nHer dal bağımsız çalışır, birinin hatası diğerini etkilemez.\n\n### Split → Process → Merge\n```javascript\n// Büyük array'i item'lara böl\nreturn bigArray.map(row => ({ json: row }));\n\n// Sonra SplitInBatches node ile batch'le (rate limit varsa)\n// Batchsize: 10, her batch arasında Wait: 1sn\n\n// Merge node ile sonunda birleştir\n// Mode: Append All Items\n```\n\n### Website Monitor Pattern\n```\nSchedule Trigger (12 saatte bir)\n → URL Listesi (Code Node) [array → her URL ayrı item]\n → HTTP Request (onError: continueErrorOutput)\n ├── [Başarı output] → IF status == \"up\" → log\n └── [Hata output] → Email / Slack alert\n```\n\n---\n\n## HATA YÖNETİMİ\n\n### HTTP Request Node\n```\nSettings → On Error: continueErrorOutput\n → output 1: başarılı yanıt\n → output 2: hata ($json.error.message)\n\nSettings → Retry On Fail: true, Max Tries: 2, Wait: 1000ms\nSettings → Timeout: 300000 (scraping işlemlerinde)\n```\n\n### Code Node — Try/Catch\n```javascript\ntry {\n const result = JSON.parse($json.responseText);\n return [{ json: { success: true, data: result } }];\n} catch (error) {\n return [{ json: { success: false, error: error.message, raw: $json.responseText } }];\n}\n```\n\n### Hata Email/Slack Bildirimi\n```javascript\n// Hata branch'inde:\nreturn [{\n json: {\n subject: `⚠️ Workflow Hatası - ${new Date().toLocaleDateString('tr-TR')}`,\n body: `Node: ${$json.node || 'bilinmiyor'}\\nHata: ${$json.error?.message || 'bilinmiyor'}`,\n recipient: 'ali@domain.com'\n }\n}];\n```\n\n---\n\n## TRIGGERLAR\n\n```\n# Zamanlama\nSchedule Trigger\n field: minutes (minutesInterval: 30)\n field: hours (hoursInterval: 6)\n field: days (hour: 9, minute: 0) → her sabah 09:00\n field: weeks → haftalık\n\n# Manuel test\nManual Trigger\n\n# Webhook (dışarıdan tetikleme)\nWebhook Trigger\n HTTP Method: POST\n URL: https://n8n-instance/webhook/UUID\n Authentication: Header Auth\n\n# Form\nn8n Form Trigger → form alanlarını tanımla\n URL: https://n8n-instance/form/UUID\n```\n\n---\n\n## CREDENTIAL TIPLERI\n\n| Servis | n8n Credential Tipi | Detay |\n|---|---|---|\n| Apify | httpQueryAuth | `?token=TOKEN` query param |\n| Apify | httpHeaderAuth | `Authorization: Bearer TOKEN` |\n| RapidAPI | httpHeaderAuth | `X-RapidAPI-Key` header |\n| Supabase | httpHeaderAuth | `apikey` + `Authorization: Bearer` |\n| OpenAI | httpHeaderAuth | `Authorization: Bearer sk-...` |\n| Brevo | httpHeaderAuth | `api-key` header |\n| Google Sheets | googleSheetsOAuth2Api | OAuth2 |\n| Gmail | gmailOAuth2 | OAuth2 |\n| GitHub | githubApi | Personal access token |\n| Slack | slackApi | Bot token |\n\n---\n\n## SIK YAPILAN HATALAR\n\n```\n1. $('NodeAdı') — node adı değişti, expression güncellenmedi → kırmızı hata\n2. Supabase anon key ile RLS bypass → 401/403, service role key kullan\n3. Apify run-sync timeout → 2dk+ işlerde async pattern'e geç\n4. Code node'da return eksik veya yanlış format → null/undefined output\n5. Google Sheets autoMapInputData — key adı kolon başlığıyla eşleşmiyor → boş satır\n6. Wait node loop'ta silinip yeniden eklendi → webhookId değişir, loop kırılır\n7. Array'i expression'a direkt koymak → [object Object], JSON.stringify kullan\n8. OpenAI prompt'unda çift tırnak içinde çift tırnak → expression bozulur\n9. Supabase batch insert'te rows boş array → 400 hatası, önce kontrol et\n10. Schedule Trigger timezone — n8n instance UTC'de, Türkiye UTC+3\n11. continueOnFail ≠ continueErrorOutput — ikincisi hata branch'i açar, birincisi açmaz\n12. SplitInBatches loop'ta $('önceki node') — loop index'ine göre değişir, dikkatli kullan\n```\n\n---\n\n## ÖNEMLİ NOTLAR\n\n- Code node çıktısı her zaman `[{ json: {...} }]` formatında olmalı\n- n8n expression'larında Luxon (tarih), lodash ve temel JS kullanılabilir\n- HTTP Request node'da JSON body gönderirken `Content-Type: application/json` header'ı ekle\n- `continueOnFail` ile `continueErrorOutput` farklı davranır — hata yönetiminde ikincisini tercih et\n- Supabase'de `Prefer: return=representation` insert sonrası eklenen satırı döndürür\n- Apify dataset URL'inde `clean=true` boş/null kayıtları filtreler\n- n8n Cloud'da timeout 1 dakika, self-hosted'da ayarlanabilir\n"}}}}