Install
openclaw skills install tiktok-account-operationsOperating doctrine for TikTok account automation — careful, value-adding creator pattern with role separation, business-suite based DM/comment ops, human-like UI flow (cliclick + nested reply textbox), strict anti-shadowban quotas, and recovery patterns. Use this for any scheduled TikTok activity (cron, agent, recurring task) where account safety, FYP eligibility and long-term reach matter more than raw output.
openclaw skills install tiktok-account-operationsThis skill is the operating doctrine for every TikTok automation run on a brand, professional or personal account.
The goal is not to comment fast. The goal is to operate TikTok like a careful human contributor: stable browser, correct context, useful action, no shadowban risk, no FYP throttling.
Drop-in for any niche (legal, medical, software, finance, creator, ecommerce). Replace the placeholders in section 0 with your own values.
Before running anything, fill these placeholders in your local copy or your agent's memory:
| Placeholder | Example | Your value |
|---|---|---|
<BRAND_NAME> | "Acme Studio" | — |
<BRAND_DOMAIN> | "acme.studio" | — |
<TIKTOK_HANDLE> | "@acmestudio" | — |
<TIKTOK_BUSINESS_ACCOUNT> | yes/no (Business Suite requires Business or Creator account) | — |
<BROWSER_PROFILE> | "tiktok-live" | — |
<BROWSER_PORT> | "18801" | — |
<NICHE_KEYWORDS> | "lost license OR speeding ticket" | — |
<PRIMARY_CTA> | "WhatsApp / form / app link — pick ONE" | — |
<WORKSPACE_DIR> | "~/.openclaw/workspace/tiktok-<brand>" | — |
All shell snippets below assume an OpenClaw browser CLI bound by CDP, but the doctrine works with any browser-automation stack (Playwright, Puppeteer, Chrome MCP). Swap the CLI calls for your own.
If your agent reads config from YAML, drop this in <WORKSPACE_DIR>/config.yaml:
brand:
name: <BRAND_NAME>
domain: <BRAND_DOMAIN>
tiktok:
handle: <TIKTOK_HANDLE> # e.g. "@acmestudio"
business_account: true # required for /business-suite/ UI
browser_profile: <BROWSER_PROFILE> # e.g. "tiktok-live"
browser_port: <BROWSER_PORT> # e.g. 18801
discovery:
niche_keywords: <NICHE_KEYWORDS>
cta:
primary: <PRIMARY_CTA> # one channel only — e.g. "wa.me/<digits>"
workspace:
dir: <WORKSPACE_DIR> # e.g. "~/.openclaw/workspace/tiktok-acme"
alerts:
channel: telegram | slack | discord
webhook: <YOUR_WEBHOOK_URL>
schedule:
timezone: Europe/Paris # everything else is relative to this
windows:
dm_check: "0,15,30,45 9-22 * * *" # every 15 min, 9am-10pm
comment_check: "3,18,33,48 9-22 * * *" # offset by 3 min so they never overlap
browser_health: "0 * * * *" # hourly
daily_recap: "21:00"
The skill is markdown — it works wherever an agent reads SKILL.md:
| Stack | Skill install path |
|---|---|
| Claude Code | ~/.claude/skills/tiktok-account-operations/ |
| OpenClaw | ~/.openclaw/skills/tiktok-account-operations/ |
| ClawHub-published | one-click install via clawhub.ai |
| Cursor / Copilot CLI | drop SKILL.md into your project's .cursorrules or AGENTS.md |
| Any LLM agent reading markdown rules | concatenate SKILL.md into your system prompt |
A single browser profile per account, attached to TikTok's Business Suite:
<BROWSER_PROFILE> (CDP direct, attached to http://127.0.0.1:<BROWSER_PORT>)User data dir: ~/.openclaw-browser-profiles/<BROWSER_PROFILE>.
Use it through the OpenClaw browser CLI:
openclaw browser --browser-profile <BROWSER_PROFILE> status
openclaw browser --browser-profile <BROWSER_PROFILE> tabs
openclaw browser --browser-profile <BROWSER_PROFILE> navigate https://www.tiktok.com/business-suite/messages
openclaw browser --browser-profile <BROWSER_PROFILE> snapshot --limit 160
openclaw browser --browser-profile <BROWSER_PROFILE> click <ref>
openclaw browser --browser-profile <BROWSER_PROFILE> evaluate --fn "<async function>"
Most TikTok operational pages (Messages, Comments, Analytics, Posts) live under:
https://www.tiktok.com/business-suite/messageshttps://www.tiktok.com/business-suite/commentshttps://www.tiktok.com/business-suite/analyticshttps://www.tiktok.com/business-suite/postsCritical UI fact: /business-suite/messages and /business-suite/comments render their content inside iframes, not in the main DOM. Many document.querySelector(...) calls will silently miss the actual content unless you traverse the right iframe. See §9.
tk-postAccount cockpit. Use for direct account action.
Use for:
/business-suite/messages/business-suite/comments (nested reply only — see §9)Default page: https://www.tiktok.com/business-suite/messages
Mental model: act as the account. Protect it. Do not clutter it.
tk-engageSearch and discovery radar. Use to find what deserves action.
Use for:
#niche hashtagsDefault page: https://www.tiktok.com/search?q=<NICHE_KEYWORDS_URL_ENCODED>&t=video
Mental model: discover, qualify, decide. Do not act impulsively from discovery.
tk-stealthQuiet maintenance bay. Use for low-noise maintenance.
Use for:
Default page: https://www.tiktok.com/foryou
Mental model: maintain quietly. No noisy engagement.
tk-post = act as the accounttk-engage = discover what to react totk-stealth = maintain quietlyNever collapse all workflows into chaotic browsing.
openclaw browser --browser-profile <BROWSER_PROFILE> status
openclaw browser --browser-profile <BROWSER_PROFILE> evaluate --fn "async () => { try { const r = await fetch('https://www.tiktok.com/api/user/detail/self/', {credentials:'include'}); const d = await r.json(); return {ok: d.statusCode === 0, uniqueId: d.userInfo?.user?.uniqueId || null}; } catch(e) { return {ok:false, error:String(e)}; } }"
status is stopped: report the blockage in your alert channel and stop. Never relaunch Chrome from inside a cron — that's a user-side action.ok is false or uniqueId is null: login expired. Stop and report.For Business Suite specifically, navigate to /business-suite/messages after the API check — if the page redirects to the public web feed or to a login screen, the Business account session is expired even though the API succeeded.
TikTok has no karma counter, but it has a strong trust gradient: brand-new or recently-warned accounts get throttled on the FYP and on DM volume. Run in two phases:
Always read <WORKSPACE_DIR>/memory/tiktok-state.md at start (last line = current phase) AND verify via /business-suite/posts (looks for a "limited" banner near the post list).
A Daily Metrics Recap cron appends a snapshot every night and pings your alert channel explicitly when the account crosses Phase A → Phase B.
You can force Phase B before the 30-day threshold by appending a line YYYY-MM-DD - phase=B (manual override) to tiktok-state.md. The override line takes priority over the threshold. Use when:
Document the decision in tiktok-learnings.md.
A DM or comment is repliable only if all of:
tiktok-reply-log.md).If any check fails: skip. Document why in the recap.
For DMs: read the entire conversation ([data-e2e="chat-item"]), not just the latest message. People often add critical context across 2-3 messages.
For comments: read the parent video caption AND the comment thread up to the target comment. A reply that ignores the obvious context reads as a bot.
Example structure (comment):
[Direct answer or empathy hook]. [One concrete tip from common sense].
Default mode = pedagogical, not promotional.
DM reply structure (≤ 6000 chars but aim for ≤ 600):
[Acknowledge the situation in 1 sentence, neutral tone.]
[General rule / framework in 2-3 short paragraphs. No exact quote of statute. No price.]
[Concrete next step — point to <PRIMARY_CTA>. NEVER list multiple channels.]
Comment reply structure (≤ 200 chars):
[1 line of contextual acknowledgement]. [Indirect signal — "feel free to DM us" / "form on bio" — never paste a URL].
ZERO outgoing links inside DMs or comments themselves. TikTok actively dampens reach for posts containing third-party URLs in any user-visible text. Indirect signaling only:
If your sending path uses cliclick t:'...' for comment posting, strip all accented characters and emoji from the rendered string before sending. The macOS keystroke path silently drops or replaces non-ASCII characters in some locales. Replace é → e, à → a, ' → ', … → ... etc. before the type call.
Forbidden in any reply:
<BRAND_DOMAIN> (and www., subdomains, shorteners).Posting cadence is outside the scope of the live operational crons described in §9 (those handle reactive DM + comment replies). The recommended rhythm for an account in Phase B is:
Hashtag discipline:
<WORKSPACE_DIR>/memory/tiktok-hashtag-state.md registry monthly.Apply a posting calendar in <WORKSPACE_DIR>/memory/tiktok-ideas.md. Run a Weekly Planning cron that picks the next 5-7 hooks from tiktok-ideas.md and confirms none of them duplicates a post in tiktok-post-log.md over the last 30 days.
| Action | Limit |
|---|---|
| Replies (DM) per 24 h | 30 max |
| Replies (DM) per Reply Pass run | 4 max |
| Replies (comment) per 24 h | 40 max |
| Replies (comment) per Reply Pass run | 6 max |
| Outbound first-contact DMs / day | 0 (Phase A) — 5 (Phase B, only if user opted-in) |
| Follow / day | 20 max (Phase B), 5 max (Phase A) |
| Unfollow / day | 20 max |
| Actions in same video thread | min 30 min apart |
| Actions globally (any) | min 1 min apart |
| Same opening phrase across replies | never (see §8) |
Quota tracking: read tiktok-reply-log.md at the start of every run, count entries with timestamps in the last 24 h, abort early if quotas already met.
To avoid two crons hammering the Business Suite at the same time:
:00, :15, :30, :45.:03, :18, :33, :48 (offset by 3 min).Never fuse the two into one cron — a single long-running cron is more likely to hit a TikTok soft block.
| Avoid | Use instead |
|---|---|
| "DM me", "check my profile", "link in bio" | (bio link does the work — don't point at it) |
<BRAND_NAME> repeated > 1 time in same reply | (mention once, max) |
| "free consultation", "first call free" | (no marketing language) |
| Phone number, email, URL | (never in body) |
| Emojis | (regulated / sober fields — drop them. Also: encoding trap on macOS — see §9) |
| All caps for emphasis | (use italics sparingly, prefer plain text) |
| Bit.ly / tinyurl / shorteners | (TikTok flags shorteners aggressively) |
6 actions in 10 min → temporary rate limit.
@user @user @user in a single reply) → spam classifier triggers.TikTok's Business Suite ships heavy front-end protections: iframes everywhere, two stacked textboxes after a "Reply" click, IME-aware focus management, and a non-trivial composer that ignores synthetic events. The only flow that survives is physical-input simulation — real mouse clicks via cliclick on macOS (or xdotool on Linux), real keystrokes, real dwells. This is the TikTok equivalent of Reddit's reCAPTCHA dodge: not a bypass, just real human signals.
URL: https://www.tiktok.com/messages?lang=<your-lang> (redirects to business-suite if Business account, with an iframe wrapping the conversation pane).
uniqueId is not null./messages?lang=<lang>. Wait for the redirect to complete (≥ 1.5 s dwell).document.querySelectorAll('iframe').length > 0. If yes, scope all subsequent queries to that iframe.[data-e2e="chat-list-item"] (within iframe).[data-e2e="chat-item"] — full context before answering.
c. Qualify the lead (see §4).
d. Draft the reply (≤ 6000 chars).1. subprocess.run(["pbcopy"], input=msg.encode("utf-8"))
2. Page.bringToFront via CDP
3. Compute the textbox's screen coordinates (see §9.3)
4. cliclick c:X,Y ← MANDATORY physical click in textbox
5. time.sleep(0.5)
6. osascript -e 'tell app "System Events" to keystroke "v" using command down'
7. time.sleep(1) ← wait for the Send button to become enabled
8. Compute screen coords of [data-e2e="message-send"]
9. cliclick c:screenX,screenY
10. Verify textbox is empty
The physical click on step 4 is non-negotiable: without it, the macOS clipboard paste lands in whichever frame currently has focus, which is rarely the message textbox.
URL: https://www.tiktok.com/business-suite/comments.
Structure of the page:
iframe[0] = messages (always present in background), iframe[1] = comments./(\d+) (nouveau|new)/ (not "Aucun" / "No").iframe[1] to load.
c. Read [data-e2e="comment-level-1"] + usernames [data-e2e="comment-username-1"].
d. Qualify the comment (see §4).
e. Click [data-e2e="comment-reply-1"] of the target comment via JS scoped to iframe[1]. Wait 1.5 s.vpY) = the nested reply textbox ← USE THIS ONE.vpY near the bottom) = the page-level "Add a comment" box. Posting here creates a new top-level comment, not a reply.vpY. Sample selector:
textboxes = [...] # all contenteditable nodes with their viewport coords
inline_tb = min(textboxes, key=lambda t: t['vpY'])
post_btns = [b for b in all_post_btns if not b['disabled']]
closest = min(post_btns, key=lambda b: abs(b['vpY'] - inline_tb['vpY']))
1. Compute screen coords of inline_tb
2. cliclick c:X,Y ← physical click in INLINE textbox
3. time.sleep(0.5)
4. cliclick t:'message_ascii_only' ← TYPE, do not paste
5. time.sleep(1)
6. Compute screen coords of the closest enabled comment-post button
7. cliclick c:X,Y
The browser CLI returns viewport coordinates (vpX, vpY) inside the page. To click via cliclick, you need screen coordinates:
screenX = window.screenX + viewportX
screenY = window.screenY + (outerHeight - innerHeight) + viewportY
The (outerHeight - innerHeight) term accounts for the Chrome top bar. On a typical macOS setup with the default Chrome chrome: chrome_bar ≈ 87.
You can fetch these via:
const m = { sX: window.screenX, sY: window.screenY, ih: window.innerHeight, oh: window.outerHeight };
| Method | Why it fails |
|---|---|
document.execCommand('insertText', false, msg) | Visually empties the textbox after typing but the message never reaches the send pipeline (TikTok's composer reads from a controlled React state, not from the DOM). |
cliclick kd:cmd t:v ku:cmd | Types the literal letter "v" instead of pasting (the cmd keydown is released before the t:v fires). |
cliclick kp:return to send | TikTok's composer ignores the Enter key in DMs and comments; you must click the send button. |
osascript ... keystroke "v" using command down for comments | Pastes into the page-level focus, which is rarely the inline reply textbox after a "Reply" click. Use cliclick t:'...' for comments. |
Picking the GENERAL textbox (largest vpY) for a nested reply | Posts a top-level comment instead of a nested reply. |
| Code | Meaning | Recap action |
|---|---|---|
| 0 | DM / comment sent, verified | status: ok, log the reply |
| 1 | Fatal error (UI ref missing, iframe absent, send button never enabled) | status: error, attach screenshot |
| 2 | Soft block / "Tap to retry" toast | status: blocked, stop, alert user |
| 3 | Session / login KO | status: blocked, alert user to re-login |
| 4 | Comment posted but landed in wrong textbox (top-level instead of nested) | status: partial, mark for manual cleanup |
cliclick t: accent encoding trap: cliclick t:'message' on macOS drops or replaces accented characters in some locales (notably fr_FR.UTF-8). Strip accents and emoji from the rendered message before typing. Same family of bug as Reddit's LC_NUMERIC trap, different symptom: instead of dwells failing silently, characters disappear from the visible reply.cliclick after navigating, before pasting/typing.iframe[0] is the messages background, iframe[1] is the comments. Always scope queries to iframe[1] on /business-suite/comments.vpY distance from your textbox, and verify it is enabled before clicking./tmp/<your-namespace>/tiktok/). They are gold when a comment silently lands in the wrong box.TikTok ships UI changes regularly. When the data-e2e attributes start returning null on selectors that used to work:
File: <WORKSPACE_DIR>/memory/tiktok-hashtag-state.md
For each hashtag used in <NICHE_KEYWORDS>:
File: <WORKSPACE_DIR>/memory/tiktok-sound-state.md
For each sound used:
Update at the end of every posting run.
| Issue | Action |
|---|---|
status: stopped | Report in recap: "Chrome <BROWSER_PROFILE> stopped, user action required (relaunch Chrome on port <BROWSER_PORT>)". Stop. |
Session API returns ok: false | Report login expired. Stop. |
Business Suite redirects to /login | Same as login expired. Stop. |
HTTP 429 from /api/user/detail/self/ | Stop the run, report "rate limited", wait next scheduled run. |
| Captcha / "puzzle" challenge | Stop. Never attempt to solve. Report for user action. |
| "Tap to retry" toast after sending | Soft block. Pause the role for ≥ 1 h. Report. |
| Comment posted but lands as a top-level (not a reply) | Auto-flag for human cleanup. Pause comment cron for 30 min. |
| Comment removed by TikTok < 10 min after publish | Auto-freeze the entire comment cron for 6 h. Append cause to tiktok-learnings.md. |
evaluate returns null with no error | Likely off-tiktok.com page → navigate first, retry once. |
| Account banner "your account is at risk of restriction" | Flip to Phase A immediately. Disable all reply crons until manual review. |
At the end of each cron:
Alert channel (Telegram / Slack / Discord) — final run message:
[Job name] — [status: ok|partial|blocked|skipped]
DMs handled: [N or "—"]
Comments handled: [N or "—"]
Hot leads: [list short OR "—"]
Phase: [A|B]
Blockers: [text OR "—"]
Next action: [1 line]
Memory — append to <WORKSPACE_DIR>/memory/tiktok-recaps.md:
## YYYY-MM-DD HH:MM TZ — <job-id> — status: <status>
- Job: <description>
- Phase: A|B
- DMs sent: <N>
- Comments sent: <N>
- Hot leads: <list or "—">
- Blockers: <text or "—">
- Next useful action: <1 line>
If no action was taken: say so clearly (no DM/comment sent, reason).
Located at: <WORKSPACE_DIR>/memory/
| File | Purpose | Update cadence |
|---|---|---|
tiktok-recaps.md | Per-run logs (mandatory append) | Every cron run |
tiktok-post-log.md | Videos published (URL, hook, date, views, watch %) | Each posting run |
tiktok-reply-log.md | Replies sent (target user, type DM/comment, URL, date, removed?) | Each reply pass |
tiktok-state.md | Daily phase + warning flags + follower count | Daily Metrics Recap |
tiktok-hashtag-state.md | Per-hashtag reach + shadowban check | After any posting run |
tiktok-sound-state.md | Per-sound reach + license info | After any posting run |
tiktok-ideas.md | Content backlog for new videos | Weekly Planning + ad hoc |
tiktok-learnings.md | Patterns observed (what got reach / removed) | Weekly Planning + ad hoc |
tiktok-alerts-sent.md | Anti-doublon log for hot-lead alerts (one row per user) | Each alert |
This account exists to be useful in public with the brand as a background signal.
<PRIMARY_CTA> link. No phone, no email in the bio.<PRIMARY_CTA> via a standard polite reply.If someone asks "are you a bot?": answer once with a sober, ambiguous line ("I'm part of the team — happy to help") and stop. Do not confirm AI use — and never lie outright if pressed.
When the Daily Metrics Recap detects (account_age_days ≥ 30) AND (last_warning_days_ago > 14) AND (followers ≥ 500):
tiktok-state.md: YYYY-MM-DD - phase=A - PHASE_B_THRESHOLD_REACHED.🎉 TikTok account in Phase B — enable comment-reply + outbound-DM crons (jobs.json: flip enabled=true).Once Phase B is enabled by the user:
Better silence than spam. Better a blockage report than a fake success.
Before enabling any cron, run through this checklist:
<PRIMARY_CTA> link.http://127.0.0.1:<BROWSER_PORT> and logged in./api/user/detail/self/ returns ok:true and a non-null uniqueId (login verified).<WORKSPACE_DIR>/memory/ directory exists with the 9 memory files (see §13) — empty is fine, scripts append.cliclick installed (brew install cliclick) and macOS Accessibility permission granted to the terminal that runs the cron.A bash one-liner to init the memory files:
mkdir -p "<WORKSPACE_DIR>/memory" && cd "$_" && touch tiktok-recaps.md tiktok-post-log.md tiktok-reply-log.md tiktok-state.md tiktok-hashtag-state.md tiktok-sound-state.md tiktok-ideas.md tiktok-learnings.md tiktok-alerts-sent.md
Q: Do I need OpenClaw to use this skill?
A: No. OpenClaw browser CLI is the example stack — the doctrine works with Playwright, Puppeteer, Chrome MCP, or any CDP-capable tool. The cliclick step in §9 is macOS-specific; on Linux replace with xdotool; on Windows use the SendInput win32 API or a wrapper like AutoHotkey.
Q: Can I use this skill for multiple TikTok accounts?
A: Yes — clone the workspace dir per account. Each account gets its own memory/ and its own browser profile. Use a different <BROWSER_PORT> per account so they don't collide.
Q: Does this work with the official TikTok API (Display / Content Posting)? A: Partially. The official API covers video upload and basic profile reads but does NOT cover comment-reply or DM-reply — exactly the surfaces this skill operates on. The UI flow in §9 is the only reliable path until TikTok ships an official DM/comment-reply API.
Q: What about TikTok Shop / Affiliate / Live? A: Out of scope. This skill covers DMs, comment replies, and FYP-eligible organic posting only. Shop and Live have separate UI and separate sanction patterns.
Q: How does this interact with the "Action blocked" toasts? A: Treat them as soft blocks (exit code 2 in §9). Pause the role for ≥ 1 h, alert the user, do not retry inside the same cron run. If the toast says "we'll let you know when you can do this again", flip the account to Phase A and require a manual re-evaluation.
Q: Can I run multiple crons in parallel?
A: Within one account, no — the cron interleave in §7 (3-minute offset) is there because the Business Suite session can only handle one operation at a time without producing inconsistent UI state. Across accounts, yes, but use different browser profiles and different <BROWSER_PORT>.
Q: What if my account is banned or shadowbanned?
A: Stop everything. Do not appeal automatically — TikTok's appeal flow is sensitive to repeated automated submissions. Manual review only. Document the exact last 20 actions in tiktok-learnings.md so the post-mortem can identify the trigger.