Zum Inhalt springen
← Alle Artikel

Eine Outreach-Pipeline ohne eine einzige Abhängigkeit

06. Okt. 2026 · Automatisierung · 5 min

·Von Dimitri Pisarev

Eine SQLite-Datei von exakt 628 MiB liegt auf einer Maschine, 658.427.904 Bytes auf der Platte. Die Datei ist das Lager. Sie enthält 20.180 Records aus einem offenen Datenfeed, dazu 5.153 Organisationen und 48.967 Adresseinträge über 2.335 Städte. Der erste Record landete am 22. September um 22:01 UTC, der letzte am 6. Oktober um 07:04 UTC. Auf dieser Datei läuft eine Outreach-Engine mit null Laufzeit-Abhängigkeiten. Ihre ersten 48 Stunden erbrachten 100 Deliveries, 50 pro Tag.

Kein Teil davon ist gemietet. Das Tooling, das Firmen sonst in diesem Feld kaufen, wird pro Kontakt abgerechnet, hält sein Scoring geschlossen und legt Prospect-Daten auf fremde Platten. Diese Engine läuft auf eigener Hardware, und jede Zahl im Folgenden stammt aus ihren Tabellen und Manifesten. Die These lautet: Der komplette Funnel von der Ingestion bis zur Reply-Triage gehört auf die Maschine, die ihn nutzt. Die Zahlen sind die Prüfung.

Warum selbst bauen, statt zu mieten?#

Mieten ist der Standard, weil der erste Tag funktioniert. Die Rechnung kommt später, und mit ihr die blinden Flecken: ein Score, der unsichtbar bleibt und trotzdem entscheidet, welcher Prospect als Nächstes kontaktiert wird, dazu Prospect-Daten und Sendehistorie hinter der API eines Anbieters. Denselben Grundgedanken habe ich in meinen deutschen PII-Redaktor übernommen, wo der Markt noch leerer ist: Niemand misst PII-Erkennung auf Deutsch.

Das zweite Problem ist leiser. Eine Pipeline mit fünf Stufen fällt ohne Rauschen aus: Ein Feed verschiebt seine Form, ein Reply landet im falschen Ordner. Ein gemietetes System macht aus diesem Ausfall ein Support-Ticket. Ein eigenes System macht daraus eine Zeile, die sich abfragen lässt.

Eine Datei als komplettes Lager#

Die Datenbank läuft im WAL-Modus. Eine einzige Datei hält alles. Zwei FTS5-Indizes tokenisieren den Text in Trigrammen, wie die SQLite-FTS5-Dokumentation es beschreibt, sodass drei Zeichen bereits einen Treffer ergeben. Der Adressindex umfasst 48.967 Einträge über 2.335 Städte. 46.121 davon sind geokodiert und decken 2.176 dieser Städte ab.

Note

Jede Zahl in diesem Artikel stammt aus einer Dateisystem-Kopie der Datenbank, geöffnet über node:sqlite im schreibgeschützten, unveränderlichen Modus. Die Originaldatei blieb geschlossen.

Fünf Stufen, kein Broker dazwischen#

Die Stufen heißen Ingestion, Matching, Anreicherung, Delivery und Triage. Jede liest aus SQLite und schreibt nach SQLite zurück. Kein Broker sitzt dazwischen. Auch ein Cloud-Dienst fehlt.

Die Ingestion ist die einzige Pflichtstufe. Die Feed-Pulls laufen seit dem 22. September und haben genau 222 Zeilen im Such-Log geschrieben, je eine pro Suchbegriff und Ausführung, rund 54 tatsächliche Läufe gezählt über die Startminute. Zwei Wochen dieses Takts füllten das Lager auf 20.180 Records.

Das Matching beginnt mit einem Gate. Jeder aktive Record passiert einen taxonomischen Filter über die Kategoriegruppen des Dataset-Katalogs. Nur die Überlebenden kommen weiter. Die Matches-Tabelle führt die Buchhaltung: 37.059 Paare aus Profil und Record über drei Suchprofile, 23.828 davon hinter dem Gate.

Die Anreicherung macht aus gefundenen Records erreichbare. 173 Kontakt-Records sind angereichert. 108 davon tragen eine E-Mail-Adresse, und zwischen 103 und 106 tragen eine Postadresse mit Straße, Code und Stadt, je nach Feld. Die Spanne entspricht dem, was die Tabellen hergeben.

Die Delivery wählt aus 55 Nachrichtenvarianten. Die Auswahl fällt per FNV-1a-Hash über die Record-Kennung, dividiert durch die Zahl der passenden Varianten. Modellaufrufe entfallen ganz. Ledger, Sendeschlange und Indexdatei stimmen überein: je 100 Zeilen. Alle 100 bleiben als archivierte .eml-Dateien erhalten. Die Kopie in den Gesendet-Ordner läuft als best-effort IMAP APPEND und liegt in 97 der Manifeste vor.

Die Triage ist ein IMAP-Leser. Er hat 30 eingehende Replies über 23 Records geparst, alle sind abgelegt. Sein Übergangs-Log zählt 19 Bestätigungen, 6 Einladungen, ein Pool-Angebot, eine Absage und einen Bounce: 28 Übergänge. Über alle 100 Deliveries liest das Ledger 77 ohne Antwort, 17 Bestätigungen, 3 Einladungen, ein Pool, eine Absage, einen Bounce.

Was das Ranking wirklich wiegt#

Das übliche Muster besteht aus Retrieval und Reranking, der Neusortierung der Kandidaten. Diese Engine teilt die Arbeit anders auf. Der gesamte Korpus passiert zuerst das Gate. Was überlebt, bekommt genau einen gemischten Score: 0,5 mal BM25, min-max-normalisiert aus FTS5, plus 0,5 mal Cosinus-Ähnlichkeit über Embeddings; ein Record ohne Vektor rangiert allein über BM25.

Die lexikalische Stufe hat eine nützliche Eigenschaft: Kein Record geht in ihr verloren. Die BM25-Abfrage stoppt bei LIMIT 100. Ein Record, den die lexikalische Abfrage nie geliefert hat, behält sein Cosinus-Gewicht und klettert trotzdem, weil die Abwesenheit aus dieser Liste nur die lexikalische Komponente auf null setzt. Die Evaluationsnotizen nennen diese Fälle „Entrants (hybrid only, cosine-driven)“.

Nur die Cosinus-Hälfte verlässt die Maschine: Embedding-Texte, und auch das nur, solange ein Provider-Key gesetzt ist. Nichts anderes geht ins Netz. Ohne Key trägt die BM25-Hälfte das komplette Ranking.

Null Abhängigkeiten als Betriebsentscheidung#

Die Zahl der Laufzeit-Abhängigkeiten ist null. Die package.json listet zwei devDependencies, TypeScript 7 und @types/node 26. Mehr steht nicht drin. Die npm-Skripte führen src/cli.ts direkt unter Node 26 (v26.5.0) aus. Die tsconfig-Datei steht auf noEmit und erasableSyntaxOnly, es gibt also keinen Build-Output, der abweichen könnte. SMTP und IMAP laufen über node:net, node:tls und node:crypto, ZIP über node:zlib. Die DOCX- und XLSX-Schreiber sind selbst geschrieben.

npm test durchläuft offline 700 Tests: 674 bestanden, 0 fehlgeschlagen, 26 übersprungen, die Live-Suite startet ohne LIVE=1 nicht. Der Lauf dauert rund 23 Sekunden, ganz ohne Netz. Diese Null an Abhängigkeiten ist auf diesem Blog eine ausgesprochene Position, im Sendepfad eine Betriebsgarantie: Ein Advisory kann keine Pipeline lahmlegen, die nichts zu patchen hat.

Der Teil, der für ein Unternehmen bleibt#

Ein Unternehmen mit Outbound-Bedarf mietet diese fünf Stufen heute bereits, pro Kontakt und Monat. Der SDR-Funnel hat eine feste Form: Zielprofil, gerankte Kandidaten, angereicherte Kontakte, personalisierte erste Nachricht, sortierte Replies. Ein Aufbau dieser Größenordnung beweist etwas Schmaleres: Der komplette Funnel läuft auf einer Maschine, in einer Datei, mit einer Score-Formel, die auf eine einzige Zeile passt.

Die Buchhaltung übersteht das Wachstum. Scores lassen sich aus gespeicherten Komponenten erklären. Das Ledger zeigt 137 validierte Statusübergänge hinter jeder Delivery. Die vollständige Fallstudie hält Architektur und Zahlen nebeneinander.

Der Test kostet nichts. Drei Fragen stehen in Woche drei jedes Outbound-Vorhabens im Raum: Was wurde gesendet, was kam zurück, was hat den Status gewechselt. Antworten, die ein Support-Ticket kosten, bedeuten ein Dashboard. Antworten, die eine Abfrage kosten, bedeuten eine Pipeline. Der Einstieg heißt Ingestion: Zeig sie auf eine öffentliche strukturierte Quelle, lass die lokale Datei wachsen und kalkuliere Stufe zwei, sobald die Daten dir gehören.