← Blog
Business Apps·05. Juli 2026·13 min Lesezeit

LookupImportPlus: Schluss mit Raten bei Dataverse-Lookup-Importen

Der Standard-Excel-Import rät bei doppelten Namen — und verknüpft still den falschen Datensatz. LookupImportPlus löst Dataverse-Lookups deterministisch auf (GUID, Business Key oder Suchfeld) oder eskaliert an dich. Mit echtem Importlauf und allen Matching-Szenarien Schritt für Schritt.

LookupImportPlus: Schluss mit Raten bei Dataverse-Lookup-Importen

Du kennst das Problem, wenn du schon mal Kontakte oder Accounts per Excel in Dataverse importiert hast: Du exportierst eine Liste, jemand trägt in einer Textspalte einen Firmennamen ein, du importierst zurück — und Dataverse verknüpft einen Teil deiner Kontakte mit dem falschen Account, weil zwei Accounts zufällig denselben Namen tragen. Kein Fehler, keine Warnung. Du merkst es erst Wochen später, wenn jemand seinen eigenen Kontakt nicht findet.

LookupImportPlus ist ein Power-Apps-Tool, das genau das verhindert. Hier zeigen wir dir, wie es funktioniert und wie du es Schritt für Schritt einsetzt — inklusive eines echten Importlaufs, bei dem du jedes Matchingverfahren einmal in Aktion siehst.

Das Problem: Raten ist kein Auflösen

Ein Lookup in Dataverse ist eine Spalte, die auf einen Datensatz einer anderen Tabelle verweist — zum Beispiel das Feld “Mutterkonto” eines Kontakts, das auf einen Account zeigt. Technisch ist ein Lookup nicht der Name, den du siehst, sondern ein Verweis über die GUID (eine eindeutige ID) plus die Zieltabelle.

Der Standard-Excel-Import von Dataverse weiß das nicht. Importierst du eine Spalte “Mutterkonto” voller Firmennamen, sucht der Import nach einem Treffer — findet er mehrere, nimmt er stillschweigend den ersten. Zwei Accounts namens “Contoso GmbH”? Dann hängt ein Teil deiner importierten Kontakte jetzt am falschen, und das Protokoll meldet trotzdem “erfolgreich”.

Die Regel von LookupImportPlus: entweder eindeutig auflösen, oder anhalten und dich fragen. Nie raten.

Warum das technisch nicht trivial ist

Ein paar Punkte, die du kennen solltest, bevor du loslegst:

  • Namen sind nicht eindeutig. Die GUID wäre der zuverlässige Identifikator — steht aber so gut wie nie in deiner Excel-Spalte, weil Fachanwender nicht mit GUIDs arbeiten.
  • Polymorphe Lookups. Manche Lookups — customerid, ownerid, regardingobjectid — können auf mehrere Tabellen zeigen. Ein customerid kann Account oder Kontakt sein. Jede Zieltabelle hat eigene Feldnamen (Account sucht über name, Kontakt über fullname), du brauchst also pro möglichem Ziel eine eigene Abfrage.
  • Den Link schreiben. Kennst du den Zieldatensatz, setzt du den Lookup über die Dataverse Web API mit dem annotierten Feld @odata.bind:
POST /contacts
{ "firstname": "Max", "lastname": "Mustermann",
  "parentcustomerid_account@odata.bind": "/accounts(8f2c…-guid)" }

Das ist eine Schreibanweisung — deshalb ist es so wichtig, vorher die richtige GUID zu finden.

  • OData-Filter. Kommen deine Quelldaten aus einer Dataverse-View, definiert in FetchXML, musst du das mit übersetzen:
<condition attribute="statecode" operator="eq" value="0"/>   →   statecode eq 0
<condition attribute="name" operator="like" value="%Contoso%"/> → contains(name,'Contoso')

Nichts davon ist exotisch — aber zusammen ist es genau die Art Problem, die bei Skalierung leise Daten kaputt macht, wenn du es nicht sauber löst.

So arbeitest du damit: Konfiguration, Export, Import

Du startest nicht bei einer Excel-Datei, sondern bei einer Job-Konfiguration: einer gespeicherten, versionierten Beschreibung, wie eine Dataverse-Tabelle exportiert und wieder importiert wird — inklusive der genauen Auflösungslogik für jede Lookup-Spalte. Jeder Importlauf speichert einen unveränderlichen Schnappschuss der verwendeten Konfiguration, damit spätere Änderungen einen alten Lauf nie nachträglich umdeuten.

Dein Ablauf sieht so aus:

Konfigurieren → Exportieren (Template oder Daten) → in Excel bearbeiten → Importieren (Hochladen → Dry Run → Konflikte lösen → Commit).

Praxis: Screen für Screen

1. Job-Konfigurationen — dein Startpunkt

Auf der Startseite siehst du jede gespeicherte Konfiguration als Karte: Zieltabelle, Operation (Anlegen / Aktualisieren / Anlegen-oder-Aktualisieren), Anzahl Spalten und Lookups, Version, Entwurfsstatus. Pro Karte hast du Export ▾ (leeres Template oder echter Datenexport), Bearbeiten, Import starten und Löschen.

Job-Konfigurationsliste mit einer Karte "Contacts – Parent Account" inklusive Version, Entität und Aktions-Buttons Die Konfigurationsliste (Dark Theme) — eine Konfiguration, “Contacts – Parent Account”, Version 4, Entität contact, createOrUpdate.

Dieselbe Konfigurationsliste im Light Theme Die helle Variante desselben Screens. Der Theme-Umschalter (Mond-/Sonne-Icon) sitzt oben rechts neben der Sprache.

Klicke Neue Konfiguration, um den geführten Assistenten zu öffnen. Spätere Tabs schalten sich frei, sobald du eine Zielentität gewählt hast:

  1. Entität & Quelle — wähle die Ziel-Dataverse-Tabelle. Metadaten (Attribute, Primärschlüssel, Anzeigename) laden automatisch. Du kannst statt der Rohtabelle auch aus einer gespeicherten View exportieren.
  2. Allgemein — Name, Beschreibung, Operationstyp, Standard-Schreibmodus: strict (nichts schreiben, bis alle Zeilen aufgelöst sind) oder partial (saubere Zeilen sofort committen, Konflikte warten).

2. Spalten — leg fest, was mitgeht

Im Tab Spalten siehst du jedes Attribut der gewählten Tabelle, mit Filtern für Suche, nur ausgewählte, nur Lookups, nur Pflichtfelder, nur beschreibbare — bei Tabellen mit 200+ Systemspalten spart dir das viel Scrollen.

Spalten-Tab mit Attributliste, Checkboxen, Filtern und Vorschau-/Export-Buttons Der Spalten-Tab — Spalten ankreuzen, Filter wie “Nur Lookups” verfügbar, darunter Daten-Vorschau / Leeres Template / Daten exportieren.

Kreuze die Spalten an, die du brauchst, und nutze direkt von hier aus Daten-Vorschau, Leeres Template oder Daten exportieren.

3. Daten-Vorschau — prüf, bevor du exportierst

Aus dem Spalten-Tab öffnest du dieses Modal: bis zu 10, 25 oder 50 echte Datensätze, umschaltbar zwischen CRM-Spalten (rohe Dataverse-Werte) und Schema-Spalten (das Excel-Layout, das der Export erzeugt), mit farbcodierten Gruppen und Legende, welche Spalten zu welchem Lookup gehören.

Daten-Vorschau-Modal, umgeschaltet auf Schema-Spalten mit farbcodierten Lookup-Gruppen und den generierten technischen Spalten Die Schema-Ansicht zeigt, was der Export wirklich erzeugt: neben “Parent Account” auch die technischen Spalten “Parent Account Id” (GUID), “Parent Account Type” und “Parent Account Number” — die Kanäle für GUID- und Business-Key-Matching.

Merk dir diese drei technischen Spalten — sie sind gleich im Szenario-Teil die Abkürzung an der Konfliktliste vorbei.

4. Lookups — hier passiert die eigentliche Arbeit

Jede ausgewählte Lookup-Spalte bekommt eine eigene Konfigurationskarte. Hier setzt du das “nie raten”-Prinzip praktisch um. Die Karte bündelt alles, was ein Matching braucht:

Lookup-Konfigurationskarte "Parent Account → parentcustomerid" mit sichtbarer Excel-Spalte, Business-Key-Spalte, Konfliktstrategie escalate, polymorphen Zieltabellen account/contact, Suchfeld pro Ziel und einer Suchbedingung auf modifiedon Eine Lookup-Konfiguration im Detail — Zielfeld parentcustomerid, Business-Key-Spalte “Parent Account Number”, Konfliktstrategie escalate, Zieltabellen account/contact (polymorpher Scope) und eine Bedingung “Modified On ≥ relatives Datum −7 Tage”.

  • Sichtbare Excel-Spalte — die menschenlesbare Spalte, die deine Leute ausfüllen (“Parent Account”).
  • Business-Key-Spalte (optional) — eine Spalte mit einem eindeutigen Alternativwert (z. B. eine Kontonummer), die vor der Namenssuche greift.
  • Konfliktstrategie — was bei mehreren Treffern passiert: escalate (in die Konfliktliste), überspringen oder als Fehler markieren.
  • Zieltabelle(n) — bei polymorphen Lookups kreuzt du nur die Tabellen an, die deine Excel tatsächlich referenziert. Pro Ziel setzt du Suchfeld, Business-Key-Attribut und Bedingungen — weil derselbe Lookup für ein Ziel account.name und für ein anderes contact.fullname braucht.
  • Bedingungen — grenzen ein, welche Ziel-Datensätze überhaupt passen: links ein Feld des Ziels, rechts ein fester Wert, eine andere Excel-Spalte derselben Zeile oder ein relatives Datum.

5. Importlauf — hochladen, Dry Run, entscheiden

Lädst du eine ausgefüllte Tabelle hoch (oder die eingebaute Demo-Datei), läuft zuerst eine Konfigurationsprüfung (Schema-Drift-Schutz), dann ein Dry Run: jede Zeile bekommt einen Status und du siehst einen echten Fortschrittsbalken, weil das Tool die Gesamtzeilenzahl von Anfang an kennt.

Importlauf-Screen mit Statuskacheln (1 Bereit, 2 Konflikte, 1 Fehler, 4 Zeilen gesamt), Strict/Partial-Umschalter und zeilenweiser Ergebnistabelle Dry-Run-Ergebnisse der Demo-Datei — vier Zeilen, vier verschiedene Ausgänge. Genau die sehen wir uns unten einzeln an.

Wähle Strict (nichts schreiben, bis alles aufgelöst ist) oder Partial (saubere Zeilen jetzt schreiben) und committe.

6. Konfliktliste — Entscheidungen bündeln

Jeder ungelöste Lookup landet hier, gruppiert nach Quellwert — “1 Zeile sagt Contoso GmbH” ist eine Entscheidung, nicht viele.

Konfliktliste gruppiert nach Quellwert (Contoso GmbH, Adventure Works, Unknown Corp) mit Zielfeld, betroffenen Zeilen, Kandidatenzahl und Status Die Konfliktliste — “Nichts wird automatisch geraten.” Jede Entscheidung wird protokolliert: Regel, gezeigte Kandidaten, gewählte GUID, Benutzer, Zeitpunkt.

7. Importhistorie

Jeder Lauf steht hier mit eingefrorenem Konfigurations-Snapshot und Zahlen (Zeilen, geschrieben, Konflikte, Status) — nachvollziehbar bis zur einzelnen Zeile und Lookup-Entscheidung.

Importhistorien-Tabelle mit einem abgeschlossenen Lauf: Konfiguration, Modus Partial, 4 Zeilen, 1 geschrieben, 2 Konflikte, Status Abgeschlossen Importhistorie (Light Theme) — ein Partial-Lauf: 4 Zeilen gelesen, 1 sauber geschrieben, 2 Konflikte offen.

Das Auflösungsmodell

Die Reihenfolge ist fest, ohne Ausnahme. Genau diese drei Stufen erklärt die App dir auch direkt im Lookups-Tab:

Erklärkasten "Wie ein Lookup aufgelöst wird" mit den drei nummerierten Stufen GUID-Spalte, Business Key, Suchfeld und der Warnung, dass mehrere/keine Treffer eskalieren Die drei Matching-Stufen, wie die App sie selbst dokumentiert — erster Treffer gewinnt.

1. GUID-Spalte         → per ID abrufen, Zieltyp prüfen → binden
2. Business Key         → per eindeutigem Alternate-Attribut abfragen
3. Suchfeld + Bedingungen → Kandidaten abfragen
     genau ein Treffer → aufgelöst | null Treffer → NotFound | mehrere → Mehrdeutig (→ Konfliktliste)

Eine Lookup-Konfiguration, vereinfacht:

{
  "lookupAttribute": "parentcustomerid",
  "visibleColumn": "Parent Account",
  "guidColumn": "Parent Account Id",
  "businessKeyColumn": "Parent Account Number",
  "targetEntities": ["account", "contact"],
  "searchAttribute": "name",
  "targetOverrides": {
    "account": { "searchAttribute": "name" },
    "contact": { "searchAttribute": "fullname" }
  },
  "conflictStrategy": "escalate"
}

Import-Szenarien: die Matchingverfahren in Aktion

Genug Modell — schauen wir es an einem echten Lauf an. Die eingebaute Demo-Datei hat vier Zeilen, und jede fällt bewusst in einen anderen Matching-Ausgang. Genau das siehst du oben im Dry-Run-Screen; hier ist die Tabelle Zeile für Zeile:

Last NameParent AccountAuflösungStatusWas greift
SchmidtFabrikam Inc.22222222… (account)✅ AufgelöstSuchfeld, genau 1 Treffer
MustermannContoso GmbH2 Kandidaten⚠️ MehrdeutigSuchfeld, >1 Treffer → eskaliert
Adventure Workskein Treffer❌ Pflichtfeld fehltValidierung vor dem Matching
BergUnknown Corpkein Treffer❌ Nicht gefundenSuchfeld, 0 Treffer

Szenario 1 — Eindeutiger Name: der Normalfall

Zeile Schmidt → Fabrikam Inc.: Es gibt genau einen Account namens “Fabrikam Inc.”. Die GUID- und Business-Key-Spalten sind leer, also greift Stufe 3 (Suchfeld). Die Abfrage name eq 'Fabrikam Inc.' liefert genau einen Treffer → die Zeile ist aufgelöst, die App zeigt die aufgelöste GUID (22222222…) an und würde beim Commit parentcustomerid_account@odata.bind auf diesen Account setzen. Kein Nutzereingriff. So sollen so viele Zeilen wie möglich durchlaufen.

Szenario 2 — Mehrdeutiger Name: zwei “Contoso GmbH”

Zeile Mustermann → Contoso GmbH: Auch hier greift Stufe 3 — aber die Namenssuche findet zwei Accounts “Contoso GmbH”. Die Konfliktstrategie steht auf escalate, also rät die App nicht, sondern schiebt die Zeile in die Konfliktliste. Öffne sie und dann die Gruppe:

Resolve-Detailseite für "Contoso GmbH": Warnung "Lookup nicht eindeutig auflösbar", die genutzte OData-Abfrage, der Zeitanker und zwei Kandidaten mit unterschiedlicher accountnumber, jeweils mit Radio-Auswahl und Deep-Link Die Resolve-Seite legt offen, worauf gematcht wurde: name eq 'Contoso GmbH' and modifiedon ge 2026-06-28… — und zeigt die zwei Kandidaten, die sich nur an der accountnumber (100230 vs. 200981) unterscheiden.

Du wählst den richtigen Datensatz, optional mit “Entscheidung auf alle Zeilen mit diesem Wert anwenden”, und übernimmst. Jede Entscheidung wird protokolliert. Das ist der manuelle Weg — aber genau diesen Konflikt kannst du an drei Stellen vorab abfangen, ohne die Liste je zu sehen:

a) GUID-Round-Trip (Stufe 1). Exportiere echte Daten statt eines leeren Templates, blende die mitgelieferte Spalte “Parent Account Id” ein, trag bei bekannt mehrdeutigen Zeilen direkt die richtige GUID ein, importiere erneut. GUID gewinnt die Reihenfolge uneingeschränkt — kein Konfliktscreen.

b) Business Key: die Kontonummer (Stufe 2). Genau dafür ist die Spalte “Parent Account Number” da. Die zwei Contoso unterscheiden sich an der accountnumber (100230 vs. 200981). Trag die richtige Nummer in die Business-Key-Spalte, und die App löst über das eindeutige Alternate-Attribut auf — noch bevor die Namenssuche überhaupt startet.

c) Bedingung: ein zweites Merkmal (Stufe 3). Hast du kein GUID/keine Nummer, aber ein anderes unterscheidendes Feld in der Excel-Zeile, häng es als Bedingung an:

name = Excel["Parent Account"]  UND  address1_country = Excel["Country"]

Jetzt trifft nur noch die deutsche Contoso GmbH zu — die Zeile löst sauber auf. (Die Demo nutzt bereits eine Bedingung auf modifiedon als Zeitanker; nach demselben Muster hängst du deine eigenen an.)

Szenario 3 — Pflichtfeld fehlt: vor dem Matching abgefangen

Zeile Adventure Works ohne Nachnamen: Der Lookup wäre auflösbar, aber der Zeile fehlt ein Pflichtfeld (lastname). Das prüft die App vor dem Schreiben und markiert die Zeile als “Pflichtfeld fehlt”, statt beim Commit in eine rohe API-Exception zu laufen. Ergänze den Wert in Excel und importiere erneut — es ist kein Lookup-Problem, sondern reine Datenvalidierung, die dir früh gemeldet wird.

Szenario 4 — Nicht gefunden: null Treffer

Zeile Berg → Unknown Corp: Stufe 3 sucht, findet aber keinen Account “Unknown Corp” → Status Nicht gefunden. Das ist ehrlicher als ein falscher Treffer. Ursachen sind meist Tippfehler in der Quelle oder ein Datensatz, den es (noch) nicht gibt. Du entscheidest: den Account anlegen, den Namen korrigieren, oder die Zeile bewusst überspringen — in der Konfliktliste bekommt auch “Nicht gefunden” eine Zeile mit “Bearbeiten →”.

Der rote Faden

Vier Zeilen, vier Ausgänge — und in keinem einzigen wird geraten. Jeder Status ist direkt auf eine Matching-Stufe zurückführbar: aufgelöst (Stufe 3, 1 Treffer), mehrdeutig (Stufe 3, >1 Treffer, escalate), nicht gefunden (Stufe 3, 0 Treffer), Pflichtfeld (Validierung davor). Und für die unangenehmen Fälle hast du mit GUID (Stufe 1) und Business Key (Stufe 2) zwei Hebel, um sie schon beim nächsten Round-Trip verschwinden zu lassen.

Unter der Haube

Falls dich das technisch interessiert: LookupImportPlus ist eine Power Apps Code App — du schreibst echten React- + Fluent-UI-Code statt Screens visuell zu bauen, mit Vite als Build-Tool. Der Datenzugriff läuft über die Dataverse-Datenquelle des @microsoft/power-apps-SDKs — die deutlich mehr kann, als die Basis-Connector-Doku vermuten lässt: vollständige Entity-/Attribut-/Beziehungsmetadaten, OData-Paging und die für polymorphe Lookups nötigen Navigation Properties. Kein eigener Connector, kein Plug-in nötig für den Kernfunktionsumfang.

Konfigurationen und Laufhistorie liegen aktuell im localStorage deines Browsers; die App selbst ist das Einzige, was du in deiner Dataverse-Umgebung installierst. Vor jedem Lauf prüft eine Schema-Drift-Preflight deine gespeicherte Konfiguration gegen die aktuellen Metadaten der Tabelle — umbenannte Spalten, entfernte Lookups, geänderte Ziele — und blockiert bei echten Fehlern, während Warnungen nur angezeigt werden. So läufst du nicht mitten im Import in eine rohe API-Exception.

Was noch fehlt

Damit du weißt, worauf du dich einstellen musst:

  • Zeilenweise Schreibvorgänge. Commits laufen einzeln über Dataverse (mit begrenzter Nebenläufigkeit und Retry pro Zeile, also durchaus flott), nicht über Bulk-Operationen wie CreateMultiple/UpdateMultiple. Kommt als nächster Durchsatz-Meilenstein.
  • Noch keine geteilte, persistierte Historie. Konfigurationen und Laufhistorie liegen im Browser-Speicher jedes einzelnen Nutzers. Team-weit geteilte, in Dataverse gespeicherte Audit-Tabellen (lip_jobconfiguration, lip_importjob usw.) sind für Phase 2 geplant, aber noch nicht gebaut.
  • Keine asynchrone Warteschlange für sehr große Läufe. Für Volumina jenseits dessen, was dein Browser live verarbeiten sollte, ist eine Dataverse-native Custom API geplant, die Zeilen als Hintergrundjob verarbeitet — bewusst keine externe Azure Function. Auch das: noch nicht gebaut.

Nichts davon blockiert das Kernversprechen — deterministische, auditierbare Lookup-Auflösung. Das ist Arbeit für später.

Leg selbst los

Der komplette Quellcode ist offen auf GitHub: github.com/brunsforge/LookupImportPlus. Klonen, lokal starten, ausprobieren:

git clone https://github.com/brunsforge/LookupImportPlus
cd LookupImportPlus
npm install && npm run dev        # lokal, Demo-Daten, kein Dataverse nötig
npm run deploy -- -EnvironmentUrl https://<org>.crm.dynamics.com   # auf eine Trial

Der lokale Modus läuft komplett gegen einen In-Memory-Mock — inklusive der zwei absichtlich kollidierenden “Contoso GmbH”-Accounts aus dem Szenario-Teil oben. So erlebst du einen Konflikt und löst ihn, bevor du überhaupt eine echte Umgebung anfasst.

Siehe auch