Skip to content

Find users assigned a Copilot license who haven't signed in recently.

Cross-references Copilot SKUs with per-user license assignments and last sign-in activity to flag underused (or unused) Copilot licenses — the input for a reclaim or adoption campaign.

Reading signInActivity needs AuditLog.Read.All; if the app lacks it the script prints the hint and exits without failing.

python underused_licenses.py
python underused_licenses.py --days 60 --limit 100

Permissions

Requires application permissions Organization.Read.All, User.Read.All and AuditLog.Read.All.

Reference

View source

from __future__ import annotations

import argparse
from datetime import datetime, timedelta, timezone

from office365.graph_client import GraphClient
from office365.runtime.client_request_exception import ClientRequestException
from tests.settings import client_id, client_secret, tenant

COPILOT_KEYWORDS = ("COPILOT",)


def main() -> None:
    parser = argparse.ArgumentParser(description="Find underused Copilot licenses")
    parser.add_argument("--days", type=int, default=30, help="No-sign-in threshold in days (default: 30)")
    parser.add_argument("--limit", type=int, default=100, help="Maximum users to list (default: 100)")
    args = parser.parse_args()

    client = GraphClient(tenant=tenant).with_client_secret(client_id, client_secret)

    sku_ids = {
        sku.sku_id
        for sku in client.subscribed_skus.get().execute_query()
        if any(keyword in (sku.sku_part_number or "").upper() for keyword in COPILOT_KEYWORDS)
    }
    if not sku_ids:
        print("No Copilot SKUs found in the tenant — nothing to check.")
        return

    try:
        users = list(
            client.users.select(["displayName", "userPrincipalName", "assignedLicenses", "signInActivity"])
            .get()
            .execute_query()
        )
    except ClientRequestException as exc:
        if exc.response is not None and exc.response.status_code in (401, 403):
            print("Could not read sign-in activity; grant the 'AuditLog.Read.All' application permission.")
            return
        raise

    cutoff = datetime.now(timezone.utc) - timedelta(days=args.days)
    underused = []
    for user in users:
        licensed = [lic for lic in (user.assigned_licenses or []) if lic.skuId in sku_ids]
        if not licensed:
            continue
        last_sign_in = user.sign_in_activity.lastSignInDateTime
        if last_sign_in is None or last_sign_in < cutoff:
            underused.append((user, last_sign_in))

    print(f"Copilot users with no sign-in for {args.days}+ days: {len(underused)}\n")
    for user, last_sign_in in underused[: args.limit]:
        print(f"  {user.user_principal_name:45s}  last sign-in: {last_sign_in or 'never'}")


if __name__ == "__main__":
    main()

← Back to Microsoft 365 Copilot