How to Automate Social Media Posting with Python and Playwright
Back to Blog
Automation· 9 min min read

How to Automate Social Media Posting with Python and Playwright

Playwright gives you programmatic browser control over any social media platform — even ones without public APIs. Here's how to build a multi-account posting system that handles Instagram, TikTok, Facebook, and YouTube from a single Python script.

NA
By NEPA AI
NEPA AI · Building autonomous systems for creators and businesses
#social media automation#playwright#python#instagram#tiktok#multi-account#selenium

Direct BMX to AI Code

Every big social media platform has an official API—depricated, rate-limited, or just plain blocks the features you need. No thanks.

Playwright browser automation lets you log in, navigate, and post like a human. Here's how to set it up.

Why Playwright Over Selenium?

Playwright is better for this job:

  • Auto-wait for elements
  • Supports Chromium, Firefox, WebKit
  • Easier network interception
  • Persistent sessions saved as JSON

The persistent session feature means you log in once and reuse forever.

Installation

pip install playwright
playwright install chromium  # download browser binary

# For headed mode debugging
pip install pytest-playwright

Session Management: Log In Once, Post Forever

Store login sessions:

import asyncio
from playwright.async_api import async_playwright
import json
from pathlib import Path

SESSIONS_DIR = Path("./sessions")
SESSIONS_DIR.mkdir(exist_ok=True)

async def save_session(account_name: str, platform: str):
    session_file = SESSIONS_DIR / f"{platform}_{account_name}.json"
    
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)  # headed for manual login
        context = await browser.new_context()
        page = await context.new_page()
        
        urls = {
            "instagram": "https://www.instagram.com/",
            "tiktok": "https://www.tiktok.com/",
            "facebook": "https://www.facebook.com/",
            "youtube": "https://studio.youtube.com/"
        }
        
        await page.goto(urls[platform])
        
        print(f"Log into {platform} as {account_name}. Press Enter when done...")
        input()
        
        # Save cookies and localStorage
        storage_state = await context.storage_state()
        session_file.write_text(json.dumps(storage_state))
        print(f"Session saved to {session_file}")
        
        await browser.close()

async def load_session(account_name: str, platform: str, playwright):
    """Load a saved session and return a ready browser context."""
    session_file = SESSIONS_DIR / f"{platform}_{account_name}.json"
    
    if not session_file.exists():
        raise FileNotFoundError(f"No session for {platform}/{account_name}. Run save_session first.")
    
    browser = await playwright.chromium.launch(headless=True)
    context = await browser.new_context(
        storage_state=str(session_file),
        viewport={"width": 1280, "height": 800},
        user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
    )
    
    return browser, context

Instagram Reel Upload

async def post_instagram_reel(
    account_name: str,
    video_path: str,
    caption: str,
    cover_frame_time: float = 1.0
):
    async with async_playwright() as p:
        browser, context = await load_session(account_name, "instagram", p)
        page = await context.new_page()
        
        await page.goto("https://www.instagram.com/")
        await page.wait_for_load_state("networkidle")
        
        # Click the + (Create) button
        create_btn = page.locator('[aria-label="New post"]')
        await create_btn.click()
        
        # Click "Reel" option
        await page.locator('text=Reel').click()
        
        # Upload video file
        async with page.expect_file_chooser() as fc_info:
            await page.locator('text=Select from computer').click()
        file_chooser = await fc_info.value
        await file_chooser.set_files(video_path)
        
        # Wait for upload + processing
        await page.wait_for_selector('[aria-label="Trim"]', timeout=60000)
        
        # Click Next (twice: trim screen, then settings screen)
        await page.locator('text=Next').click()
        await page.wait_for_timeout(1000)
        await page.locator('text=Next').click()
        
        # Fill caption
        caption_box = page.locator('[aria-label="Write a caption..."]')
        await caption_box.click()
        await caption_box.type(caption, delay=30)  # human-like typing speed
        
        # Share
        await page.locator('text=Share').click()
        
        # Wait for confirmation
        await page.wait_for_selector('text=Your reel has been shared', timeout=120000)
        
        print(f"✓ Instagram Reel posted for @{account_name}")
        await browser.close()

TikTok Upload

async def post_tiktok_video(
    account_name: str,
    video_path: str,
    caption: str,
    hashtags: list[str] = None
):
    hashtag_string = " ".join(f"#{tag}" for tag in (hashtags or []))
    full_caption = f"{caption}\n\n{hashtag_string}".strip()
    
    async with async_playwright() as p:
        browser, context = await load_session(account_name, "tiktok", p)
        page = await context.new_page()
        
        await page.goto("https://www.tiktok.com/upload")
        await page.wait_for_load_state("domcontentloaded")
        
        # Upload via file input
        file_input = page.locator('input[type="file"]')
        await file_input.set_files(video_path)
        
        # Wait for video processing
        await page.wait_for_selector('.upload-progress-bar', state="hidden", timeout=120000)
        
        # Fill caption
        caption_input = page.locator('[data-text="true"]').first
        await caption_input.click()
        await caption_input.type(full_caption)
        
        # Post
        await page.locator('button:text("Post")').click()
        
        # Wait for success redirect
        await page.wait_for_url("**/profile**", timeout=60000)
        
        print(f"✓ TikTok posted for @{account_name}")
        await browser.close()

Multi-Account, Multi-Platform Orchestrator

import asyncio
from dataclasses import dataclass
from typing import Optional

@dataclass
class PostJob:
    platform: str
    account: str
    video_path: str
    caption: str
    hashtags: list[str] = None

async def run_post_job(job: PostJob) -> dict:
    """Run a single post job, return result."""
    try:
        if job.platform == "instagram":
            await post_instagram_reel(job.account, job.video_path, job.caption)
        elif job.platform == "tiktok":
            await post_tiktok_video(job.account, job.video_path, job.caption, job.hashtags)
        
        return {"platform": job.platform, "account": job.account, "status": "success"}
    except Exception as e:
        return {"platform": job.platform, "account": job.account,
                "status": "failed", "error": str(e)}

async def broadcast_post(
    video_path: str,
    caption: str,
    hashtags: list[str],
    accounts: list[dict]
):
    """Post to multiple platform/account combos concurrently."""
    
    jobs = [
        PostJob(
            platform=acc["platform"],
            account=acc["account"],
            video_path=video_path,
            caption=caption,
            hashtags=hashtags
        )
        for acc in accounts
    ]
    
    # Run all posts concurrently (each opens its own browser)
    results = await asyncio.gather(*[run_post_job(job) for job in jobs])
    
    # Report
    for result in results:
        status = "✓" if result["status"] == "success" else "✗"
        print(f"{status} {result['platform']} / {result['account']}: {result['status']}")
    
    return results

# Example: post to all accounts
asyncio.run(broadcast_post(
    video_path="./export/final_reel.mp4",
    caption="New video dropping 🔥 Full breakdown in the link",
    hashtags=["ai", "tech", "automation", "python"],
    accounts=[
        {"platform": "instagram", "account": "nepa_ai"},
        {"platform": "tiktok", "account": "billy_kennedy_bmx"},
        {"platform": "instagram", "account": "billy_kennedy_bmx"},
    ]
))

Rate Limiting and Anti-Detection

Platforms monitor for bot-like behavior. Here's how to avoid it:

import random
import asyncio

async def human_delay(min_ms: int = 500, max_ms: int = 2000):
    """Random delay to simulate human interaction timing."""
    delay = random.uniform(min_ms, max_ms) / 1000
    await asyncio.sleep(delay)

async def human_type(element, text: str):
    """Type with randomized speed to simulate human typing."""
    for char in text:
        await element.type(char, delay=random.uniform(50, 150))
        if char in '.!?':
            await asyncio.sleep(random.uniform(0.3, 0.8))

# Space out posts across accounts
async def staggered_broadcast(jobs: list[PostJob], stagger_seconds: int = 30):
    """Post to accounts with delay between each to avoid suspicious burst patterns."""
    results = []
    for i, job in enumerate(jobs):
        if i > 0:
            print(f"Waiting {stagger_seconds}s before next post...")
            await asyncio.sleep(stagger_seconds)
        result = await run_post_job(job)
        results.append(result)
    return results

Scheduling

Combine with APScheduler for time-based posting:

from apscheduler.schedulers.asyncio import AsyncIOScheduler

scheduler = AsyncIOScheduler()

# Schedule a post for 9am EST daily
scheduler.add_job(
    broadcast_post,
    'cron',
    hour=9, minute=0,
    timezone='America/New_York',
    kwargs={
        "video_path": "./queue/next_post.mp4",
        "caption": "Daily post",
        "hashtags": ["daily"],
        "accounts": [{"platform": "instagram", "account": "nepa_ai"}]
    }
)

scheduler.start()

The NEPA AI Social Poster Pack is a production-ready version of this system: session management, retry logic, queue management, and multi-account orchestration pre-built and tested across Instagram, TikTok, YouTube, Facebook, and Pinterest.

→ Get the Social Poster Pack at /shop/social-poster-pack

Post everywhere, once, automatically.