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¶
- 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.
- Den Scope Manage user data via APIs (
api) wählen. - 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.
- 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. - 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:

| 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.

Edit öffnet den Inline-Editor:

- 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:
POST <My-Domain-URL>/services/oauth2/tokenmitgrant_type=client_credentials(Formular-Body — Zugangsdaten stehen nie in einer URL). Das Token wird im Prozessspeicher gecacht und wiederverwendet; es wird nie persistiert.-
POST <My-Domain-URL>/services/data/v66.0/sobjects/ExpectoWebhookEvent__cmitAuthorization: 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" } -
Ein
201mit{"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:

- 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. |