Migrating Our Company from Discord to Slack in 20 Minutes

With all my projects, even as I lean heavily on AI for code, I'm trying to learn and understand things better. For this project: data formats, JSON transformation, and how different platforms structure the same information differently.


I needed to move my company off Discord and onto Slack. We'd been using Discord for a few years — 17 channels, 32 users, tons of institutional knowledge buried in those conversations. I didn't want to lose all that.

Looked around for migration tools and... there's not much out there. Slack's import only works for other Slack workspaces. Most advice is "just start fresh." Not helpful.

But I found DiscordChatExporter, an open-source tool that dumps your Discord history to JSON. Then I used Claude to write a Python script that converts that JSON into Slack's import format. Uploaded it to Slack as if it was a workspace export, and boom — full history preserved.

Twenty minutes, start to finish. Would've taken me hours to figure out the format specs myself, if I even could. Here's the whole process.


Step 1: Export Discord

Install DiscordChatExporter CLI:

# Mac (Apple Silicon)
cd ~/Downloads
curl -L -o DiscordChatExporter.zip https://github.com/Tyrrrz/DiscordChatExporter/releases/download/2.43.3/DiscordChatExporter.Cli.osx-arm64.zip
unzip DiscordChatExporter.zip -d DiscordChatExporter
chmod +x ~/Downloads/DiscordChatExporter/DiscordChatExporter.Cli

Get your Discord token (open Discord in browser → DevTools → Network tab → click a channel → find authorization in request headers). Keep this private.

Get your server ID from the URL (discord.com/channels/SERVER_ID/...).

Export everything:

mkdir -p ~/Downloads/discord-export
~/Downloads/DiscordChatExporter/DiscordChatExporter.Cli exportguild \
  -g YOUR_SERVER_ID \
  -t "YOUR_TOKEN" \
  -o ~/Downloads/discord-export \
  -f Json \
  --media

What's Actually Happening Here

This is the part I wanted to understand better.

Discord's export format: Each channel becomes a JSON file with an array of messages. Each message has an author object, timestamp, content, attachments, etc. It's structured around Discord's data model - how Discord thinks about messages.

Slack's import format: Slack wants a specific folder structure. A channels.json file listing all channels. A users.json file listing all users. Then a folder for each channel containing JSON files named by date (2024-01-15.json), each with that day's messages. Different structure, different assumptions.

The transformation: The Python script reads Discord's format, pulls out the same underlying information (who said what, when, in which channel), and reorganizes it into Slack's expected structure. Same data, different shape.

Discord: one big JSON per channel, messages as array
    ↓
Python script reads, restructures
    ↓  
Slack: folder per channel, one JSON file per day

It's like translating between languages - the meaning stays the same, but the grammar is different.

Step 2: Convert to Slack Format

This is the script Claude wrote:

import json
import os
import glob
from datetime import datetime
import re

discord_export_dir = os.path.expanduser("~/Downloads/discord-export")
slack_output_dir = os.path.expanduser("~/Downloads/slack-import")

os.makedirs(slack_output_dir, exist_ok=True)

all_users = {}
channels = []

for json_file in glob.glob(os.path.join(discord_export_dir, "*.json")):
    with open(json_file, 'r', encoding='utf-8') as f:
        data = json.load(f)
    
    channel_name = data.get('channel', {}).get('name', 'unknown')
    slack_channel_name = re.sub(r'[^a-z0-9-]', '', channel_name.lower().replace(' ', '-'))
    if not slack_channel_name:
        slack_channel_name = 'imported'
    
    channel_id = data.get('channel', {}).get('id', '')
    
    channel_dir = os.path.join(slack_output_dir, slack_channel_name)
    os.makedirs(channel_dir, exist_ok=True)
    
    messages_by_date = {}
    
    for msg in data.get('messages', []):
        author = msg.get('author', {})
        user_id = author.get('id', 'unknown')
        
        if user_id not in all_users:
            all_users[user_id] = {
                "id": user_id,
                "name": author.get('name', 'unknown').lower().replace(' ', '_'),
                "real_name": author.get('name', 'Unknown'),
                "profile": {
                    "display_name": author.get('nickname') or author.get('name', 'Unknown'),
                    "image_72": author.get('avatarUrl', '')
                }
            }
        
        timestamp_str = msg.get('timestamp', '')
        try:
            dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
            ts = str(dt.timestamp())
            date_str = dt.strftime('%Y-%m-%d')
        except:
            ts = "0"
            date_str = "1970-01-01"
        
        content = msg.get('content', '')
        
        for att in msg.get('attachments', []):
            att_url = att.get('url', '')
            if att_url:
                content += f"\n[Attachment: {att.get('fileName', 'file')}]"
        
        for embed in msg.get('embeds', []):
            if embed.get('title'):
                content += f"\n[Embed: {embed.get('title')}]"
            if embed.get('url'):
                content += f" {embed.get('url')}"
        
        slack_msg = {
            "type": "message",
            "user": user_id,
            "text": content,
            "ts": ts
        }
        
        if msg.get('reference'):
            ref_id = msg['reference'].get('messageId')
            if ref_id:
                slack_msg['thread_ts'] = ref_id
        
        if date_str not in messages_by_date:
            messages_by_date[date_str] = []
        messages_by_date[date_str].append(slack_msg)
    
    for date_str, messages in messages_by_date.items():
        date_file = os.path.join(channel_dir, f"{date_str}.json")
        with open(date_file, 'w', encoding='utf-8') as f:
            json.dump(messages, f, indent=2)
    
    channels.append({
        "id": channel_id,
        "name": slack_channel_name,
        "created": 0,
        "creator": "",
        "is_archived": False,
        "is_general": slack_channel_name == "general",
        "members": list(all_users.keys()),
        "topic": {"value": ""},
        "purpose": {"value": f"Imported from Discord #{channel_name}"}
    })
    
    print(f"Processed: {channel_name} -> {slack_channel_name}")

with open(os.path.join(slack_output_dir, "channels.json"), 'w', encoding='utf-8') as f:
    json.dump(channels, f, indent=2)

with open(os.path.join(slack_output_dir, "users.json"), 'w', encoding='utf-8') as f:
    json.dump(list(all_users.values()), f, indent=2)

print(f"\nDone! Slack import ready at: {slack_output_dir}")
print(f"Channels: {len(channels)}")
print(f"Users: {len(all_users)}")

Save as discord-to-slack.py and run:

python3 discord-to-slack.py

Step 3: Import to Slack

Zip it up:

cd ~/Downloads && zip -r slack-import.zip slack-import/

Then:

  1. Go to https://YOUR-WORKSPACE.slack.com/services/import
  2. Click Import next to Slack
  3. Upload slack-import.zip
  4. Choose "Import just their messages" for users
  5. Choose "Create new channels"
  6. Done

What Worked

All 17 channels imported with full history. Messages searchable. Timestamps preserved. User attribution intact. Years of conversations, all there.

Limitations

Attachments show as [Attachment: filename] rather than transferring the actual files. Threading doesn't map perfectly. Reactions don't come over. And you need Slack Pro — free tier can't import.

Cleanup

Change your Discord password after (invalidates the token you used). Then archive Discord whenever you're ready.


Want to Do This Yourself?

If you're migrating your own company from Discord to Slack, everything you need is above. The script works as-is - just update the folder paths if needed.

The flow:

  1. Install DiscordChatExporter and export your server
  2. Run the Python script to convert the format
  3. Zip and upload to Slack's import tool

You'll need Slack Pro or higher (free tier can't import). The whole thing takes about 20 minutes.

Built with DiscordChatExporter and Claude.