Der klassische Einzeiler, den wir alle kennen:
Set objOutlook = CreateObject("Outlook.Application")
Dieser Code funktioniert nicht mehr, wenn ein Anwender auf den New Outlook gewechselt hat. Kein Fehler mit Erklärung, keine Warnung — einfach nichts. Und seit den Windows-Updates vom Juni 2026 gibt es noch eine zweite Baustelle: Microsoft hat mit einem Sicherheits-Patch OLE Automation für Drittanwendungen vorübergehend komplett blockiert, was Access-Anwendungen, die Office-Programme per COM steuern, ohne Vorwarnung in den Stand-by geschickt hat. Das war zwar ein Bug und kein Feature, aber er zeigt, wie fragil die COM-Brücke geworden ist.
Es ist also ein guter Zeitpunkt, sich Microsoft Graph anzusehen.
Was Microsoft Graph überhaupt ist
Microsoft Graph ist eine REST-API. Hinter dem Endpunkt https://graph.microsoft.com steckt der zentrale Datenzugang zu allem, was Microsoft 365 ist: Mails, Kalender, Kontakte, Teams-Nachrichten, Planner-Aufgaben, SharePoint-Listen, OneDrive-Dateien, Microsoft To Do und mehr. Ein Endpunkt, ein Token, fast alle M365-Dienste.
Graph ist keine neue Idee — die API gibt es seit 2015. Aber für VBA-Entwickler war der COM-Weg bisher einfacher. Das ändert sich gerade.
Graph ist Cloud-only. On-Premises-Exchange oder ein lokales SharePoint werden nicht über Graph angesteuert. Wer noch eine lokale Exchange-Infrastruktur betreibt, braucht für diese Systeme weiterhin andere Wege (Exchange Web Services oder SMTP/IMAP direkt). Für alle M365-Tenants — und das sind heute die meisten KMU-Kunden — ist Graph der richtige Weg nach vorn.
Warum COM in Outlook ausläuft
Der New Outlook ist keine Weiterentwicklung des klassischen Outlook-Clients. Er basiert auf Web-Technologien (intern läuft er über WebView2) und kennt kein COM-Objektmodell. Outlook.Application, NameSpace, MailItem — diese Klassen existieren im New Outlook schlicht nicht.
Microsoft hat klargestellt, dass COM-Add-ins im New Outlook nicht unterstützt werden und auch nicht nachgerüstet werden sollen. JavaScript-basierte Web-Add-ins sind das offizielle Nachfolgeformat — aber die lassen sich nicht aus VBA aufrufen.
Für Access-Entwickler bedeutet das: Alles, was per CreateObject("Outlook.Application") Mails versendet, Termine anlegt oder Aufgaben schreibt, ist auf Benutzern mit New Outlook bereits kaputt.
Einmal App registrieren, dann geht es los
Bevor Graph angesprochen werden kann, braucht es eine App-Registrierung im Microsoft Entra-Portal (früher Azure Active Directory). Das klingt bürokratischer als es ist.
- portal.azure.com → Entra ID → App-Registrierungen → Neue Registrierung
- Name vergeben (z. B. „Access-Graph-Client“), Typ: Öffentliche Client-App (kein Geheimnis für Desktop-Anwendungen)
- Umleitungs-URI:
https://login.microsoftonline.com/common/oauth2/nativeclient - API-Berechtigungen setzen — delegierte Berechtigungen, nicht Anwendungsberechtigungen
Delegierte Berechtigungen bedeutet: Graph handelt im Namen des angemeldeten Benutzers. Das ist für die meisten Access-Szenarien das Richtige — der Code tut, was der Benutzer tun darf, nicht mehr.
Für To Do braucht es die delegierte Berechtigung Tasks.ReadWrite. Für E-Mail: Mail.Send und Mail.Read. Für Teams-Nachrichten: ChannelMessage.Send. Für Planner: Tasks.ReadWrite (ja, dieselbe). Für SharePoint-Listen: Sites.ReadWrite.All oder granularer Sites.Selected.
Nach der Registrierung hat die App eine Client-ID (eine GUID). Die Tenant-ID steht ebenfalls im Portal. Beide Werte werden im VBA-Code als Konstanten hinterlegt.
Wie die Authentifizierung aus VBA funktioniert
Das Knifflige an Graph aus VBA ist die Authentifizierung. Graph verwendet OAuth 2.0 — kein Passwort, sondern ein kurzlebiges Token (standardmäßig eine Stunde gültig).
Für Desktop-Anwendungen ohne eigene Webserver-Infrastruktur ist der Device Code Flow der pragmatischste Weg. Der Benutzer öffnet einmalig einen Browser, gibt einen kurzen Code ein, meldet sich an — der VBA-Code pollt in der Zwischenzeit, bis das Token vorliegt. Danach kann der Code das Token im Arbeitsspeicher halten und bei Bedarf per Refresh-Token erneuern, solange die Session aktiv ist.
Der folgende Code zeigt den vollständigen Ablauf: Token holen, To-Do-Listen lesen, eine Aufgabe anlegen.
Modul 1: Token holen per Device Code Flow
Option Compare Database
Option Explicit
' -----------------------------------------------
' Konstanten — aus der App-Registrierung im Entra-Portal
' -----------------------------------------------
Private Const CLIENT_ID As String = "DEINE-CLIENT-ID-HIER"
Private Const TENANT_ID As String = "DEINE-TENANT-ID-ODER-common"
Private Const SCOPE As String = "Tasks.ReadWrite offline_access"
' Token-Cache im Modul (Laufzeit, kein persistentes Speichern)
Private m_strAccessToken As String
Private m_strRefreshToken As String
Private m_dtmTokenExpiry As Date
' -----------------------------------------------
' Einstieg: gültiges Token zurückgeben oder neu holen
' -----------------------------------------------
Public Function GetAccessToken() As String
If m_strAccessToken <> "" And Now() < m_dtmTokenExpiry Then
GetAccessToken = m_strAccessToken
Exit Function
End If
If m_strRefreshToken <> "" Then
If RefreshToken() Then
GetAccessToken = m_strAccessToken
Exit Function
End If
End If
' Kein Token, kein Refresh — Device Code Flow starten
DeviceCodeFlow
GetAccessToken = m_strAccessToken
End Function
' -----------------------------------------------
' Device Code Flow: Browser-Anmeldung einmalig nötig
' -----------------------------------------------
Private Sub DeviceCodeFlow()
Dim oHTTP As Object
Dim strURL As String
Dim strBody As String
Dim strResp As String
Dim strCode As String
Dim strMsg As String
Dim strInterval As String
Set oHTTP = CreateObject("MSXML2.XMLHTTP.6.0")
' Schritt 1: Device Code anfordern
strURL = "https://login.microsoftonline.com/" & TENANT_ID & "/oauth2/v2.0/devicecode"
strBody = "client_id=" & CLIENT_ID & "&scope=" & EncodeURL(SCOPE)
oHTTP.Open "POST", strURL, False
oHTTP.setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
oHTTP.Send strBody
strResp = oHTTP.responseText
' Antwort parsen — einfaches JSON-Parsing ohne externe Bibliothek
strCode = JsonValue(strResp, "device_code")
strMsg = JsonValue(strResp, "message")
strInterval = JsonValue(strResp, "interval")
If strInterval = "" Then strInterval = "5"
' Benutzer informieren
MsgBox "Bitte melde dich an:" & vbCrLf & vbCrLf & strMsg, vbInformation, "Graph-Anmeldung"
' Schritt 2: Auf Token warten (polling)
Dim strPollURL As String
Dim strPollBody As String
Dim lngTimeout As Long
lngTimeout = Now() + TimeValue("00:05:00") ' 5 Minuten Timeout
strPollURL = "https://login.microsoftonline.com/" & TENANT_ID & "/oauth2/v2.0/token"
strPollBody = "grant_type=urn:ietf:params:oauth:grant-type:device_code" & _
"&client_id=" & CLIENT_ID & _
"&device_code=" & strCode
Do While Now() < lngTimeout
Sleep CLng(strInterval) * 1000
oHTTP.Open "POST", strPollURL, False
oHTTP.setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
oHTTP.Send strPollBody
strResp = oHTTP.responseText
If InStr(strResp, "access_token") > 0 Then
StoreTokens strResp
Exit Do
End If
' "authorization_pending" ist normal und wird ignoriert
Loop
Set oHTTP = Nothing
End Sub
' -----------------------------------------------
' Token erneuern per Refresh Token
' -----------------------------------------------
Private Function RefreshToken() As Boolean
Dim oHTTP As Object
Dim strURL As String
Dim strBody As String
Dim strResp As String
Set oHTTP = CreateObject("MSXML2.XMLHTTP.6.0")
strURL = "https://login.microsoftonline.com/" & TENANT_ID & "/oauth2/v2.0/token"
strBody = "grant_type=refresh_token" & _
"&client_id=" & CLIENT_ID & _
"&refresh_token=" & m_strRefreshToken & _
"&scope=" & EncodeURL(SCOPE)
oHTTP.Open "POST", strURL, False
oHTTP.setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
oHTTP.Send strBody
strResp = oHTTP.responseText
If InStr(strResp, "access_token") > 0 Then
StoreTokens strResp
RefreshToken = True
End If
Set oHTTP = Nothing
End Function
' -----------------------------------------------
' Token aus JSON-Response auslesen und speichern
' -----------------------------------------------
Private Sub StoreTokens(ByVal strResp As String)
m_strAccessToken = JsonValue(strResp, "access_token")
m_strRefreshToken = JsonValue(strResp, "refresh_token")
Dim lngExpiry As Long
lngExpiry = CLng(JsonValue(strResp, "expires_in"))
m_dtmTokenExpiry = Now() + lngExpiry / 86400 ' Sekunden in Tagesbruchteile
End Sub
Das JsonValue-Hilfsfunktion und Sleep kommen im zweiten Modul. Ohne externe JSON-Bibliothek läuft das über einfaches String-Parsing — für die wenigen Felder, die hier relevant sind, ausreichend.
Modul 2: Hilfsfunktionen
' -----------------------------------------------
' Minimales JSON-Parsing: einen String-Wert per Key holen
' Ausreichend für flache Graph-Antworten ohne Verschachtelung
' -----------------------------------------------
Public Function JsonValue(ByVal strJson As String, ByVal strKey As String) As String
Dim lngStart As Long
Dim lngEnd As Long
Dim strSearch As String
strSearch = """" & strKey & """:"
lngStart = InStr(strJson, strSearch)
If lngStart = 0 Then Exit Function
lngStart = lngStart + Len(strSearch)
' Wert in Anführungszeichen
If Mid(strJson, lngStart, 1) = """" Then
lngStart = lngStart + 1
lngEnd = InStr(lngStart, strJson, """")
JsonValue = Mid(strJson, lngStart, lngEnd - lngStart)
Else
' Numerischer Wert
lngEnd = lngStart
Do While Mid(strJson, lngEnd, 1) Like "[0-9]"
lngEnd = lngEnd + 1
Loop
JsonValue = Mid(strJson, lngStart, lngEnd - lngStart)
End If
End Function
' -----------------------------------------------
' URL-Encoding für Scope und andere Parameter
' -----------------------------------------------
Public Function EncodeURL(ByVal strText As String) As String
Dim i As Integer
Dim strChar As String
Dim strOut As String
For i = 1 To Len(strText)
strChar = Mid(strText, i, 1)
Select Case strChar
Case "A" To "Z", "a" To "z", "0" To "9", "-", "_", ".", "~"
strOut = strOut & strChar
Case " "
strOut = strOut & "+"
Case Else
strOut = strOut & "%" & Hex(Asc(strChar))
End Select
Next i
EncodeURL = strOut
End Function
' -----------------------------------------------
' Sleep — Windows-API, läuft in 32- und 64-Bit-Access
' -----------------------------------------------
#If VBA7 Then
Public Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr)
#Else
Public Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
#End If
Das JsonValue funktioniert für flache JSON-Antworten wie Token-Responses und einfache Felder. Für verschachtelte Objekte (z. B. "body": {"content": "..."}) braucht es etwas mehr — dazu unten im SharePoint-Abschnitt ein Hinweis.
Modul 3: To Do — Listen lesen und Aufgabe anlegen
' -----------------------------------------------
' Alle To-Do-Listen des angemeldeten Benutzers lesen
' Gibt eine Collection zurück: Key = Listen-ID, Value = Displayname
' -----------------------------------------------
Public Function GetToDoLists() As Collection
Dim oHTTP As Object
Dim strURL As String
Dim strResp As String
Dim colOut As New Collection
Set oHTTP = CreateObject("MSXML2.XMLHTTP.6.0")
strURL = "https://graph.microsoft.com/v1.0/me/todo/lists"
oHTTP.Open "GET", strURL, False
oHTTP.setRequestHeader "Authorization", "Bearer " & GetAccessToken()
oHTTP.setRequestHeader "Content-Type", "application/json"
oHTTP.Send
strResp = oHTTP.responseText
' Ergebnis in Debug-Fenster — zum Einlesen der Listen-IDs
Debug.Print strResp
Set oHTTP = Nothing
' Für die Collection: im Produktivcode per JSON-Parser verarbeiten
Set GetToDoLists = colOut
End Function
' -----------------------------------------------
' Offene Aufgaben aus einer bestimmten Liste holen
' strListID: ID der todoTaskList (aus GetToDoLists)
' -----------------------------------------------
Public Function GetOpenTasks(ByVal strListID As String) As String
Dim oHTTP As Object
Dim strURL As String
Set oHTTP = CreateObject("MSXML2.XMLHTTP.6.0")
' $filter: nur nicht abgeschlossene Aufgaben
strURL = "https://graph.microsoft.com/v1.0/me/todo/lists/" & strListID & _
"/tasks?$filter=status ne 'completed'&$select=id,title,status,dueDateTime"
oHTTP.Open "GET", strURL, False
oHTTP.setRequestHeader "Authorization", "Bearer " & GetAccessToken()
oHTTP.setRequestHeader "Accept", "application/json"
oHTTP.Send
GetOpenTasks = oHTTP.responseText
Set oHTTP = Nothing
End Function
' -----------------------------------------------
' Neue Aufgabe in einer Liste anlegen
' strListID: ID der Zielliste
' strTitle: Aufgabentitel
' strDueDate: Fälligkeit im Format "2026-12-31" (optional, "" = keine)
' -----------------------------------------------
Public Function CreateToDoTask( _
ByVal strListID As String, _
ByVal strTitle As String, _
ByVal strDueDate As String) As String
Dim oHTTP As Object
Dim strURL As String
Dim strBody As String
Set oHTTP = CreateObject("MSXML2.XMLHTTP.6.0")
strURL = "https://graph.microsoft.com/v1.0/me/todo/lists/" & strListID & "/tasks"
' JSON-Body zusammenbauen
If strDueDate <> "" Then
strBody = "{" & _
"""title"": """ & strTitle & """," & _
"""dueDateTime"": {" & _
" ""dateTime"": """ & strDueDate & "T00:00:00""," & _
" ""timeZone"": ""Europe/Berlin""" & _
"}" & _
"}"
Else
strBody = "{""title"": """ & strTitle & """}"
End If
oHTTP.Open "POST", strURL, False
oHTTP.setRequestHeader "Authorization", "Bearer " & GetAccessToken()
oHTTP.setRequestHeader "Content-Type", "application/json"
oHTTP.Send strBody
' HTTP 201 = Created, alles andere ist ein Fehler
If oHTTP.Status <> 201 Then
MsgBox "Fehler beim Anlegen der Aufgabe: " & oHTTP.Status & vbCrLf & oHTTP.responseText, vbCritical
End If
CreateToDoTask = oHTTP.responseText
Set oHTTP = Nothing
End Function
Die Listen-ID bekommst du beim ersten Aufruf von GetToDoLists über das Debug-Fenster — sie steht im JSON-Feld "id". Das ist eine lange GUID. Einmal herausschreiben, dann als Konstante im Code hinterlegen.
Was sonst noch geht — und wie der Aufruf aussieht
Das Muster ist in allen Graph-Endpunkten identisch: Token holen, HTTP-Request abschicken, JSON-Antwort verarbeiten. Deshalb lohnt es sich, die Auth-Schicht einmal sauber zu kapseln — dann ist ein neuer Dienst meist in einer Handvoll Zeilen erledigt.
E-Mails senden läuft über POST /me/sendMail mit einem JSON-Body, der Empfänger, Betreff und Body enthält. Keine Outlook-Instanz, kein Sichtbarwerden im Tray.
E-Mails lesen geht über GET /me/messages mit $filter und $select, um nur die relevanten Felder zu holen. Graph gibt standardmäßig maximal 1.000 Einträge pro Seite zurück — wer Postfächer mit mehr Mails abfragt, muss über @odata.nextLink paginieren.
Teams-Nachrichten senden klappt über POST /teams/{teamId}/channels/{channelId}/messages. Die Team- und Kanal-IDs bekommst du per GET /me/joinedTeams und GET /teams/{id}/channels. Für das Senden braucht die App die Berechtigung ChannelMessage.Send — dafür ist in den meisten Tenants Admin-Zustimmung nötig.
Planner-Aufgaben landen über POST /planner/tasks. Hier wird eine planId und bucketId benötigt, die du per GET /me/planner/plans ermittelst. Planner und To Do nutzen übrigens denselben Tasks.ReadWrite-Scope — Microsoft hat die Berechtigungsstruktur hier zusammengefasst.
SharePoint-Listen erreichst du über GET /sites/{site-id}/lists/{list-id}/items. Die Site-ID bekommst du per GET /sites?search=* oder gezielter per GET /sites/{hostname}:/{pfad}. Bei SharePoint-Antworten sind die eigentlichen Felder im fields-Objekt verschachtelt — da reicht das simple JsonValue oben nicht mehr aus. Für SharePoint empfehle ich einen kleinen Parser, der mit Split und InStr die relevanten Felder extrahiert, oder — wenn das Projekt es rechtfertigt — die Referenz auf Microsoft Scripting Runtime und eine Dictionary-basierte Lösung.
Wo der Ansatz an Grenzen stößt
Der Device Code Flow erfordert einmalig eine Browser-Anmeldung. Das ist für interaktive Access-Anwendungen in Ordnung, für vollautomatische Hintergrundprozesse ohne Benutzerinteraktion aber nicht geeignet. Dort müsste man auf den Client Credentials Flow wechseln — der benötigt ein Client Secret oder ein Zertifikat und läuft ohne Benutzerkontext. Für To Do ist das dann allerdings nicht mehr möglich, weil To Do-Daten immer an einen bestimmten Benutzer gebunden sind und Tasks.ReadWrite keine Anwendungsberechtigung kennt.
Das einfache JSON-Parsing per InStr funktioniert für flache Antworten. Für tief verschachtelte Objekte, Arrays mit vielen Einträgen oder Felder mit Sonderzeichen wird es schnell unhandlich. Wer regelmäßig Graph nutzt, lohnt sich einen stabilen Mini-Parser zu schreiben oder auf eine COM-seitige JSON-Bibliothek zurückzugreifen (z. B. VBA-JSON von Tim Hall, MIT-Lizenz, GitHub).
Graph ist außerdem gedrosselt: Zu viele Requests in kurzer Zeit werden mit HTTP 429 beantwortet. Bei großen Datenmengen braucht es Retry-Logik mit Exponential Backoff.
Was das für Access-Projekte bedeutet
Wer heute noch CreateObject("Outlook.Application") im Code hat, sollte das kennen. Nicht weil es morgen aufhört zu funktionieren — der klassische Outlook-Client läuft noch eine Weile. Aber bei jedem Kunden, der auf New Outlook wechselt, bricht die Automatisierung lautlos. Graph ist der Weg, der das langfristig löst und gleichzeitig mehr Dienste ansprechbar macht als COM je konnte.
Der Einstieg über To Do ist dabei kein schlechter — das API ist überschaubar, die Authentifizierung ist dieselbe wie für alle anderen Dienste, und das Debugging über den Immediate-Window-Ausdruck des rohen JSON reicht für den Anfang aus.
Wenn dich nicht der Code interessiert, sondern ob so etwas in deiner Access-Anwendung konkret umsetzbar ist: Ich schaue mir bestehende Projekte gern einmal an — sesoft.de/kontakt.
Quellen
- Microsoft Learn: Microsoft Graph — Übersicht und Endpunkt-Referenz: https://learn.microsoft.com/de-de/graph/overview
- Microsoft Learn: To-Do-API in Microsoft Graph: https://learn.microsoft.com/de-de/graph/todo-concept-overview
- Microsoft Learn: Device Code Flow: https://learn.microsoft.com/de-de/entra/identity-platform/v2-oauth2-device-code
- AccessForever.org: The New Outlook and Access/VBA: https://www.accessforever.org/post/the-new-outlook-and-access-vba (Dezember 2024)
- NoLongerSet.com: Killing VBA in Outlook (Mai 2026)
- Windows News: Microsoft June 2026 Windows Updates Disable Office OLE Automation (Juni 2026)
- VBA-JSON (Tim Hall, MIT): https://github.com/VBA-tools/VBA-JSON
Autor
Sönke Schäfer berät seit über 25 Jahren norddeutsche KMU bei der Entwicklung und Modernisierung von Microsoft-Access-Anwendungen. Sein Schwerpunkt liegt auf VBA-Entwicklung, SQL-Server-Migration und der Anbindung von Bestandssystemen an aktuelle Schnittstellen — darunter zunehmend REST-APIs wie Microsoft Graph. Er arbeitet direkt und ohne Subunternehmer aus Sierksdorf in Ostholstein.

