Social and communities

tiktok.com

TikTok Studio - Upload Video

Notes an AI agent reads before it works on tiktok.com: what to open, what to call, what breaks. Written by browser-harness contributors after real runs, and kept on GitHub under the MIT license.

Give it to your agent

Any coding agent

Start the task with this line. The Markdown version is the same notes without the page around them.

Read https://browser-use.com/skills/tiktok.md, then <your task>

Browser Harness

Save the files into your workspace and turn domain skills on. The code below calls its helpers, such as http_get and js. What Browser Harness is.

d="${BH_AGENT_WORKSPACE:-$HOME/.config/browser-harness/agent-workspace}/domain-skills/tiktok"
mkdir -p "$d" && cd "$d"
curl -sLO https://raw.githubusercontent.com/browser-use/browser-harness/main/agent-workspace/domain-skills/tiktok/upload.md
export BH_DOMAIN_SKILLS=1

URL: https://www.tiktok.com/tiktokstudio/upload?from=upload&lang=en (always append &lang=en)

Prerequisites

  • Logged into TikTok in the Chrome profile browser-harness is attached to
  • Video file on local disk (mp4, <50MB)

Stale draft banner

TikTok shows "A video you were editing wasn't saved" if a previous upload was abandoned. Dismiss it:

  1. Find the banner Discard button (y < 300 in the page)
  2. CDP click_at_xy(x, y) on it
  3. A confirmation modal appears - find the red Discard button (y > 300) and CDP click_at_xy(x, y)
  4. Repeat if multiple stale drafts are stacked

Upload flow

1. Attach file

upload_file('input[type="file"]', "/path/to/video.mp4")
wait(12)  # processing takes ~10s for 5-10MB

2. Caption

TikTok pre-fills caption with the filename. Clear it first:

js("document.querySelector('div[contenteditable=\"true\"][role=\"combobox\"]').focus()")
press_key("End")
for _ in range(25): press_key("Backspace")  # clear filename
type_text("your caption here #hashtag1 #hashtag2")
press_key("Escape")  # dismiss hashtag suggestions
click_at_xy(700, 50)        # click away to deselect

Verify: js('document.querySelector(\'div[contenteditable="true"][role="combobox"]\').innerText')

3. Schedule

Click the Schedule radio label:

js("(()=>{var l=document.querySelectorAll('label');for(var i=0;i<l.length;i++){if(l[i].textContent.trim()==='Schedule'){l[i].click();break}}})()")

Time picker - uses a scroll-wheel list, NOT a native select. Each scroll(dy=32) steps +1 unit, dy=-32 steps -1 unit.

# 1. ScrollIntoView and open the time picker
js("...scrollIntoView the time input...")
click_at_xy(time_input_x, time_input_y)
 
# 2. Read default time, calculate difference
default_hour, default_min = 13, 5  # from input value
target_hour, target_min = 20, 25
 
# 3. Scroll hour column (left, x ≈ 349)
for _ in range(target_hour - default_hour):
    scroll(349, dropdown_y, dy=32)  # +1 hour per step
 
# 4. Scroll minute column (right, x ≈ 437)
for _ in range((target_min - default_min) // 5):
    scroll(437, dropdown_y, dy=32)  # +5 min per step
 
# 5. Close and verify
press_key("Escape")

Date picker - click the date input, then click the target day number span.

4. AI-generated content disclosure

Under "Show more" section. Toggle is [aria-checked] inside the "AI-generated content" parent.

# Expand settings
js("...click 'Show more' span...")
# ScrollIntoView the toggle
js("...scrollIntoView 'ai-generated content' span...")
# Read state and click if false
# A "Turn on" confirmation dialog may appear — click it

5. Submit

Scroll the Schedule button into view, then CDP click_at_xy(x, y). After success, page redirects to /tiktokstudio/content.

js("...scrollIntoView Schedule button (offsetWidth > 100)...")
click_at_xy(button_x, button_y)
wait(6)
assert "content" in page_info()["url"]

Gotchas

  • JS .click() doesn't work on TikTok's time picker items - must use CDP click_at_xy(x, y)
  • Time picker uses virtual scroll - scroll(x, y, dy=32) changes value, NOT regular DOM scroll
  • Caption contenteditable appends on type - always clear with End + Backspace first, never set innerHTML (breaks React state)
  • beforeunload dialog blocks navigation if upload is in progress - use cdp("Page.handleJavaScriptDialog", accept=True) to dismiss (see interaction-skills/dialogs.md)
  • Schedule button text is "Schedule" only after the Schedule radio is selected (otherwise "Post")
  • "Show more" section expands the page and pushes the time picker off-viewport - collapse it before adjusting time, expand after
  • Unicode narrow no-break space (char 8239) appears between time and AM/PM in scheduled post listings - use .indexOf('12:30') not exact string match

upload.md · updated Apr 28, 2026 · 514 words

Sites change. These notes describe what worked when they were last updated, not a guarantee they still do. If something broke, the fix is a pull request to browser-harness.