From be74536f7bcdb13c201f96db4c2b3e2b0fd11bf3 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Wed, 8 Jul 2026 16:43:20 +0700 Subject: [PATCH 1/3] fix(tiktok): steer the agent away from the UPLOAD posting method The agent (chat / MCP) had to pick content_posting_method with no guidance: the setting is a bare required enum with no description or default, and TikTok's agent rules said nothing about it. For a video post "UPLOAD" reads like the obvious choice - but UPLOAD does not publish, it only drops the video into the user's TikTok app inbox, where it must be completed manually within ~24h or it is discarded. Users hit this as "post marked successful but never published". TikTok's @Rules text now tells the agent to use DIRECT_POST unless the user explicitly asks to review/edit in the app first, and explains that UPLOAD does not publish. No settings default is needed to back this up: content_posting_method is required in TikTokDto (@IsIn with no @IsOptional, validated with skipMissingProperties: false), so an omitted key cannot fall through to UPLOAD - validatePosts rejects it and the tool hands the error back to the agent to retry. The failure mode is the agent *choosing* UPLOAD, and only prose the agent reads can prevent that. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/integrations/social/tiktok.provider.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index 87be989289..89a2bbf259 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -19,7 +19,9 @@ import { Integration } from '@prisma/client'; import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator'; @Rules( - 'TikTok can have one video or one picture or multiple pictures, it cannot be without an attachment' + 'TikTok can have one video or one picture or multiple pictures, it cannot be without an attachment. ' + + 'For the "content_posting_method" setting always use "DIRECT_POST" unless the user explicitly asks to review or edit the post inside the TikTok app before publishing. ' + + '"UPLOAD" does NOT publish the post: it only sends it to the user\'s TikTok app inbox, where they must manually complete it within 24 hours or it is discarded.' ) export class TiktokProvider extends SocialAbstract implements SocialProvider { identifier = 'tiktok'; From 0cdb8e55397c137bfa6dcd275c8e461045660d0e Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Fri, 10 Jul 2026 10:27:19 +0700 Subject: [PATCH 2/3] docs(tiktok): describe content_posting_method in the generated schema `content_posting_method` was exposed to schema consumers as a bare `enum: ["DIRECT_POST", "UPLOAD"]` with no description, so an LLM asked to "upload this video to TikTok" would reasonably pick the value literally named UPLOAD - which never publishes, it only drops the media into the user's TikTok app inbox where it expires after 24 hours. Annotate the field with `@JSONSchema({ description })`. This is metadata only: it is read by class-validator-jsonschema when generating the schema and registers no validator, so `@IsIn([...])` still rejects a missing or invalid value and the field stays required. Verified against the generated schema: `description` is present, `enum` is unchanged, and `content_posting_method` remains in `required`. The annotation reaches every `getValidationSchemas()` consumer: - `integrationSchema` MCP tool (internal web agent + external MCP agents) - `GET /public/v1/integration-settings/:id` (public API, and the CLI's `postiz integrations:settings`) The dashboard is unaffected - its React form hardcodes DIRECT_POST. For agents this layers on top of the @Rules text in the previous commit: both are prose the model reads before it picks a value, which is the only thing that helps here - the field is required, so the model always sends *something*, and no server-side default can rescue an explicit UPLOAD. For public API callers that read the schema but not `rules`, this description is the only in-band signal. Precedent for the decorator: whop.dto.ts, discord.dto.ts, listmonk.dto.ts. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/dtos/posts/providers-settings/tiktok.dto.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/tiktok.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/tiktok.dto.ts index 8417315f86..d36ccffe93 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/tiktok.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/tiktok.dto.ts @@ -1,6 +1,7 @@ import { IsBoolean, ValidateIf, IsIn, IsString, MaxLength, IsOptional } from 'class-validator'; +import { JSONSchema } from 'class-validator-jsonschema'; export class TikTokDto { @ValidateIf((p) => p.title) @@ -44,5 +45,12 @@ export class TikTokDto { @IsIn(['DIRECT_POST', 'UPLOAD']) @IsString() + @JSONSchema({ + description: + 'Required. Use "DIRECT_POST" to actually publish the post to TikTok. ' + + '"UPLOAD" does NOT publish: it only sends the media to the user\'s TikTok app inbox, ' + + 'where they must manually finish and publish it within 24 hours or it is discarded. ' + + 'Only use "UPLOAD" when the user explicitly asks to review or edit the post inside the TikTok app before publishing.', + }) content_posting_method: 'DIRECT_POST' | 'UPLOAD'; } From 1fb3983bbee9d0f19c760f0e87125d933756c295 Mon Sep 17 00:00:00 2001 From: Gilad Resisi Date: Mon, 13 Jul 2026 14:33:07 +0700 Subject: [PATCH 3/3] fix(tiktok): treat a missing content_posting_method as DIRECT_POST Three places in the provider read `content_posting_method`, and they did not agree on what a missing value means: - `postingMethod()` already defaulted it: its `switch` has `case 'DIRECT_POST': default:`, so an absent value picks `/video/init/` (publish) rather than `/inbox/video/init/`. - `buildTikokPostInfoBody()` compared `=== 'DIRECT_POST'`, so an absent value fell into the inbox-shaped body with no `privacy_level`. - `buildTikokSourceInfoBody()` compared the same way, so an absent value gave a photo `post_mode: 'MEDIA_UPLOAD'` - the app inbox, which never publishes. Net effect of the disagreement: a photo with no method silently went to the inbox, and a video with no method hit the *publish* endpoint with a body missing `privacy_level`, which TikTok rejects. Normalize once in `contentPostingMethod()` - only an explicit 'UPLOAD' selects the inbox flow, everything else (including a missing value) publishes - and use it at all three sites, so the video endpoint's existing default is now the behaviour of the whole publish path. Settings validation keeps a missing value out of the publish path for normal posts (the field is required in TikTokDto), so this is the belt-and-braces layer for anything that bypasses it - drafts, older rows written before the field existed, direct repository writes. Verified by driving the three builders with photo/video posts and the method present/absent: absent now matches DIRECT_POST exactly (endpoint, post_info privacy_level, post_mode), and explicit UPLOAD is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/integrations/social/tiktok.provider.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index 89a2bbf259..475ddaf445 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -478,6 +478,18 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { ); } + // UPLOAD does not publish - it only drops the media into the user's TikTok + // inbox - so only an explicit UPLOAD selects it. A missing value (drafts, or + // any caller that skipped the setting) publishes instead of silently landing + // in the inbox. + private contentPostingMethod( + firstPost: PostDetails + ): TikTokDto['content_posting_method'] { + return firstPost?.settings?.content_posting_method === 'UPLOAD' + ? 'UPLOAD' + : 'DIRECT_POST'; + } + private postingMethod( method: TikTokDto['content_posting_method'], isPhoto: boolean @@ -493,7 +505,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { private buildTikokPostInfoBody(firstPost: PostDetails) { const isPhoto = !hasExtension(firstPost?.media?.[0]?.path, 'mp4'); - const method = firstPost?.settings?.content_posting_method; + const method = this.contentPostingMethod(firstPost); if (method === 'DIRECT_POST') { return { @@ -750,7 +762,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { if (isPhoto) { return { post_mode: - firstPost?.settings?.content_posting_method === 'DIRECT_POST' + this.contentPostingMethod(firstPost) === 'DIRECT_POST' ? 'DIRECT_POST' : 'MEDIA_UPLOAD', media_type: 'PHOTO', @@ -796,7 +808,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { } = await ( await this.fetch( `https://open.tiktokapis.com/v2/post/publish${this.postingMethod( - firstPost.settings.content_posting_method, + this.contentPostingMethod(firstPost), isPhoto )}`, {