Setting Up M365 Connectors

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.


What You'll Set Up

ConnectorWhat It DoesPermission Needed
Teams BotCaptures decisions from Teams conversations, sends capture notifications, handles opt-out and delete commandsRSC: ChatMessage.Read.Chat, ChannelMessage.Read.Group
Email WatcherMonitors Exchange Online inboxes for unread emails, reconstructs threads, ingests into ThreadWeaveMail.Read
SharePoint WatcherDiscovers SharePoint sites and monitors document libraries for changesSites.Read.All
Only need your own mailbox? The email watcher can read it as you, with no client secret and no admin consent to request (an admin approves once only if your tenant blocks user consent), so you can skip the app-only registrations in the steps below. It needs the same one Entra step: an app registration with "Allow public client flows" enabled and the delegated Mail.Read permission (reuse ThreadWeave-GraphReader from this guide), and its client ID in THREADWEAVE_MAIL_CLIENT_ID.

Sign in once, then run the watcher in single-user mode:
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.

Microsoft's own Azure CLI client does NOT work for this (AADSTS65002, first-party clients are blocked for Mail.Read), which is why the client ID has to be an app you registered. For a one-shot trial without the daemon, ingest_graph_mail.py is still in the repository: python ingest_graph_mail.py --dry-run.

Prerequisites

Don't have a tenant? The free M365 E5 developer sandbox is no longer open to everyone as of 2026. You now need a Visual Studio Professional/Enterprise subscription or membership in the Microsoft AI Cloud Partner Program. Check with your IT department for a dev/test tenant.

Step 1: Create the Azure App Registrations

We recommend two separate app registrations — different permissions, different security boundaries.

App 1: ThreadWeave-GraphReader

Used by the Email Watcher and SharePoint Watcher.

  1. Go to Azure Portal → Microsoft Entra ID → App registrations → New registration
  2. Name: ThreadWeave-GraphReader
  3. Supported account types: "Accounts in this organizational directory only"
  4. Redirect URI: leave blank (app-only flow)
  5. Click Register

After creation, note these values:

Application (client) ID:  _______________
Directory (tenant) ID:    _______________

Now create a client secret and set permissions:

  1. Certificates & secrets → New client secret → name it → 24 months → Add
  2. Copy the secret Value immediately (not the Secret ID — that's different)
  3. API permissions → Add a permission → Microsoft Graph
  4. Select the Application permissions tab (not Delegated)
  5. Add: Mail.Read, Sites.Read.All
  6. Click "Grant admin consent for <tenant>"
Critical: Adding permissions is not enough. You must click "Grant admin consent." Also, make sure you're on the Application permissions tab — the dialog defaults to Delegated, but ThreadWeave uses app-only flow which needs Application permissions.

Step 2: Set Environment Variables

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"

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"

Step 3: Verify Authentication

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:


Step 4: Test Each Connector

Email Watcher

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.

SharePoint Watcher

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.


Step 5: Install the Teams App

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:


Troubleshooting

ErrorLikely CauseFix
AADSTS7000215: Invalid client secretUsed Secret ID instead of ValueCopy the Value field from Certificates & secrets, not the Secret ID GUID
AADSTS700016: Application not foundWrong tenant directoryCheck top-right of Azure Portal for active directory. App registrations are per-tenant
roles is empty in JWTDelegated permissions used, or admin consent not grantedSwitch to Application permissions tab, re-add, click "Grant admin consent"
401 Unauthorized on mailboxMailbox not in tenant, or no Exchange Online licenseVerify user has Exchange Online and is in the same tenant as the app registration
400: Tenant does not have a SPO licenseNo SharePoint Online in tenantTenant needs SharePoint Online license. Bare Entra ID tenants don't include it
AADSTS65002: Consent between first party applicationDevice-code flow used Microsoft's Azure CLI client IDUse your own app registration (public client flows enabled) via THREADWEAVE_PUBLISH_CLIENT_ID
AADSTS50059: No tenant-identifying informationDevice-code authority used common instead of the tenantSet AZURE_TENANT_ID so the authority is tenant-scoped
403: Caller doesn't have permissions to publish to the Teams App catalogDirect upload without review; AppCatalog.Submit submits for review onlyUse ?requiresReview=true (the CLI does this automatically), then approve in the admin center
409 AppDefinitionAlreadyExistsThe same package version is already in the org catalogExpected on re-publish; the CLI prints "Already published" and exits cleanly. Bump --version for updates

Production Checklist


View on GitHub