Zum Inhalt

Salesforce

Schreiben Sie bei jedem Meilenstein einer Session einen Datensatz nach Salesforce — typischerweise das Ergebnis der automatischen Prüfungen, damit ein Vorgang in Salesforce zeigt, ob eine Lieferung bestanden hat, Warnungen hat oder fehlgeschlagen ist, ohne dass jemand Expecto öffnet.

Richtung: nur outbound. Expecto authentifiziert sich mit dem OAuth 2.0 Client-Credentials-Flow der Connected App Ihrer Org und erzeugt pro Ereignis einen sObject-Datensatz mit genau den Feldern, die Sie mappen.

Session-Ereignis ──▶ Outbound Binding ──▶ Salesforce-Connection ──▶ POST /services/data/vXX.X/sobjects/<Objekt>
                     (welche Ereignisse,    (My-Domain-URL,
                      welches Objekt,        Consumer Key/Secret,
                      Feld-Mapping)          API-Version)

1. Salesforce-Org vorbereiten

Alles in diesem Abschnitt geschieht im Salesforce-Setup und braucht einen Salesforce-Administrator. Expecto stellt über den API-Zugang hinaus keine Anforderungen an die Edition der Org.

Benutzerdefiniertes Objekt und Felder

Legen Sie das Objekt an (oder wählen Sie es), in das Expecto schreiben soll, z. B. ExpectoWebhookEvent__c, samt der Felder, die befüllt werden sollen. Ein typischer Satz:

Feld Typ Befüllt aus
SessionId__c Text (36) {{ session_id }}
RunId__c Text (36) {{ run_id }}
Period__c Text {{ period }}
Status__c Auswahlliste {{ verdict }} über eine Wertetabelle
Phase__c Auswahlliste {{ phase }} über eine Wertetabelle — technical (Parse-Ergebnis) oder content (fachliche Prüfung)
ProcessId__c Text / Lookup-Schlüssel {{ context.process_id }} — Ihre eigene Korrelations-ID, im Session-Kontext übergeben

Expecto sendet nur die gemappten Felder. Salesforce lehnt eine Anfrage ab, die ein Feld nennt, das das Objekt nicht hat (HTTP 400, INVALID_FIELD) — das Objekt muss also vor der ersten Zustellung mit jedem gemappten Feld existieren.

Eingeschränkte Auswahllisten

Ist Status__c eine eingeschränkte Auswahlliste, müssen ihre Werte zu dem passen, was Expecto sendet. Entweder nehmen Sie Expectos Verdict-Werte (passed, warnings, failed) in die Liste auf, oder Sie behalten Ihre eigenen Werte und übersetzen mit value_maps (Schritt 3). Ein Mismatch schickt die Zustellung mit INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST in den Dead-Letter.

Connected App mit Client-Credentials-Flow

  1. Setup → App Manager → New Connected App (oder External Client App Manager in neueren Orgs). OAuth-Einstellungen aktivieren; die Callback-URL ist beliebig — dieser Flow nutzt sie nicht.
  2. Den Scope Manage user data via APIs (api) wählen.
  3. Client Credentials Flow aktivieren und den Run As-Benutzer wählen. Alle Datensätze werden als dieser Benutzer erzeugt; geben Sie ihm ein Profil oder Berechtigungsset mit Create auf dem Objekt und Edit auf jedem gemappten Feld.
  4. Die Seite Manage Consumer Details der App öffnen und Consumer Key und Consumer Secret kopieren. Das sind OAuth-client_id / client_secret, die Expecto verwendet.
  5. Die My-Domain-URL der Org notieren (Setup → My Domain), z. B. https://acme.my.salesforce.com. Der Token-Endpunkt ist <My-Domain-URL>/services/oauth2/token — er muss aus dem Internet per https erreichbar sein.

2. Connection in Expecto anlegen

Settings → Connections → Add Connection, Typ Salesforce:

Dialog Add Connection mit ausgewähltem Typ Salesforce

Feld Wert
Name Beliebig, z. B. Salesforce – production
My Domain URL Die My-Domain-URL der Org, nur https://, ohne Pfad
API version (optional) Version der Salesforce-REST-API, Standard 66.0
Consumer Key / Consumer Secret Aus der Connected App. Verschlüsselt gespeichert, nie wieder angezeigt

Klicken Sie Test connection: Expecto fordert ein Token an und meldet das Ergebnis, ohne einen Datensatz zu erzeugen. auth_error bedeutet, dass das Paar Consumer Key/Secret abgelehnt wurde oder der Client-Credentials-Flow für die App nicht aktiv ist; connection_error, dass die My-Domain-URL nicht geantwortet hat.

Per MCP-Tool oder API ist dieselbe Connection:

POST /api/integration-connections
{
  "name": "Salesforce – production",
  "type": "salesforce",
  "base_url": "https://acme.my.salesforce.com",
  "config": { "api_version": "66.0" },
  "credentials": { "client_id": "<Consumer Key>", "client_secret": "<Consumer Secret>" }
}

3. Session-Ereignisse an das Objekt binden

Öffnen Sie das Projekt, Seite Context, Karte Outbound Bindings. Ein Projekt kann bis zu 20 Bindings halten; jedes nennt eine Connection, die Ereignisse, auf die es reagiert, und das Mapping.

Karte Outbound Bindings auf der Context-Seite des Projekts

Edit öffnet den Inline-Editor:

Editor für Outbound Bindings mit dem Salesforce-Feld-Mapping

  • Binding name — ^[A-Za-z][A-Za-z0-9_]{0,63}$, eindeutig im Projekt. Erscheint im Zustellungsprotokoll.
  • Connection — nur outbound-fähige Connections werden angeboten.
  • Event types — siehe welche Ereignisse was tragen.
  • Salesforce object — der API-Name des Objekts (ExpectoWebhookEvent__c).
  • Fields — je Zeile ein Salesforce-Feld: API-Name des Felds und eine Wertevorlage. Eine Vorlage ist ein Literal, ein Platzhalter oder Text mit eingebetteten Platzhaltern (FIBU {{ period }}). Haken Sie Required bei Feldern an, die nicht leer aufgelöst werden dürfen.
  • Active — abwählen, um das Binding zu pausieren, ohne es zu löschen.

Dasselbe Binding als JSON für PUT /api/projects/{id}/outbound-bindings oder das MCP-Tool set_project_outbound_bindings:

{
  "bindings": [{
    "name": "salesforce_check_verdict",
    "connection_id": "<connection uuid>",
    "events": ["session.parsed", "session.checked", "session.failed", "session.exported"],
    "mapping": {
      "sobject": "ExpectoWebhookEvent__c",
      "fields": {
        "Status__c":    "{{ verdict }}",
        "Phase__c":     "{{ phase }}",
        "SessionId__c": "{{ session_id }}",
        "RunId__c":     "{{ run_id }}",
        "Period__c":    "{{ period }}",
        "ProcessId__c": "{{ context.process_id }}"
      },
      "required_fields": ["SessionId__c", "ProcessId__c"],
      "value_maps": {
        "Status__c": { "passed": "Passed", "warnings": "Warnings", "failed": "Error", "*": "Error" },
        "Phase__c":  { "technical": "Technical", "content": "Content", "*": "Context" }
      }
    },
    "is_active": true
  }]
}

PUT ersetzt die gesamte Liste — senden Sie jedes Binding des Projekts, {"bindings": []} entfernt alle.

Das Mapping im Detail

Schlüssel Pflicht Bedeutung
sobject ja API-Name des Objekts: Buchstaben, Ziffern, Unterstriche, max. 80 Zeichen.
fields ja Feld-API-Name → Wertevorlage. Nicht leer. Genau diese Felder werden übertragen.
required_fields nein Teilmenge von fields. Löst eines davon zu null oder "" auf, geht die Zustellung vor dem Senden in den Dead-Letter und nennt das Feld.
value_maps nein Feld → Tabelle {gerenderter Wert: Ersatz}, angewendet nach dem Rendern und vor der Pflichtfeldprüfung. "*" ist der Fallback für nicht gelistete Werte (auch null). Schlüssel müssen gemappte Felder nennen.

Alles andere wird beim Speichern als unbekannter Schlüssel abgelehnt. Platzhalter werden ebenfalls beim Speichern geprüft: Jedes {{ context.<name> }} muss ein im Kontextschema des Projekts deklarierter Skalar sein, jedes Envelope-Feld einer der bekannten Namen.

verdict mappen, nicht health

verdict ist das Ergebnis der automatischen Prüfungen — passed, warnings, failed oder null, solange es keines gibt — und bleibt über spätere Stufen stabil. health ist der Dashboard-Chip (created, in_progress, blocked, attention, ready, completed); er mischt Fortschritt und Ergebnis und ändert sich, während die Session weiterläuft.

Zwei Phasen: erst das technische, dann das inhaltliche Ergebnis

Salesforce ruft POST /sessions/{id}/run meist wenige Sekunden nach dem Anlegen der Session auf, während Expecto die Kontextquellen des Projekts noch abruft. Das Parsen braucht diesen Kontext nicht und läuft sofort: innerhalb von Sekunden erhält das Binding das technische Ergebnis — session.parsed (phase = technical, parse_error_count / parse_warning_count) oder session.failed mit stage = parse (defekte Datei, fehlende Pflichtspalte, falsche Kodierung). Danach wartet der Run vor der Transformation auf den Abruf: Status waiting, waiting_for = context_fetch, dieselbe run_id. Auf Salesforce-Seite ist nichts zu wiederholen. Ist der Abruf abgeschlossen, läuft der Run von selbst weiter und das inhaltliche Ergebnis kommt als session.checked oder session.failed (phase = content). Schlägt der Abruf fehl oder dauert das Warten länger als das Server-Timeout (Standard 30 min), wird der Run als errors geschlossen und session.failed kommt mit stage = context_fetch, reason = context_fetch_failed oder context_fetch_timeout, phase = context und message mit Quelle und Fehler.

Mappen Sie {{ phase }} auf eine Auswahlliste (Phase__c oben), wenn Salesforce beide Ergebnisse getrennt führt; ein Binding, das nur das Endergebnis braucht, lässt session.parsed weg und ignoriert das Feld.

Welche Ereignisse abonnieren

Ereignis Feuert wenn run_id verdict
session.parsed Alle Dateien geparst — das technische Ergebnis (phase = technical, Parse-Zähler) gesetzt null / warnings
session.checked Geschäftsregeln fehlerfrei durchgelaufen (phase = content) gesetzt passed / warnings
session.failed Eine Pipeline-Stufe ist fehlgeschlagen (stage ist parse → phase = technical; transform, validation_gating, validate oder check → phase = content), oder der Lauf wurde nach dem Parsen am Kontext-Gate gestoppt (context_fetch, context_validation → phase = context); reason ist ein Code, message erklärt gesetzt failed; null bei einem Gate-Stopp — über den *-Standard abbilden
session.exported Das Paket wurde erzeugt — Stand beim Export
session.confirmed, session.approved Menschliche Freigabe — Stand zu dem Zeitpunkt
session.created, session.uploaded, session.transformed Übrige Pipeline-Stufen nur session.transformed null

run_id gibt es nur bei Pipeline-Lauf-Ereignissen

Ein Mapping, das {{ run_id }} bei einem Ereignis ohne Lauf verwendet (z. B. session.created), geht mit dem Variablennamen in den Dead-Letter. Abonnieren Sie das Binding nur auf Lauf-Ereignisse oder lassen Sie RunId__c aus dem Mapping.

Für den zweiphasigen Ablauf ist der praktische Satz session.parsed + session.checked + session.failed; nur für das Endergebnis session.checked + session.failed. Ergänzen Sie session.exported, wenn Salesforce auch erfahren soll, dass das Paket raus ist.

4. Was Expecto sendet

Je passendem Ereignis eine Zustellung:

  1. POST <My-Domain-URL>/services/oauth2/token mit grant_type=client_credentials (Formular-Body — Zugangsdaten stehen nie in einer URL). Das Token wird im Prozessspeicher gecacht und wiederverwendet; es wird nie persistiert.
  2. POST <My-Domain-URL>/services/data/v66.0/sobjects/ExpectoWebhookEvent__c mit Authorization: Bearer … und einem JSON-Body aus genau den gemappten Feldern nach den Wertetabellen:

    {
      "Status__c": "Passed",
      "SessionId__c": "7c3a76c0-6a00-44a5-a0ad-9093e6d219e1",
      "RunId__c": "0e1c1c3e-3a5c-4b39-9d24-6d4a3e6b9a1f",
      "Period__c": "2026-08",
      "ProcessId__c": "PRC-000123"
    }
    
  3. Ein 201 mit {"id": "a0B…"} markiert die Zustellung als succeeded; die Datensatz-ID wird zur Nachvollziehbarkeit an der Zustellung gespeichert.

Das Request-Timeout beträgt 15 s je Aufruf. Redirects werden nicht gefolgt.

Fehlerbehandlung

Zustellungen nutzen die gemeinsame Webhook-Retry-Maschinerie (Backoff von 30 s bis 1 h, 6 Versuche, dann Dead-Letter), Salesforce-Antworten werden aber zuerst klassifiziert:

Salesforce antwortet Expecto tut
5xx, 429 (API-Limit), Timeout, Netzwerkfehler, eine 200-Token-Antwort, die kein JSON ist Retry mit Backoff — dieselbe Anfrage kann später gelingen.
401 beim Datensatz-Aufruf Token einmal erneuern und im selben Versuch wiederholen; ein zweites 401 geht als auth_error in den Dead-Letter.
400 (unbekanntes Feld, ungültiger Wert, Auswahlliste), 403 (keine Berechtigung), 404 (falscher Objektname oder API-Version) Sofort Dead-Letter — ein Retry kann ohne Änderung der Anfrage nicht gelingen. Die ersten 2 KiB des Antwort-Bodys bleiben an der Zustellung.
Token-Endpunkt lehnt die Zugangsdaten ab Dead-Letter als auth_error; der Antwort-Body wird nicht gespeichert, weil Salesforce Anfrageparameter zurückspiegelt.

Nach 10 aufeinanderfolgenden Dead-Letters wird das Binding automatisch deaktiviert (disabled_reason: consecutive_failures) und erzeugt keine Zustellungen mehr; ein Erfolg setzt den Zähler zurück. Aktivieren Sie es nach Behebung der Ursache im Editor wieder — beim Speichern wird der Zähler zurückgesetzt.

5. Überwachen und wiederholen

Settings → Webhooks listet jedes Outbound Binding der Organisation neben den Webhook-Endpunkten, mit Status-Badge — Active, Inactive oder Auto-disabled mit der Fehlerzahl. Actions → Show deliveries öffnet das Protokoll:

Zustellungsprotokoll des Salesforce-Bindings mit erfolgreicher, fehlgeschlagener und ausstehender Zustellung

  • Result ist bei Erfolg die Salesforce-Datensatz-ID, bei Fehlschlag Fehlerklasse und -meldung (Hover zeigt den gespeicherten Antwortauszug), bei ausstehendem Versuch der Zeitpunkt des nächsten Retries.
  • Replay führt die Zustellung mit dem aktuellen Binding und der aktuellen Connection erneut über den Adapter aus — nutzen Sie es nach einer Korrektur des Mappings oder des Salesforce-Objekts. Das Replay ist eine neue Zustellung, verknüpft mit dem Original.

Programmatisch: GET /api/webhook-deliveries?connection_id=<uuid> und POST /api/webhook-deliveries/{id}/replay, oder die MCP-Tools list_webhook_deliveries / replay_webhook_delivery.

Troubleshooting

Symptom im Zustellungsprotokoll Ursache Abhilfe
delivery_rejected … INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST Der gerenderte Wert ist nicht in der Auswahlliste value_maps-Eintrag für das Feld ergänzen oder die Liste erweitern.
delivery_rejected … INVALID_FIELD / No such column Ein gemapptes Feld existiert nicht am Objekt Feld-API-Namen korrigieren (benutzerdefinierte Felder enden auf __c).
delivery_rejected: Salesforce rejected the record (HTTP 404) Falscher sobject-Name oder nicht unterstützte API-Version Objekt-API-Namen und api_version der Connection prüfen.
delivery_rejected: Salesforce rejected the record (HTTP 403) Dem Run-As-Benutzer fehlt Create am Objekt oder Edit an einem Feld Berechtigungsset des Benutzers anpassen.
auth_error: Token endpoint rejected the credentials Consumer Key/Secret falsch, Flow deaktiviert oder kein Run-As-Benutzer Connected App prüfen; Secret unter Edit rotieren.
delivery_rejected: Query placeholder 'run_id' has no value Das Binding ist auf ein Ereignis ohne Lauf abonniert Ereignisse auf session.checked / session.failed beschränken oder das Feld entfernen.
delivery_rejected: Required mapping field(s) resolved to an empty value: … Ein Pflichtfeld hat null gerendert — meist ein Kontextskalar, den die Session nicht gesetzt hat Wert im Session-Kontext liefern oder Required abwählen.
connection_inactive / binding_inactive Connection oder Binding wurde deaktiviert Wieder aktivieren; die Zustellung lässt sich dann per Replay wiederholen.
Binding zeigt Auto-disabled 10 aufeinanderfolgende Dead-Letters Ursache beheben, Binding reaktivieren, fehlgeschlagene Zustellungen replayen.