Skip to content
Startseite Guides Automation
Automation Intermediate

Lead-Qualifizierung mit KI automatisieren (kein manuelles Scoring)

Eingehende Anfragen automatisch bewerten: HubSpot-Daten, Claude für den Freitext, dokumentierte Scoring-Regeln und Routing an die richtige Person. Mit Code, Datenschutz und Grenzen.

Fabian Weiss, Gründer von FW Delta Fabian Weiss
10 min 2-3 Stunden
Das Problem

Manuelle Qualifizierung ist langsam und inkonsistent. Gute Anfragen warten, während Vertriebszeit an Anfragen geht, die nie zum Kunden werden.

Die Lösung

Ein Lead-Scoring-System aus HubSpot-Daten, einer Freitext-Bewertung durch Claude und dokumentierten Regeln, das Leads automatisch bewertet, zuweist und aus Deal-Ergebnissen lernt.

Warum manuelle Lead-Qualifizierung nicht skaliert

Jede Anfrage landet im CRM und jemand muss entscheiden: anrufen, in die Nurture-Strecke oder ignorieren. Solange das eine Person nach Gefühl macht, ist das Ergebnis subjektiv, hängt von der Tagesform ab und bricht zusammen, sobald mehr Anfragen kommen als Zeit da ist. Die Lösung ist keine Magie, sondern ein nachvollziehbarer Ablauf: Daten einsammeln, nach dokumentierten Regeln bewerten, zuweisen und die Regeln regelmäßig an den echten Deal-Ergebnissen prüfen.

Unser Automation-Service baut genau diesen Ablauf für B2B-Unternehmen. In diesem Guide bekommst du die Version, die du selbst umsetzen kannst: Google Apps Script als Laufzeit, HubSpot als CRM, Claude für die Bewertung des Freitexts in der Anfrage. Ob du das später in Apps Script, n8n oder Zapier betreibst, ist zweitrangig, unser Vergleich mit Zapier zeigt, wann welches Werkzeug passt.

Die Architektur

Drei Schichten, die du unabhängig voneinander austauschen kannst:

Daten-Schicht:

  • CRM-Daten aus HubSpot: E-Mail, Firma, Lifecycle-Phase, Lead-Status, Erstell- und Änderungsdatum
  • Der Freitext aus dem Kontaktformular (die Nachricht der Person)
  • Website-Verhalten, sofern du es pro Kontakt im CRM hast. HubSpot füllt mit installiertem Tracking-Code eigene Analytics-Eigenschaften wie Anzahl der Sitzungen und Zeitpunkt der letzten Sitzung

Bewertungs-Schicht:

  • Claude bewertet den Freitext: Passt die Anfrage zum Angebot? Ergebnis ist ein Fit-Wert von 0 bis 100 plus ein Satz Begründung
  • Ein gewichtetes Regelwerk kombiniert Fit-Wert, Firmografie, Verhalten und Lifecycle-Phase zu einem Score von 0 bis 100
  • Alle Gewichte stehen an einer Stelle im Code und in einer Tabelle in diesem Guide, damit das Vertriebsteam sie lesen kann

Aktions-Schicht:

  • Score ab 80: Zuweisung an die erfahrene Vertriebsperson, Lifecycle auf Sales Qualified Lead, Slack-Nachricht
  • Score 40 bis 79: Zuweisung an die zweite Vertriebsperson, Lifecycle auf Marketing Qualified Lead
  • Score unter 40: bleibt in der Nurture-Strecke, kein Owner
  • Disqualifikationsregeln fangen Testadressen und unpassende Anfragen vorher ab

Warum kein trainiertes Machine-Learning-Modell? Für die meisten Teams reichen die Daten nicht: ein paar hundert abgeschlossene Deals ergeben kein belastbares Modell, aber sie reichen völlig, um Gewichte zu prüfen. Regeln kann jede Person im Vertrieb lesen und anpassen. Der KI-Anteil sitzt dort, wo Regeln versagen: beim Verstehen von Freitext.

Wenn du lieber in n8n baust: Der HubSpot-Node deckt Kontakte lesen, suchen und aktualisieren ab, Claude rufst du über einen HTTP-Request-Node mit demselben Request-Body wie unten auf. Die Logik bleibt identisch.

Schritt 1: Zugang zu HubSpot einrichten

HubSpot hat im UI erstellte Private Apps auf Legacy-Status gesetzt. Bestehende Private Apps laufen weiter, für neue Integrationen empfiehlt HubSpot Service Keys, die du unter Development, Keys, Service Keys anlegst. Service Keys sind laut HubSpot noch in der öffentlichen Beta. Beide Varianten schickst du als Bearer-Token im Authorization-Header und beide sollten laut HubSpot alle sechs Monate rotiert werden. Die alte Authentifizierung per API-Key gibt es nicht mehr.

Scopes, die dieser Guide braucht:

ScopeWofür
crm.objects.contacts.read und crm.objects.contacts.writeKontakte lesen und Score, Owner, Lifecycle schreiben
crm.objects.owners.readOwner-IDs für die Zuweisung nachschlagen
crm.objects.deals.readDeal-Ergebnisse für den Feedback-Loop lesen
crm.schemas.contacts.writeDie Eigenschaft lead_score einmalig anlegen

Die Scopes stehen in der Contacts-API-Dokumentation, der Owners-API und der Properties-API.

Lege danach die Eigenschaft für den Score an. HubSpot hat mit hubspotscore eine eigene Score-Eigenschaft für das eingebaute Lead-Scoring, die schreibst du nicht per API. Eine eigene Zahl-Eigenschaft legst du mit einem POST auf /crm/v3/properties/contacts an:

{
  "groupName": "contactinformation",
  "name": "lead_score",
  "label": "Lead Score",
  "type": "number",
  "fieldType": "number"
}

Rate Limits: Laut HubSpot-Nutzungsrichtlinien gelten pro App 100 Requests je 10 Sekunden (Free und Starter) beziehungsweise 190 Requests je 10 Sekunden (Professional und Enterprise), pro Account 250.000, 625.000 oder 1.000.000 Requests am Tag. Bei Überschreitung antwortet HubSpot mit Status 429 und errorType: "RATE_LIMIT", die Header X-HubSpot-RateLimit-Remaining und X-HubSpot-RateLimit-Daily-Remaining zeigen dir den Stand. Die Such-API ist separat auf 5 Requests pro Sekunde je Account begrenzt.

Schritt 2: Kontakte nach Google Sheets synchronisieren

Google Sheets dient als Zwischenspeicher, weil du dort jeden Zwischenwert sehen kannst. Alle Geheimnisse liegen in den Script Properties des Apps-Script-Projekts: HUBSPOT_TOKEN, SHEET_ID, ANTHROPIC_API_KEY, SLACK_WEBHOOK, SENIOR_REP_EMAIL, JUNIOR_REP_EMAIL. Optional: MESSAGE_PROPERTY mit dem internen Namen der Eigenschaft, in der dein Formular die Nachricht speichert. Ebenfalls optional: SESSIONS_PROPERTY mit dem internen Namen der Sitzungs-Eigenschaft. Interne Namen liest du mit GET /crm/v3/properties/contacts aus der Properties-API.

Zuerst ein Helfer, der jeden HubSpot-Aufruf mit Fehlerbehandlung ausführt. muteHttpExceptions sorgt dafür, dass Apps Script bei Fehlerstatus nicht abbricht, sondern dir den Statuscode gibt. Bei 429 oder Serverfehlern wartet der Helfer die im Header Retry-After genannte Zeit oder zehn Sekunden und versucht es bis zu dreimal.

function fetchJson(url, options) {
  options.muteHttpExceptions = true;
  for (let attempt = 1; attempt <= 3; attempt++) {
    const response = UrlFetchApp.fetch(url, options);
    const code = response.getResponseCode();
    if (code === 429 || code >= 500) {
      const retryAfter = Number(response.getHeaders()['retry-after'] || response.getHeaders()['Retry-After'] || 10);
      Utilities.sleep(retryAfter * 1000);
      continue;
    }
    if (code >= 400) {
      throw new Error(options.method + ' ' + url + ' antwortete mit ' + code + ': ' + response.getContentText());
    }
    return code === 204 ? null : JSON.parse(response.getContentText());
  }
  throw new Error(url + ' schlug dreimal hintereinander fehl');
}

function hubspotRequest(path, method, payload) {
  const token = PropertiesService.getScriptProperties().getProperty('HUBSPOT_TOKEN');
  const options = {
    method: method,
    headers: { Authorization: 'Bearer ' + token },
    contentType: 'application/json'
  };
  if (payload) options.payload = JSON.stringify(payload);
  return fetchJson('https://api.hubapi.com' + path, options);
}

Dann die Synchronisation. Die Contacts-API liefert maximal 100 Kontakte pro Aufruf und gibt im Feld paging.next.after den Cursor für die nächste Seite zurück. Bestehende Zeilen werden anhand der Kontakt-ID aktualisiert, damit die Spalten mit KI-Bewertung und Routing-Status erhalten bleiben.

const BASE_PROPERTIES = ['email', 'firstname', 'lastname', 'company', 'lifecyclestage', 'hs_lead_status', 'createdate', 'lastmodifieddate'];

function syncHubSpotLeads() {
  const props = PropertiesService.getScriptProperties();
  const messageProperty = props.getProperty('MESSAGE_PROPERTY');
  const sessionsProperty = props.getProperty('SESSIONS_PROPERTY');
  const properties = BASE_PROPERTIES.concat([messageProperty, sessionsProperty].filter(Boolean)).join(',');
  const sheet = SpreadsheetApp.openById(props.getProperty('SHEET_ID')).getSheetByName('Leads');
  const rows = [];
  let after = '';
  do {
    const page = hubspotRequest('/crm/v3/objects/contacts?limit=100&properties=' + properties + (after ? '&after=' + after : ''), 'get');
    page.results.forEach(contact => {
      const p = contact.properties;
      rows.push([
        contact.id, p.email, p.firstname, p.lastname, p.company, p.lifecyclestage, p.hs_lead_status,
        p.createdate, p.lastmodifieddate,
        messageProperty ? (p[messageProperty] || '') : '',
        sessionsProperty ? (Number(p[sessionsProperty]) || 0) : 0
      ]);
    });
    after = page.paging && page.paging.next ? page.paging.next.after : '';
  } while (after);
  upsertRows(sheet, rows);
}

function upsertRows(sheet, rows) {
  const existing = sheet.getDataRange().getValues();
  const rowNumberById = {};
  for (let i = 1; i < existing.length; i++) rowNumberById[String(existing[i][0])] = i + 1;
  rows.forEach(row => {
    const rowNumber = rowNumberById[String(row[0])];
    if (rowNumber) sheet.getRange(rowNumber, 1, 1, row.length).setValues([row]);
    else sheet.appendRow(row);
  });
}

Die Spalten im Blatt Leads: A Kontakt-ID, B E-Mail, C Vorname, D Nachname, E Firma, F Lifecycle, G Lead-Status, H Erstellt, I Geändert, J Nachricht, K Sitzungen, L KI-Fit, M KI-Begründung, N Score, O Routing, P Disqualifikation.

Zwei Grenzen, bevor du das alle 15 Minuten laufen lässt: Apps Script erlaubt laut Quotas 20.000 URL-Fetch-Aufrufe am Tag für private Google-Konten und 100.000 für Google-Workspace-Konten. Jede Ausführung darf außerdem höchstens 6 Minuten dauern. Bei 10.000 Kontakten sind das 100 Aufrufe je Lauf, bei 96 Läufen am Tag also 9.600 Aufrufe nur für die Synchronisation. Ab dieser Größe filterst du besser über die Such-API nach hs_lastmodifieddate und holst nur geänderte Kontakte.

Website-Verhalten kommt bewusst aus HubSpot und nicht aus GA4: Die GA4 Data API kennt keine Dimension für E-Mail-Adressen und die Google-Analytics-Richtlinien verbieten das Senden personenbezogener Daten wie E-Mail-Adressen an Google Analytics. Brauchst du Verhaltensdaten pro Kontakt ohne HubSpot-Tracking, dann schreibt dein eigenes Tracking sie in eine CRM-Eigenschaft. Unsere Tracking-Implementierung deckt genau diese Events ab.

Schritt 3: Freitext mit Claude bewerten

Regeln erkennen keine Absicht. Ob “Wir brauchen bis Q4 ein Tracking-Setup für drei Shops” oder “Ich schreibe eine Bachelorarbeit und hätte Fragen” hinter einer Anfrage steckt, sieht ein Sprachmodell sofort. Dafür geht nur die Nachricht selbst an die Claude Messages API, keine Namen, keine E-Mail-Adressen. Die Antwort kommt über Structured Outputs als garantiert gültiges JSON zurück, ein Beta-Header ist dafür nicht mehr nötig.

Als Modell nutzt der Guide claude-opus-5-5, laut Modellübersicht die Empfehlung für die meisten Aufgaben. Die aktuellen IDs sind claude-fable-5-1, claude-opus-5-5, claude-sonnet-5 und claude-haiku-4-5-20251001, ältere IDs mit Datumssuffix wie claude-3-5-sonnet-20241022 gehören nicht mehr in neuen Code. Laut Preisliste kostet Claude Opus 5.5 4 US-Dollar je Million Eingabe-Token und 20 US-Dollar je Million Ausgabe-Token, Claude Haiku 4.5 als günstigste Option 1 und 5 US-Dollar. Eine Anfrage mit ein paar hundert Wörtern liegt damit im Bruchteil eines Cents.

function qualifyMessagesWithClaude() {
  const props = PropertiesService.getScriptProperties();
  const sheet = SpreadsheetApp.openById(props.getProperty('SHEET_ID')).getSheetByName('Leads');
  const data = sheet.getDataRange().getValues();
  for (let i = 1; i < data.length; i++) {
    const message = String(data[i][9] || '').trim();
    if (!message || data[i][11] !== '') continue;
    const result = classifyEnquiry(message);
    sheet.getRange(i + 1, 12, 1, 2).setValues([[result.fit, result.reason]]);
  }
}

function classifyEnquiry(message) {
  const payload = {
    model: 'claude-opus-5-5',
    max_tokens: 1024,
    system: 'Du bewertest eingehende Anfragen an eine Agentur für Tracking, Dashboards und Workflow-Automatisierung. Gib fit als ganze Zahl von 0 bis 100 an: 100 bedeutet ein konkretes Projekt, das genau zum Angebot passt. Studienanfragen, Bewerbungen, Verkaufsangebote an die Agentur und Spam bekommen 0. Begründe in einem Satz.',
    messages: [{ role: 'user', content: message }],
    output_config: {
      format: {
        type: 'json_schema',
        schema: {
          type: 'object',
          properties: {
            fit: { type: 'integer' },
            reason: { type: 'string' }
          },
          required: ['fit', 'reason'],
          additionalProperties: false
        }
      }
    }
  };
  const body = fetchJson('https://api.anthropic.com/v1/messages', {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'x-api-key': PropertiesService.getScriptProperties().getProperty('ANTHROPIC_API_KEY'),
      'anthropic-version': '2023-06-01'
    },
    payload: JSON.stringify(payload)
  });
  if (body.stop_reason !== 'end_turn') {
    return { fit: 0, reason: 'Keine Bewertung, stop_reason: ' + body.stop_reason };
  }
  const text = body.content.find(block => block.type === 'text').text;
  return JSON.parse(text);
}

Der Helfer fetchJson aus Schritt 2 behandelt auch hier 429-Antworten: Die Claude API schickt laut Rate-Limits-Dokumentation bei Überschreitung einen retry-after-Header mit der Wartezeit in Sekunden. Der Code prüft außerdem stop_reason, weil eine Antwort mit max_tokens oder refusal keinen verwertbaren Text enthält.

Schritt 4: Das Scoring-Regelwerk

Hier liegen die Regeln und nirgendwo sonst. Wenn das Vertriebsteam fragt, warum ein Lead 72 Punkte hat, ist die Antwort diese Tabelle:

SignalPunkteQuelle
Firma angegeben+10HubSpot company
Geschäftliche E-Mail-Domain+15, private Domain -15HubSpot email
Mehr als 10 Sitzungen+20, mehr als 3 Sitzungen +10Sitzungs-Eigenschaft
KI-Fit der Anfragebis +35 (Fit-Wert mal 0,35)Claude
Lifecycle Marketing Qualified Lead+20, Lead +10HubSpot lifecyclestage

Maximal sind 100 Punkte möglich, negative Werte werden auf 0 begrenzt.

function calculateLeadScores() {
  const props = PropertiesService.getScriptProperties();
  const sheet = SpreadsheetApp.openById(props.getProperty('SHEET_ID')).getSheetByName('Leads');
  const data = sheet.getDataRange().getValues();
  const scores = [];
  for (let i = 1; i < data.length; i++) {
    const lead = {
      email: String(data[i][1] || ''),
      company: String(data[i][4] || ''),
      lifecycleStage: data[i][5],
      sessions: Number(data[i][10]) || 0,
      aiFit: Number(data[i][11]) || 0
    };
    let score = 0;
    if (lead.company.length > 0) score += 10;
    if (/@(gmail|googlemail|yahoo|hotmail|outlook|gmx|web|t-online)\./i.test(lead.email)) score -= 15;
    else score += 15;
    if (lead.sessions > 10) score += 20;
    else if (lead.sessions > 3) score += 10;
    score += Math.round(lead.aiFit * 0.35);
    if (lead.lifecycleStage === 'marketingqualifiedlead') score += 20;
    else if (lead.lifecycleStage === 'lead') score += 10;
    scores.push([Math.max(0, Math.min(score, 100))]);
  }
  if (scores.length) sheet.getRange(2, 14, scores.length, 1).setValues(scores);
}

Gewichte prüfen statt raten: Exportiere vergangene Leads mit ihrem Ergebnis (gewonnen oder verloren) und rechne den Score nach. Wenn gewonnene Deals im Schnitt nicht deutlich höher liegen als verlorene, stimmen die Gewichte nicht. Schritt 6 automatisiert genau diese Prüfung.

Schritt 5: Routing in HubSpot

Die Owner-ID holst du aus der Owners-API, das Feld id aus der Antwort ist der Wert für hubspot_owner_id. Wichtig für die Lifecycle-Phase: Laut Contacts-API kannst du lifecyclestage per API nur vorwärts setzen. Ein Kontakt, der schon Sales Qualified Lead ist, wird also nicht zurück auf Marketing Qualified Lead gesetzt, der Code prüft das vorher. Die Standardwerte sind subscriber, lead, marketingqualifiedlead, salesqualifiedlead, opportunity, customer und evangelist.

const STAGE_ORDER = ['subscriber', 'lead', 'marketingqualifiedlead', 'salesqualifiedlead', 'opportunity', 'customer', 'evangelist'];

function movesForward(current, target) {
  return STAGE_ORDER.indexOf(target) > STAGE_ORDER.indexOf(current || '');
}

function routeQualifiedLeads() {
  const props = PropertiesService.getScriptProperties();
  const sheet = SpreadsheetApp.openById(props.getProperty('SHEET_ID')).getSheetByName('Leads');
  const data = sheet.getDataRange().getValues();
  const ownerIdByEmail = getOwnerIdsByEmail();
  for (let i = 1; i < data.length; i++) {
    if (data[i][14] || data[i][15]) continue;
    const contactId = data[i][0];
    const score = Number(data[i][13]);
    const update = { lead_score: score };
    let targetStage = null;
    if (score >= 80) {
      update.hubspot_owner_id = ownerIdByEmail[props.getProperty('SENIOR_REP_EMAIL')];
      targetStage = 'salesqualifiedlead';
    } else if (score >= 40) {
      update.hubspot_owner_id = ownerIdByEmail[props.getProperty('JUNIOR_REP_EMAIL')];
      targetStage = 'marketingqualifiedlead';
    }
    if (targetStage && movesForward(data[i][5], targetStage)) update.lifecyclestage = targetStage;
    hubspotRequest('/crm/v3/objects/contacts/' + contactId, 'patch', { properties: update });
    if (score >= 80) sendSlackAlert(data[i][1], score);
    sheet.getRange(i + 1, 15).setValue(score >= 40 ? 'Zugewiesen' : 'Nurture');
  }
}

function getOwnerIdsByEmail() {
  const ownerIdByEmail = {};
  let after = '';
  do {
    const page = hubspotRequest('/crm/v3/owners' + (after ? '?after=' + after : ''), 'get');
    page.results.forEach(owner => { ownerIdByEmail[owner.email] = owner.id; });
    after = page.paging && page.paging.next ? page.paging.next.after : '';
  } while (after);
  return ownerIdByEmail;
}

function sendSlackAlert(email, score) {
  const webhook = PropertiesService.getScriptProperties().getProperty('SLACK_WEBHOOK');
  const message = {
    text: 'Heißer Lead: ' + email + ' mit Score ' + score + '/100',
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: '*Heißer Lead*\nE-Mail: ' + email + '\nScore: ' + score + '/100\n_Automatisch zugewiesen_'
        }
      }
    ]
  };
  UrlFetchApp.fetch(webhook, {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(message)
  });
}

Die Slack-Nachricht folgt dem Format für Incoming Webhooks: POST mit JSON, das Feld text dient als Fallback für Benachrichtigungen, blocks für die Darstellung.

Für viele Kontakte auf einmal nutzt du statt einzelner PATCH-Aufrufe den Batch-Endpunkt POST /crm/v3/objects/contacts/batch/update mit bis zu 100 Datensätzen pro Aufruf. Das schont das Rate Limit und die Apps-Script-Quota.

Schritt 6: Feedback-Loop mit Deal-Ergebnissen

Ob die Gewichte stimmen, zeigen nur abgeschlossene Deals. Welche Deal-Phase “gewonnen” bedeutet, steht nicht fest im Code, sondern kommt aus der Pipelines-API: Jede Phase trägt in metadata die Felder isClosed und probability, gewonnene Phasen sind geschlossen mit Wahrscheinlichkeit 1. Die Deals-API liefert mit dem Parameter associations=contacts die IDs der verknüpften Kontakte.

function reviewScoringAccuracy() {
  const props = PropertiesService.getScriptProperties();
  const sheet = SpreadsheetApp.openById(props.getProperty('SHEET_ID')).getSheetByName('Leads');
  const data = sheet.getDataRange().getValues();
  const scoreByContactId = {};
  for (let i = 1; i < data.length; i++) scoreByContactId[String(data[i][0])] = Number(data[i][13]);
  const closedStages = getClosedStages();
  let correct = 0;
  let total = 0;
  let after = '';
  do {
    const page = hubspotRequest('/crm/v3/objects/deals?limit=100&properties=dealstage&associations=contacts' + (after ? '&after=' + after : ''), 'get');
    page.results.forEach(deal => {
      const won = closedStages[deal.properties.dealstage];
      if (won === undefined) return;
      const contacts = deal.associations && deal.associations.contacts ? deal.associations.contacts.results : [];
      contacts.forEach(contact => {
        const score = scoreByContactId[String(contact.id)];
        if (score === undefined) return;
        if ((score >= 60 && won) || (score < 60 && !won)) correct++;
        total++;
      });
    });
    after = page.paging && page.paging.next ? page.paging.next.after : '';
  } while (after);
  const accuracy = total ? Math.round((correct / total) * 100) : 0;
  Logger.log('Trefferquote ' + accuracy + '% bei ' + total + ' Kontakten mit abgeschlossenem Deal');
  if (total >= 30 && accuracy < 70) sendSlackAlert('Scoring-Trefferquote unter 70%, Gewichte prüfen', accuracy);
}

function getClosedStages() {
  const closedStages = {};
  const pipelines = hubspotRequest('/crm/v3/pipelines/deals', 'get');
  pipelines.results.forEach(pipeline => {
    pipeline.stages.forEach(stage => {
      if (String(stage.metadata.isClosed) === 'true') {
        closedStages[stage.id] = Number(stage.metadata.probability) === 1;
      }
    });
  });
  return closedStages;
}

Die Schwelle von 30 Kontakten verhindert Alarm bei zu kleiner Stichprobe. Sinkt die Trefferquote, passt du die Tabelle in Schritt 4 an, nicht das Sprachmodell.

Schritt 7: Disqualifikationsregeln

Manche Kontakte sollen gar nicht erst im Routing landen. Die Regeln laufen vor dem Scoring und schreiben den Grund in Spalte P, damit nachvollziehbar bleibt, warum ein Kontakt nie zugewiesen wurde.

function applyDisqualificationRules() {
  const props = PropertiesService.getScriptProperties();
  const sheet = SpreadsheetApp.openById(props.getProperty('SHEET_ID')).getSheetByName('Leads');
  const data = sheet.getDataRange().getValues();
  const rules = [
    { test: lead => /^test@|@example\./i.test(lead.email), reason: 'Testadresse' },
    { test: lead => lead.aiFit === 0, reason: 'Anfrage passt nicht zum Angebot' },
    { test: lead => lead.sessions === 0 && lead.ageInDays > 30, reason: 'Seit 30 Tagen inaktiv' }
  ];
  for (let i = 1; i < data.length; i++) {
    if (data[i][15]) continue;
    const lead = {
      email: String(data[i][1] || ''),
      aiFit: data[i][11] === '' ? null : Number(data[i][11]),
      sessions: Number(data[i][10]) || 0,
      ageInDays: (Date.now() - new Date(data[i][7]).getTime()) / 86400000
    };
    const hit = rules.find(rule => rule.test(lead));
    if (!hit) continue;
    sheet.getRange(i + 1, 16).setValue('Disqualifiziert: ' + hit.reason);
    hubspotRequest('/crm/v3/objects/contacts/' + data[i][0], 'patch', { properties: { lead_score: 0 } });
  }
}

Zum Schluss die Trigger. Laut Apps-Script-Referenz akzeptiert everyMinutes nur die Werte 1, 5, 10, 15 und 30.

function runPipeline() {
  syncHubSpotLeads();
  qualifyMessagesWithClaude();
  applyDisqualificationRules();
  calculateLeadScores();
  routeQualifiedLeads();
}

function createTriggers() {
  ScriptApp.newTrigger('runPipeline').timeBased().everyMinutes(15).create();
  ScriptApp.newTrigger('reviewScoringAccuracy').timeBased().onWeekDay(ScriptApp.WeekDay.MONDAY).atHour(7).create();
}

Datenschutz und Einwilligung

Sobald Anfragen an einen KI-Dienst gehen, verarbeitest du personenbezogene Daten bei einem Auftragsverarbeiter. Vier Dinge müssen vor dem Go-Live stehen:

  1. Datenminimierung: Der Code oben sendet nur den Nachrichtentext. Name, E-Mail, Firma und HubSpot-ID bleiben im CRM. Wenn Personen in der Nachricht selbst Kontaktdaten nennen, ist das Teil der Verarbeitung, nicht mehr.
  2. Auftragsverarbeitungsvertrag: Anthropic stellt sich in seinem Data Processing Addendum als Auftragsverarbeiter (Processor) des Kunden dar und bindet die EU-Standardvertragsklauseln ein. Das DPA ist laut Commercial Terms Teil der Nutzungsbedingungen, die außerdem festhalten, dass Anthropic nicht mit Kundeninhalten aus der API trainiert.
  3. Speicherdauer: Laut Anthropic Privacy Center werden API-Eingaben und -Ausgaben standardmäßig innerhalb von 30 Tagen gelöscht, eine Zero-Data-Retention-Vereinbarung ist möglich. Trag das in dein Verarbeitungsverzeichnis ein.
  4. Information: Deine Datenschutzerklärung muss die automatisierte Bewertung und den Dienstleister nennen. Für Kontakte, die per Formular anfragen, ist die Verarbeitung zur Bearbeitung der Anfrage in der Regel von Art. 6 Abs. 1 lit. b oder f DSGVO gedeckt, die Einschätzung dafür trifft deine Datenschutzberatung, nicht dieser Guide.

Dasselbe gilt für HubSpot und Google als Auftragsverarbeiter: Beide brauchst du im Verarbeitungsverzeichnis, beide haben Standard-AV-Verträge.

Grenzen und Fehlerquellen

Veraltete Daten: Ein Score ist so gut wie die letzte Synchronisation. Wenn die Quota keinen 15-Minuten-Takt erlaubt, filtere über die Such-API nach Änderungsdatum statt seltener zu synchronisieren.

Lifecycle nur vorwärts: HubSpot lässt lifecyclestage per API nur vorwärts setzen. Ein Zurückstufen erfordert erst das Leeren des Werts. Der Code umgeht das, indem er nur vorwärts setzt.

Firmografie überbewertet: Die Firmengröße sagt wenig darüber, ob jemand jetzt kaufen will. Deshalb stecken 35 der 100 Punkte im Inhalt der Anfrage und 20 im Verhalten. Prüfe die Gewichte mit Schritt 6 statt nach Bauchgefühl.

Sprachmodell als Black Box: Claude liefert eine Begründung in einem Satz. Speichere sie (Spalte M) und zeige sie im CRM, sonst vertraut das Team dem Score nicht.

Laufzeit und Quota: 6 Minuten pro Ausführung und 20.000 URL-Fetch-Aufrufe am Tag für private Google-Konten sind schnell erreicht. Ab einigen tausend Kontakten gehört der Ablauf in n8n oder einen eigenen Dienst.

Checkliste vor dem Go-Live

  1. Mit historischen Daten testen: Scoring auf 100 vergangene Leads laufen lassen und die Trefferquote aus Schritt 6 anschauen
  2. Monitoring: Die wöchentliche Prüfung läuft und der Slack-Kanal ist besetzt
  3. Override: Das Vertriebsteam darf lead_score im CRM manuell überschreiben, das Routing respektiert Spalte O und fasst zugewiesene Kontakte nicht erneut an
  4. Regeln dokumentiert: Die Tabelle aus Schritt 4 liegt dort, wo das Team sie findet
  5. Parallelbetrieb: Zwei Wochen KI-Scoring neben der manuellen Einschätzung laufen lassen und Abweichungen besprechen
  6. Datenschutz: AV-Verträge, Verarbeitungsverzeichnis und Datenschutzerklärung sind aktualisiert

Professionelle Hilfe benötigt?

Unser KI-Automation-Service baut diesen Ablauf auf deinen Daten:

  • Scoring-Regelwerk: Gewichte aus deinen historischen Deals abgeleitet und mit dem Vertriebsteam abgestimmt
  • CRM-Integration: HubSpot, Salesforce, Pipedrive oder ein eigenes CRM
  • Verhaltens-Tracking: GA4, Server-side Tracking oder eigene Events, die als Eigenschaften im CRM landen
  • Betrieb: Monitoring, wöchentliche Prüfung der Trefferquote und Anpassung der Gewichte
  • Datenschutz: Datenflüsse dokumentiert, Verträge geprüft, Datenschutzerklärung ergänzt

Buche ein kostenloses 30-minütiges Gespräch zur Prüfung deines aktuellen Lead-Qualifikationsprozesses: Hier Termin vereinbaren

Verwandte Services

Newsletter

Research für technische Entscheidungen

Neue Reports, Benchmarks und technische Analysen zu SaaS-Ökonomie, AI Engineering und eigener Infrastruktur.

Original Research Öffentliche Quellen Keine Sales-Mails

Mit der Anmeldung erhältst du neue Analysen und Updates von FW Delta per E-Mail. Du kannst deine Einwilligung jederzeit widerrufen. Weitere Informationen in der Datenschutzerklärung.