"Yeni lead gelince sıradaki danışmana atansın" cümlesi, CRM projelerinde en hızlı yazılan ve en geç bitirilen gereksinimlerden biridir. Ben Koray Yalçın; lead yönetimi, CRM mimarisi, dijital pazarlama ve yapay zekâ destekli otomasyon üzerine, özellikle gayrimenkul ve PropTech tarafında çalışıyorum. Kurduğum hemen her satış operasyonunda lead dağıtımı ilk gün "basit bir round-robin" olarak başlıyor, üçüncü haftada ise herkesin şikâyet ettiği bir kara kutuya dönüşüyor.
Şikâyetler tanıdık: "Bana hep soğuk lead geliyor", "İzindeydim ama bana atandı", "Aynı müşteri iki danışmana düştü", "Bu lead neden bende değil de onda?". Bu soruların hiçbiri sıranın kendisiyle ilgili değil; hepsi dağıtım motorunun tasarımıyla ilgili.
Bu yazıda lead routing'i (lead yönlendirme) geliştirici gözüyle ele alıyorum: kuralları koddan nasıl ayırırsınız, uygunluk filtresini nasıl kurarsınız, ağırlıklı ve kapasiteye duyarlı sırayı nasıl yazarsınız, aynı anda gelen lead'lerde çift atamayı nasıl önlersiniz, cevapsız kalan lead'i nasıl yeniden atarsınız ve her kararı nasıl açıklanabilir hale getirirsiniz.
Routing'in üç aşaması
Dağıtım motorunu kafamda her zaman üç ayrı aşama olarak tutuyorum. Bunları tek bir fonksiyona sıkıştırmak, sonradan hata ayıklamayı neredeyse imkânsız kılıyor.
- Sınıflandırma: Lead'in özelliklerini çıkarmak. Hangi proje, hangi dil, hangi bütçe aralığı, hangi kaynak, mevcut bir müşteri mi?
- Uygunluk: Bu lead'i kim alabilir? Projeye yetkili, dili konuşan, şu an mesaide olan, kapasitesi dolmamış danışmanlar.
- Seçim: Uygun adaylar arasından tek bir kişiyi seçmek. Round-robin, ağırlıklı sıra, en az yük ya da sahiplik kuralı.
Bu ayrımın en büyük faydası şudur: "Neden bana gelmedi?" sorusunun cevabı her zaman bu üç aşamadan birine düşer. Ya lead yanlış sınıflandırılmıştır, ya danışman uygun listede değildir, ya da seçim stratejisi başka birini öne almıştır.
Kuralları kod değil, veri olarak tutmak
Satış müdürleri kuralları sık değiştirir. Yeni proje açılır, bir ekip yurtdışı lead'lerine ayrılır, kampanya bitince öncelik değişir. Her değişiklik için deploy gerektiren bir sistem, ekibin kuralı Excel'de tutup elle atama yapmasıyla sonuçlanır.
Bu yüzden kuralları sıralı bir tablo olarak saklıyorum. Her kural bir koşul ve bir hedef havuz içerir; ilk eşleşen kural kazanır.
CREATE TABLE routing_rule ( id bigserial PRIMARY KEY, priority int NOT NULL, -- küçük olan önce değerlendirilir name text NOT NULL, conditions jsonb NOT NULL, -- {"project":"P12","language":["en","ru"]} pool_id bigint NOT NULL REFERENCES routing_pool(id), strategy text NOT NULL CHECK (strategy IN ('weighted_rr','least_load','owner_first')), active boolean NOT NULL DEFAULT true, valid_from timestamptz, valid_until timestamptz ); Koşulları bilerek sınırlı tutuyorum: eşitlik, liste içinde olma ve sayısal aralık. Kural motoruna serbest ifade dili eklemek cazip görünür, ama birkaç ay sonra kimsenin okuyamadığı kurallar birikir. Daha karmaşık bir ihtiyaç doğduğunda onu sınıflandırma aşamasında yeni bir özellik olarak hesaplayıp kurala basit bir alan olarak veriyorum. Örneğin "yurtdışı ve yüksek bütçe" koşulu, sınıflandırmada segment = "intl_premium" olarak üretiliyor.
Son kural her zaman koşulsuz bir "yakalama" kuralıdır ve genel bir havuza ya da yönetici kuyruğuna gider. Hiçbir kurala uymayan lead'in sessizce sahipsiz kalması, routing sistemlerinde gördüğüm en pahalı hatadır.
Uygunluk filtresi: Mesai, izin ve kapasite
Bir danışmanın havuzda olması, onun şu an lead alabileceği anlamına gelmez. Uygunluk filtresinde dört kontrol yapıyorum:
- Yetki: Danışman bu projeye ve bu lead tipine atanabilir mi?
- Durum: İzinde, eğitimde ya da manuel olarak "dağıtıma kapalı" mı?
- Mesai: Danışmanın kendi saat dilimine göre şu an çalışma saatinde mi?
- Kapasite: Henüz ilk teması yapılmamış açık lead sayısı belirlediğimiz eşiğin altında mı?
Mesai kontrolünde saat dilimi detayı önemli. Kıbrıs'taki bir satış ofisinde İngiltere, Almanya ve Türkiye'deki alıcılarla çalışan danışmanlar olabiliyor; çalışma saatlerini her danışman için kendi dilimiyle saklıyorum ve kontrolü o dilime çevirerek yapıyorum. Sunucu saatine göre yapılan kontrol, yaz saati geçişlerinde mutlaka birini yanlış saatte uyandırır.
Kapasite için toplam lead sayısı yerine "ilk teması bekleyen lead" sayısını kullanmayı tercih ediyorum. Danışmanın elindeki yüzlerce eski kayıt, yeni lead'i hızlı arayıp aramayacağı hakkında bir şey söylemez; ama henüz aranmamış beş yeni lead çok şey söyler.
def eligible_agents(pool, lead, now_utc): result = [] for agent in pool.members: if not agent.routing_enabled or agent.on_leave(now_utc): continue if lead.project_id not in agent.project_ids: continue if lead.language and lead.language not in agent.languages: continue local = now_utc.astimezone(agent.tz) if not agent.schedule.is_open(local): continue if agent.pending_first_contact >= agent.max_pending: continue result.append(agent) return result Uygun aday çıkmazsa motor hata vermez; lead'i havuzun "bekleme kuyruğuna" alır ve ilk uygun danışman mesaiye girdiğinde ya da kapasitesi boşaldığında yeniden değerlendirir. Gece gelen lead'lerin sabah sırayla dağıtılması böyle çalışıyor.
Seçim stratejisi: Ağırlıklı round-robin
Saf round-robin herkese eşit lead verir, ama ekipler eşit değildir. Yeni başlayan bir danışmana deneyimli birinin yarısı kadar lead vermek ya da yüksek dönüşümlü bir danışmana biraz daha fazla pay ayırmak isteyebilirsiniz.
Burada "smooth weighted round-robin" yaklaşımını kullanıyorum. Her danışmanın bir ağırlığı ve bir sayaç değeri var. Her seçimde tüm uygun adayların sayacı kendi ağırlığı kadar artırılır, en yüksek sayaca sahip olan seçilir ve onun sayacından toplam ağırlık düşülür. Sonuç, ağırlıklara uygun ama ardışık yığılma yapmayan bir dağılımdır.
def pick_weighted(candidates, state): total = sum(c.weight for c in candidates) for c in candidates: state[c.id] = state.get(c.id, 0) + c.weight chosen = max(candidates, key=lambda c: (state[c.id], -c.id)) state[chosen.id] -= total return chosen Önemli bir ayrıntı: Sayaç durumu yalnızca o an uygun olan adaylar üzerinden güncelleniyor. İzinden dönen bir danışmanın, kaçırdığı günlerin "telafisini" bir anda alıp lead'e boğulmasını istemiyorum. Bu yüzden izin dönüşünde sayacı havuzun ortalamasına çekiyorum.
Sahiplik önce gelir. Strateji ne olursa olsun, aynı telefon ya da e-posta ile daha önce gelmiş ve hâlâ aktif sahibi olan bir müşteri, sırayla değil doğrudan mevcut sahibine gider. Bu kontrolü tekilleştirme adımından sonra yapıyorum; aksi halde aynı alıcı iki farklı formdan geldiğinde iki danışmana düşer ve aynı gün iki kez aranır.
Eşzamanlılık: Aynı anda gelen iki lead
Kampanya saatlerinde aynı saniyede onlarca lead gelebilir. İki işçi süreci aynı sayaç durumunu okuyup aynı danışmanı seçerse, hem dağılım bozulur hem de kapasite sınırı aşılır.
En güvenilir çözüm, seçimi havuz düzeyinde bir kilit altında yapmak. PostgreSQL'de havuz satırını kilitleyip seçim ve atama işlemini tek transaction içinde tamamlıyorum.
BEGIN; SELECT rr_state FROM routing_pool WHERE id = $1 FOR UPDATE; -- uygulama: uygun adaylar + ağırlıklı seçim UPDATE lead SET owner_id = $2, assigned_at = now(), routing_version = routing_version + 1 WHERE id = $3 AND owner_id IS NULL; UPDATE routing_pool SET rr_state = $4 WHERE id = $1; COMMIT; Lead güncellemesindeki owner_id IS NULL koşulu ikinci bir emniyet kemeri. Aynı lead için iki atama olayı işlenirse ikincisi hiçbir satırı güncellemez ve sessizce sonlanır. Havuz başına kilit, tüm sistemi kilitlemekten çok daha az darboğaz yaratır; farklı projelerin lead'leri birbirini beklemez.
SLA ve yeniden atama
Atama, işin yarısıdır. Atanan lead belirli süre içinde aranmazsa ne olacağı, routing tasarımının diğer yarısıdır. Burada iki hatadan kaçınıyorum: süre dolduğunda hiçbir şey olmaması ve süre dolduğunda lead'in danışmandan sessizce alınması.
Her atamada, SLA süresi dolduğunda çalışacak bir zamanlanmış iş oluşturuyorum. İş çalıştığında önce lead'in hâlâ aynı atama sürümünde olup olmadığını ve ilk temasın kaydedilip kaydedilmediğini kontrol ediyor.
def on_sla_timer(lead_id, expected_version): lead = repo.get(lead_id) if lead.routing_version != expected_version: return # bu arada başka biri atanmış, zamanlayıcı eskidi if lead.first_contact_at is not None: return # SLA karşılandı if lead.reassign_count >= MAX_REASSIGN: escalate_to_manager(lead) return notify(lead.owner_id, "SLA doldu, lead yeniden dağıtılıyor") route(lead, exclude=[lead.owner_id], reason="sla_breach") routing_version alanı burada kritik. Lead elle başka bir danışmana devredildiyse ya da müşteri tekrar form doldurup sahibine yönlendirildiyse, eski zamanlayıcı yanlış kişiden lead almaya çalışmamalı. Sürüm numarası, eskimiş zamanlayıcıları ek bir kilit olmadan zararsız hale getiriyor.
Yeniden atamada önceki danışmanı aday listesinden çıkarıyorum, yeniden atama sayısına bir üst sınır koyuyorum ve sınır aşıldığında lead'i yöneticiye yükseltiyorum. Sonsuz bir "sıcak patates" döngüsü, lead'in hiç atanmamasından daha kötü bir müşteri deneyimi yaratır.
Her karar açıklanabilir olmalı
Routing tartışmalarını bitiren tek şey kayıttır. Her atamada bir karar kaydı yazıyorum: hangi kural eşleşti, uygunluk aşamasında kim neden elendi, hangi adaylar kaldı, hangi strateji kimi seçti.
{ "lead_id": 48211, "rule": "P12-intl", "excluded": {"a17": "on_leave", "a22": "outside_hours", "a31": "capacity"}, "candidates": ["a09", "a14"], "strategy": "weighted_rr", "chosen": "a14", "routing_version": 1 } Bu kayıt sayesinde "neden bana gelmedi?" sorusu bir dakikada cevaplanıyor. Daha önemlisi, kural değişikliklerinin etkisini ölçebiliyorum: yeni bir kural eklendikten sonra hangi havuzun daraldığını, kimin sürekli kapasite nedeniyle elendiğini ve bekleme kuyruğunun nerede biriktiğini bu kayıtlardan raporluyorum. Kayıtta müşterinin kişisel verilerini değil yalnızca kimliklerini tutuyorum; ayrıntı gerektiğinde CRM'deki yetkili ekrana gidiliyor.
Kural değişikliğini canlıya almadan test etmek
Kurallar veri olduğu için, değişikliği canlıya almadan önce simülasyonla deneyebiliyorum. Son birkaç haftanın lead'lerini, karar anındaki danışman durumlarıyla birlikte yeni kural setinden geçiriyor ve iki şeyi karşılaştırıyorum: her danışmana düşecek lead dağılımı ve hiçbir kurala uymayıp yakalama kuralına düşen lead sayısı.
Bunun için motorun saf bir fonksiyon gibi çalışabilmesi gerekiyor: lead, kural seti, danışman durumları ve sayaç durumu girdi; karar çıktı. Veritabanına, saate ve bildirim servisine doğrudan erişen bir routing kodu simüle edilemez. Bu yüzden saati ve depoları dışarıdan veriyorum. Aynı yapı birim testlerini de kolaylaştırıyor: "izindeki danışmana atama yapılmaz", "mesai dışında bekleme kuyruğuna alınır", "mevcut sahip önce gelir" gibi kuralları birkaç satırlık testlerle sabitliyorum.
Sık yapılan hatalar
- Atamayı form entegrasyonunun içinde yapmak. Make, Zapier ya da webhook akışının içine gömülen atama mantığı, kaynak sayısı arttığında her kaynakta farklı davranır. Lead'i önce CRM'e kaydedip routing'i tek bir yerden tetiklemek daha sağlıklı.
- Tekilleştirmeyi routing'den sonra yapmak. Önce atayıp sonra birleştirirseniz, iki danışmandan birinin lead'ini elinden almak zorunda kalırsınız.
- Kapasiteyi toplam kayıtla ölçmek. Eski kayıtlar yeni lead'e ayrılabilecek dikkat hakkında bilgi vermez.
- Yeniden atamayı sınırsız bırakmak. Üst sınır ve yönetici yükseltmesi olmadan lead ekip içinde dolaşıp durur.
- Kararı kaydetmemek. Kayıt yoksa her tartışma bir güven sorununa dönüşür.
Sık Sorulan Sorular (SSS)
Round-robin mi, en az yük mü?
İkisi farklı problemleri çözüyor. Round-robin adaleti önceliklendirir; en az yük ise hızı. Lead hacmi yüksek ve ilk temas süresi kritikse en az yükü, ekip içinde eşit fırsat algısı daha önemliyse ağırlıklı round-robin'i kullanıyorum. Çoğu projede kapasite eşiğiyle sınırlanmış ağırlıklı round-robin iyi bir orta yol oluyor.
Lead skoru routing'e dahil edilmeli mi?
Edilebilir, ama dikkatli olmak gerekiyor. Yüksek skorlu lead'leri yalnızca deneyimli danışmanlara yönlendirmek mantıklı görünür; ancak bu, yeni danışmanların hiç iyi lead görmemesine ve kendini geliştirememesine yol açabilir. Ben skoru genellikle ayrı bir havuz koşulu olarak değil, ağırlıkları hafifçe etkileyen bir sinyal olarak kullanıyorum.
CRM'in kendi atama özelliği yeterli değil mi?
HubSpot, Salesforce, Bitrix24 gibi sistemlerin yerleşik atama kuralları basit senaryolar için yeterli. Proje, dil, mesai, kapasite ve SLA ile yeniden atamanın birlikte gerektiği durumlarda ise genellikle CRM'in API'si üzerinden çalışan ayrı bir routing servisi kuruyorum. Karar servis tarafında veriliyor, sonuç CRM'deki sahiplik alanına yazılıyor.
Danışman lead'i reddedebilmeli mi?
Kontrollü olarak evet. Reddetme bir gerekçe kodu ile kayda geçmeli ve lead yeniden routing'e girmeli. Gerekçesiz ve sınırsız reddetme, sistemin "kirazı toplama" aracına dönüşmesine neden olur.
Gece gelen lead'ler ne olmalı?
Mesai dışında gelen lead'e otomatik bir bilgilendirme mesajı gidebilir, ama atama sabah mesai başında yapılmalı. Gece atanıp sabaha kadar SLA'sı dolan lead, hem danışmanı hem müşteriyi haksız yere cezalandırır. Bekleme kuyruğu bu yüzden var.
Kapanış
İyi bir lead routing motoru; sınıflandırma, uygunluk ve seçimi ayıran, kuralları veri olarak tutan, kapasite ve mesaiyi bilen, eşzamanlı atamaya karşı korunan, SLA dolduğunda kontrollü yeniden atama yapan ve her kararını kaydeden bir sistemdir. Bu parçaların her biri tek başına basit; birlikte ise "bu lead neden bende değil?" sorusunu tartışma olmaktan çıkarıp bir sorguya dönüştürüyor.
Buradaki kodlar kendi projelerimde kullandığım yapıların sadeleştirilmiş halleri. Farklı bir CRM ya da veritabanı kullanıyorsanız mantık aynı kalıyor.
Koray Yalçın — lead yönetimi, CRM mimarisi ve satış otomasyonu üzerine çalışıyor.
