TikTok Caption Writer
Write the caption that sits under the video: a tight caption inside the 2,200
char API limit, a mixed-reach hashtag set, and the platformSettings.tiktok
flags that control reach and interaction. On TikTok the caption supports the
video, it does not carry it. The hook is in the first 1-3 seconds of the clip
(see tt-hook-scripter); the caption's job is to add context and earn a comment.
When to use
- User has a video (or a script) and needs the caption, hashtags, and settings
- User is about to upload and wants the posting flags right
- User wants Claude Code or Codex to schedule a rendered .mp4 via Publora
What this skill produces
- Caption (<= 2,200 chars on the API; aim much shorter), first line front-
loaded with a reason to read or comment
- Hashtag set (3 to 5, mixed reach), placed at the end
- platformSettings.tiktok flags with a plain-English summary
- On approval, the publish call (draft -> upload -> schedule) when a video file
is supplied
The TikTok settings (platformSettings.tiktok)
Build these with lib.tiktok_settings(...). Defaults match Publora's.
| Setting |
Values / default |
What it does |
viewerSetting |
PUBLIC_TO_EVERYONE (default), MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY |
who can view. Effectively required; an empty value is rejected |
allowComments |
true (default) |
viewers can comment |
allowDuet |
false (default) |
viewers can Duet |
allowStitch |
false (default) |
viewers can Stitch |
commercialContent |
false (default) |
the video is commercial |
brandOrganic |
false (default) |
promoting your own brand |
brandedContent |
false (default) |
paid partnership / sponsored |
Two gotchas you must surface to the user
- Boolean inversion bug. Publora currently maps
allowComments,
allowDuet, allowStitch to TikTok's disable_* flags, so the booleans can
land inverted (sending allowDuet: true may disable duets). The status cannot
be confirmed from the API alone. Test with a SELF_ONLY draft before
trusting these values. Tell the user this whenever they change a flag.
- Commercial disclosure. If
commercialContent is true, at least one of
brandOrganic (your own brand) or brandedContent (paid partnership) must
also be true, or Publora rejects it. lib.tiktok_settings enforces this.
Posting reality
- Unaudited apps post PRIVATE only. Until the publishing app passes TikTok's
review, posts are forced to
SELF_ONLY regardless of viewerSetting. Warn the
user if a "public" post lands private.
- For reach, use
PUBLIC_TO_EVERYONE. For an end-to-end test, use SELF_ONLY.
Steps
Voice profile first (all drafts). If ../../references/voice-profile.md has filled: yes, load it and match the user's voice fingerprint, hard rules, and CTA/link style throughout. If it is not filled, mention once that tt-humanizer --mode profile can learn their voice from a few posts, then proceed with the generic voice rules.
- Gather inputs, goal first. Ask (or infer) what the caption should earn,
then shape for it: comments -> end on one specific question tied to the video;
saves -> promise reference value ("the 3 settings are in the caption");
shares -> a claim people will want to show someone; profile taps -> an open
loop the pinned video pays off. Then gather the video topic/script, the
niche, whether it is commercial, and whether a rendered
.mp4 path exists.
- Draft the caption. Front-load the first visible line with a reason to read
or a specific question to comment on. Keep it tight. Em dashes under the cap
(about one per 100 words; a short caption rarely needs one), no AI vocab
cluster. Optionally restate the hook's open loop as a comment prompt.
- Pick hashtags. 3 to 5, mixed reach: one broad, one to two niche-defining,
one to two specific to the video. Put them at the end. Never stuff.
- Set the flags. Default to
PUBLIC_TO_EVERYONE, comments on, duet/stitch
off, no commercial flags. Adjust per the user, and surface the two gotchas.
Build with lib.tiktok_settings(...).
- Char check. Confirm caption + hashtags <= 2,200. If over, tighten.
- Humanizer pass. Scrub 2026 AI vocab by density, cap em dashes (about
one per 100 words, never swap one for a period), break stacked triads,
generic openers, reveal bridges and sincerity openers ("not gonna lie").
Leave the rhythm alone; never chop a line for punch. Canonical rules:
tt-humanizer V3.
- Approval card. Show: caption, char count, hashtag set, settings summary,
and the resolved
viewerSetting (flag if it may post private).
- On approval. Call
lib.publish("video", caption, target_url=<upload or profile URL>, video_path=<path or None>, platform_settings=<settings>, scheduled_time=<iso or None>, platforms=[<TIKTOK_PLATFORM_ID>]). With a video
path and Publora configured, it runs draft -> upload -> schedule. Without one,
it returns the caption and settings to upload in-app.
Hard rules
Global voice rules: see root SKILL.md Voice rules. Additional skill-specific
rules:
- Caption + hashtags must fit 2,200 chars (API), hashtags included. Aim far under.
- 3 to 5 hashtags, mixed reach, at the end. Never 10+, never mid-sentence.
- The caption never does the hook's job. The hook is in the video.
- Always set
viewerSetting. Never ship an empty value.
- Surface the boolean inversion warning whenever an interaction flag changes.
Anti-patterns (skill will refuse)
- Hashtag walls (10+), or hashtags jammed into the sentence.
- Em dashes above the cap (more than about one per 100 words), or an em dash swapped for a period.
- "The result?" reveals and "No X. No Y. Just Z." staccato stacks added for punch.
- "Follow for part 2 / like and subscribe / don't forget to share" stacks.
- A caption that repeats the spoken hook word for word.
commercialContent: true with neither brand flag set.
- Claiming a post will be public when the app is unaudited (it will be private).
Resources
../../references/voice-rules.md - caption and hashtag rules
../../references/algorithm-heuristics.md - caption, hashtag, and settings heuristics
references/settings-matrix.md - every platformSettings.tiktok value, defaults, and the known bugs
lib/publora_client.py - tiktok_settings(...), publish_video(...), create_draft(...)
Optional illustration
Offer a generated image when a visual would lift reach. Draft a prompt and call
lib.illustrate(prompt, kind="story"), pulling brand handle/color from Voice &
Brand Profile section 6 for a pixel-exact overlay. Show the returned url + cost,
for a TikTok photo post, attach the image via media_urls=[url] (TikTok photo carousel); for a video, TikTok's API has no cover-image field, so set the cover in the TikTok app. Full workflow (incl. quote-cards):
../tt-humanizer/sub-skills/illustration.md. No Pixfaro key -> it drafts the prompt for you to generate manually.
Related skills
tt-hook-scripter - the in-video hook (the caption does not replace it)
tt-humanizer - scrub the caption before publishing
tt-content-planner - schedule captions across a posting week
1---2name: tt-caption-writer3description: Write a TikTok caption under 2,200 chars (hashtags included), pick a tasteful 3 to 5 hashtag set with mixed reach, and set the platformSettings.tiktok flags (viewer setting, comments, duet, stitch, commercial and branded content). Front-loads a reason to comment, runs the humanizer pass, and publishes the rendered video via Publora on approval. Use to caption a finished video. Not for the in-video hook (use tt-hook-scripter) or scrubbing a script (use tt-humanizer).4---56# TikTok Caption Writer78Write the caption that sits under the video: a tight caption inside the 2,2009char API limit, a mixed-reach hashtag set, and the `platformSettings.tiktok`10flags that control reach and interaction. On TikTok the caption supports the11video, it does not carry it. The hook is in the first 1-3 seconds of the clip12(see `tt-hook-scripter`); the caption's job is to add context and earn a comment.1314## When to use1516- User has a video (or a script) and needs the caption, hashtags, and settings17- User is about to upload and wants the posting flags right18- User wants Claude Code or Codex to schedule a rendered .mp4 via Publora1920## What this skill produces2122- **Caption** (<= 2,200 chars on the API; aim much shorter), first line front-23 loaded with a reason to read or comment24- **Hashtag set** (3 to 5, mixed reach), placed at the end25- **platformSettings.tiktok** flags with a plain-English summary26- On approval, the publish call (draft -> upload -> schedule) when a video file27 is supplied2829## The TikTok settings (platformSettings.tiktok)3031Build these with `lib.tiktok_settings(...)`. Defaults match Publora's.3233| Setting | Values / default | What it does |34|---|---|---|35| `viewerSetting` | PUBLIC_TO_EVERYONE (default), MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY | who can view. Effectively required; an empty value is rejected |36| `allowComments` | true (default) | viewers can comment |37| `allowDuet` | false (default) | viewers can Duet |38| `allowStitch` | false (default) | viewers can Stitch |39| `commercialContent` | false (default) | the video is commercial |40| `brandOrganic` | false (default) | promoting your own brand |41| `brandedContent` | false (default) | paid partnership / sponsored |4243### Two gotchas you must surface to the user44451. **Boolean inversion bug.** Publora currently maps `allowComments`,46 `allowDuet`, `allowStitch` to TikTok's `disable_*` flags, so the booleans can47 land inverted (sending `allowDuet: true` may disable duets). The status cannot48 be confirmed from the API alone. **Test with a `SELF_ONLY` draft before49 trusting these values.** Tell the user this whenever they change a flag.502. **Commercial disclosure.** If `commercialContent` is true, at least one of51 `brandOrganic` (your own brand) or `brandedContent` (paid partnership) must52 also be true, or Publora rejects it. `lib.tiktok_settings` enforces this.5354### Posting reality5556- **Unaudited apps post PRIVATE only.** Until the publishing app passes TikTok's57 review, posts are forced to `SELF_ONLY` regardless of `viewerSetting`. Warn the58 user if a "public" post lands private.59- For reach, use `PUBLIC_TO_EVERYONE`. For an end-to-end test, use `SELF_ONLY`.6061## Steps6263**Voice profile first (all drafts).** If `../../references/voice-profile.md` has `filled: yes`, load it and match the user's voice fingerprint, hard rules, and CTA/link style throughout. If it is not filled, mention once that `tt-humanizer --mode profile` can learn their voice from a few posts, then proceed with the generic voice rules.64651. **Gather inputs, goal first.** Ask (or infer) what the caption should earn,66 then shape for it: comments -> end on one specific question tied to the video;67 saves -> promise reference value ("the 3 settings are in the caption");68 shares -> a claim people will want to show someone; profile taps -> an open69 loop the pinned video pays off. Then gather the video topic/script, the70 niche, whether it is commercial, and whether a rendered71 .mp4 path exists.722. **Draft the caption.** Front-load the first visible line with a reason to read73 or a specific question to comment on. Keep it tight. Em dashes under the cap74 (about one per 100 words; a short caption rarely needs one), no AI vocab75 cluster. Optionally restate the hook's open loop as a comment prompt.763. **Pick hashtags.** 3 to 5, mixed reach: one broad, one to two niche-defining,77 one to two specific to the video. Put them at the end. Never stuff.784. **Set the flags.** Default to `PUBLIC_TO_EVERYONE`, comments on, duet/stitch79 off, no commercial flags. Adjust per the user, and surface the two gotchas.80 Build with `lib.tiktok_settings(...)`.815. **Char check.** Confirm caption + hashtags <= 2,200. If over, tighten.826. **Humanizer pass.** Scrub 2026 AI vocab by density, cap em dashes (about83 one per 100 words, never swap one for a period), break stacked triads,84 generic openers, reveal bridges and sincerity openers ("not gonna lie").85 Leave the rhythm alone; never chop a line for punch. Canonical rules:86 `tt-humanizer` V3.877. **Approval card.** Show: caption, char count, hashtag set, settings summary,88 and the resolved `viewerSetting` (flag if it may post private).898. **On approval.** Call `lib.publish("video", caption, target_url=<upload or90 profile URL>, video_path=<path or None>, platform_settings=<settings>,91 scheduled_time=<iso or None>, platforms=[<TIKTOK_PLATFORM_ID>])`. With a video92 path and Publora configured, it runs draft -> upload -> schedule. Without one,93 it returns the caption and settings to upload in-app.9495## Hard rules9697Global voice rules: see root `SKILL.md` Voice rules. Additional skill-specific98rules:99100- Caption + hashtags must fit 2,200 chars (API), hashtags included. Aim far under.101- 3 to 5 hashtags, mixed reach, at the end. Never 10+, never mid-sentence.102- The caption never does the hook's job. The hook is in the video.103- Always set `viewerSetting`. Never ship an empty value.104- Surface the boolean inversion warning whenever an interaction flag changes.105106## Anti-patterns (skill will refuse)107108- Hashtag walls (10+), or hashtags jammed into the sentence.109- Em dashes above the cap (more than about one per 100 words), or an em dash swapped for a period.110- "The result?" reveals and "No X. No Y. Just Z." staccato stacks added for punch.111- "Follow for part 2 / like and subscribe / don't forget to share" stacks.112- A caption that repeats the spoken hook word for word.113- `commercialContent: true` with neither brand flag set.114- Claiming a post will be public when the app is unaudited (it will be private).115116## Resources117118- `../../references/voice-rules.md` - caption and hashtag rules119- `../../references/algorithm-heuristics.md` - caption, hashtag, and settings heuristics120- `references/settings-matrix.md` - every platformSettings.tiktok value, defaults, and the known bugs121- `lib/publora_client.py` - `tiktok_settings(...)`, `publish_video(...)`, `create_draft(...)`122123## Optional illustration124125Offer a generated image when a visual would lift reach. Draft a prompt and call126`lib.illustrate(prompt, kind="story")`, pulling brand handle/color from Voice &127Brand Profile section 6 for a pixel-exact overlay. Show the returned `url` + `cost`,128for a TikTok photo post, attach the image via `media_urls=[url]` (TikTok photo carousel); for a video, TikTok's API has no cover-image field, so set the cover in the TikTok app. Full workflow (incl. quote-cards):129`../tt-humanizer/sub-skills/illustration.md`. No Pixfaro key -> it drafts the prompt for you to generate manually.130## Related skills131132- `tt-hook-scripter` - the in-video hook (the caption does not replace it)133- `tt-humanizer` - scrub the caption before publishing134- `tt-content-planner` - schedule captions across a posting week