feat(tiktok): add basic TikTok integration#5504
Conversation
Adds TikTok as a full OAuth-based integration: provider registration (with TikTok's comma-separated scope and client_key requirements), 9 tools covering profile info, video listing/querying, creator info, direct video/photo posting (URL or file upload), inbox drafts, and post status polling, plus the TikTok block, icon, and generated docs. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview OAuth & credentials — Registers TikTok with Workflow surface — Nine tools cover profile/stats, video list/query, creator posting constraints, direct video/photo publish (public URL or workflow file), inbox drafts, and publish status. A new Webhooks & triggers — Fixed callback Product wiring — Reviewed by Cursor Bugbot for commit cb1a766. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
Few initial comments:
|
Adds a file-typed avatarFile output (sourced from the largest available avatar URL) alongside the existing string avatar fields, so the profile picture can be materialized as a UserFile and chained into file-consuming blocks (e.g. attached to an email), per PR review feedback. Co-authored-by: Cursor <cursoragent@cursor.com>
"The value of total_chunk_count should be equal to video_size divided by chunk_size, rounded down to the nearest integer... Each chunk must be at least 5 MB but no greater than 64 MB, except for the final chunk, which can be greater than chunk_size (up to 128 MB) to accommodate any trailing bytes." Their own example uses the identical numbers this comment flagged: video_size: 50000123, chunk_size: 10000000 → total_chunk_count: 5 (Math.floor(50000123/10000000)=5), with the last chunk absorbing the trailing 123 bytes as an oversized final chunk. Switching to Math.ceil would actually introduce a real bug: for files like 41MB, Math.ceil(41/10)=5 chunks produces a final chunk of only 1MB, violating TikTok's documented 5MB minimum chunk size. Keeping Math.floor as-is. Resolving.
Video (Direct Post Video / Upload Video Draft) already follows this exact pattern — a file-upload subblock in basic mode and a short-input block-reference subblock in advanced mode, both sharing canonicalParamId: 'file', normalized through the same normalizeFileInput() helper the Gmail attachment field uses. The URL input (videoUrl) is a structurally separate subblock only shown when the user picks the "Public URL" source — it's never mixed into the file subblock itself. The one large-file path — chunked video upload in publish-video/route.ts — downloads the workflow file into memory before forwarding it to TikTok. This is capped at TikTok's documented 4GB max video size (TIKTOK_MAX_VIDEO_BYTES), passed as maxBytes into downloadFileFromStorage, with a PayloadSizeLimitError caught and turned into a clean 413 response instead of letting an oversized file buffer unbounded. I intentionally left video/photo cover images (coverImageUrl in List/Query Videos) as plain URL strings rather than converting them too. Those are returned per-video inside an array of up to 20 videos per call, so converting them would mean silently downloading and storing up to 20 thumbnails into workflow storage on every List Videos call, most of which won't ever be used. Happy to add that too if there's a specific use case for it, but wanted to flag the cost tradeoff rather than do it by default. |
|
"The one large-file path — chunked video upload in publish-video/route.ts — downloads the workflow file into memory before forwarding it to TikTok. This is capped at TikTok's documented 4GB max video size (TIKTOK_MAX_VIDEO_BYTES), passed as maxBytes into downloadFileFromStorage, with a PayloadSizeLimitError caught and turned into a clean 413 response instead of letting an oversized file buffer unbounded." --> Can we make it so we have at most 250MB in memory for this instead of 4GB. "I intentionally left video/photo cover images (coverImageUrl in List/Query Videos) as plain URL strings rather than converting them too. Those are returned per-video inside an array of up to 20 videos per call, so converting them would mean silently downloading and storing up to 20 thumbnails into workflow storage on every List Videos call, most of which won't ever be used. Happy to add that too if there's a specific use case for it, but wanted to flag the cost tradeoff rather than do it by default." --> are these publicly accesible URLs? If they are not, we should probably convert them too to be user files. "Added a file-typed avatarFile output to Get User Info, sourced from the largest available avatar URL (avatar_large_url, falling back to avatar_url). It's additive — the existing avatarUrl/avatarUrl100/avatarLargeUrl string fields are unchanged — so this doesn't break anything for people already using the string outputs, but now the avatar can also be wired directly into a file-typed input on another block (e.g. a Gmail attachment), the same way slack_download and image_generate outputs work" --> this is an unreleased feature right now, so no need to maintain backwards compatibility. Only have the avatarFile output -- don't need the other string outputs. This applies in general. We need to make sure the outputs are lean and make sense. |
…tputs Cap the file-upload video buffer at 250MB instead of TikTok's 4GB ceiling — relaying that much through this server's memory per request isn't safe under concurrent load, and larger files can still go through the PULL_FROM_URL path, which never buffers on our server. Also drop the now-redundant avatarUrl/avatarUrl100/avatarLargeUrl string outputs from Get User Info in favor of the file-typed avatarFile output alone, since the feature is unreleased and the raw URL is still reachable via avatarFile.url. Cover image URLs on List/Query Videos are confirmed to be signed, expiring TikTok CDN links; left as strings (no file-output conversion path exists for fields nested inside array items) but documented the expiry behavior more clearly. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Memory cap → 250MB. Done. TIKTOK_MAX_VIDEO_BYTES is now 250 * 1024 * 1024 instead of 4GB, with the 413 error message updated to match. This only affects the file-upload path (uploading from Sim storage) — the public-URL path is untouched since TikTok pulls those bytes directly and our server never buffers them, so larger files still work by using a URL instead. Avatar outputs → lean. Done. Removed avatarUrl, avatarUrl100, and avatarLargeUrl from Get User Info, keeping only avatarFile. Since this hasn't shipped yet there's no back-compat need, and the raw URL is still reachable via avatarFile.url if anyone needs the string form. Cover image URLs → left as strings. Confirmed they're signed, expiring TikTok CDN URLs (not permanent), so the instinct to convert them was right. But our file-output pipeline only auto-converts top-level outputs, not fields nested inside array items like videos[].coverImageUrl — doing it properly would mean a separate coverImageFiles[] array matched to videos by index, which is fragile (nothing but array position ties them together) and still downloads up to 20 thumbnails per call whether or not they're used. Given that cost/fragility tradeoff, I left it as a string but tightened the description to clearly say it's signed and time-limited, so it's used correctly rather than cached. |
…h-video route The TikTok integration adds one new Zod-backed internal API route (app/api/tools/tiktok/publish-video), which trips the route-count ratchet in check-api-validation-contracts.ts. Bumping totalRoutes and zodRoutes from 917 to 918 (nonZodRoutes stays 0) to acknowledge the new route is properly validated. Co-authored-by: Cursor <cursoragent@cursor.com>
After removing the avatar string outputs, avatar_url_100 was still requested from TikTok's user info endpoint but never surfaced anywhere. Removed it from the default field list and the field descriptions, and noted that avatar_url/avatar_large_url feed the avatarFile output. Co-authored-by: Cursor <cursoragent@cursor.com>
…onfig.params The block's params function built a local `credential` variable from params.oauthCredential and returned it under the key `credential` in every switch case. That literal token is the raw subBlock id, which is deleted after canonical transformation into `oauthCredential` — the blocks.test.ts canonical-param-validation suite flags any params function that still references it. It was also redundant: oauthCredential is already part of the base resolved inputs, which the executor merges into the tool call before config.params overrides are applied, so the OAuth token resolution (which reads contextParams.oauthCredential) worked regardless. Removed the explicit credential plumbing, matching the convention already used by other OAuth blocks like dropbox.ts. Co-authored-by: Cursor <cursoragent@cursor.com>
query_creator_info had no request.body function, and
formatRequestParams() only attaches a body when tool.request.body is
defined at all — so despite sending Content-Type: application/json,
the request went out with no body whatsoever. Added body: () => ({}),
matching the convention already used by other parameterless-POST tools
in this codebase (Google Vault, Supabase, Square, Gmail, etc.).
Co-authored-by: Cursor <cursoragent@cursor.com>
cursor, photoCoverIndex, and videoCoverTimestampMs all used a truthy
check (params.x && {...}) to decide whether to include an optional
numeric override, which drops a legitimate 0 (first page has no
cursor issue aside, photoCoverIndex 0 is TikTok's own default cover
photo, and timestamp 0 is a valid first-frame cover). Switched to
explicit undefined/empty-string checks, matching the !== undefined
convention the underlying tools already use.
In today's resolution pipeline these fields always arrive as strings
(even chained block references get stringified by the template
resolver), and a non-empty string like "0" is truthy, so this wasn't
actively broken end-to-end - but it was relying on that subtlety
rather than being correct by construction, and was inconsistent with
the tools' own undefined checks.
Co-authored-by: Cursor <cursoragent@cursor.com>
videoIds is a long-input (multiline textarea), the same widget used for the newline-separated photoImages field on this block, but its parser only split on commas. Entering one ID per line - the natural pattern for a multiline field, and the one already used elsewhere on this block - produced a single concatenated garbage string instead of an array, so TikTok's query would fail or return nothing. Now splits on commas or newlines, and updated the placeholder/description to reflect both formats. Co-authored-by: Cursor <cursoragent@cursor.com>
Screen.Recording.2026-07-08.at.2.06.34.PM.movDemo of the new feature! |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 83841a3. Configure here.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
@greptile please do a final check of the new tiktok webhook code! |
|
Demo of the tiktok webhook trigger! Screen.Recording.2026-07-08.at.6.38.17.PM.mov |

Adds TikTok as a full OAuth-based integration: provider registration (with TikTok's comma-separated scope and client_key requirements), 9 tools covering profile info, video listing/querying, creator info, direct video/photo posting (URL or file upload), inbox drafts, and post status polling, it also adds webhooks, plus the TikTok block, icon, and generated docs.
Summary
Brief description of what this PR does and why.
Fixes #(issue)
Type of Change
Testing
How has this been tested? What should reviewers focus on?
Checklist
Screenshots/Videos