Library · Assistants and chatbots

Microsoft 365 + Claude: the full integration

Builder90 minUpdated: October 2026
59 of 105 in the library

Time: about 30 min of theory + 60 min of practice


The gist

Corporate clients live in Microsoft. Teams instead of Slack, SharePoint instead of Google Drive, Outlook instead of Gmail, Power Automate instead of Zapier. If you want to sell AI to large companies, you learn the Microsoft language. It's no harder than what you already know, just a different dialect.

🎨 Picture this: You speak English fluently. Your client lives in Mexico. The product is the same; you just need to explain it in Spanish. Microsoft 365 is corporate Spanish.


Key concepts

  • Microsoft Graph API: a single entry point to all Microsoft 365 data
  • Teams webhooks (the Workflows app): Claude sends notifications to Teams without a bot
  • Office integration: Word, Excel, PowerPoint through Python libraries
  • Power Platform: Power Automate + Claude with no code
  • Azure and Microsoft Foundry: infrastructure for corporate Claude
  • Microsoft 365 Agents SDK (formerly Bot Framework): a full bot in Teams

Theory

Microsoft 365 architecture

Code
Microsoft 365 ecosystem
├── Communication: Teams, Outlook
├── Documents: Word, Excel, PowerPoint, OneNote
├── Storage: SharePoint, OneDrive
├── Automation: Power Automate, Power Apps
├── Data: Power BI, Dataverse
└── Infrastructure: Azure, Active Directory

Microsoft Graph API is one API that gives you access to everything. One token, one entry point.

Code
https://graph.microsoft.com/v1.0/
├── /me                        → current user's data
├── /users/{id}/messages       → Outlook emails
├── /teams/{id}/channels       → Teams channels
├── /sites/{id}/drives         → SharePoint files
└── /me/calendar/events        → Calendar events

Microsoft Teams + Claude

Option 1: Incoming Webhook (the simplest)

A webhook is a link. You send a POST request to that link, and a message shows up in a Teams channel. No bot, no app registration.

⚠️ Microsoft is retiring the old webhooks built on Office 365 Connectors, and creating new ones will soon be blocked. Webhooks are now made through the Workflows app in Teams, which is built on Power Automate.

Getting a webhook:

  1. Teams → Channel → ... → Workflows
  2. Pick the "Send webhook alerts to a channel" template, set the parameters and click Save
  3. Copy the webhook link (Teams menu names change; for the current steps, see the Microsoft Learn documentation)

Limits: a message can be no larger than 28 KB, and if you send more than four requests per second, Teams throttles the connection. A workflow is tied to its owner: if that person leaves the organization, the webhook is left without an owner, so add co-owners.

Basic sending:

python
import os
import requests

def notify_teams(webhook_url: str, message: str):
    """A simple text message to Teams."""
    payload = {"text": message}
    response = requests.post(webhook_url, json=payload)
    return response.status_code in (200, 202)

# Example
notify_teams(
    webhook_url=os.environ["TEAMS_WEBHOOK_URL"],
    message="✅ The weekly report is ready. Claude analyzed 1,247 transactions."
)

Adaptive Cards (rich format):

A Workflows webhook accepts an Adaptive Card. It doesn't display the old MessageCard format with buttons, so buttons are made with Action.OpenUrl. Check how the card looks in your channel.

python
import os
import json
import requests
import anthropic
from datetime import datetime

def notify_teams_rich(webhook_url: str, title: str, facts: dict, actions: list = None):
    """
    An Adaptive Card with a table of facts and buttons.
    facts = {"Key": "Value", ...}
    actions = [{"title": "Open", "url": "https://..."}]
    """
    card = {
        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
        "type": "AdaptiveCard",
        "version": "1.2",
        "body": [
            {"type": "TextBlock", "text": title, "weight": "Bolder", "size": "Medium", "wrap": True},
            {"type": "TextBlock", "text": f"Generated: {datetime.now().strftime('%m/%d/%Y %H:%M')}",
             "isSubtle": True, "wrap": True},
            {"type": "FactSet", "facts": [{"title": k, "value": v} for k, v in facts.items()]}
        ]
    }

    if actions:
        card["actions"] = [
            {"type": "Action.OpenUrl", "title": a["title"], "url": a["url"]} for a in actions
        ]

    payload = {
        "type": "message",
        "attachments": [{
            "contentType": "application/vnd.microsoft.card.adaptive",
            "content": card
        }]
    }
    requests.post(webhook_url, json=payload)

# Claude analyzes the data → sends it to Teams
def weekly_sales_report():
    data = get_weekly_sales()
    
    client = anthropic.Anthropic()
    analysis = "".join(b.text for b in client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=300,
        messages=[{
            "role": "user",
            "content": f"Give a brief analysis: sales {data['total']}, new customers {data['new_clients']}, top product: {data['top_product']}"
        }]
    ).content if b.type == "text")
    
    notify_teams_rich(
        webhook_url=os.environ["TEAMS_WEBHOOK_URL"],
        title="📊 Weekly sales report",
        facts={
            "Revenue": f"${data['total']:,}",
            "New customers": str(data['new_clients']),
            "Top product": data['top_product'],
            "AI analysis": analysis[:200]
        },
        actions=[{"title": "Full report", "url": "https://dashboard.company.com"}]
    )

Option 2: An AI bot in Teams (the full version)

A real bot that replies to messages in Teams. Microsoft is replacing the old Bot Framework SDK with the Microsoft 365 Agents SDK: there is a migration guide for it, and it makes sense to start new bots on it right away (there is also the Teams SDK). Below is a version built on the Agents SDK for Python.

python
# bot.py — a Teams bot on the Microsoft 365 Agents SDK (requires Python 3.10+)
# pip install microsoft-agents-hosting-core microsoft-agents-hosting-aiohttp \
#             microsoft-agents-hosting-teams microsoft-agents-authentication-msal anthropic
#
# AGENT_APP (AgentApplication) is created following the Agents SDK documentation:
# CloudAdapter, MsalAuth.from_environment(), state storage.
from microsoft_agents.hosting.core import AgentApplication, TurnState, TurnContext
import anthropic

claude = anthropic.AsyncAnthropic()
conversations = {}  # History by conversation_id (in production, a database or Redis)

@AGENT_APP.activity("message")
async def on_message(context: TurnContext, _state: TurnState):
    user_message = context.activity.text
    conversation_id = context.activity.conversation.id

    # Load the conversation history
    history = conversations.get(conversation_id, [])
    history.append({"role": "user", "content": user_message})

    # Claude replies
    response = await claude.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=1000,
        system="You are the company's corporate AI assistant. Keep answers short and to the point.",
        messages=history
    )

    reply = "".join(b.text for b in response.content if b.type == "text")
    history.append({"role": "assistant", "content": reply})

    # Save the history (only the last 20 messages)
    conversations[conversation_id] = history[-20:]

    await context.send_activity(reply)

Registering the bot in Azure:

  1. portal.azure.com → Bot Services → Create
  2. Choose "Azure Bot"
  3. Get the App ID and App Password
  4. Set up the Teams Channel
  5. Deploy through Azure App Service

Azure portal menu names change; for the current steps, see the Microsoft 365 Agents SDK documentation.

Microsoft Office + Claude

Word: generating documents

python
import json
from datetime import datetime
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
import anthropic

def generate_word_report(topic: str, data: dict, output_path: str):
    """Claude writes the content → python-docx builds the document."""
    client = anthropic.Anthropic()
    
    # Claude generates structured content
    content_response = "".join(b.text for b in client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=3000,
        messages=[{
            "role": "user",
            "content": f"""
            Write a business report on the topic: {topic}
            
            Data to include:
            {json.dumps(data, ensure_ascii=False, indent=2)}
            
            Format (strictly):
            TITLE: [name]
            SUMMARY: [3-4 sentences]
            SECTION1: [name]
            [section text]
            SECTION2: [name]  
            [section text]
            CONCLUSIONS: [2-3 points]
            """
        }]
    ).content if b.type == "text")
    
    # Parse and create the Word document
    doc = Document()
    
    # Styles
    style = doc.styles['Normal']
    style.font.name = 'Arial'
    style.font.size = Pt(11)
    
    lines = content_response.split('\n')
    for line in lines:
        if line.startswith('TITLE:'):
            title = doc.add_heading(line.replace('TITLE:', '').strip(), 0)
            title.alignment = WD_ALIGN_PARAGRAPH.CENTER
        
        elif line.startswith('SUMMARY:'):
            p = doc.add_paragraph()
            run = p.add_run(line.replace('SUMMARY:', '').strip())
            run.italic = True
            p.paragraph_format.space_after = Pt(12)
        
        elif line.startswith('SECTION'):
            section_title = line.split(':', 1)[1].strip() if ':' in line else line
            doc.add_heading(section_title, level=1)
        
        elif line.startswith('CONCLUSIONS:'):
            doc.add_heading('Conclusions', level=1)
        
        elif line.strip() and not any(line.startswith(k) for k in ['TITLE', 'SUMMARY', 'SECTION', 'CONCLUSIONS']):
            doc.add_paragraph(line.strip())
    
    # Add the date and a signature
    doc.add_page_break()
    doc.add_paragraph(f"Generated: {datetime.now().strftime('%m/%d/%Y')}")
    doc.add_paragraph("Prepared with Claude AI")
    
    doc.save(output_path)
    return output_path

# Usage
generate_word_report(
    topic="Quarterly sales report",
    data={"revenue": "100,000", "growth": "8%", "top_regions": ["North", "South"]},  # sample data
    output_path="sales_report.docx"
)

Excel: analyzing data with Claude

python
import json
import anthropic
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.chart import BarChart, Reference

def analyze_excel_with_claude(excel_path: str) -> str:
    """Claude analyzes the data from Excel and returns insights."""
    wb = openpyxl.load_workbook(excel_path)
    ws = wb.active
    
    # Extract the data as text
    data_rows = []
    headers = [cell.value for cell in ws[1]]
    
    for row in ws.iter_rows(min_row=2, values_only=True):
        if any(cell is not None for cell in row):
            data_rows.append(dict(zip(headers, row)))
    
    # The first 50 rows for analysis
    sample = data_rows[:50]
    
    client = anthropic.Anthropic()
    analysis = "".join(b.text for b in client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=1500,
        messages=[{
            "role": "user",
            "content": f"""
            Analyze this data from Excel:
            Columns: {headers}
            Total rows: {len(data_rows)}
            First records: {json.dumps(sample[:10], ensure_ascii=False, default=str)}
            
            Give me:
            1. A short description of what's in the table
            2. Key observations (3-5 points)
            3. Anomalies, if any
            4. Recommendations
            """
        }]
    ).content if b.type == "text")
    
    return analysis

def generate_excel_report(data: list, output_path: str, title: str = "AI Report"):
    """Create a polished Excel report with data and a chart."""
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "Data"
    
    # Title
    ws.merge_cells('A1:E1')
    title_cell = ws['A1']
    title_cell.value = title
    title_cell.font = Font(bold=True, size=14, color="FFFFFF")
    title_cell.fill = PatternFill(start_color="0076D7", fill_type="solid")
    title_cell.alignment = Alignment(horizontal='center')
    
    # Column headers
    if data:
        headers = list(data[0].keys())
        for col, header in enumerate(headers, 1):
            cell = ws.cell(row=2, column=col, value=header)
            cell.font = Font(bold=True)
            cell.fill = PatternFill(start_color="E0E7FF", fill_type="solid")
    
    # Data
    for row_idx, row_data in enumerate(data, 3):
        for col_idx, value in enumerate(row_data.values(), 1):
            ws.cell(row=row_idx, column=col_idx, value=value)
    
    wb.save(output_path)
    return output_path

PowerPoint: automatic presentations

python
from datetime import datetime
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
import anthropic

def generate_pptx_from_claude(topic: str, key_points: list, output_path: str):
    """Claude creates the content → python-pptx builds the presentation."""
    client = anthropic.Anthropic()
    
    # Generate the slides
    slides_content = "".join(b.text for b in client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=2000,
        messages=[{
            "role": "user",
            "content": f"""
            Create an outline for a 5-slide presentation on the topic: {topic}
            
            Key points: {', '.join(key_points)}
            
            Format for each slide:
            SLIDE N:
            TITLE: [name]
            POINTS:
            - [point 1]
            - [point 2]
            - [point 3]
            NOTES: [for the speaker]
            ---
            """
        }]
    ).content if b.type == "text")
    
    prs = Presentation()
    prs.slide_width = Inches(13.33)
    prs.slide_height = Inches(7.5)
    
    # Title slide
    slide = prs.slides.add_slide(prs.slide_layouts[0])
    slide.shapes.title.text = topic
    slide.placeholders[1].text = f"Prepared: {datetime.now().strftime('%m/%d/%Y')}"
    
    # Parse and add the slides
    current_slide_data = {}
    for line in slides_content.split('\n'):
        if line.startswith('SLIDE'):
            if current_slide_data.get('title'):
                _add_content_slide(prs, current_slide_data)
            current_slide_data = {'title': '', 'bullets': [], 'notes': ''}
        elif line.startswith('TITLE:'):
            current_slide_data['title'] = line.replace('TITLE:', '').strip()
        elif line.startswith('- '):
            current_slide_data['bullets'].append(line[2:].strip())
        elif line.startswith('NOTES:'):
            current_slide_data['notes'] = line.replace('NOTES:', '').strip()
    
    if current_slide_data.get('title'):
        _add_content_slide(prs, current_slide_data)
    
    prs.save(output_path)
    return output_path

def _add_content_slide(prs, data):
    slide = prs.slides.add_slide(prs.slide_layouts[1])
    slide.shapes.title.text = data['title']
    tf = slide.placeholders[1].text_frame
    tf.text = ''
    for bullet in data['bullets']:
        p = tf.add_paragraph()
        p.text = bullet
        p.level = 0

Power Platform + Claude

Power Automate: HTTP connector

In Power Automate, create a Flow:

Code
Trigger: Manual / Schedule / Email received
    ↓
Action: HTTP
    Method: POST
    URL: https://api.anthropic.com/v1/messages
    Headers:
        x-api-key: [your key]
        anthropic-version: 2023-06-01
        content-type: application/json
    Body: {
        "model": "claude-haiku-4-5",
        "max_tokens": 500,
        "messages": [{"role": "user", "content": "@{triggerBody()?['text']}"}]
    }
    ↓
Action: Send email / Post to Teams / Update SharePoint

This is the simplest no-code integration: Claude without a single line of code. The HTTP action is one of Power Automate's premium connectors, so you need a suitable license. Don't paste the API key into the flow as plain text: keep it in a secure secrets store (for example, Azure Key Vault).

Power Apps: custom connector

  1. Power Apps → Custom Connectors → New
  2. Add the Anthropic API as a connector
  3. Set up actions: sendMessage, etc.
  4. Use it in any Power App as a component

Microsoft Graph API: a single data hub

python
from msal import ConfidentialClientApplication
import requests

class MSGraphClient:
    def __init__(self):
        app = ConfidentialClientApplication(
            client_id=os.environ["AZURE_CLIENT_ID"],
            client_credential=os.environ["AZURE_CLIENT_SECRET"],
            authority=f"https://login.microsoftonline.com/{os.environ['AZURE_TENANT_ID']}"
        )
        token_result = app.acquire_token_for_client(
            scopes=["https://graph.microsoft.com/.default"]
        )
        self.headers = {
            "Authorization": f"Bearer {token_result['access_token']}",
            "Content-Type": "application/json"
        }
        self.base = "https://graph.microsoft.com/v1.0"
    
    def get_recent_emails(self, user_id: str, count: int = 10) -> list:
        url = f"{self.base}/users/{user_id}/messages"
        params = {"$top": count, "$orderby": "receivedDateTime desc"}
        r = requests.get(url, headers=self.headers, params=params)
        return r.json().get("value", [])
    
    def send_teams_message(self, team_id: str, channel_id: str, message: str):
        # Posting to a channel through Graph only works with a delegated token (on behalf of a user).
        # With an app token, as in this class, Graph returns 403: for notifications, use a Workflows webhook.
        url = f"{self.base}/teams/{team_id}/channels/{channel_id}/messages"
        payload = {"body": {"content": message}}
        requests.post(url, headers=self.headers, json=payload)
    
    def get_sharepoint_files(self, site_id: str, folder: str = "root") -> list:
        url = f"{self.base}/sites/{site_id}/drive/root:/{folder}:/children"
        r = requests.get(url, headers=self.headers)
        return r.json().get("value", [])

# Claude processes corporate data
def process_emails_with_claude(user_id: str):
    graph = MSGraphClient()
    emails = graph.get_recent_emails(user_id, count=20)
    
    email_texts = [
        f"From: {e['from']['emailAddress']['address']}\nSubject: {e['subject']}\n"
        f"Date: {e['receivedDateTime'][:10]}"
        for e in emails
    ]
    
    client = anthropic.Anthropic()
    summary = "".join(b.text for b in client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=500,
        messages=[{
            "role": "user",
            "content": f"Write a short summary of these emails and flag the urgent ones:\n" + "\n---\n".join(email_texts)
        }]
    ).content if b.type == "text")
    
    return summary

Azure + Claude: corporate infrastructure

Two approaches:

1. The Claude API called directly from Azure infrastructure:

Code
Azure App Service → Claude API (api.anthropic.com)
Azure Functions → Claude API
Azure Container Apps → Claude API

Your code runs in Azure and calls Anthropic directly. Simple and reliable.

2. Claude in Microsoft Foundry (formerly Azure AI Foundry):

If the client says "our data can't leave Azure," Claude is available in Microsoft Foundry. There are two hosting options: on Azure infrastructure ("Hosted on Azure") and on Anthropic infrastructure ("Hosted on Anthropic infrastructure"); not every model is available in both. You need a paid Azure subscription in a supported country and a subscription to the model through Azure Marketplace. Check exactly where the data is processed and which regions are available in the Microsoft Learn documentation and in the contract: the word "Azure" alone doesn't settle compliance requirements.

python
# Claude through Microsoft Foundry: the same Anthropic SDK, a different client and address
import os
from anthropic import AnthropicFoundry

client = AnthropicFoundry(
    api_key=os.environ["AZURE_AI_KEY"],
    base_url=os.environ["AZURE_AI_ENDPOINT"],  # https://<resource-name>.services.ai.azure.com/anthropic
)

response = client.messages.create(
    model=os.environ["AZURE_DEPLOYMENT_NAME"],  # the deployment name you chose when deploying
    max_tokens=500,
    messages=[{"role": "user", "content": "Hello"}]
)
print("".join(b.text for b in response.content if b.type == "text"))

Instead of a key, it's better to use Microsoft Entra ID (DefaultAzureCredential): see the example in the Microsoft Learn documentation ("Deploy and use Claude models in Microsoft Foundry").


Practice

Step 1: Teams Webhook (15 min)

  1. Create a test Teams channel
  2. Create a webhook through the Workflows app and copy the link
  3. Run the basic example from the lesson
  4. Make sure the message showed up

Step 2: Word with python-docx (20 min)

bash
pip install python-docx anthropic

Generate a Word document on any topic using the code from the lesson. Open the file and check the formatting.

Step 3: Excel analysis (15 min)

bash
pip install openpyxl

Take any Excel file with data. Run analyze_excel_with_claude(). See what Claude says about the data.

Step 4: Power Automate (optional, requires M365)

If you have access to Microsoft 365:

  1. Create a Flow in Power Automate
  2. Trigger: manual
  3. HTTP action → Claude API (premium connector)
  4. Send the result to Teams

Step 5: Practice assignment

Pick one real scenario:

Option A: A weekly report in Teams. A script collects data from any source, Claude analyzes it, and the result is a card in a Teams channel.

Option B: A Word report from a template. Claude generates content for a specific task, and python-docx formats it as a corporate document.

Option C: An email digest. Graph API reads the mailbox, Claude writes a summary, and the result goes to Outlook or Teams.


Tools and resources

  • python-docx: pip install python-docx
  • openpyxl: pip install openpyxl
  • python-pptx: pip install python-pptx
  • MSAL (Microsoft auth): pip install msal
  • Microsoft Graph Docs: learn.microsoft.com/graph
  • Teams Webhooks Docs: learn.microsoft.com/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook
  • Microsoft Foundry (formerly Azure AI Foundry): ai.azure.com
  • Microsoft 365 Agents SDK: learn.microsoft.com/microsoft-365/agents-sdk
  • Power Automate: make.powerautomate.com
  • Adaptive Cards Designer: adaptivecards.io/designer

Key takeaways

The corporate market lives in Microsoft. Knowing how to integrate Claude with Teams, Office and Power Platform is a useful skill for working with corporate clients. Start with the simplest thing: a Workflows webhook for Teams takes a few dozen lines of code. Next, Graph API opens up the organization's data (within the permissions you've been granted). Microsoft Foundry helps answer the data-residency question for enterprise clients, but check compliance requirements in the documentation and the contract.


The mark stays in this browser only and is never sent anywhere. My progress