{"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${date}
\nOtomatik rapor
\n