n8n ile iş akışı otomasyonu: gerçek dünya desenleri
Kendi sunucunuzdaki n8n için pratik desenler: webhook, hata yakalama ve yeniden deneme, idempotency, sır yönetimi, sürümleme ve neyin otomasyona değmediği.
n8n ile iş akışı otomasyonu kurmak ilk gün heyecan verici, üçüncü ay ise öğreticidir. Kendi sunucumda bir n8n örneğini uzun süredir çalıştırıyorum; sipariş bildirimlerini, form gönderimlerini, günlük raporları ve birkaç iç bakım işini buradan yönetiyorum. Bu sürede öğrendiğim şey şu: sürükle-bırak arayüzü işi başlatmayı kolaylaştırıyor, ama bir akışı aylarca gözetimsiz ve güvenilir çalıştırmak arayüzle değil, birkaç mühendislik deseniyle mümkün oluyor.
Bu yazıda kendi kendine barındırılan (self-hosted) n8n üzerinde her projede tekrar tekrar kullandığım desenleri anlatacağım: webhook tetikleyicisinin doğru kurulumu, hata yakalama ve yeniden deneme, aynı olayın iki kez işlenmesini engelleyen idempotency, sırların akış dosyalarına sızmadan yönetilmesi ve iş akışlarının sürümlenmesi. Her bölümde çalışan bir yapılandırma ya da kod parçası var.
Bir de madalyonun öteki yüzü var: neyin otomasyona değmediği. Otomasyon danışmanı olarak bazen en faydalı cümlem "bunu otomatikleştirmeyin" oluyor; yazının sonunda o kısmı da açıkça yazdım.
Webhook tetikleyici: ilk düğüm, ilk tuzaklar
Gerçek dünyadaki akışların çoğu bir zamanlayıcıyla değil, dışarıdan gelen bir olayla başlar: ödeme sağlayıcısı sipariş bildirir, form aracı yeni kayıt gönderir, izleme sistemi alarm üretir. Bunların hepsi Webhook düğümüne düşer ve bu düğümün ters ayarlanması, akış daha çalışmadan kaybetmenize yol açar. Önce altyapı: n8n'i ters vekil (reverse proxy) arkasında çalıştırıyorsanız WEBHOOK_URL değişkenini vermediğiniz sürece n8n dışarıdan çağrılamayan webhook adresleri üretir. Kullandığım Compose iskeleti şu:
services:
n8n:
image: docker.n8n.io/n8nio/n8n:1.100.1 # sürümü sabitleyin, latest değil
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678" # dışarıya ters vekil açar
environment:
- N8N_HOST=otomasyon.ornek.com.tr
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://otomasyon.ornek.com.tr/
- GENERIC_TIMEZONE=Europe/Istanbul
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=db
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD_FILE=/run/secrets/db_parola
- N8N_ENCRYPTION_KEY_FILE=/run/secrets/n8n_anahtar
- NODE_FUNCTION_ALLOW_BUILTIN=crypto
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=168
secrets: [db_parola, n8n_anahtar]
volumes:
- n8n_veri:/home/node/.n8n
depends_on: [db]
db:
image: postgres:16
restart: unless-stopped
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD_FILE=/run/secrets/db_parola
secrets: [db_parola]
volumes:
- pg_veri:/var/lib/postgresql/data
secrets:
db_parola:
file: ./gizli/db_parola
n8n_anahtar:
file: ./gizli/n8n_anahtar
volumes:
n8n_veri:
pg_veri:
- Test ve üretim adresi farklıdır. Test URL yalnızca düzenleyicide dinlemeyi başlattığınız sürece açıktır; üretim URL'si ise akış etkinleştirilmeden yanıt vermez. Karşı tarafa adresi vermeden önce akışı etkinleştirin; "webhook 404 dönüyor" şikâyetlerinin çoğu buradan çıkar.
- Hemen yanıt verin. Respond ayarını "Immediately" bırakın; karşı sistem 200 alsın, ağır iş arkada devam etsin. Yanıtı akışın sonuna bağlarsanız kaynak sistemin zaman aşımı sizin sorununuza dönüşür.
- Kimlik doğrulama ekleyin. Header Auth ya da Basic Auth seçeneği düğümün içinde var; açık bir webhook adresi, internete açık bir komut satırıdır.
- Gelen gövdeyi ilk düğümde doğrulayın. Beklediğiniz alanlar yoksa akışı orada durdurun; bozuk veriyi beş düğüm sonra fark etmek çok daha pahalı.
Compose dosyasındaki proje adı sözleşmesini ve aynı sunucuda birden fazla stack'i çakıştırmadan çalıştırmayı Docker Compose proje izolasyonu yazısında ayrıntılı anlatmıştım; n8n kurulumunda da aynı kurallar geçerli.
Hata yakalama ve yeniden deneme
Bir akış ilk hafta hatasız çalışır, çünkü dış dünya o hafta uslu durmuştur. Sonra API bir gece bakıma girer, DNS bir saatliğine takılır, birisi tabloya beklenmedik bir değer yazar. n8n bu durumlar için üç ayrı katman sunuyor ve bence üçü birlikte kullanılmalı:
- Düğüm düzeyinde yeniden deneme. Her düğümün ayarlarında "Retry On Fail" var; deneme sayısını ve denemeler arası bekleme süresini oradan veriyorsunuz. Bunu yalnızca geçici hatalar için açın: ağ kesintisi, zaman aşımı, 5xx. Bir 400 ya da 401 yanıtını üç kez tekrar denemek kimseye fayda sağlamaz.
- "On Error" davranışı. Aynı panelde düğümün hata verince akışı durdurmak yerine hata çıkışından devam etmesini seçebiliyorsunuz ("Continue using error output"). Böylece başarısız kayıtları ayrı bir dala alıp loglar, geri kalanını işlemeye devam edersiniz. Toplu işlemlerde bir kaydın hatası diğer bin kaydı durdurmamalı.
- Hata iş akışı (Error Workflow). Ayrı bir akışı Error Trigger düğümüyle başlatıp diğer akışların ayarlarında "Error Workflow" olarak seçiyorsunuz. Hangi akış, hangi düğüm, hangi hata mesajı; hepsi tetikleyiciye gelir. Ben bunu Telegram'a düşen kısa bir mesaja bağlıyorum. Küçük bir uyarı: bu akış yalnızca üretim çalıştırmalarında tetiklenir, düzenleyicideki manuel testlerde değil.
Bir nokta daha: yeniden deneme ile idempotency birbirinden ayrılamaz. Üç kez denenen bir "sipariş oluştur" isteği, karşı taraf ilk denemeyi aslında işlemişse üç sipariş üretir. Bu yüzden bir sonraki desen olmadan yeniden denemeyi açmayın.
Idempotency: aynı olay iki kez gelirse
Webhook gönderen sistemlerin çoğu "en az bir kez" teslimat mantığıyla çalışır; yani aynı olayı iki kez, bazen üç kez gönderebilir. Yanıt gecikirse, sizin tarafta kısa bir kesinti olursa ya da karşı taraf kendi yeniden deneme mantığını çalıştırırsa aynı sipariş bildirimi iki kez düşer. Akışınız bunu fark etmiyorsa müşteriye iki e-posta, muhasebeye iki kayıt gider.
Çözüm basit ama disiplin ister: her olay için kararlı bir anahtar üretin ve işlemeden önce "bunu daha önce gördüm mü" diye sorun. Kaynak sistem bir olay kimliği gönderiyorsa doğrudan onu kullanın; göndermiyorsa olayı tanımlayan alanlardan bir özet (hash) üretin. Aşağıdaki Code düğümü (JavaScript, "Run Once for All Items" modunda) bunu yapıyor:
// Code düğümü: Webhook'tan gelen her kayıt için idempotency anahtarı üret
const crypto = require('crypto');
return $input.all().map(item => {
const b = item.json.body ?? item.json;
const kaynak = [b.siparisNo, b.durum, b.guncellemeZamani]
.map(v => String(v ?? ''))
.join('|');
const idempotencyKey = crypto
.createHash('sha256')
.update(kaynak)
.digest('hex');
return { json: { ...b, idempotencyKey } };
});
require('crypto') çağrısı için konteynerde NODE_FUNCTION_ALLOW_BUILTIN=crypto tanımlı olmalı; yukarıdaki Compose dosyasında var. Kod yazmak istemiyorsanız n8n'in hazır Crypto düğümü de aynı özeti üretir. Anahtar üretildikten sonra Postgres düğümünde şu sorguyu çalıştırıyorum; değerleri düğümün "Query Parameters" seçeneğiyle veriyorum:
CREATE TABLE IF NOT EXISTS islenen_olaylar (
idempotency_key text PRIMARY KEY,
siparis_no text NOT NULL,
alindi timestamptz NOT NULL DEFAULT now()
);
INSERT INTO islenen_olaylar (idempotency_key, siparis_no)
VALUES ($1, $2)
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING idempotency_key;
Olay daha önce işlendiyse INSERT satır döndürmez, Postgres düğümünün çıktısı boş kalır ve n8n'de çıktısı boş olan düğümden sonraki düğümler çalışmaz; akış kendiliğinden durur. İlk kez geliyorsa satır döner ve iş devam eder. Kontrol ile kaydı tek sorguda yapmak önemli: önce SELECT, sonra INSERT yaparsanız iki paralel çalıştırma arasındaki boşlukta yine çift işleme düşersiniz.
Sır yönetimi ve iş akışını sürümleme
n8n bütün kimlik bilgilerini (credentials) veritabanında şifreli tutar ve şifreleme anahtarı N8N_ENCRYPTION_KEY değişkeninden gelir. Bu anahtarı kaybederseniz veritabanı yedeğiniz elinizde olsa bile hiçbir API anahtarını geri okuyamazsınız. Kendi kurulumlarımda uyduğum kurallar:
- Şifreleme anahtarı düz metin dosyada durmaz. Ben SOPS ile şifreli bir kasada tutuyor, konteynere
_FILEsonekli değişkenle (N8N_ENCRYPTION_KEY_FILE) bir Docker secret üzerinden veriyorum. Veritabanı parolası da aynı yoldan gidiyor. - API anahtarı Code düğümünün içine ya da HTTP Request başlığına elle yazılmaz. Her zaman bir credential kaydı açın; Header Auth çoğu durumda yeter. Elle yazılmış anahtar, akışı dışa aktardığınız anda JSON dosyasında düz metin olarak gezmeye başlar.
- Dışa aktarılan akış dosyası sır içermez, ama fark etmeden içerebilir. Akış JSON'unda credential'lar yalnızca kimlik ve adla geçer, değerleri yoktur. Buna karşılık bir Set düğümüne "geçici olarak" yapıştırdığınız token orada kalır. Dışa aktarmadan önce aramak beş saniye sürer.
Sürümlemeye gelince: n8n'in arayüzünde bir geçmiş görünümü var, ama ben akışları düzenli aralıklarla JSON olarak dışa aktarıp bir Git deposuna işliyorum. Hem fark (diff) okunabiliyor hem de yanlış bir düzenlemeyi dakikalar içinde geri alabiliyorum. Tek satırlık iş:
docker compose exec n8n \
n8n export:workflow --all --separate --pretty \
--output=/home/node/.n8n/yedek/akislar/
Dosyaları docker compose cp ile ana makineye alıp depoya işliyorum. Düğümlerin konumu da JSON'da bulunduğu için bir düğümü arayüzde iki piksel kaydırmak bile diff üretir; bunu kabul edin, önemli olan mantıktaki değişikliği görebilmek. Kesinlikle yapmamanız gereken şey ise n8n export:credentials --decrypted çıktısını depoya koymak. O komut sırları düz metne çevirir ve yalnızca sunucu taşıma sırasında, geçici olarak kullanılmalıdır.
Neyi otomasyona bağlamamalı
n8n'in en tehlikeli özelliği, her şeyi otomatikleştirmeyi kolay göstermesi. Danışmanlık görüşmelerinde sık sık "bunu da n8n'e koyalım" cümlesini duyuyorum ve bazen cevabım hayır oluyor. Kendi süzgecim şu:
- Nadir ve kısa işler. Ayda bir yapılan on dakikalık bir iş için kurulan akış, ilk API değişikliğinde sessizce bozulur ve siz bunu aylar sonra fark edersiniz. Bu tür işleri kontrol listesiyle elle yapmak daha güvenilir.
- Girdisi standart olmayan işler. Onlarca tedarikçiden farklı biçimlerde gelen PDF faturaları "otomatik okuyacak" akış, aslında onlarca ayrı akıştır ve her tedarikçi şablon değiştirdiğinde biri kırılır. Önce girdiyi standartlaştırın, sonra otomatikleştirin.
- Kuralı her hafta değişen süreçler. Karar mantığı henüz oturmamışsa akış da oturmaz; her hafta düğüm düzenlersiniz. Süreç kâğıt üstünde bir süre aynı kalmadan otomasyona alınmamalı.
- Geri alınamaz ve maliyeti yüksek adımlar. Toplu e-posta göndermek, ödeme başlatmak, kayıt silmek. Bunlar tamamen otomatik olmamalı; n8n'in Wait düğümü bir webhook çağrısı ya da form gönderimiyle devam edebildiği için akışa insan onayı adımı eklemek kolay. Bir tık, bir felaketi önler.
- Büyüyüp programa dönüşmüş akışlar. Onlarca düğümlü, birkaç yerde dallanan, içinde çok sayıda Code düğümü olan bir akış artık görsel değil, okunması zor bir programdır. Bu noktada işi düz bir Python betiğine taşımak; test yazabilmek, sürümlemek ve büyük veriyle çalışmak açısından çok daha sağlıklı. SQLite ile küçük ama sağlam veri boru hatları yazısında tam olarak bu tür işler için önerdiğim yapıyı anlatmıştım.
Ortak nokta şu: n8n bir orkestrasyon aracı; veri işleme motoru ya da iş kuralları deposu değil. Olayları toplayıp doğru yere yönlendirmek, sistemleri birbirine bağlamak ve insanlara haber vermek için çok iyi; ağır hesaplama ve karmaşık mantık için değil.
Özetlersem: webhook'u ters vekil arkasında doğru adresle kurun ve hemen yanıt verin; yeniden denemeyi yalnızca geçici hatalar için açın ve bir hata akışıyla haberdar olun; her olaya bir idempotency anahtarı verip kontrolü tek sorguda yapın; şifreleme anahtarını kasada tutup akışları Git'e işleyin; otomasyona değmeyen işleri de cesaretle elle bırakın. Bu beş desen, benim deneyimimde, bir n8n kurulumunun oyuncak ile altyapı arasındaki farkını oluşturuyor. Kendi süreçlerinizde hangi işlerin otomasyona değdiğini birlikte ayıklamak isterseniz danışmanlık hizmetlerimi anlattığım ana sayfadan /#contact bölümüne uğrayıp bana yazabilirsiniz.