n8n trifft SAP — die Brücke über OData und HTTP
Artikel 10 · Serie: Einstieg in n8n
Am Ende von Artikel 9 ist die Klassifikationsstufe resilient: zwei AI-Backends, Round-Robin, Failover, regelbasierter Notausgang. Was bisher fehlt, ist der Schritt aus n8n heraus in ein Fremdsystem. Dieser Artikel baut ihn, am naheliegendsten Ziel für eine Support-Pipeline: SAP. Das klassifizierte Ticket wird mit Kunden-Stammdaten aus einem SAP-Backend angereichert, über OData und HTTP, mit demselben Prinzip der graceful degradation, das die AI-Seite schon trägt.
Ein Hinweis vorweg, weil er die ganze Stufe einordnet. Ich binde hier kein produktives S/4 an, sondern die öffentliche SAP Business Accelerator Hub Sandbox, ein geteiltes S/4HANA-Cloud-Backend mit Demodaten, erreichbar mit einem kostenlosen SAP-ID-Account. Die Auth-Mechanik ist hier ein einzelner Header. Den echten OAuth-Flow gegen einen xsuaa-Token-Endpunkt hebe ich mir für Artikel 13 auf.
Der Code zu diesem Artikel liegt auf Codeberg, Tag v0.10: codeberg.org/rotecodefraktion/n8n-einstieg.
OData ist nicht REST
Der Endpunkt ist der OData-Service API_BUSINESS_PARTNER der Hub-Sandbox. Ein erster Aufruf zeigt sofort, dass SAP-OData von dem abweicht, was man von REST-APIs gewohnt ist:
curl -H "APIKey: <key>" -H "Accept: application/json" \
"https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner?\$top=2&\$select=BusinessPartner,BusinessPartnerFullName"
{"d":{"results":[
{"BusinessPartner":"11","BusinessPartnerFullName":"Cust15 Cust15"},
{"BusinessPartner":"202","BusinessPartnerFullName":"Nue tech inc"}
]}}
Drei Dinge fallen auf. Erstens das Protokoll: Die Sandbox bedient OData V2, nicht V4. Die Nutzdaten stecken in einem d-Wrapper, Kollektionen zusätzlich in einem results-Array, jede Entität trägt einen __metadata-Block. Wer den Response wie eine flache REST-Antwort behandelt, greift ins Leere. Zweitens die Auth: ein statischer APIKey-Header, den man auf api.sap.com am jeweiligen Service über „Show API Key" zieht. Drittens die Abfragesprache: $select für die Feldauswahl, $filter für Bedingungen, $inlinecount=allpages für die Gesamtzahl. Letzteres liefert im Feld d.__count die Treffermenge, hier 1262 Business Partner, der Einstieg in jede Pagination.
Der wichtigste Unterschied zeigt sich aber im Fehlerfall. Eine unbekannte Entität beantwortet SAP nicht mit einem RFC-Problem-Dokument, sondern mit einem eigenen Gateway-Envelope:
{"error":{"code":"/IWBEP/CM_MGW_RT/020",
"message":{"lang":"en","value":"Resource not found for segment 'A_BusinessPartnerType'"},
"innererror":{"transactionid":"AB004E5DA0010B6C2F605A449D13608D",
"timestamp":"20260612...","Error_Resolution":{"SAP_Note":"See SAP Note 1797736 ..."}}}}
Diese transactionid ist kein Zierrat. Sie ist der Schlüssel, mit dem ein SAP-Administrator den Fehler in /IWFND/ERROR_LOG auf dem Gateway wiederfindet. Genau deshalb will man sie später im eigenen Log haben, nicht nur ein generisches „Request failed".
Die Brücke als Sub-Workflow
Die Anbindung selbst ist ein eigener Sub-Workflow, v0.10 SAP OData Bridge, den der Dispatcher aus Artikel 9 am Anreicherungs-Schritt aufruft. Diese Kapselung ist dieselbe wie bei den Classifiern: ein klar abgegrenztes Stück Logik, isoliert testbar, einzeln exportierbar.

Das eingehende Ticket trägt eine customerId, eine BusinessPartner-ID. Der Kern der Brücke ist ein HTTP-Request-Node, der die Entität direkt über ihren Schlüssel adressiert:
GET A_BusinessPartner('{{ $json.customerId }}')
?$select=BusinessPartner,BusinessPartnerFullName,BusinessPartnerCategory,BusinessPartnerGrouping
&$format=json
Den APIKey-Header liefert ein Header-Auth-Credential, damit der Schlüssel nicht im Node klebt und nicht in den Export wandert. Entscheidend ist die Einstellung On Error: Continue (using error output). Damit bekommt der Node zwei Ausgänge, und ein unbekannter Kunde reißt die Pipeline nicht ab:
- Erfolg führt in einen Code-Node
Enrich Ticket, der die Stammdaten ins Ticket mischt undcustomerLookup: "ok"setzt. - Fehler führt in
Mark Customer Unknown, dercustomerLookup: "unknown"setzt und das Ticket trotzdem weiterlaufen lässt.
Das ist dasselbe Muster wie das AI-Failover aus Artikel 9: Ein Ausfall verschlechtert die Qualität, er verwirft das Ticket nicht. Ein Ticket ohne Kundenzuordnung ist immer noch ein geroutetes Ticket.

Beim Bau fiel eine Eigenheit von n8n auf, die man kennen muss. Die Referenz auf den Trigger über $('Ticket Input').item bricht im Fehlerzweig: Über den Error-Ausgang eines Nodes geht die paired-item-Verknüpfung verloren, mit der n8n Items über Knoten hinweg paart. Im Erfolgszweig hält sie, im Fehlerzweig nicht. Die Lösung ist $('Ticket Input').first() statt .item, eindeutig, weil der Sub-Workflow pro Lauf genau ein Ticket verarbeitet. Dieselbe Umstellung war auch eine Ebene höher nötig, im Enrich-Node des Dispatchers, der customerId und id aus dem Webhook zieht: Über die Execute-Workflow-Kaskade hinweg löst .item unzuverlässig auf, .first() deterministisch.
Den SAP-Fehler zurückgewinnen
Der interessanteste Teil ist der Fehlerpfad, und zwar wegen eines Stolpersteins, den man erst im Log bemerkt. Stellt man den Marker naiv zusammen und liest den SAP-Code aus dem Fehlerobjekt, steht da nicht der SAP-Code, sondern ERR_BAD_REQUEST. n8n reicht im Error-Ausgang den transportnahen Axios-Fehler durch, nicht den SAP-Envelope.
Der Envelope ist trotzdem da, nur vergraben. Er steckt in $json.error.message als status-präfixierter, doppelt JSON-kodierter String:
404 - "{\"error\":{\"code\":\"/IWBEP/CM_MGW_RT/020\", ... }}"
Um den SAP-Code, die Meldung und die transactionid herauszuholen, schneide ich das 404 - ab und parse den Rest zweimal, einmal den JSON-String, dann das Envelope:
function parseSapError(errObj) {
const out = { httpStatus: null, sapCode: null, sapMessage: null, transactionId: null };
if (!errObj) return out;
const msg = typeof errObj === 'string' ? errObj : (errObj.message ?? '');
const mStatus = msg.match(/(\d{3})/);
if (mStatus) out.httpStatus = Number(mStatus[1]);
const dash = msg.indexOf(' - ');
let env = null;
if (dash >= 0) {
const payload = msg.slice(dash + 3).trim();
try {
const body = payload.startsWith('"') ? JSON.parse(payload) : payload;
env = JSON.parse(body);
} catch (e) { env = null; }
}
if (env && env.error) {
out.sapCode = env.error.code ?? null;
out.sapMessage = env.error.message?.value ?? null;
out.transactionId = env.error.innererror?.transactionid ?? null;
}
return out;
}
Damit landet im Log, was die Fehleranalyse wirklich braucht:
{"marker":"n8n-sap-lookup-failed","ticketId":"SAP-ERR4","customerId":"99999999",
"httpStatus":404,"sapCode":"/IWBEP/CM_MGW_RT/020",
"sapMessage":"Resource not found for segment 'A_BusinessPartnerType'",
"transactionId":"AB004E5DA0010B6C2F605A449D13608D"}
Derselbe sapCode wird zusätzlich als customerLookupError ans Ticket geschrieben. Wer das überspringt und sich mit ERR_BAD_REQUEST zufriedengibt, verschenkt genau den Faden, an dem man im SAP-Backend den Fehler aufdröselt.
Zum Testen: Beim Bau dieser Stufe griffen zwei bereits dokumentierte Stolpersteine. Das Duplizieren des Dispatchers erzeugte denselben Zufalls-Webhook-Pfad und die Doppel-Aktiv-Kollision wie in Artikel 9. Und eine Änderung am Sub-Workflow wirkt im Produktivlauf erst nach einem expliziten Publish, dem n8n-2.0-Modell aus Artikel 8.
n8n und AIF gehören nicht in denselben Topf
Eine Verwechslung lohnt es auszuräumen, weil sie in SAP-Kontexten häufig ist. n8n ersetzt nicht das Application Interface Framework. AIF überwacht Nachrichten innerhalb der SAP-Welt, bietet fachliche Recovery und ein Compliance-taugliches Fehler-Monitoring. n8n orchestriert davor und dahinter: Es nimmt das Ticket entgegen, klassifiziert, reichert über OData an, routet. Trifft ein OData-Aufruf eine Schnittstelle, die AIF absichert, dann gehört der Wiederanlauf innerhalb von SAP zu AIF, der systemübergreifende Fluss zu n8n. Das sind komplementäre Schichten, keine konkurrierenden. Wer n8n als AIF-Ersatz positioniert, stellt eine Orchestrierungs- gegen eine Recovery-Ebene, die verschiedene Aufgaben haben.
Workflows in Git versionieren
Ein Workflow, der ein Fremdsystem anbindet, gehört versioniert, nicht nur im n8n-Editor. Dafür liegt im Repo scripts/export-workflows.sh: Es zieht alle Workflows über die n8n-REST-API, behält nur die portablen Felder und reicht sie durch strip-workflow.py, das instanzspezifische Daten entfernt, Credential-IDs, webhookId, pinData, versionId.
N8N_API_KEY=... ./scripts/export-workflows.sh
git diff workflows/
Credentials werden dabei nur über ihren Namen referenziert. Beim Import bindet n8n sie an das lokale Credential gleichen Namens. So liegt der Workflow nachvollziehbar in Git, und kein Secret, weder der API-Key noch ein Token, wandert je ins Repository.
Joule Studio als Ausblick, nicht als Versprechen
SAP hat für 2026 angekündigt, n8n enger in die eigene Tooling-Welt zu integrieren, unter dem Stichwort Joule Studio. Wie das am Ende aussieht und ab wann es stabil verfügbar ist, steht zum Zeitpunkt dieses Artikels nicht fest. Der Weg, den ich hier zeige, ist der stabile: HTTP und OData sind seit Jahren da und ändern sich nicht über Nacht. Ein eingebettetes Joule Studio kann das später ergänzen, ersetzen muss man den OData-Weg dafür nicht.
Übergang zu Artikel 11
Die Pipeline spricht jetzt SAP, klassifiziert resilient und reichert an. Was noch fehlt, ist der Schritt vom funktionierenden Demo-Aufbau zum produktiven Betrieb. Im aktuellen Setup läuft n8n hinter einem Reverse-Proxy, der App-Port ist nicht nach außen offen, der Webhook erreichbar nur über HTTPS, und Ausführungen laufen im Hauptprozess. Für echten Durchsatz und Ausfallsicherheit braucht es mehr. Artikel 11 nimmt sich den Queue-Mode vor, die Trennung von Editor, Webhook-Annahme und Worker-Prozessen, und eine Produktions-Checkliste.
Modell-Backends starten
Der Brücken-Workflow liest die Ticket-Kategorie aus dem Classifier mit, deshalb müssen die Modell-Backends laufen: Ollama auf :11434 und der Hummingbird-MLX-Gateway auf :8080, beide über host.docker.internal aus dem n8n-Container erreichbar.
# Ollama
ollama serve
# Hummingbird-MLX-Gateway (siehe Artikel 6)
./hummingbird-mlx serve --port 8080
Wer den MLX-Gateway nicht betreibt, kommt mit einem einzelnen Ollama-Backend durch, die Round-Robin-Stufe aus Artikel 9 fällt dann auf ein Backend zurück. Die SAP-Brücke selbst braucht keines der Modelle, nur einen APIKey für die Hub-Sandbox.