Migrate direct messages (1:1 and group DMs) from Slack to Microsoft Teams with full timestamp and user attribution fidelity.
This project was built and is maintained with the assistance of Claude, an AI assistant by Anthropic.
- Three destination modes: native Teams chats, archive channels, or automatic hybrid routing
- 1:1 and group DMs: migrates both
im(1:1) andmpim(group) Slack conversations - Timestamp preservation: messages appear with their original Slack timestamps
- User attribution: messages are attributed to the correct user via Azure AD mapping
- Bot attribution fallback: unmapped users' messages are posted by the app with
[Display Name]prepended - File migration: downloads files from Slack and uploads to Teams/SharePoint
- Resumable: state file tracks progress per-conversation for safe restart
- Formatting conversion: Slack mrkdwn converted to Teams-compatible HTML (bold, italic, code, links, mentions, blockquotes, emoji)
- Rate limiting: built-in Bottleneck-based rate limiting for both Slack and Teams APIs
- Dry-run mode: preview what would be migrated without posting anything
- Node.js >= 18
- A Slack Bot Token (
xoxb-...) — see Slack Setup below - An Azure AD App Registration with application permissions — see Azure AD / Microsoft Teams Setup below
Not every mode needs every permission. Use these tables to grant only what you need:
| Scope | Chat mode | Channel mode | Auto mode | Purpose |
|---|---|---|---|---|
im:read |
Required | Required | Required | List 1:1 DM conversations |
im:history |
Required | Required | Required | Read 1:1 DM message history |
mpim:read |
Required | Required | Required | List group DM conversations |
mpim:history |
Required | Required | Required | Read group DM message history |
users:read |
Required | Required | Required | Resolve user display names |
users:read.email |
Required | Required | Required | Look up user emails for user mapping |
files:read |
Required | Required | Required | Download file attachments |
| Permission | Chat mode | Channel mode | Auto mode | Purpose |
|---|---|---|---|---|
Teamwork.Migrate.All |
Required | Required | Required | Enable migration mode for timestamp backdating and user attribution |
User.Read.All |
Required | Required | Required | Look up Azure AD users by email for user mapping |
Chat.ReadWrite.All |
Required | — | Required | Create Teams chats and post messages |
Team.ReadBasic.All |
— | Required | Required | Read team information |
Channel.Create |
— | Required | Required | Create private archive channels |
ChannelMessage.ReadWrite.All |
— | Required | Required | Post messages to channels |
Files.ReadWrite.All |
— | Required | Required | Upload file attachments to SharePoint |
Auto mode needs all permissions from both chat and channel modes because it routes per-conversation at runtime.
- Go to api.slack.com/apps
- Click Create New App → From scratch
- Name it something like
DM Migratorand select your workspace - Click Create App
- In the left sidebar, click OAuth & Permissions
- Scroll down to Scopes → Bot Token Scopes
- Click Add an OAuth Scope and add each of the following:
| Scope | What it does |
|---|---|
im:read |
Allows the bot to list 1:1 DM conversations in the workspace |
im:history |
Allows the bot to read messages in 1:1 DMs |
mpim:read |
Allows the bot to list group DM conversations in the workspace |
mpim:history |
Allows the bot to read messages in group DMs |
users:read |
Allows the bot to look up user display names and profile info |
users:read.email |
Allows the bot to read user email addresses (needed for matching to Azure AD accounts) |
files:read |
Allows the bot to download shared file attachments |
- Scroll to the top of the OAuth & Permissions page
- Click Install to Workspace (or Reinstall to Workspace if updating scopes)
- Review the permissions and click Allow
- Copy the Bot User OAuth Token — it starts with
xoxb-
This token is your SLACK_TOKEN / --slack-token value.
Note on DM access: A bot token with the scopes above can read DM history for all users in the workspace. Unlike channels, the bot does not need to be a member of each DM conversation. These scopes grant workspace-wide read access to direct message data.
- Go to the Azure Portal → App registrations
- Click New registration
- Fill in:
- Name:
Slack DM Migrator(or any name you prefer) - Supported account types: Accounts in this organizational directory only (Single tenant)
- Redirect URI: Leave blank (not needed for client credentials flow)
- Name:
- Click Register
- On the Overview page, note down:
- Application (client) ID — this is your
TEAMS_CLIENT_ID - Directory (tenant) ID — this is your
TEAMS_TENANT_ID
- Application (client) ID — this is your
- In the left sidebar, click API permissions
- Click Add a permission → Microsoft Graph → Application permissions
- Search for and add the permissions you need based on your mode:
| Permission | When you need it | What it enables |
|---|---|---|
Teamwork.Migrate.All |
Always | Posts messages with original timestamps and user attribution via migration mode |
User.Read.All |
Always | Looks up Azure AD users by email address for the generate-user-map command |
Chat.ReadWrite.All |
Chat mode or Auto mode | Creates 1:1 and group chats in Teams and posts messages to them |
Team.ReadBasic.All |
Channel mode or Auto mode | Reads team metadata to verify the app has access |
Channel.Create |
Channel mode or Auto mode | Creates private archive channels in the target team |
ChannelMessage.ReadWrite.All |
Channel mode or Auto mode | Posts messages into Teams channels during migration |
Files.ReadWrite.All |
Channel mode or Auto mode | Uploads file attachments to the channel's SharePoint document library |
Important: Make sure you select Application permissions, not Delegated permissions. Application permissions allow the tool to run unattended using client credentials (no signed-in user required).
- After adding permissions, you'll see them listed with a status of "Not granted"
- Click the Grant admin consent for [Your Organization] button
- Confirm by clicking Yes
- All permissions should now show a green checkmark with status "Granted for [Your Organization]"
This step requires Global Administrator or Privileged Role Administrator privileges in your Azure AD tenant. If you don't have this role, ask your IT admin to grant consent.
- In the left sidebar, click Certificates & secrets
- Click New client secret
- Add a description (e.g.,
DM Migrator) and choose an expiration period - Click Add
- Immediately copy the secret Value — it is only shown once
This is your TEAMS_CLIENT_SECRET. If you lose the value, you'll need to create a new secret.
Note: The Secret ID is not the same as the Secret Value. You need the Value (the long string), not the ID (the GUID).
If using --mode channel or --mode auto, you need the Team GUID where archive channels will be created:
- In Microsoft Teams, navigate to the team you want to use
- Click the three dots (⋯) next to the team name → Get link to team
- The link contains the Team ID as the
groupIdparameter:https://teams.microsoft.com/l/team/...?groupId=<YOUR-TEAM-ID>&... - Copy the
groupIdvalue — this is yourTEAMS_TEAM_ID
npm install -g slack-to-teams-dm-migratorOr clone and build from source:
git clone https://github.com/Kilowhisky/slack-to-teams-dm-migrator.git
cd slack-to-teams-dm-migrator
npm install
npm run buildCopy the example environment file and fill in your credentials:
cp .env.example .env
# Edit .env with your Slack token and Azure AD credentialsOr pass credentials as CLI flags or set them as environment variables directly.
Match Slack users to Azure AD accounts by email:
slack-to-teams-dm generate-user-map \
--slack-token $SLACK_TOKEN \
--teams-tenant-id $TEAMS_TENANT_ID \
--teams-client-id $TEAMS_CLIENT_ID \
--teams-client-secret $TEAMS_CLIENT_SECRET \
-o user-map.jsonThis fetches all Slack users, looks up each email in Azure AD, and writes the matched pairs to user-map.json. Review the output — it will list any unmatched users.
slack-to-teams-dm validate \
--slack-token $SLACK_TOKEN \
--teams-tenant-id $TEAMS_TENANT_ID \
--teams-client-id $TEAMS_CLIENT_ID \
--teams-client-secret $TEAMS_CLIENT_SECRETslack-to-teams-dm list-conversations --slack-token $SLACK_TOKEN --types bothslack-to-teams-dm migrate \
--slack-token $SLACK_TOKEN \
--teams-tenant-id $TEAMS_TENANT_ID \
--teams-client-id $TEAMS_CLIENT_ID \
--teams-client-secret $TEAMS_CLIENT_SECRET \
--user-map-file user-map.json \
--mode autoCreates native Teams 1:1 or group chats. Messages appear in the Teams "Chat" sidebar with original timestamps and user attribution. Uses the Graph beta API for migration mode.
Best for: organizations where most Slack users have Azure AD accounts and you want DMs to feel native in Teams.
Creates private archive channels in a designated Team (requires --teams-team-id). One channel per conversation, named like dm-alice-bob. Uses the stable v1.0 channel migration API.
Best for: archival purposes, or when many participants don't have Azure AD accounts.
Routes per-conversation: if both sides of a 1:1 DM (or 2+ participants of a group DM) have Azure AD mappings, uses chat mode. Otherwise falls back to channel mode (if --teams-team-id is provided).
Best for: most migrations — gets the best result for each conversation automatically.
| Option | Description | Default |
|---|---|---|
--slack-token <token> |
Slack Bot token (xoxb-...) |
$SLACK_TOKEN |
--teams-tenant-id <id> |
Azure AD tenant ID | $TEAMS_TENANT_ID |
--teams-client-id <id> |
Azure AD app client ID | $TEAMS_CLIENT_ID |
--teams-client-secret <secret> |
Azure AD app client secret | $TEAMS_CLIENT_SECRET |
--mode <mode> |
chat, channel, or auto |
auto |
--teams-team-id <id> |
Teams team GUID (channel mode) | $TEAMS_TEAM_ID |
--conversation-ids <ids> |
Comma-separated Slack conversation IDs | all |
--conversation-types <types> |
im, mpim, or both |
both |
--skip-bot-dms |
Skip DMs where the other participant is a bot | false |
--oldest <date> |
Earliest message date (ISO 8601 or Unix timestamp) | — |
--latest <date> |
Latest message date | — |
--user-map-file <path> |
Path to user mapping JSON file | — |
--state-file <path> |
Path to state file | ./dm-migration-state.json |
--dry-run |
Fetch and transform without posting to Teams | false |
--concurrency <n> |
Concurrent Teams API requests (1–5) | 1 |
--verbose |
Enable debug logging | false |
List Slack DM conversations with participant names.
slack-to-teams-dm list-conversations --slack-token $SLACK_TOKEN --types both
slack-to-teams-dm list-conversations --slack-token $SLACK_TOKEN --types im --jsonGenerate a JSON mapping of Slack user IDs to Azure AD user IDs by matching email addresses.
slack-to-teams-dm generate-user-map \
--slack-token $SLACK_TOKEN \
--teams-tenant-id $TEAMS_TENANT_ID \
--teams-client-id $TEAMS_CLIENT_ID \
--teams-client-secret $TEAMS_CLIENT_SECRET \
-o user-map.jsonYou can also create or edit the mapping file manually:
{
"U012AB3CD": "aad-user-guid-for-alice",
"U098ZYX": "aad-user-guid-for-bob"
}Preflight check: test Slack and Teams credentials and verify API access for your chosen mode.
slack-to-teams-dm validate \
--slack-token $SLACK_TOKEN \
--teams-tenant-id $TEAMS_TENANT_ID \
--teams-client-id $TEAMS_CLIENT_ID \
--teams-client-secret $TEAMS_CLIENT_SECRET \
--mode autoShow migration progress from the state file. No API credentials needed.
slack-to-teams-dm status
slack-to-teams-dm status --state-file ./my-state.json --jsonAll required options can be set via environment variables. Copy .env.example to .env:
cp .env.example .env| Variable | CLI Flag | Description |
|---|---|---|
SLACK_TOKEN |
--slack-token |
Slack Bot token (xoxb-...) |
TEAMS_TENANT_ID |
--teams-tenant-id |
Azure AD directory (tenant) ID |
TEAMS_CLIENT_ID |
--teams-client-id |
Azure AD application (client) ID |
TEAMS_CLIENT_SECRET |
--teams-client-secret |
Azure AD client secret value |
TEAMS_TEAM_ID |
--teams-team-id |
Teams team GUID (channel mode only) |
The migration runs in 4 phases:
- Initialize — Authenticate with Slack and Teams APIs, load or create the state file, load the user mapping
- Discover — List DM conversations from Slack, resolve participant names and Azure AD mappings, filter by CLI options
- Migrate — For each conversation: fetch messages, determine destination mode, create the Teams chat or channel, enter migration mode, post messages with original timestamps, upload files, complete migration mode
- Report — Print per-conversation and overall summary of migrated, skipped, and failed messages
When a Slack user cannot be matched to an Azure AD account, their messages are still migrated. The message is posted by the application identity with the user's display name prepended to the body:
[Alice Smith] Hey, here's that document you asked about
This ensures no messages are ever skipped due to missing user mappings.
The state file tracks progress per-conversation and per-message. If the process is interrupted, simply re-run the same command. Completed conversations and individual messages are skipped automatically — no duplicates are created.
Chat mode uses the Microsoft Graph beta API endpoints startMigration and completeMigration on chats. While in migration mode:
- Messages can be posted with custom
createdDateTime(original Slack timestamps) - Messages can be attributed to specific users via the
fromfield - The chat is locked — participants cannot send new messages until migration completes
- Beta API: Chat mode relies on Graph beta API endpoints (
/beta/chats/{id}/startMigration). These may change without notice. The beta calls are isolated to a single module (src/teams/chat-migration.ts) for easy updating. - Reactions: Rendered as text in the message body (e.g., "Reactions: 👍 5, ❤️ 2"). The Teams migration API does not support programmatic reaction import.
- Thread flattening: Thread replies are flattened into the main timeline chronologically. The Teams chat migration API does not support threaded replies in chats.
- @mentions: Show as bold display names (e.g., @Alice) but are not clickable Teams mentions.
- Block Kit messages: Complex Slack Block Kit layouts use the plain text fallback.
- Slack-specific features: Workflows, Canvas documents, Huddle recordings, and Slack Connect messages are not migrated.
- Message edits: Only the latest version of each message is migrated.
- Self-DMs: Skipped with a warning (Teams has no self-chat equivalent).
- Verify you added Application permissions (not Delegated) in the Azure AD app registration
- Ensure you clicked Grant admin consent — all permissions should show a green checkmark in the portal
- Double-check that
Teamwork.Migrate.Allis granted — this is the most commonly missed permission - If using channel mode, verify
Files.ReadWrite.Allis also granted
You need Global Administrator or Privileged Role Administrator role in Azure AD. Ask your IT admin to grant consent on the app registration, or have them run the validate command to confirm everything is set up correctly.
If the process crashes while a chat/channel is in migration mode, simply re-run the migration command. The state file tracks which conversations have active migration and will resume from where it left off, eventually calling completeMigration to unlock the conversation.
- Verify the
--teams-team-idvalue is the correct GUID (not the team name or display name) - Ensure the Azure AD app has
Team.ReadBasic.Allpermission with admin consent granted
The tool handles rate limits automatically with Bottleneck:
- Slack: ~50 requests per 60 seconds (tier 3)
- Teams: ~5 requests per second
Large DM histories will take time. The --concurrency flag (1–5) controls parallel Teams API requests, but stay conservative to avoid throttling.
If generate-user-map can't match a Slack user to Azure AD:
- The Slack user may not have an email address set in their profile
- The email may not match any Azure AD account (e.g., different email domains between Slack and Microsoft 365)
- You can manually add entries to the user map JSON file
Messages from unmapped users are always migrated with bot attribution — they are never skipped.
Contributions are welcome! Please open an issue or pull request.
# Development
npm install
npm run dev -- --help
# Run tests
npm test
# Build
npm run buildMIT