Connect ThreadWeave to Exchange Online, SharePoint, and Microsoft Copilot via Microsoft Graph.
ThreadWeave has three M365 connectors that use app-only client credentials to read your organization's Microsoft 365 data. This guide walks through the Azure setup for each one.
| Connector | What It Does | Permission Needed |
|---|---|---|
| Email Watcher | Monitors Exchange Online inboxes for unread emails, reconstructs threads, ingests into ThreadWeave | Mail.Read |
| SharePoint Watcher | Discovers SharePoint sites and monitors document libraries for changes | Sites.Read.All |
| Graph External Connector | Pushes ThreadWeave entries to Microsoft Graph so they appear in Copilot and Microsoft Search results | ExternalConnection.ReadWrite.OwnedBy |
ingest_graph_mail.py instead — it uses device-code OAuth (browser sign-in, MFA supported) and needs no Azure app registration. The connectors below are for organization-wide, automated ingestion.
threadweave serve)We recommend two separate app registrations — different permissions, different security boundaries.
Used by the Email Watcher and SharePoint Watcher.
ThreadWeave-GraphReaderAfter creation, note these values:
Application (client) ID: _______________ Directory (tenant) ID: _______________
Now create a client secret and set permissions:
Mail.Read, Sites.Read.AllUsed by the Graph External Connector to sync ThreadWeave entries to Copilot.
Repeat the same registration steps, with these differences:
ThreadWeave-CopilotConnectorExternalConnection.ReadWrite.OwnedBy (Application)Set these before starting ThreadWeave:
# For Email Watcher + SharePoint Watcher export AZURE_TENANT_ID="your-tenant-id" export AZURE_CLIENT_ID="your-graphreader-client-id" export AZURE_CLIENT_SECRET="your-graphreader-secret" # For Graph External Connector (Copilot) export THREADWEAVE_GRAPH_TENANT_ID="your-tenant-id" export THREADWEAVE_GRAPH_CLIENT_ID="your-copilotconnector-client-id" export THREADWEAVE_GRAPH_CLIENT_SECRET="your-copilotconnector-secret"
On Windows PowerShell:
$env:AZURE_TENANT_ID = "your-tenant-id" $env:AZURE_CLIENT_ID = "your-graphreader-client-id" $env:AZURE_CLIENT_SECRET = "your-graphreader-secret"
Before running the connectors, verify your app registrations work:
from msal import ConfidentialClientApplication
import json, base64
app = ConfidentialClientApplication(
client_id="your-client-id",
client_credential="your-secret",
authority=f"https://login.microsoftonline.com/your-tenant-id",
)
result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
if "access_token" not in result:
print(f"FAILED: {result.get('error_description', result)}")
else:
token = result["access_token"]
parts = token.split('.')
payload = base64.urlsafe_b64decode(parts[1] + '=' * (4 - len(parts[1]) % 4))
claims = json.loads(payload)
roles = claims.get('roles', [])
print(f"OK — roles: {roles}")
If roles is empty:
Xmf8Q~...Fetches unread emails from an Exchange Online inbox and reconstructs conversation threads:
uv run python -c "
import asyncio, os, sys
sys.path.insert(0, '.')
from threadweave.connectors.email.watcher import MailWatcher
async def test():
watcher = MailWatcher()
emails = await watcher.fetch_unread(
mailbox='user@your-tenant.com',
max_results=5,
)
print(f'Fetched {len(emails)} unread emails')
for e in emails:
print(f' {e.subject[:60]} — {e.sender_name}')
asyncio.run(test())
"
Expected: lists unread emails from the specified mailbox.
Discovers SharePoint sites in your tenant:
uv run python -c "
import asyncio, os, sys
sys.path.insert(0, '.')
from threadweave.connectors.sharepoint.watcher import GraphClient
async def test():
client = GraphClient()
sites = await client.list_sites()
print(f'Found {len(sites)} SharePoint sites')
for s in sites:
print(f' {s.display_name} — {s.web_url}')
asyncio.run(test())
"
Expected: lists SharePoint sites. If you get 400 Bad Request: Tenant does not have a SPO license, your tenant lacks SharePoint Online.
Creates an external connection and registers the ThreadWeave schema:
uv run python -c "
from threadweave.connectors.graph.connector import ThreadWeaveGraphConnector
connector = ThreadWeaveGraphConnector()
print(f'Configured: {connector.is_configured}')
# Create connection and register schema
result = connector.register_schema()
print(f'Schema registered: {result}')
# Verify connection
info = connector.get_connection()
print(f'Connection state: {info}')
"
Expected: connection created (201), schema registered. If schema registration fails with 400, your tenant may lack Graph Connectors licensing.
| Error | Likely Cause | Fix |
|---|---|---|
AADSTS7000215: Invalid client secret | Used Secret ID instead of Value | Copy the Value field from Certificates & secrets, not the Secret ID GUID |
AADSTS700016: Application not found | Wrong tenant directory | Check top-right of Azure Portal for active directory. App registrations are per-tenant |
roles is empty in JWT | Delegated permissions used, or admin consent not granted | Switch to Application permissions tab, re-add, click "Grant admin consent" |
401 Unauthorized on mailbox | Mailbox not in tenant, or no Exchange Online license | Verify user has Exchange Online and is in the same tenant as the app registration |
400: Tenant does not have a SPO license | No SharePoint Online in tenant | Tenant needs SharePoint Online license. Bare Entra ID tenants don't include it |
500 on POST /external/connections | No Graph connectors license | Requires M365 E5 or Graph connectors add-on. Dev sandbox may not support it |
403 on upsert/delete | Schema not registered yet | Run register_schema() first — items can't be created without a schema |
/api/v1/audit/recent for connector activity--dry-run and small --max values before full ingestion