Microsoft Outlook, powered by Microsoft Graph, gives AI agents access to one of the most complete enterprise communication ecosystems available — email, calendar, contacts, and Teams in a single API. For organizations running on Microsoft 365, Outlook AI integration is often the highest-leverage automation available because it connects to the systems employees already use throughout their workday.
This guide covers building an Outlook email and calendar agent using Microsoft Graph API and LangChain.
What AI Agents Can Do With Outlook Access#
Email Management
- Triage incoming email by urgency and category, applying folders and flags automatically
- Draft contextually appropriate replies using thread history and previous email patterns
- Extract action items and deadlines from emails and sync them to Planner or Tasks
- Generate executive summaries of daily email volume for quick scanning
Calendar Automation
- Schedule meetings with Teams links based on mutual availability
- Reschedule conflicting events and notify attendees automatically
- Block focus time to protect deep work from meeting creep
- Generate pre-meeting briefs from email threads and attachments
Cross-Platform Workflows
- Route high-priority emails to Teams channels for team visibility
- Sync Outlook tasks with SharePoint project lists
- Trigger workflows based on email content (invoice received → start approval flow)
Setting Up Microsoft Graph API Access#
Register an Azure App#
- Go to Azure Portal → Azure Active Directory → App registrations
- Click New registration — name your app, set redirect URI to
http://localhost - Note your Application (client) ID and Directory (tenant) ID
- Go to Certificates & secrets → New client secret → copy the value
- Go to API permissions → Add permission → Microsoft Graph → Delegated:
Mail.ReadWrite,Calendars.ReadWrite,User.Read
- Click Grant admin consent
pip install msal requests langchain langchain-openai python-dotenv
import os
import msal
import requests
from dotenv import load_dotenv
load_dotenv()
CLIENT_ID = os.getenv("AZURE_CLIENT_ID")
CLIENT_SECRET = os.getenv("AZURE_CLIENT_SECRET")
TENANT_ID = os.getenv("AZURE_TENANT_ID")
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
SCOPES = ["https://graph.microsoft.com/Mail.ReadWrite",
"https://graph.microsoft.com/Calendars.ReadWrite",
"https://graph.microsoft.com/User.Read"]
GRAPH_ENDPOINT = "https://graph.microsoft.com/v1.0"
def get_access_token():
"""Get Microsoft Graph access token using device code flow."""
app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY)
# Try cache first
accounts = app.get_accounts()
if accounts:
result = app.acquire_token_silent(SCOPES, account=accounts[0])
if result and "access_token" in result:
return result["access_token"]
# Device code flow for first run
flow = app.initiate_device_flow(scopes=SCOPES)
print(flow.get("message", "")) # Shows login URL and code
result = app.acquire_token_by_device_flow(flow)
return result.get("access_token")
def graph_get(endpoint: str, params: dict = None) -> dict:
token = get_access_token()
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
response = requests.get(f"{GRAPH_ENDPOINT}/{endpoint}", headers=headers, params=params)
response.raise_for_status()
return response.json()
def graph_post(endpoint: str, data: dict) -> dict:
token = get_access_token()
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
response = requests.post(f"{GRAPH_ENDPOINT}/{endpoint}", headers=headers, json=data)
response.raise_for_status()
return response.json()
Option 1: No-Code with n8n#
Daily Email Digest Workflow#
- Schedule Trigger: Run at 7am weekdays
- Microsoft Outlook node (n8n built-in): Fetch emails received since yesterday
- Code node: Filter unread emails, group by sender domain
- OpenAI: "Summarize these emails into a 5-bullet daily digest. Flag any requiring urgent response."
- Microsoft Teams node: Post digest to personal chat or team channel
n8n's Microsoft Outlook node handles OAuth token refresh automatically using the configured credential.
Option 2: LangChain with Python#
Build Microsoft Graph / Outlook Tools#
from langchain.tools import tool
from datetime import datetime, timedelta, timezone
@tool
def list_unread_emails(folder: str = "inbox", max_results: int = 10) -> str:
"""List unread emails from Outlook. folder: 'inbox', 'junkemail', 'drafts'."""
messages = graph_get(
f"me/mailFolders/{folder}/messages",
params={
"$filter": "isRead eq false",
"$select": "id,subject,from,receivedDateTime,bodyPreview",
"$top": max_results,
"$orderby": "receivedDateTime desc"
}
)
items = messages.get("value", [])
if not items:
return "No unread emails found"
result = [f"Unread emails ({len(items)} shown):"]
for msg in items:
sender = msg.get("from", {}).get("emailAddress", {})
result.append(f"\nID: {msg['id'][:20]}...\nFrom: {sender.get('name', 'Unknown')} <{sender.get('address', '')}>\nSubject: {msg.get('subject', 'No subject')}\nPreview: {msg.get('bodyPreview', '')[:200]}")
return "\n".join(result)
@tool
def get_email_body(message_id: str) -> str:
"""Get the full body of an Outlook email by message ID."""
msg = graph_get(
f"me/messages/{message_id}",
params={"$select": "subject,from,body,receivedDateTime,toRecipients"}
)
sender = msg.get("from", {}).get("emailAddress", {})
body_content = msg.get("body", {}).get("content", "")
# Strip HTML tags for plain text
import re
clean_body = re.sub(r'<[^>]+>', '', body_content)[:3000]
return f"From: {sender.get('name')} <{sender.get('address')}>\nSubject: {msg.get('subject')}\nDate: {msg.get('receivedDateTime')}\n\n{clean_body}"
@tool
def create_draft_reply(message_id: str, body_text: str) -> str:
"""Create a draft reply to an Outlook email. Draft appears in Drafts folder for review."""
draft = graph_post(f"me/messages/{message_id}/createReply", {})
draft_id = draft.get("id")
# Update the draft body
token = get_access_token()
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
requests.patch(
f"{GRAPH_ENDPOINT}/me/messages/{draft_id}",
headers=headers,
json={"body": {"contentType": "Text", "content": body_text}}
)
return f"Draft reply created (ID: {draft_id[:20]}...) — review in Outlook Drafts before sending"
@tool
def create_calendar_event(subject: str, start_datetime: str, duration_minutes: int = 30,
attendee_emails: list = None, body: str = "",
add_teams_link: bool = True) -> str:
"""
Create a calendar event in Outlook with optional Teams link.
start_datetime format: 'YYYY-MM-DDTHH:MM:SS' (local time).
"""
start = datetime.fromisoformat(start_datetime)
end = start + timedelta(minutes=duration_minutes)
event_body = {
"subject": subject,
"body": {"contentType": "Text", "content": body},
"start": {"dateTime": start.isoformat(), "timeZone": "America/New_York"},
"end": {"dateTime": end.isoformat(), "timeZone": "America/New_York"},
}
if attendee_emails:
event_body["attendees"] = [
{"emailAddress": {"address": email}, "type": "required"}
for email in attendee_emails
]
if add_teams_link:
event_body["isOnlineMeeting"] = True
event_body["onlineMeetingProvider"] = "teamsForBusiness"
event = graph_post("me/events", event_body)
join_url = event.get("onlineMeeting", {}).get("joinUrl", "No Teams link")
return f"Event created: '{subject}'\nTime: {start.strftime('%B %d at %I:%M %p')}\nTeams: {join_url}"
@tool
def get_todays_calendar() -> str:
"""Get today's Outlook calendar events."""
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
tomorrow = today + timedelta(days=1)
events = graph_get("me/calendarView", params={
"startDateTime": today.isoformat() + "Z",
"endDateTime": tomorrow.isoformat() + "Z",
"$select": "subject,start,end,attendees,isOnlineMeeting",
"$orderby": "start/dateTime"
})
items = events.get("value", [])
if not items:
return "No events today"
lines = [f"Today's calendar ({today.strftime('%A, %B %d')}):"]
for event in items:
start_str = event.get("start", {}).get("dateTime", "")
start_time = datetime.fromisoformat(start_str[:19]).strftime("%I:%M %p") if start_str else "TBD"
attendee_count = len(event.get("attendees", []))
online = "Teams" if event.get("isOnlineMeeting") else "In-person"
lines.append(f" {start_time}: {event.get('subject', 'No title')} | {attendee_count} attendees | {online}")
return "\n".join(lines)
Outlook AI Assistant Agent#
from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
llm = ChatOpenAI(model="gpt-4o", temperature=0.2)
tools = [list_unread_emails, get_email_body, create_draft_reply,
create_calendar_event, get_todays_calendar]
prompt = ChatPromptTemplate.from_messages([
("system", """You are a Microsoft 365 AI assistant with access to Outlook email and calendar.
Your core behaviors:
- Always create drafts, never send emails autonomously
- Check calendar context before suggesting meeting times
- Extract action items from emails into clear bullet points
- Summarize email threads concisely before drafting replies
Communication style: professional, concise, action-oriented."""),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True, max_iterations=8)
Rate Limits and Best Practices#
| Microsoft Graph limit | Value |
|---|---|
| Per user throttle | 10,000 req/10 min |
| Batch request size | Max 20 requests |
| Throttle response | 429 with Retry-After header |
Best practices:
- Use batch requests: Combine up to 20 API calls in one POST /$batch request for bulk processing
- Cache the access token: MSAL handles this, but ensure you reuse tokens until they expire rather than acquiring new ones per call
- Use delta queries: For agents monitoring mailbox changes, use
/me/messages/deltainstead of polling the full message list - Scope minimally: Request only the Graph permissions your agent actually uses to reduce security exposure
Next Steps#
- AI Agents Gmail Integration — Gmail-based email automation patterns
- AI Agents Google Calendar Integration — Google Calendar scheduling automation
- AI Agents Slack Integration — Bridge Outlook and Slack workflows
- Build an AI Agent with LangChain — Complete framework tutorial