Skip to content

Teams migration (export)

Back up Teams through the unified MigrationJob adapters — team structure, members, channels, tabs, apps, messages (with replies and inline images), and optionally channel files. The job is resumable: it persists what to copy (the manifest) and what is done (the checkpoint), so --resume continues an interrupted run. Import is planned next.

This is the heavier sibling of ../export/: use export/ for a quick inventory, and this toolkit when you need a portable, verifiable archive.

Scripts

Script What it does
migrate/export_team.py Resumable team archive (structure, members, channels, messages, --files)
migrate/export_messages.py Compliance message export via deferred collection adapters

Archive layout

<output>/
  <team-id>/
    team.json                        # the team as Graph spells it, with members, installedApps, channels (members, tabs)
    messages.ndjson                  # one message/reply per line
    attachments/<channel>/<msg>_<content>.<ext>
    files/<channel>.zip              # --files

Commands

# One team (repeat --team for several), with channel files
python migrate/export_team.py --team 00000000-0000-0000-0000-000000000000 --output /tmp/backup --files

# Every team in the tenant
python migrate/export_team.py --all --output /tmp/backup

# Resume an interrupted run (reloads the manifest + checkpoint)
python migrate/export_team.py --all --output /tmp/backup --resume

# Compliance message export (deferred collection adapters)
python migrate/export_messages.py --output /tmp/messages --team <team-id>
python migrate/export_messages.py --output /tmp/messages --user user@contoso.com
python migrate/export_messages.py --output /tmp/messages --tenant
python migrate/export_messages.py --output /tmp/messages --filter "createdDateTime gt 2024-01-01T00:00:00Z"

Channel-level members are skipped by default and included only with --channel-members, because reading them needs ChannelMember.Read.All, which many tenants do not consent. export_messages.py defaults to --user; --team and --tenant need their own scopes, and the script explains the missing one instead of crashing.

Permissions (application)

Scenario Permissions
Structure, members, channels, tabs, apps Team.ReadBasic.All, TeamMember.Read.All
Channel-level members (--channel-members) ChannelMember.Read.All
Messages ChannelMessage.Read.All, Chat.Read.All, Teamwork.Migrate.All
Channel files (--files) Files.Read.All, Sites.Read.All

Library usage

from pathlib import Path

from office365.graph_client import GraphClient
from office365.migration import MigrationJob
from office365.migration.teams import TeamsArchiveSource, TeamsArchiveTarget, TeamsExportOptions

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

out = Path("/tmp/backup")
job = MigrationJob(
    TeamsArchiveSource(client, ["00000000-..."], TeamsExportOptions(include_files=True)),
    TeamsArchiveTarget(out),
    manifest_path=out.with_name(out.name + ".manifest.json"),
    checkpoint_path=out.with_name(out.name + ".checkpoint.json"),
)
job.plan()
stats = job.run()
print(stats.summary(), job.verify().summary())

# Messages only — deferred collection adapters
with open("/tmp/messages.ndjson", "w") as f:
    client.teams.get_all_messages().filter("createdDateTime gt 2024-01-01T00:00:00Z").get_all().to_ndjson(
        f
    ).download_hosted_contents("/tmp/attachments").execute_query()

Pass both manifest_path and checkpoint_path for resume() to work: the first restores what to copy, the second restores what is done.

Reference