Connect ThreadWeave to Exchange Online and SharePoint via Microsoft Graph.
ThreadWeave has three M365 connectors for your organization's Microsoft 365 data: the email watcher and the SharePoint watcher read app-only with client credentials, and the Teams bot receives messages through the Bot Framework. This guide walks through the Azure setup for each one.
| Connector | What It Does | Permission Needed |
|---|---|---|
| Teams Bot | Captures decisions from Teams conversations, sends capture notifications, handles opt-out and delete commands | RSC: ChatMessage.Read.Chat, ChannelMessage.Read.Group |
| 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 |
Mail.Read permission (reuse ThreadWeave-GraphReader from this guide), and its client ID in THREADWEAVE_MAIL_CLIENT_ID.uv run python -m threadweave.cli email login export THREADWEAVE_PROFILE=personal uv run python -m threadweave.cli email watch
email login prints a code and a URL: open https://microsoft.com/devicelogin, enter the code, and sign in. The token is cached and refreshed silently from then on. Under the personal profile the watcher reads your own mailbox over /me and files captures in the personal tenant, where you are the only reader. The profile is documented in the repository: docs/personal-profile.md.ingest_graph_mail.py is still in the repository: python ingest_graph_mail.py --dry-run.
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.AllSet 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"
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.
The Teams bot is distributed as an app package. Build it with the packaged CLI so every org gets a validated, deterministic zip:
uv run python -m threadweave.cli teams package \ --bot-id YOUR-BOT-APP-ID --version 1.0.1 # → dist/threadweave-teams-app-1.0.1.zip
Two ways to get the package into an org. For a pilot, upload it manually in the Teams admin center:
admin.teams.microsoft.com → Teams apps → Manage apps → Upload new app → Publish
For customer onboarding, publish it with one command. The admin signs in once with a device code, and the app lands in the org catalog pending approval:
uv run python -m threadweave.cli teams publish \ --package dist/threadweave-teams-app-1.0.1.zip # 1. Open https://microsoft.com/devicelogin # 2. Enter the code, sign in as a Teams admin # → Uploaded, then approve in admin.teams.microsoft.com → Publish
Setup notes for the publish command:
THREADWEAVE_PUBLISH_CLIENT_ID to an app registration with the delegated AppCatalog.Submit permission and public client flows enabledAZURE_TENANT_ID; the sign-in authority must be tenant-scoped, the generic common authority fails with AADSTS50059requiresReview=true; AppCatalog.Submit submits for review only, a direct upload returns 403 even for global admins| 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 |
AADSTS65002: Consent between first party application | Device-code flow used Microsoft's Azure CLI client ID | Use your own app registration (public client flows enabled) via THREADWEAVE_PUBLISH_CLIENT_ID |
AADSTS50059: No tenant-identifying information | Device-code authority used common instead of the tenant | Set AZURE_TENANT_ID so the authority is tenant-scoped |
403: Caller doesn't have permissions to publish to the Teams App catalog | Direct upload without review; AppCatalog.Submit submits for review only | Use ?requiresReview=true (the CLI does this automatically), then approve in the admin center |
409 AppDefinitionAlreadyExists | The same package version is already in the org catalog | Expected on re-publish; the CLI prints "Already published" and exits cleanly. Bump --version for updates |
/api/v1/audit/recent for connector activity--dry-run and small --max values before full ingestion