Skip to main content
Glama
Hermoso

Hermoso

Official

Hermoso — MCP, CLI 및 Skills

모든 AI 에이전트에서 마케팅 운영 전체를 실행하세요: Claude Code, Claude.ai, Cursor, Codex 또는 자체 스크립트. 시장에서 이미 성과를 내고 있는 광고를 조사하고, 완성된 이미지 및 비디오 광고(실제 제품 합성, 카피 + CTA 포함)를 생성하고, 자체 소셜 채널에 게시하고, 그 뒤에 있는 광고 캠페인을 구축 및 관리하세요 — 모두 MCP 도구, CLI 또는 설치 가능한 Claude 스킬을 통해.

718개의 도구. tools/list는 항상 권위 있는 세트입니다. hermoso_capabilities(무료)는 정확한 렌더링당 크레딧 비용과 전체 기능 맵을 포함한 실시간 모델 카탈로그를 반환합니다.

연결 대상. 광고 플랫폼: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads 및 ChatGPT Ads, 그리고 Google Merchant Center의 제품 피드. 게시 및 예약 — 10개 채널: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky 및 Telegram. 메시징: WhatsApp(사람에게 메시지를 보내므로 11번째 게시 채널이 아닙니다). 광고 조사: Meta, Google 및 LinkedIn 광고 라이브러리와 유기적 TikTok, Instagram, YouTube, Threads 및 Reddit. 분석: Google Analytics 4, Google Search Console 및 연결된 모든 플랫폼의 자체 게시물 및 캠페인 인사이트. 파일: Google Drive, Sheets, Docs 및 OneDrive.

전부 아니면 전무가 아닙니다. 조사, 생성, 게시/예약 및 광고 관리는 네 가지 독립적인 영역입니다 — 어떤 도구도 다른 도구를 먼저 사용해야 한다고 요구하지 않습니다. 이미 보유한 크리에이티브를 게시하거나 예약하고 여기서 아무것도 생성하지 마세요(upload_file은 모든 로컬 또는 외부 파일을 모든 게시, 예약 및 광고 빌드 도구가 수락하는 URL로 변환합니다). 자체 광고 계정에서 자체 크리에이티브로 캠페인을 구축하고 읽으세요. 브랜드 초안이나 채널 연결 없이 경쟁사를 조사하세요. 또는 아무것도 연결하지 않고 파일을 생성한 다음 다운로드하세요. 필요한 한 조각만 사용하거나 모두 함께 사용하세요.

에이전트는 어떤 표면을 사용해야 하나요?

두 가지 형태가 있으며, 올바른 것은 클라이언트가 할 수 있는 것에 의해 결정됩니다. 우리가 선호하는 것이 아닙니다.

클라이언트

사용

이유

브라우저에서 실행 — Claude.ai, ChatGPT, Claude Desktop

호스팅 커넥터 https://app.hermoso.ai/mcp

로컬 프로세스를 생성할 수 없으므로 URL이 유일한 형태입니다. 설치할 것도, 붙여넣을 키도 없으며 저장된 브랜드 컨텍스트와 함께 전체 도구 세트가 제공됩니다. 이러한 클라이언트에게는 이것이 올바른 답이며, 덜 중요한 것이 아닙니다.

셸 실행 가능 — Claude Code, Cursor, Codex, Cline, OpenClaw, Hermes, 자체 스크립트

CLI, npm install -g hermoso

도구 매니페스트는 도구가 호출되는지 여부와 관계없이 모든 세션에 로드됩니다. 셸 명령은 실행될 때까지 비용이 들지 않으며 모든 도구에 도달합니다(기본 명단이 아닌).

측정된 차이 (2026-08-27, 바이트가 아닌 실제 도구 정의로 계산):

범위 내 도구

세션당 로드됨

호스팅 커넥터, 기본 명단

306

181,713 토큰

호스팅 커넥터, ?tools=all

718

472,062 토큰

stdio 서버 (npx -y hermoso mcp)

306

181,713 토큰

CLI

전체 718

0

CLI는 대신 요청 시 동일한 질문에 답하며, 요청된 경우에만:

npx -y hermoso tools --search reddit   # every matching tool, name + one line   2,459 tokens
npx -y hermoso tools plan_ad           # one tool's full argument schema           633 tokens
npx -y hermoso call plan_ad --json '{"product":"…"}'   # run it

따라서 터미널 에이전트는 전체 명단이 범위 내에 있는 상태에서 약 3.4K 토큰으로 첫 호출에 도달하는 반면, 그 일부에 대해서는 182K입니다. tools 및 tools <name>은 패키지에 번들된 레지스트리를 읽습니다 — 키, 네트워크, 로그인 없음 — 따라서 에이전트는 누군가 로그인하기 전에 전체 제품을 탐색할 수 있습니다. call만 비용이 들며, hermoso auth login이 한 번만 필요합니다.

둘 다 동시에 사용해도 괜찮으며, Claude Code에 권장하는 방식입니다. 한 번의 hermoso auth login으로 CLI 와 claude mcp add hermoso -- npx -y hermoso mcp가 env 블록 없이 키를 선택할 수 있으므로, 에이전트는 구조화된 결과를 원할 때 네이티브 도구에 도달하고 폭을 원할 때 셸 아웃할 수 있습니다. 하나만 원한다면 CLI를 선택하세요: 엄격히 더 많은 것을 포함합니다.

셸 지원 클라이언트에서 커넥터가 여전히 더 나은 선택인 경우: 한 영역에 많은 호출을 할 세션입니다. enable_tools({groups:['ads']})는 단일 무료 호출로 캠페인 관리를 켜고 도구는 네이티브가 됩니다 — 셸 따옴표 없음, 구조화된 결과. 단일 도구에 대해 221K 토큰 그룹을 로드하는 것보다 셸 왕복이 낫습니다. 세션이 해당 영역에 정착하면 그 반대가 됩니다.

Related MCP server: Ad Library MCP

에이전트가 스스로 가입할 수 있습니다

Hermoso 계정이 없는 에이전트는 계정을 프로비저닝하고, 자체 키를 얻고, 같은 세션에서 광고를 렌더링할 수 있습니다. 브라우저에서 사람이 없고, 티켓도 없고, 대기도 없습니다.

# 1. Start a signup. This call takes no credential, because the credential is what it creates.
curl -sX POST https://app.hermoso.ai/v1/signup \
  -H 'content-type: application/json' \
  -d '{"plan":"pro","period":"mo"}'
# -> { "id": "cs_...", "checkout_url": "https://checkout.stripe.com/...", "claim_token": "hsc_..." }

# 2. Pay at checkout_url. Store claim_token first: it is returned only in that response.

# 3. Claim it. Poll until status is "ready".
curl -sX POST https://app.hermoso.ai/v1/signup/cs_.../claim \
  -H 'content-type: application/json' \
  -d '{"claim_token":"hsc_..."}'
# -> { "status": "ready", "api_key": "hmk_...", "credits": 3000 }

그 hmk_ 키는 이 페이지의 다른 모든 것이 사용하는 것과 동일한 자격 증명입니다: /v1, MCP 서버, CLI. 클라이언트를 그것에 연결하면 전체 표면이 열립니다.

결제는 브라우저 지원 에이전트가 이미 스스로 할 수 있는 일입니다. 체크아웃은 Stripe의 자체 호스팅 페이지이므로 Chrome의 Claude와 같은 클라이언트는 오늘 무인으로 완료할 수 있습니다. 다른 모든 것은 원클릭 핸드오프입니다: checkout_url을 카드를 보유한 사람에게 보내세요. 나중에 실행 중일 때도 동일한 형태가 적용됩니다: buy_credits 및 upgrade_plan은 더 많은 크레딧 또는 더 큰 플랜에 대한 지불 준비 링크를 생성하고, billing_status는 언제든지 잔액을 읽습니다.

에이전트 경로는 유료 플랜이 필요합니다. 그 중 아무거나. 무료 플랜은 app.hermoso.ai에서 가입하는 사람을 위한 것이며, 여기서 요청하면 그렇게 말하는 거부가 반환됩니다. 결제가 완료될 때까지 아무것도 생성되지 않으므로 미결제 가입은 계정을 남기지 않고 비용을 청구하지 않습니다.

한 가지는 여전히 사람이 필요하며, 미리 알아두는 것이 좋습니다. 소셜 또는 광고 계정을 연결한다는 것은 OAuth 동의 화면을 의미하며, 동의 화면은 어떤 플랫폼에서도 헤드리스로 완료할 수 없습니다. list_connectors는 이미 연결된 것과 연결되지 않은 것을 보여줍니다. 다른 모든 것은 브라우저 없이 실행됩니다: 조사, 생성, 이미 연결된 채널에 게시, 캠페인 구축, 보고.

전체 요청 및 응답 형태와 다른 모든 엔드포인트는 app.hermoso.ai/openapi.json의 OpenAPI 문서에 있으며, 라우트를 마운트하는 동일한 테이블에서 실시간으로 제공됩니다.

즉시: 호스팅 Claude.ai 커넥터

https://app.hermoso.ai/mcp 를 Claude → 설정 → 커넥터 → 사용자 지정 커넥터 추가에 붙여넣고, Hermoso 계정으로 승인하면 끝입니다 — 저장된 브랜드 컨텍스트가 포함된 전체 도구 세트, 플랜에 청구됩니다.

Claude Code 빠른 시작 (한 줄)

  1. 계정 만들기 app.hermoso.ai — 무료 티어 포함; 플랜 및 크레딧은 웹 Studio에서 사용하는 것과 동일합니다. 또는 브라우저를 완전히 건너뛰고 에이전트가 POST /v1/signup(위)으로 유료 플랜에 스스로 가입하게 하세요.

  2. 한 줄 실행. 브라우저가 로그인을 위해 한 번 열립니다. 붙여넣을 것도 없고, 키가 .claude.json에 저장되지 않습니다:

npm install -g hermoso && hermoso auth login && claude mcp add hermoso -- npx -y hermoso mcp
  1. 원하는 것을 요청하세요, 일반적인 프롬프트에서. Claude Code는 도구에 도달하거나 터미널에서 hermoso 명령을 실행합니다. 작업에 필요한 쪽으로. 당신은 둘 다 입력하지 않습니다.

광고 캠페인 및 분석 도구는 enable_tools로 켤 때까지 도구 목록에 남아 있지 않으므로 목록이 작게 유지됩니다. 브라우저가 없는 머신에서는 설정 → 에이전트 및 API의 키를 사용하여 hermoso auth login --token hmk_…으로 로그인하거나, 로그인을 건너뛰고 키를 클라이언트에 전달하세요:

claude mcp add hermoso -e HERMOSO_TOKEN=hmk_… -- npx -y hermoso mcp

호스팅 URL은 Claude Code에서도 작동하지만, 거기서는 더 나쁜 경로이며 그 이유를 아는 것이 좋습니다: claude mcp add --transport http hermoso https://app.hermoso.ai/mcp는 수락되지만, claude mcp list는 ! Needs authentication을 보고합니다. 클라이언트가 OAuth 흐름을 스스로 시작하지 않기 때문입니다 — 세션을 열고, /mcp를 실행하고, 서버를 찾아 인증을 눌러야 합니다. 2026-08-23에 Claude Code 2.1.241로 측정했습니다.

이제 에이전트는 작업 공간의 컨텍스트가 포함된 전체 스튜디오를 갖습니다: 웹 앱에서 설정한 브랜드 프로필, 제품, 로고 및 학습된 메모리가 자동으로 적용됩니다(get_brand는 저장된 것을 보여줍니다. plan_ad/plan_variations에서 brand를 생략하면 사용됩니다). 렌더링은 Hermoso 크레딧으로 청구됩니다 — Studio와 동일한 가격.

1. MCP 서버 (stdio) — Claude Code / Cursor / Codex

hermoso mcp는 전체 도구 세트를 노출하는 stdio MCP 서버를 실행합니다. 게시된 hermoso 패키지는 클론이 필요 없음을 의미합니다 — npx -y hermoso mcp가 가져와서 실행합니다. CLI로 한 번 로그인하면 키가 클라이언트 구성에 들어가지 않습니다. hermoso mcp가 저장된 bearer hermoso auth login을 읽기 때문입니다:

npm install -g hermoso && hermoso auth login && claude mcp add hermoso -- npx -y hermoso mcp

Cursor / Codex — 같은 방식으로 로그인한 다음 mcp.json에 추가하세요(Codex는 TOML에 해당하는 것을 사용합니다). 위에서 로그인했다면 env 블록을 완전히 제거하세요. 프로세스가 홈 디렉토리를 읽을 수 없는 CI를 위한 것입니다:

{ "mcpServers": { "hermoso": { "command": "npx", "args": ["-y", "hermoso", "mcp"],
  "env": { "HERMOSO_API_BASE": "https://app.hermoso.ai", "HERMOSO_TOKEN": "<your token>" } } } }

그런 다음 에이전트에게 요청하세요: "Hermoso로 이미지 광고를 생성해 줘."

718개 도구가 다루는 것

광고 스파이 / 조사 — find_competitors, competitor_teardown, pull_competitor_ads, research_ads; Meta / Google / LinkedIn 광고 라이브러리(search_meta_ads, search_google_ads, search_linkedin_ads); 유기적 소셜(search_tiktok, search_instagram, search_youtube, search_reddit, search_threads); fetch_social_data, mine_angles, analyze_video, check_ad_policy, list_skills / get_skill.

생성 — draft_brand → plan_ad → render_ad (Studio 품질 파이프라인: 합성 텍스트, 깨끗한 음성, 음악, 브랜드 엔드 카드), 또는 generate_image / generate_video / generate_avatar (UGC 크리에이터 + 립싱크). 작업 공간의 저장된 캐스트는 재사용 가능합니다: list_creators는 저장된 각 크리에이터를 초상화 URL과 함께 반환하고, save_creator는 추가하고, delete_creator는 제거합니다 — 초상화를 generate_avatar / generate_video / recast_motion에 다시 전달하면 매 렌더링마다 새 얼굴 대신 동일한 사람이 모든 광고에 출연합니다. 또한 make_template_ad (네이티브 HTML 광고 형식), make_explainer, product_sizzle, make_thumbnail, remix_static, recast_motion, reframe_video, upscale_video, dub_video, change_voice, finish_video, fix_beat, stitch_video, clip_video, post_edit, 그리고 plan_variations + score_ad로 확장 및 순위 지정. 길이는 직접 설정할 수 있습니다: plan_ad에 durationSeconds를 전달하면 스토리보드가 그 길이에 맞게 작성됩니다 — 렌더 모델의 한 클립에 맞는 길이는 단일 연속 테이크로 렌더링되고, 더 길면 액트에서 이어붙여집니다(15초 클립 모델에서 40초 = 15+15+10), 시간 압축은 절대 없습니다. 한 클립에 맞는 것은 고정된 숫자가 아니라 모델 자체의 최대값입니다: 대부분의 비디오 모델은 클립을 15초로 제한하며, 가장 긴 클립 모델은 30초를 끊김 없는 단일 테이크로 네이티브 동기화 오디오와 함께 처리합니다. hermoso_capabilities는 실시간 목록입니다 — 지속 시간, 해상도 및 각 티어의 정확한 크레딧 비용 — 그리고 model에서 해당 모델을 지정하는 것이 그것을 얻는 방법입니다. 이름 없는 렌더는 더 좁은 자동 풀로 라우팅되기 때문입니다.

원시 모델 플레이그라운드 — 광고 프레이밍 없이 전체 카탈로그(30개 이상의 이미지/비디오/음성/글쓰기 모델, 각각의 정확한 렌더당 크레딧 비용 포함): generate_image / generate_video(useBrand:false 사용), generate_voice, generate_text.

자체 채널에 게시 — 열 개 채널: Facebook, Instagram, Threads(post_to_meta), TikTok(post_to_tiktok), YouTube(post_to_youtube + update_youtube_video, youtube_video_insights, 댓글 읽기/답글), X(post_to_x, x_post_metrics, x_post_insights, x_mentions, list_x_dms, send_x_dm), LinkedIn 프로필 및 회사 Pages(post_to_linkedin, post_to_linkedin_page), Pinterest(post_to_pinterest + 보드), Bluesky(post_to_bluesky, delete_bluesky_post, bluesky_post_metrics, 그리고 list_bluesky_convos / read_bluesky_dm / send_bluesky_dm), Telegram(post_to_telegram, delete_telegram_message, list_telegram_chats). schedule_post / list_scheduled / cancel_scheduled는 정확히 그 채널 집합에 대한 하나의 콘텐츠 캘린더를 제공합니다. upload_file은 Hermoso 렌더뿐 아니라 모든 외부 또는 로컬 미디어를 가져옵니다. X 게시는 API 호출당 크레딧이 청구됩니다(X는 요청당 비용을 부과함). 링크가 포함된 게시물은 링크가 없는 게시물보다 13배의 비용이 듭니다. 보류되었으며, 숨기지 않고 명시적으로 언급됨: Google Business Profile은 구현되어 있지만(post_to_google_business, 리뷰, Q&A, 인사이트) 제공되지 않습니다 — Google이 해당 API를 프로젝트별로 허용 목록에 등록하며, 우리의 API는 0 QPM으로 읽히므로 모든 호출이 모든 사용자에게 403을 반환할 것입니다. 이는 schedule_post의 채널 enum에 포함되어 있으며, 큐에 넣을 때 거부됩니다.

WhatsApp으로 고객에게 메시지 보내기 — 메시징이지, 열한 번째 게시 채널이 아닙니다: 사람에게 메시지를 보내는 것이며, 여기의 어떤 것도 피드에 게시하지 않습니다. list_whatsapp_accounts는 비즈니스 계정과 그 번호를 찾고, list_whatsapp_templates / create_whatsapp_template / delete_whatsapp_template은 Meta가 검토하는 템플릿을 관리하며, send_whatsapp_message는 메시지를 보냅니다 — 실제 전화번호에 도달하고 Meta가 대화에 대해 비즈니스에 비용을 청구하므로 확인 게이트가 적용됩니다. Meta의 API에 대한 영구적인 사실인 두 가지 제한 사항(진행 중인 것이 아님): Hermoso는 WhatsApp 웹훅을 수신하지 않으므로 읽을 메시지 기록이 없습니다 — 받은 편지함 표면이 아니며 list_inbox가 이를 포함하지 않습니다 — 그리고 고객이 먼저 메시지를 보낼 때 열리는 24시간 창 밖에서는 WhatsApp이 승인된 템플릿(APPROVED template) 외에는 아무것도 허용하지 않습니다.

광고 실행 — 전체 캠페인 트리, 일시중지 상태로 생성되고 아무것도 보고되기 전에 다시 읽히며, 모든 지출 변경에 확인 게이트가 적용되며, 열한 개 플랫폼에서: Meta, Google Ads, LinkedIn Ads, Reddit Ads, Pinterest Ads, Microsoft Advertising, ChatGPT Ads(OpenAI의 Advertiser API), X Ads, TikTok Ads, Snapchat Ads, Apple Ads(App Store의 Apple Search Ads). 각각 목록 + 보고서 + 생성 + 예산/상태 도구가 있습니다(예: list_google_ads_campaigns, google_ads_report, create_google_ads_campaign, set_google_ads_budget, set_google_ads_status). Snapchat은 다른 플랫폼에는 없는 추가 단계가 하나 필요합니다: 광고는 CREATIVE를 가리켜야 하며, 모든 Snapchat 크리에이티브는 Public Profile id를 포함해야 합니다 — upload_snapchat_ads_creative로 생성하세요.

쇼핑 표면에 공급 — Google Merchant Center는 소매 Performance Max 또는 Shopping 캠페인이 광고하는 카탈로그입니다(create_google_ads_performance_max_campaign은 merchantCenterId를 받습니다). 여기서 관리합니다: 계정 및 계정 상태, 데이터 소스, 제품 upsert / update / delete, 지역별 재고, 할당량, 제품 수준 성과를 위한 merchant_report, 알림 및 전환 소스, 그리고 거부 루프 — list_merchant_issues는 무엇이 잘못되었는지 말하고 merchant_issue_help는 Google의 공식 문서화된 해결책을 반환합니다. 프로모션에는 Google의 프로모션 프로그램에 대한 판매자의 자체 등록이 필요합니다. 등록이 없으면 Google이 해당 하위 API를 완전히 거부합니다. Microsoft Merchant Center는 동일한 형태(스토어, 카탈로그, 제품, 이슈)로 Bing Shopping에 대해 다루어집니다.

광고가 달성한 것을 측정 — Google Analytics 4가 루프를 닫습니다. 여기의 다른 모든 커넥터는 광고가 비용으로 얼마인지 보고합니다. 이것은 광고가 무엇을 했는지 보고하는 유일한 커넥터입니다. analytics_report는 세션, 사용자, 전환 및 수익을 채널, 소스/매체, 캠페인, 랜딩 페이지, 국가, 기기 또는 날짜별로 분류하므로, Hermoso가 만든 캠페인과 그것이 창출한 수익이 하나의 대화에 함께 있습니다. analytics_realtime은 지금 사이트에 누가 있는지 보여줍니다. list_analytics_properties에서 시작하세요 — 도구는 추적 스니펫의 G-XXXXXXXXX Measurement ID가 아닌 숫자 속성 ID를 받으며, 이것이 둘을 서로 연결해 주는 것입니다. 읽기뿐 아니라 쓰기도 합니다: create_analytics_key_event는 GA4가 이미 수집하는 이벤트를 핵심 이벤트로 표시합니다 — 이것이 Google Ads에서 전환으로 가져올 수 있게 만드는 것입니다 — 그리고 create_analytics_custom_dimension은 이벤트 매개변수를 등록하여 보고서가 이를 기준으로 분류할 수 있게 하며, list_analytics_definitions는 속성이 이미 측정하는 것을 보여줍니다. Google Ads, YouTube, Drive와 동일한 Google 계정으로 로그인하지만, 별도의 연결입니다. GA4 전용 — API에는 Universal Analytics 표면이 없습니다. 사용자 지정 차원은 보관할 수 있지만 삭제할 수는 없으며, 속성은 이벤트 범위 차원 50개를 보유합니다.

파일 — Google Drive CRUD(save_to_drive, list_drive_files, update_drive_file, delete_drive_file, create_drive_folder), Google Sheets(create_sheet, append_to_sheet, read_sheet), Google Docs(create_doc, append_to_doc), OneDrive(save_to_onedrive + 전체 CRUD).

워크스페이스 및 계정 — 브랜드 워크스페이스(list_brands, create_brand, use_brand, update_brand, delete_brand — 하나의 계정이 여러 브랜드를 보유하므로 에이전시는 모든 클라이언트를 여기서 운영합니다), 메모리(remember, forget, list_memory), 사용자 지정 스킬(save_skill, get_skill, list_skills, delete_skill — 이전 AI-Employee 페르소나를 흡수한 단일 라이브러리), 팀(list_team, invite_member, remove_member, set_role), 설정(get_settings, update_settings — 모든 광고, 스크립트, 플랜이 작성되는 언어 포함), 커넥터(list_connectors, list_connector_accounts, set_connector_accounts, disconnect_connector), 결제(hermoso_credits, billing_status, buy_credits, upgrade_plan, set_auto_reload), 그리고 비동기 렌더를 위한 list_jobs / get_job.

커넥터 계정은 추측이 아니라 선택됩니다. 한 사람이 여러 Facebook Page, Google Ads 고객 또는 LinkedIn 회사 Page를 관리하는 경우가 많습니다. 브랜드에 대해 선택된 계정만 사용 가능합니다 — 서버 측에서 강제되며, 빈 선택은 아무것도 공유하지 않습니다. 새 계정 연결은 헤드리스가 아닌 유일한 단계입니다(OAuth 동의 화면이므로 사용자가 앱에서 직접 수행합니다).

렌더 작업은 서버 측에서 큐에 대기하고 완료될 때까지 폴링한 후 제공된 URL을 반환합니다.

2. CLI — 터미널 에이전트를 위한 토큰 절약 경로

bin/hermoso.mjs는 전체 MCP 도구 세트를 하위 프로세스 명령으로 노출하므로, 에이전트는 방대한 도구 매니페스트를 유지하는 대신 셸 아웃을 할 수 있습니다.

npm install -g hermoso                             # installs `hermoso`
hermoso capabilities                               # valid model ids + costs (run first)
hermoso create --brand "YourBrand" --product "your best-selling product" --format image
hermoso generate image --prompt "…" --ref ./product.png --wait
hermoso generate video --prompt "…" --duration 8 --wait
hermoso competitors yourbrand.com
hermoso research "Liquid Death’s longest-running ads"

모든 명령에 --json을 추가하면 머신 출력을 얻을 수 있습니다.

그 단축키는 일반적인 경로이지, 한계가 아닙니다. MCP 서버가 가진 모든 도구는 여기서도 접근 가능하며, 커넥터가 기본 로스터에서 제외하는 광고 캠페인 및 분석 그룹도 포함합니다:

hermoso tools                          # every tool, grouped, name + one line
hermoso tools --group ads --search reddit   # narrow it
hermoso tools create_meta_campaign     # that tool's full argument schema
hermoso call create_meta_campaign --json '{"name":"…"}'   # run it
hermoso create_meta_campaign --name "…"                   # same thing, shorter

call은 MCP 서버가 사용하는 것과 동일한 핸들러, 동일한 인수 검증, 동일한 확인/지출 게이트를 거칩니다 — 이탈할 두 번째 구현이 없습니다. tools 및 tools <name>은 패키지에 번들된 레지스트리를 읽으므로 키, 네트워크, 로그인이 필요 없습니다.

3. Claude 스킬 — CLI를 감싸는 슬래시 명령

skills/에는 네 개의 설치 가능한 스킬이 있습니다: hermoso-generate, hermoso-ad-from-brand, hermoso-product-photoshoot, hermoso-research.

cp -r skills/* ~/.claude/skills/

그런 다음 /hermoso-ad-from-brand an ad for yourbrand.com — our hero product를 호출하세요.

구성

환경 변수

의미

HERMOSO_API_BASE

Hermoso API 원본(기본값 https://app.hermoso.ai — 앱을 직접 실행하는 경우 http://localhost:3000으로 설정)

HERMOSO_TOKEN

Bearer 에이전트 키(hmk_…) — 호스팅 앱에 필수

HERMOSO_PROFILE

여러 브랜드 프로필이 있는 계정의 브랜드 워크스페이스 ID

HERMOSO_OWNER

다른 계정이 당신과 공유한 브랜드(팀 워크스페이스)에만 해당: 소유 계정 ID. HERMOSO_PROFILE과 함께 설정하고, HERMOSO_PROFILE을 해당 워크스페이스의 profileUuid로 설정하세요 — 브랜드의 짧은 슬러그는 거부됩니다. list_brands(또는 CLI에서 hermoso list_brands)를 실행하여 들어갈 수 있는 모든 워크스페이스의 두 값을 출력하세요. 서버는 모든 요청에서 쌍을 다시 인증하므로 잘못된 값은 신뢰되지 않고 거부됩니다.

mcp/http.mjs는 호스팅된 원격 커넥터 전송입니다(Claude.ai → Connectors에 URL 붙여넣기). 투명성을 위해 이 저장소에 포함되어 있으며, 인증된 신원 없이는 마운트를 거부합니다 — 익명 지출은 절대 없습니다.

라이선스

MIT © Hermoso

Available Tools

205 tools
add_subtitlesAdd subtitles to a videoAInspect

Burn subtitles into ANY video and get the .srt too; nothing is cut or re-rendered. Set textStyle only when the user describes a look (default: white sentence case, thin outline, bottom). Timed per sentence, or auto:'words' for phrases on their spoken words. Only when asked, textStyle 'highlight' (or a highlightColor) lights the word being said, timed word by word. Or pass cues to burn your own lines exactly, untranscribed (reel-style phrases, stage directions). burn:false returns only the .srt. Takes a video file or URL, a YouTube/Vimeo-style page, or a TikTok / Reel / X post link. No speech is refused and refunded. Runs in the background and lands in the Library.

NameRequiredDescriptionDefault
autoNosentences (default) or words: timing unit for transcribed subtitles
burnNofalse = only the .srt
cuesNoyour own lines in seconds, no overlap, max 90 chars, no emoji
videoYesthe video to subtitle; a URL, not a local file path; use upload_file for local files
textStyleNothe look: words, a preset or fields; omit for the default
wordsPerCueNowords per subtitle line when auto is words
analyze_videoAnalyze videoAInspect

A video ad's structure: verbatim transcript (voiceover + on-screen text), beat list, duration and sampled frame times; study a reference before remixing it. ~A transcription call.

NameRequiredDescriptionDefault
urlYesthe video URL (a served /generated/ path or a public http(s) video); a URL, not a local file path; use upload_file for local files
wordsNotrue | 'only': each spoken word's start/end
framesNoalso return the frames as images
anchorsNo{afterWord|beforeWord|atWord, occurrence?, offset?} -> s
append_to_docAppend text to a Google DocAInspect

Append text to the end of a Google Doc Hermoso can reach — one it created (pass the documentId from create_doc) or one the user handed over with the Google file picker in the app (find its id with list_drive_files). Optional dropdown:{title, options:[2-50], selected} appends a Google Docs dropdown chip after the text — e.g. text "Status: " + dropdown {title:"Status", options:["Draft","In review","Approved"]} gives a brief a status the team can click to change; the chip is confirmed by reading the doc back. Set it later with update_doc dropdowns.

NameRequiredDescriptionDefault
textNotext to append at the end of the doc (optional when a dropdown is passed)
dropdownNoa dropdown chip to append after the text
documentIdYesthe document id from create_doc
append_to_notion_pageAdd to a Notion pageAInspect

Add to the END of an existing Notion page (pageId or its link, from list_notion_pages): markdown content, a swipefileCollection, or includeSchedule:true. Nothing already on the page changes. Free.

NameRequiredDescriptionDefault
pageIdYespage id or link
contentNomarkdown to add
includeScheduleNotrue appends the scheduled posts too
swipefileCollectionNoswipefile collection name or id to append, with each ad's creative, copy, dates and link
append_to_sheetAppend rows to a Google SheetAInspect

Append rows to a Google Sheet Hermoso can reach — one it created (pass the spreadsheetId from create_sheet) or one the user handed over with the Google file picker in the app (find its id with list_drive_files). rows = array of row arrays.

NameRequiredDescriptionDefault
rowsYesrows to append — array of row arrays
rangeNorange to append at (default A1 / first sheet)
spreadsheetIdYesthe spreadsheet id from create_sheet
backfill_postsImport a channel’s past postsA
Idempotent
Inspect

Import this brand's PAST posts from a channel into the performance record, so 'which hook works' can draw on history rather than only on what was published since Hermoso started recording. Supports facebook, instagram, threads, youtube, tiktok, pinterest and bluesky; the others say plainly why they cannot (LinkedIn has no enumerate-my-posts endpoint on our grant, X bills per read so it is excluded from bulk import, Google Business has had no per-post insights since 2023, and Telegram's Bot API cannot read a chat's past messages at all — nothing published before Hermoso is recoverable through a bot token). BOUNDED, RESUMABLE AND QUOTED: it runs as a DRY RUN by default and tells you how many posts it found and what reading them will cost — pass confirm:true to import, and pass the returned cursor to continue. AN IMPORTED POST IS WEAKER EVIDENCE THAN A RECORDED ONE and is labelled 'backfilled': its hook is recovered ONLY where the post matches a Hermoso creation by asset or caption. A post made outside Hermoso stays UNATTRIBUTED — it counts toward channel and format totals but never votes on which hook works. Never guess a hook from a caption. Free.

NameRequiredDescriptionDefault
limitNohow many posts this page (default 50, max 200)
cursorNoresume from a previous run
channelYeswhich channel to import from
confirmNoactually import — omit for a dry run that only quotes the cost
accountRefNowhich Page / account, when the profile has more than one
billing_statusBilling statusA
Read-onlyIdempotent
Inspect

Show this account's billing at a glance: current plan (quote plan.priceUsd per plan.period), credit balance, whether auto-reload is on, whether a card is on file, and whether YOU (this key) have ADMIN rights to change billing. Read-only, free. Call it before upgrade_plan / set_auto_reload to know what's possible — members have read-only billing. IN A SHARED TEAM WORKSPACE A MEMBER SEES THE PLAN AND THE BALANCE ONLY: the workspace owner's payment card and auto-reload belong to them and are not reported (billingScope:'member'). Never tell a member there is no card on file — the honest answer is that you cannot see it. plan.memberRate: media models at cost + 10%; plan.legacy: an older plan kept at its price. posting: this month's posting allowance ("N of 10 posts used this month" on Free, unlimited on every paid plan).

NameRequiredDescriptionDefault

No parameters

buy_creditsBuy creditsAInspect

Out of credits? Top up with a credit PACK. Call with no argument to list the available packs (id · credits · price). If the account has a saved card and you have billing-admin rights, calling with pack quotes the exact charge and calling again with confirm:true AND the quote's quote_token charges the saved card instantly (same one-click top-up as the app — no redirect). If there's no saved card yet, you get a Stripe checkout URL to hand your human for the FIRST purchase; their card saves for one-click after that. Packs only; subscriptions are managed by a person in Billing. IF YOU ARE AN AGENT HOLDING YOUR OWN PAYMENT CREDENTIAL, there is a third path that needs no human at all: POST /api/billing/machine-payment with a packId answers HTTP 402 carrying an MPP challenge, and grants the pack once you authorise and retry with the credential — the same packs, the same prices, the same credits. GET /api/billing/config carries a machinePayments block listing the packs with their per-credit rates and saying whether that lane is enabled on this server. Most agents do NOT have their own credential yet, so the checkout link above remains the normal path. To stop running out entirely, turn on auto-reload with set_auto_reload (admin) — low balances then top themselves up from the saved card automatically.

NameRequiredDescriptionDefault
packNothe pack id to buy (e.g. pack-2k) — omit to list the available packs first
confirmNoset true to actually charge the saved card for `pack` (required for the one-click charge; ignored on the checkout-link path)
quote_tokenNothe quoteToken returned by the quote step — REQUIRED (with confirm:true) to charge; it binds the exact pack + price you quoted (10-minute validity) and makes a retried confirm idempotent
call_toolRun any Hermoso tool by nameA
Destructive
Inspect

Run ANY Hermoso tool by name — including the paid-campaign, analytics and channel-admin tools that are not in this session's starting list — with the same permissions, the same account and the same result as calling it directly. Get the exact name and its args from find_tools first. This is the route on hosts that cannot reload their tool list mid-conversation (claude.ai, ChatGPT): enable_tools switches a group on server-side, but such a host keeps the list it fetched at connect time. Arguments are validated against the tool's own schema and a mistake is answered with the expected parameters, not a silent default. Refused by name, with the way out, when the tool needs a connector this workspace has not made or is withheld by the host's own policy.

NameRequiredDescriptionDefault
argsNothe tool's arguments as an object, exactly as its own schema takes them
nameYesthe tool name exactly as find_tools returned it, e.g. create_meta_lead_form
cancel_scheduledCancel a scheduled postA
DestructiveIdempotent
Inspect

Remove a queued post before it goes out. Get the id from list_scheduled. Only works while it is still queued — something already published cannot be unsent (use manage_meta_post to delete a Facebook/Instagram post after the fact).

NameRequiredDescriptionDefault
idYesthe scheduled post id from list_scheduled
brandNoWHICH PROFILE this post is in — id or exact name from list_brands; needed when it is not the profile this connection is pinned to. A name that matches no profile, or two, is REFUSED.
change_voiceChange narrator voiceAInspect

Swap the narration of a finished video into a different voice — keeps the performance, lip-sync, and background sound. Use when the user likes the video but wants a different narrator voice; use dub_video only for language translation. Paid; returns the served URL.

NameRequiredDescriptionDefault
videoYesthe source video URL; a URL, not a local file path; use upload_file for local files
voiceNotarget narrator voice preset name, e.g. 'Aria', 'George', 'Rachel', 'Sarah', 'Brian', 'Charlotte' (defaults to a warm female read). A saved VOICE CLONE of the user's own voice counts as a preset here — name it the way it is saved on their cast
check_ad_policyCheck ad policyAInspect

Pre-flight ad copy against Meta's REAL, live Advertising Standards before you run it — a flat 1-credit check. Pulls Meta's actual policy pages and returns a verdict (pass / fix / block) where every flagged issue QUOTES Meta's own policy text verbatim plus a compliant rewrite that keeps the sell. It's a check, not an edit — it never changes the creative. Especially worth running for regulated-adjacent categories (health/supplements, weight-loss or beauty results claims, finance/crypto/insurance, alcohol, dating, gambling) or ANY strong/absolute/guaranteed claim.

NameRequiredDescriptionDefault
copyYesthe ad copy / script / on-screen text to check
claimsNothe claims / proof points the ad makes
categoryNothe product category — helps pick the relevant policy pages
imageDescriptionNoa description of the creative / image when relevant
check_videoCheck a videoAInspect

Check a finished video for named, mechanical issues, ONLY when the user asks (never after every render, and it never re-renders): loudness vs a target (default -14 LUFS) and true peak, black and frozen stretches, captions or text outside the platform-safe band, text running off the frame. Optional assertions about named elements: appearsBy {element, bySec}, before {a, b}, staysInFrame {element}, keepsMoving {maxStaticSec}. Each issue comes with a Fix line. The loudness, black and frozen checks are free; the on-screen text and element read is a small paid frame read (frames:false skips it).

NameRequiredDescriptionDefault
urlYesthe video: a /generated/ path, a public URL or a post link
framesNofalse skips the paid text and element read
assertionsNowhat must hold, checked on sampled frames
loudnessTargetNointegrated loudness target in LUFS, default -14
clear_sheet_rangeClear a range in a Google SheetA
DestructiveIdempotent
Inspect

Empty a range of cells in a Google Sheet, leaving the rows themselves in place. DESTRUCTIVE: call it WITHOUT confirm first and nothing is cleared — you get back the real number of filled cells in that exact range. Show the user that number, get an unambiguous yes, then call again with confirm:true AND confirmCells set to it. The echo is not ceremony: it is what catches naming A1:Z1000 when you meant A1:Z10, which is the mistake that actually happens. There is deliberately NO default range. The clear is read back and reported as confirmed only if the range really is empty afterwards. To remove a whole tab instead, use manage_sheet_tabs.

NameRequiredDescriptionDefault
rangeYesthe range to clear, e.g. "A2:D50" or "Sheet1!A2:D50"
confirmNotrue clears the range; call without it first to get the filled-cell count
sheetUrlNoGoogle Sheets link, instead of spreadsheetId
confirmCellsNoecho back the filled-cell count the unconfirmed call reported
spreadsheetIdNoGoogle spreadsheet id; use this or sheetUrl
click_to_adPlan a video ad from a product linkAInspect

Paste a product page link (Shopify, Amazon or any store page) and get a creator-style video ad planned for that product. Hermoso reads the page (name, photos, what it says about the product, price), saves the product to the brand's catalog, checks the page's real product photo and pins it to this ad (the brand's saved default photo is not changed), then plans the ad. A store that hides its page from servers is read in a real browser; failing that the name comes from the link and the photo from a search, and the reply says so. Nothing renders here: show the user the concept, then call render_ad with the returned creative (dryRun:true first for the exact price). Works with or without a saved brand.

NameRequiredDescriptionDefault
sayNowhat the creator should say, or the angle, in the user's words
urlYesthe product page link, https://…
styleNougc_selfie (talking to camera, default), ugc_unboxing, ugc_demo (how-to), ugc_tryon (try-on or haul), ugc_product (product close-ups, no face), ugc_shopping (in-store find), ugc_delivery, transformation (before and after), day_in_life (at home), grwm (get ready with me), ugc_addiction (obsessed with it)
languageNolanguage for the script and copy, default English
durationSecondsNolength in seconds, 4-180; default 15
clip_videoClip a long videoAInspect

Cut ONE long video into several RANKED, ready-to-post short clips (podcast, webinar, interview, talk, long ad cut -> Reels/Shorts/TikTok). Transcribes with timestamps, picks the strongest SELF-CONTAINED moments, then cuts + reframes each with ffmpeg — no video model renders anything, so it is fast and cheap. THE VERTICAL REFRAME IS SUBJECT-AWARE: one cheap vision call per clip (billed as its own event) picks a SINGLE crop offset held for the whole clip, so a speaker sitting camera-left is not cropped out and the framing never drifts inside a clip; with nothing to discard or no single subject it stays dead centre — read reframedToSubject and each clip's reframeWhy back rather than assuming either way. ACCEPTS: a YouTube link (or Vimeo / Loom / Dailymotion / Streamable / Rumble / Wistia / Twitch / TED), a direct https .mp4/.mov/.webm, or a Hermoso /generated/ URL (upload_file turns a local file into one). NOT supported: TikTok / Instagram / Facebook links, and anything age-restricted, private, members-only, geo-blocked or still LIVE — those fail fast with the real reason and are fully refunded, so ask for a direct file or an upload rather than retrying. Source ~15s to ~600MB; only the first ~40 minutes is analysed (truncated:true says so). Cost: a ~7-credit hold settled to the exact transcription + encode cost, plus the clip-selection model's tokens as their own small event. RETURNS clips[] — each its OWN served mp4 URL, title, hook, ready-to-post caption, 0-100 score and source timecode. SUBTITLES ARE BURNED IN BY DEFAULT (slim white CAPS, thin black outline, bottom safe band, no box) because short-form is watched on mute; captions:false for clean footage. TIMING IS APPROXIMATE, NOT WORD-LEVEL — cues follow the transcript's per-sentence timestamps, split by character count; never promise frame-accurate sync. captionsBurned counts the clips that really carry a burned track and captionNote says why any are bare.

NameRequiredDescriptionDefault
countNohow many clips to cut, 1-8 (default 4)
videoYesthe long video to clip — a YouTube/Vimeo/Loom/Dailymotion/Streamable/Rumble/Wistia/Twitch/TED watch URL, a direct https .mp4/.mov/.webm, or a Hermoso /generated/ URL; a URL, not a local file path; use upload_file for local files
captionsNoburn subtitles into every clip. DEFAULT TRUE — a clip cut from a podcast or a talk is watched on mute, and the words are the product. Set false for clean footage. A clip whose window carries no readable speech is delivered bare rather than captioned with a guess, and the result says which.
aspectRatioNoclip shape: '9:16' (default), '1:1', '16:9', any 'W:H', or 'keep' for the source framing
clone_staticClone a static adAInspect

One-click STATIC-AD CLONE (the web app calls it Clone): rebuild a competitor/reference STATIC (image) ad as an on-brand version — SAME layout, composition and energy, but YOUR product, brand colours, logo and voice, with every trace of the source brand removed. Pass imageUrl = the static ad image to clone. Uses your saved brand (pass brandId to target a specific profile — that switches this key's active profile like use_brand). IMAGES ONLY — for a video ad use clone_video with its link, then render_ad. Bills as one image generation.

NameRequiredDescriptionDefault
brandIdNoa profile id/name from list_brands to clone for; omit to use the active profile
imageUrlYesthe URL of the static ad image to clone. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded.
clone_videoClone a video for your brandAInspect

Remake a video you like FOR THIS BRAND from its link — a TikTok, Instagram Reel, Facebook video or reel, X post, YouTube Short or video, or a direct video file URL. Hermoso WATCHES it first (frames across the whole clip plus a transcript of the voiceover, on-screen text and cut map), then plans a storyboard that keeps its hook device, structure, jump cuts and pacing while swapping in THIS brand's product, cast, setting and words — never the original's words, face or brand. The new ad MATCHES THE ORIGINAL'S LENGTH (capped at 60s) unless durationSeconds is given. Renders nothing: pass the returned creative to render_ad to make the video. Costs the plan plus about 2 credits to read the link. The reply says exactly what was watched, and when a platform will not hand over the footage (YouTube sometimes refuses servers) it says the plan rests on the captions and thumbnail only. For a local file, upload_file it first and pass the URL.

NameRequiredDescriptionDefault
urlYesthe video to clone — a TikTok / Instagram Reel / Facebook / X / YouTube link, or a direct https video file URL; a URL, not a local file path; use upload_file for local files
brandNobrand name or profile object; OMIT to use the workspace's saved brand (see get_brand)
changesNowhat to change or keep from the original, in the user's words (e.g. "same hook but in a gym", "keep the jump cut, older creator")
productNowhat the new ad sells, plus any angle or offer; omit to use the saved brand's product
languageNolanguage for the new ad's script and copy — default English
durationSecondsNooverride the length in seconds; omit to match the original
collect_post_metricsRead how the recorded posts performedA
Idempotent
Inspect

Fetch fresh performance numbers for this brand's recorded posts and store them as a time-series. Metrics ACCRUE, so a post is read at ~24 hours and again at ~7 days; this collects whichever readings are due and skips the ones already taken. A channel that cannot report a metric records it as ABSENT with the reason — never as zero — and a read that fails is recorded as 'could not tell', which contributes to nothing. X IS SKIPPED BY DEFAULT because X bills us per API call: pass includeMetered:true to include it, and tell the user it costs credits BEFORE you do. The skip is always reported so a channel missing from the numbers is never mistaken for one that performed badly. Free except for X.

NameRequiredDescriptionDefault
maxNocap how many posts to read in this run (default 40)
brandNoWHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done.
remeasureNoALSO re-read posts older than 7 days whose every reading came back empty or failed — use after post_performance reports posts "read but empty", or once a channel's reader has been fixed. Otherwise those windows stay closed.
includeMeteredNoalso read X, which BILLS CREDITS per post read — ask the user first
competitor_teardownCompetitor teardownAInspect

Tear a competitor's ad strategy down into an actionable playbook: their opening-hook MIX, longest-running campaign THEMES, the WHITE SPACE nobody in their set runs, 2-3 render-ready COUNTER-PLAYS, and the territories they own that you should avoid. Pass competitor {name, domain?}. CONTRACT: supply ads (raw ad objects from a prior pull_competitor_ads / search_meta_ads call) to tear exactly those down, OR omit ads and this pulls the competitor's real Meta ads first (spends the same ad-library reads search_meta_ads does, longest-running = proven winners). Auto-tailors the white space + counter-plays to YOUR saved brand. Spends credits (free when you pass ads).

NameRequiredDescriptionDefault
adsNoad objects to tear down (from pull_competitor_ads / search_meta_ads). Omit to auto-pull their Meta ads first.
languageNooutput language (default English)
competitorYesthe competitor to tear down
connect_connectorConnect a paste-a-key accountA
DestructiveIdempotent
Inspect

Connect an account that links with a KEY rather than a sign-in screen, from here, with no browser: AppLovin Ads, Stripe, Intercom, Mailchimp, Klaviyo, Vercel, ChatGPT Ads, Apple Ads, Bluesky, Telegram, Bing Webmaster Tools, PostHog, Mixpanel, Amplitude, WordPress, Slack, Discord, Webhook. Pass provider and fields in that provider's own field names (listed below, * = required). The key is checked live with the provider before anything is saved, exactly as the app's Connectors page checks it, and the reply is read back from the saved connection. OFFER BOTH WAYS AND LET THE USER CHOOSE: a key pasted into this chat stays in this conversation's history, while pasting it in the app (Workspace > Connectors, or the one-click link https://app.hermoso.ai/?connect=) keeps it out of the chat. Hermoso never repeats a submitted key back. An account that connects through the provider's own sign-in screen (OAuth) cannot be connected here: this answers with the link to hand the user instead. Apple Ads with no key material first generates a signing key pair and returns the public key to register with Apple plus a setupToken to send back. Fields: applovin_ads {reportKey, eventKey, apiKey, capiKey, accountId} · stripe {apiKey*} · intercom {accessToken*, region} · mailchimp {apiKey*, server} · klaviyo {apiKey*} · vercel {token*, team} · openai_ads {apiKey*} · apple_ads {clientId, teamId, keyId, privateKey, setupToken, orgId} · bluesky {identifier*, appPassword*, pds} · telegram {token*} · bing_webmaster {apiKey*} · posthog {apiKey*, region, host, projectId} · mixpanel {username*, secret*, projectId*, region, workspaceId} · amplitude {apiKey*, secretKey*, region, host} · wordpress {siteUrl*, username*, appPassword*} · slack {webhookUrl, announceWebhookUrl} · discord {webhookUrl, announceWebhookUrl} · webhook {webhookUrl*}.

NameRequiredDescriptionDefault
fieldsNothat provider's own field names and values, e.g. {"apiKey":"…"}; the names for each provider are in the description
providerYesthe connector id: applovin_ads, stripe, intercom, mailchimp, klaviyo, vercel, openai_ads, apple_ads, bluesky, telegram, bing_webmaster, posthog, mixpanel, amplitude, wordpress, slack, discord, webhook
convert_onedrive_fileConvert a OneDrive file to PDF or JPGAInspect

Turn a file already in the user’s OneDrive into a PDF or a JPG — Microsoft does the conversion on its own servers, so nothing is re-encoded here and nothing is lost in a screenshot. It reads about 130 source formats, which is the point: PowerPoint and Word decks, Excel, Photoshop PSD, Illustrator AI, Sketch, 3D (fbx/glb/obj), video (mp4/mov/webm), HEIC from an iPhone, and the raw camera formats (CR2, NEF, ARW, DNG) that nothing else in this product can open. Use it to turn a client’s deck into images you can actually put in an ad, to get a usable JPG out of a designer’s PSD or a photographer’s raw file, or to hand someone a PDF of a spreadsheet. CONVERTING TO JPG REQUIRES BOTH width AND height — Microsoft refuses the call without them. The result is stored at a durable Hermoso URL you can pass straight to a render or a post; Microsoft’s own conversion link expires within minutes, so do not hand that one to anyone. Needs OneDrive connected — no new permission.

NameRequiredDescriptionDefault
widthNoREQUIRED for jpg — output width in pixels
fileIdYesthe OneDrive item id, from list_onedrive_files
formatNodefault pdf
heightNoREQUIRED for jpg — output height in pixels
create_brandCreate a profileA
Idempotent
Inspect

Add a NEW profile (a brand, client, creator or personal workspace) and switch to it. Each has its OWN brand details, memory, swipefile, Library, avatars, skills, playbooks and connectors; nothing leaks between them. Use draft_brand to FILL it and update_brand to edit it. Re-running with the same name returns the existing profile instead of a duplicate. Creating it is free. Drafting a NEW brand into it (draft_brand) needs the credits for its research on the balance, about 100; with fewer, draft_brand refuses before anything runs or is charged and says how many it needs and how many the account has.

NameRequiredDescriptionDefault
nameYesthe name for the new profile (a brand, client or creator name)
activateNoswitch this connection to the new profile (default true); everything you do next scopes to it
create_docCreate a Google DocAInspect

Create a new Google Doc in the user’s Drive with a title + optional body text — e.g. export ad copy, a creative brief, or a report. Returns the document id + URL. Needs Google Drive connected (Settings > Connectors > Google Drive — one connection covers Drive, Sheets and Docs).

NameRequiredDescriptionDefault
textNobody text to insert
titleNodocument title
create_drive_folderCreate a Drive folderAInspect

Create a folder in the user’s Google Drive (optionally nested under parentId) to organize saved files. Returns the folder id + webViewLink. Use that ID as update_drive_file’s moveToFolderId or as parentId for a nested folder. NOTE: save_to_drive’s folder is a NAME, not this id — it find-or-creates a folder by that name, so pass the folder NAME there (or omit and just save, then move with update_drive_file).

NameRequiredDescriptionDefault
nameYesfolder name
parentIdNoparent folder id for a nested folder (default: Drive root)
create_onedrive_folderCreate a OneDrive folderAInspect

Create a folder in the user’s OneDrive (optionally nested under parentId) to organize saved files. Returns the folder id + webViewLink. Use that id as update_onedrive_file’s moveToFolderId or as parentId for a nested folder. NOTE: save_to_onedrive’s folder is a NAME (find-or-created), not this id.

NameRequiredDescriptionDefault
nameYesfolder name
parentIdNoparent folder id for a nested folder (default: OneDrive root)
create_sheetCreate a Google SheetAInspect

Create a new Google Spreadsheet in the user’s Drive and optionally fill it with rows — e.g. export a swipefile, ad list, or performance report. Pass rows as an array of row arrays (first row = headers). Returns the spreadsheet id + URL. Needs Google Drive connected (Settings > Connectors > Google Drive — one connection covers Drive, Sheets and Docs).

NameRequiredDescriptionDefault
rowsNorows to write — array of row arrays; first row = headers
titleNospreadsheet title
delete_brandDelete a profileA
DestructiveIdempotent
Inspect

PERMANENTLY delete a profile and EVERYTHING in it — brand details, memory, swipefile, Library creations, generated assets, avatars, skills, playbooks, chats — and disconnect its connected accounts. Irreversible, and it applies to everyone the workspace is shared with. Call it WITHOUT confirm first: it reports exactly what that workspace holds. Show the user that inventory verbatim, get an unambiguous yes, then call again with confirm:true — plus, if the workspace is not empty, confirmName set to its exact name and confirmConnectors set to the number of connected accounts it reported. Those two exist because confirming INTENT does not prove you picked the right WORKSPACE, and a wrong target is how a live profile was destroyed. The account's FIRST/anchor profile cannot be deleted this way (it holds the workspace's root storage) — that one is replaced from the app.

NameRequiredDescriptionDefault
brandYesprofile id or exact name from list_brands
confirmNoREQUIRED true — this destroys the whole profile and cannot be undone
confirmNameNothe workspace's EXACT name, required when it is not empty — copy it from the inventory this tool returned, after the user has agreed to it
confirmConnectorsNothe number of connected accounts the inventory reported, required when there is at least one — the user must specifically agree to losing them, because reconnecting each needs a browser and no agent can do it
delete_creatorDelete a creatorA
DestructiveIdempotent
Inspect

Remove a saved creator from this workspace’s cast by id (from list_creators). Records a cross-device delete so they don’t reappear on the user’s other devices. It only drops the roster entry — ads already rendered with that person are untouched — and the same portrait can be saved again with save_creator, so no confirm is needed.

NameRequiredDescriptionDefault
idYesthe creator id (from list_creators)
brandNoprofile id/name from list_brands, this call only
delete_drive_fileDelete a Drive fileA
DestructiveIdempotent
Inspect

Delete a Drive file. By default it goes to Trash (recoverable); pass permanent:true to delete it forever. Pass fileId (from list_drive_files) + confirm:true. Irreversible when permanent — confirm with the user first.

NameRequiredDescriptionDefault
fileIdYesthe Drive file id
confirmNoREQUIRED true
permanentNotrue = delete forever; default trashes (recoverable)
delete_library_itemDelete from LibraryA
DestructiveIdempotent
Inspect

PERMANENTLY delete images/videos from this brand's Library: the entry and the stored file. Pass ids (L0… from list_library) or urls. Without confirm it lists what would be deleted; ask the user, then call again with confirm:true. Free.

NameRequiredDescriptionDefault
idsNoLibrary ids from list_library
urlsNofile links
confirmNotrue deletes
delete_linkedin_lead_subscriptionStop a LinkedIn lead webhookA
DestructiveIdempotent
Inspect

Remove a lead notification webhook (subscriptionId from list_linkedin_lead_subscriptions). Leads themselves are unaffected and stay readable; only the real-time delivery stops. Read back from LinkedIn. Free.

NameRequiredDescriptionDefault
pageIdNothe company Page that owns the form — from list_linkedin_pages; omit when one Page is shared
adAccountIdNoread forms owned by an AD ACCOUNT instead of a Page
subscriptionIdYeswebhook id from list_linkedin_lead_subscriptions
delete_onedrive_fileDelete a OneDrive fileA
DestructiveIdempotent
Inspect

Delete a OneDrive item — it moves to the OneDrive recycle bin (recoverable there). Pass fileId (from list_onedrive_files) + confirm:true. Confirm the exact file with the user first.

NameRequiredDescriptionDefault
fileIdYesthe OneDrive item id
confirmNoREQUIRED true
delete_playbookDelete a playbookA
DestructiveIdempotent
Inspect

Delete a saved playbook by id (from list_playbooks). Records a cross-device delete so it does not come back on the next sync. Minor + re-creatable, so no confirm needed.

NameRequiredDescriptionDefault
idYesthe playbook id (from list_playbooks)
brandNoprofile id/name from list_brands, this call only
delete_skillDelete a custom skillA
DestructiveIdempotent
Inspect

Delete one of the workspace’s CUSTOM skills by id (from list_skills). Built-in skills/recipes can’t be deleted. Minor + re-creatable, so no confirm needed.

NameRequiredDescriptionDefault
idYesthe custom skill id (from list_skills)
brandNoprofile id/name from list_brands, this call only
diagnose_postsWhat to fix next, post by postA
Read-onlyIdempotent
Inspect

WHAT TO FIX NEXT, post by post — and the tool that fixes it. post_performance tells you which hook is AHEAD; this tells you what is WRONG with a given post and where the next edit goes. Under-distributed is A HOOK PROBLEM (change the opening: mine_angles, then list_hooks, then plan_variations). Seen but not held is A RETENTION PROBLEM (plan_variations to rebuild the middle against the same hook). Seen, held, and still not converting is AN OFFER PROBLEM. FOUR REFUSALS, AND YOU SHOULD REPEAT THEM RATHER THAN PAPER OVER THEM: (1) a post younger than ~24h is TOO EARLY and is never called a failure — it has not had its run; (2) a metric the platform does not publish is UNMEASURED, never zero — Facebook has published no post reach since 2026-06-15, Reddit publishes no impressions, and Google Business publishes nothing per-post at all; (3) below 5 measured posts on a channel there is no baseline of the brand's own, and the ONLY fallback is a published short-video hook floor that is NOT our measured number and does not transfer off TikTok/Instagram/YouTube — it is attributed in the output and you should attribute it too; (4) it does not always find a problem, and 'nothing here needs fixing' is a real answer rather than a failure to look. Hermoso cannot see conversions for an organic post — no channel reports installs or purchases against a post id — so the offer rung runs ONLY when the user tells you they are not converting and you pass converting:false. Print summary verbatim. Read-only, 0 credits.

NameRequiredDescriptionDefault
limitNohow many recent posts to diagnose (default 25, max 200). The baseline is always built from EVERY post recorded for the brand, never only these, so a bad month can never become its own definition of normal.
channelNorestrict to one channel: facebook, instagram, threads, x, linkedin, youtube, tiktok, pinterest, bluesky
convertingNopass false ONLY when the user has told you these posts are getting seen and are not converting — it re-reads the ones that are earning their reach as an offer problem instead of a win. Omit when you do not know; we cannot measure it.
disconnect_connectorDisconnect a connected accountA
DestructiveIdempotent
Inspect

Disconnect a third-party account from this workspace (Meta, Google Ads, Google Drive/Sheets/Docs, YouTube, TikTok, LinkedIn, X, Reddit Ads, Pinterest, Google Business, Microsoft Advertising/OneDrive, Slack, …). This always drops the stored credentials, so every tool for that provider stops working immediately and posts/campaigns already published are NOT affected. IT NEVER REVOKES THE GRANT AT THE PROVIDER: the authorisation stays in place, so the same account keeps working in any other brand it is connected to, until the user removes Hermoso in that provider's own settings. The unconfirmed call names where to do that (list_connectors carries revokesAtProvider:"local") — relay it rather than promising a revoke. RECONNECTING NEEDS A BROWSER (the provider's consent screen) — an agent cannot undo this. Name the provider to the user, then call with confirm:true. Use list_connectors for the exact provider ids.

NameRequiredDescriptionDefault
accountNoon a channel with several connected accounts (TikTok, X, YouTube, Threads, Bluesky, Telegram, Pinterest): remove ONLY this account (@handle or id from list_connector_accounts) and keep the others
confirmNoREQUIRED true — reconnecting a sign-in account afterwards needs the user's browser; a paste-a-key account is reconnected with connect_connector
providerYesprovider id exactly as list_connectors reports it, e.g. "meta", "google_ads", "youtube", "linkedin"
draft_brandDraft profileA
Destructive
Inspect

Fill in a profile (a business brand or a creator) from a website domain, a free-text description, or a social handle, into a {name, products, logo, …} object you can pass to plan_ad / generate. IMPORTANT: a domain can resolve to a DIFFERENT company than intended (e.g. bala.com is an engineering firm, not the Bala fitness brand at shopbala.com). Before spending any credits on research or renders, VERIFY the returned name (and summary) match the brand the user meant; if it looks wrong, re-draft with the correct domain or a description (pass save:false until confirmed) — this tool cannot ask the user, so the caller owns that check.

NameRequiredDescriptionDefault
saveNosave onto the active profile (like Studio onboarding) so plan_ad/create use it automatically. Default: saves only when NO brand is saved yet; pass true to REPLACE the saved profile (the drafted fields overwrite the saved ones), false to never save
domainNoa website to scrape, or a social link (profile, YouTube channel or one post) to draft from the account it names
platformNosocialHandle platform: instagram/tiktok/youtube/twitter/threads
confirmedNouser confirmed it is their brand: read + save site product photos (paid)
descriptionNoa free-text brand description (no website)
socialHandleNoa social handle (creators); needs platform, never guessed
dub_videoDub videoAInspect

Localize a finished video into another language WITHOUT re-rendering it: each spoken line is said where it was said, in a native voice, lip-synced, background kept. Pass the video and the language; the script is read off the source (script only overrides it). Paid by length; dryRun quotes. Returns the localized video URL.

NameRequiredDescriptionDefault
videoYesthe source video URL; a URL, not a local file path; use upload_file for local files
voiceNooptional, e.g. 'Aria' (woman) or 'George' (man): a native voice of that sex speaks. Default: the source speaker's sex.
dryRunNoquote only
scriptNoOPTIONAL override for the original spoken words. Leave this out — the source video is transcribed automatically. Only pass it when you already know the exact script and the auto-transcript got it wrong.
languageYestarget language, e.g. 'Spanish', 'de', 'French (Canada)'
duplicate_scheduledDuplicate a scheduled postA
Destructive
Inspect

Copy an existing scheduled or already-published post into a NEW queued post — the way to run a creative again, reuse a post that worked as the starting point for the next one, or re-send something after it went out. It copies the caption, media, per-channel captions, title, description, tags and the target board / Page / company Page / listing, and ANY of those can be overridden in the same call. Give a new time in at, or useQueue:true to drop it into the brand’s next free posting slot. The copy is INDEPENDENT — editing or cancelling it never touches the original — and it is a genuinely new post rather than a re-send, so it publishes even where the original already did. To re-fire only the channels that FAILED, use retry_scheduled instead.

NameRequiredDescriptionDefault
atNowhen the copy goes out: local wall clock (2026-10-30T10:00), UTC or epoch ms (default: an hour from now)
idYesthe post to copy, from list_scheduled
linkNodestination URL for the copy (Pinterest, Facebook text post or Google Business post)
brandNoWHICH PROFILE this post is in — id or exact name from list_brands; needed when it is not the profile this connection is pinned to. A name that matches no profile, or two, is REFUSED.
titleNoPinterest or YouTube headline for the copy, max 100 characters
chatIdNoTELEGRAM — which chat, group or channel the copy goes to (@username or numeric id)
ideaIdNoshort id of the content-plan idea this post came from
pageIdNoFACEBOOK / INSTAGRAM / THREADS — which connected Page (list_meta_pages)
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
boardIdNoPINTEREST — the board for the copy (list_pinterest_boards)
messageNoa different caption for the copy
captionsNoper-channel caption overrides for the copy
channelsNopost the copy to these channels instead of the original’s
imageUrlNoreplacement image for the copy: a Hermoso render URL or an upload_file URL
timezoneNoIANA zone for a wall-clock `at` or the queue
useQueueNoinstead of naming a time, take the brand’s next free posting slot
videoUrlNoreplacement video for the copy: a Hermoso render URL or an upload_file URL
imageUrlsNoCAROUSEL — an ORDERED list of image URLs published as ONE swipeable post
locationIdNoGOOGLE BUSINESS — which listing (list_business_locations)
visibilityNopublic (default), unlisted or private (YouTube), or draft (TikTok or an unpublished Facebook Page post)
whopExperienceIdNoWHOP — which forum or chat the copy goes to (exp_…, from list_whop_destinations).
linkedinOrganizationIdNoLINKEDIN — publish the copy as this company Page (list_linkedin_pages)
edit_imageEdit an imageAInspect

EDIT an existing image in place with a plain-language instruction and keep everything else: 'make the headline bigger', 'add our logo bottom right', 'swap the background for a kitchen', 'remove the person on the left', 'erase all the text'. Pass image (URL, Library item, upload_file URL or local path) and instruction. The same edit the web Studio's Edit runs: composition, aspect ratio, people and every untouched line of text stay as they are; the saved brand's real name and website are pinned so an added line never invents one, and the brand's real logo FILE is put on when the instruction asks for the logo (never redrawn by the model): an ask only about the logo ('the logo is wrong', 'make the logo bigger'; or logo:true, any language) runs no image model. Set removal:true when the edit STRIPS text, branding or an object, so nothing branded is put back. When the saved brand has a product photo and the ad shows that product, the photo rides with the edit and the product's label is read and checked against it: re-printed from the photo only when it came out wrong (the check alone is charged when it is right; one small extra charge finds the product first). fixLabel:false turns that off. For a precise region, pass mask (see generate_image). One image edit's credits; returns the new image URL. For a new image from a prompt use generate_image; to rebuild a competitor's ad for your brand use clone_static.

NameRequiredDescriptionDefault
logoNo
maskNooptional mask image (URL or local path) marking the region to change: transparent = change, or white = change on an opaque mask
imageYesthe image to edit: URL, Library item URL, upload_file URL or local path
modelNothe editor, only when the user names one: gpt-image-2.6, gpt-image-2.6-flare (its own faster editor) or gpt-image-2.5. Omit for the default
dryRunNotrue = return what this edit usually costs in credits and render nothing
markupNoMARKUP-GUIDED EDIT: drawn annotations (circles, arrows, scribbles) showing what to change, as a URL or local path: a transparent PNG of just the marks, or a copy of the same image with the marks drawn on it. Say the change in `instruction` ("make the circled vase blue"); the marks are not in the result. Runs on gpt-image-2.6 (or the editor named in `model`). Never with a mask
removalNotrue when the edit REMOVES text, branding, a logo, a watermark, a person or an object, so the brand name and logo are not re-added
fixLabelNofalse = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product)
complexityNogpt-image-2.6 editors only: how much it plans before drawing (default high; low suits a small focused change)
instructionYesthe change to make, in plain words (pass the user’s own words for a removal or plain photo edit)
edit_videoEdit a video clipAInspect

SWAP AN OBJECT IN A VIDEO ('replace the cup with our can' + the product photo as elements) or EDIT an existing clip from a plain instruction (video-to-video): the motion, timing, framing and cut stay, the named thing changes. 'change only the mug to red', 'make it nighttime', 'restyle it as claymation'. Your words are wrapped so the model keeps everything else identical, changes only what you named and repeats that lock; lighting is kept unless the change needs new light (set lighting to force either); literal:true sends your words as written. One change per call holds best. previewFirstFrame:true edits ONE still first (one image edit; previewAt picks the second) and quotes the clip; nothing else runs until you call again, ideally with previewStill. The reply scores how well the shot held outside the change (free) and flags an edit that touched more than asked. NOT for cuts/trims/end cards (post_edit), a new video (generate_video / render_ad), translating what is SPOKEN (dub_video) or a saved creator's face (recast_motion). Translating or rewriting the on-screen words of a video laid out from a design (a template ad, a custom design) is textChanges with every line at once: one re-make of the design, about 2 credits. Best on 3-15s clips.

NameRequiredDescriptionDefault
videoYesthe source video URL (a render, job result or list_library); a URL, not a local file path; use upload_file for local files
engineNodefault auto: Seedance 2.5 Edit without a real-looking person, else Kling 4.0 Edit
literalNotrue = send the instruction exactly as written, with no preserve/lock wrapper
elementsNoOPTIONAL identity/product grounding (≤4): a creator portrait or the real product photo, so the edit restores the REAL thing. Describe each one in the instruction
lightingNodefault auto: keep the source light unless the change needs new light (night, a lamp, fire). 'preserve' or 'relight' forces it
faceRouteNo'face_lane' = the user's "I own the rights to this face" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own.
keepAudioNodefault true: keep the source audio; false = silent
previewAtNosecond to preview (default 0); resend with previewStill
referenceNoOPTIONAL image URL that anchors the MATERIAL of what changes (a fabric, a finish, a colour swatch, the real product). Only its surface is used, never its framing or light
instructionYesthe change, in the user’s own words
textChangesNoEVERY change of on-screen words at once ([{from, to}], e.g. each line and its translation). A video laid out from a design is made again with all of them in ONE render, about 2 credits, never a video edit
previewStillNothe still a previewFirstFrame call returned; the clip then matches the changed thing to it
interactionIdNoOPTIONAL: the interactionId an earlier Gemini Omni render or edit returned; the edit continues that clip on the same Omni model. If it cannot run, the video editor edits it and the reply says so.
previewFirstFrameNotrue = edit ONE still of the first frame first and quote the clip; the paid clip does not run
enable_toolsTurn on more Hermoso toolsA
Idempotent
Inspect

LIST a group of tools that is not in this session's roster. IT IS NOT HOW YOU REACH A TOOL — call_tool runs any Hermoso tool whether or not it is listed, and that works everywhere. Use this when the session will use MANY tools from one area and you want them in your list. WORKS ON CLIENTS THAT RE-READ THE TOOL LIST (stdio, the CLI, Cursor, Claude Code); a host that fixed its roster at connect time — claude.ai and ChatGPT do — will not show the new tools until it reconnects, and this tool says so in its reply rather than reporting a success you cannot use. The connect-time route that always works is ?tools=all on the server URL. The default roster is CORE-FIRST: the core tools plus a few that make the connection drivable. Every other tool is held out of the LIST on SIZE alone — the whole registry is several hundred thousand tokens of schema re-sent on every turn, and a roster far past the 30-50 tool mark measurably degrades tool choice. The heaviest groups are ads, analytics, channel_admin: paid-campaign management across every ad platform is most of the total schema weight. NOTHING held out is unfinished or unsafe, and nothing is unreachable — find_tools finds it and call_tool runs it. CALL THIS WHEN A WHOLE AREA IS IN PLAY. If the user settles into building, budgeting, targeting or reporting on ad campaigns, call enable_tools({groups:['ads']}) and the tools appear. If they ask about their own site or product analytics, a tag/tracking container, or how a search engine crawls, indexes or ranks their site, call enable_tools({groups:['analytics']}). Groups: core, research, create, channels, channel_admin, analytics, ads, files, workspace — or 'all'. Free, instant, and it never turns anything off.

NameRequiredDescriptionDefault
groupsYesGroups to switch on, e.g. ['ads']. Unknown names are refused by name rather than ignored.
error_detailError detailA
Read-onlyIdempotent
Inspect

One error group in full by fingerprint (from list_errors): every field, plus the most recent redacted occurrences — status, connector, job id, workspace, and a shape-only echo of the inputs. This is what makes a bug reproducible. Read-only, 0 credits.

NameRequiredDescriptionDefault
fingerprintYesthe `fp` value from list_errors
export_schedule_to_notionExport the schedule to NotionAInspect

Export the posting schedule to a Notion database, one row per post: date, channels, caption, media link and status. Creates a new database (under parentPageId, or private in the workspace) or writes into an existing one (databaseId), matching its columns by name. historyDays adds posts already published. Up to 100 rows per run; the answer says if more were left. Free.

NameRequiredDescriptionDefault
titleNoname for a new database
databaseIdNoan existing database id or link
historyDaysNo0-90 days of published posts to include
dataSourceIdNodata source id of an existing database to write rows into, matched to columns by name
parentPageIdNoNotion page to create the new database under; omit for a private page in the workspace
export_swipefile_deckSwipefile to Google SlidesAInspect

Turn a SWIPEFILE COLLECTION into a real Google Slides deck — one slide per saved ad, carrying the creative, the brand, the ad copy, the run dates with the run length, and the platform. This is the thing a marketer actually presents to a client or a team. Returns the presentation id + URL. Creates a NEW deck every time: under the drive.file scope Hermoso can only touch files it created, so it cannot add slides to a deck the user already has. A creative whose ad-library link has expired cannot be embedded — Meta signs those URLs with a short expiry — so that slide says so in words and keeps its copy and run dates, and the reply reports how many. Needs Google Drive connected (Settings > Connectors > Google Drive — one connection covers Drive, Sheets, Docs and Slides).

NameRequiredDescriptionDefault
limitNomax ads to include, 1-60 (default 30)
titleNodeck title (default: the collection name)
collectionNothe swipefile collection to export, by name or id (default: the first collection)
fetch_app_screensFetch App Store screensA
Idempotent
Inspect

Pull an APP brand's REAL store screenshots (Google Play or App Store) into the workspace brand, so a screen-hungry native format can use them. Use when the brand has 0–1 app screens on file and you want make_template_ad(template:'app-ui-tour'), or the user asks to 'pull my app's screenshots'. A store link or package id goes in storeUrl and is read exactly; else pass appName (defaults to the saved brand's name). A web app with no store listing gets the screenshots of the product its OWN website shows (pictures the site labels as its interface). Call it before telling the user you have no screenshots of their app. FREE — a keyless store lookup or one read of the brand's homepage. It needs a CONFIDENT match: an ambiguous or unknown app returns 0 screens and saves nothing, which you should relay plainly rather than retrying with guesses. On success the screens are saved to the brand (durable URLs) and are immediately usable.

NameRequiredDescriptionDefault
appNameNothe app's name to look up on the App Store — defaults to the saved brand's name
brandIdNoa profile id/name from list_brands to save the screens onto; omit to use the active profile
storeUrlNoa Google Play / App Store link or package id
fetch_assetFetch assetA
Read-onlyIdempotent
Inspect

Resolve a generated asset reference (a /generated/… path or any URL) to a clickable absolute URL + a direct download URL.

NameRequiredDescriptionDefault
urlYesthe asset url or /generated/ path
nameNooptional filename for the download
fetch_social_dataFetch social dataAInspect

Generic escape hatch for any ALLOWLISTED long-tail social/web endpoint the dedicated search_* tools don't cover — e.g. {path:'/v1/instagram/profile', params:{handle:'nike'}}. Allowlisted platform families: TikTok (+ TikTok Shop), Instagram, YouTube, Facebook (organic profiles/posts/events/marketplace), LinkedIn (organic posts/companies), Twitter/X, Reddit, Threads, Snapchat, Pinterest, Twitch, Bluesky, Truth Social, Rumble, Spotify, SoundCloud, GitHub, Google search, link-in-bio pages (Linktree etc.). Param names vary per endpoint (profiles use handle, keyword searches use query, Reddit uses subreddit). WARNING: returns RAW provider JSON — large and messy; prefer the dedicated search_* tools. Spends credits.

NameRequiredDescriptionDefault
pathYesexact endpoint path, e.g. '/v1/tiktok/profile' — non-allowlisted paths are rejected
paramsNoendpoint query params, e.g. {handle:'nike'}
find_competitorsFind competitorsAInspect

Discover a brand's competitor / similar / adjacent brands from its domain (Claude grounded by web search). mode=competitors (default, excludes the searched company), inspiration (best relevant ads incl. it), or company. Bills the discovery model's token cost (no ad-data charge); free inside a new account's first-brand setup.

NameRequiredDescriptionDefault
modeNo'competitors' (default, excludes the searched company), 'inspiration' (best relevant ads incl. it), or 'company'
domainYesthe brand domain, e.g. flourish.com
find_creatorsFind the creators already winning in a nicheAInspect

Scan organic TikTok, Instagram Reels and YouTube for a niche across a few query variants, fold the posts into creators, and rank them on median views, engagement rate and how often they show up for that niche; the top rows get follower counts AND public contact info (an Instagram business email / phone / category, the bio link, an email in a TikTok bio) so outreach can start from the result. Real people, not AI actors — for influencer sourcing, UGC casting and partnership prospecting ("who should we send product to?"). About one credit per search call (platforms × queries, default 3 × 3) plus one per enriched profile; repeats inside 20 minutes are free. Then shortlist (save_to_swipefile), check a profile (instagram_profile / fetch_social_data), draft outreach (generate_text), or approve them for Partnership Ads (manage_meta_partnership_creator). marketplace:true also searches Instagram’s creator marketplace (Meta’s own creator directory: followers, badges, marketplace email) for the same niche and returns those rows beside the ranked list.

NameRequiredDescriptionDefault
limitNocreators to return, 1–30 (default 12)
nicheYesproduct category, topic or hashtag — "calorie tracker app", "matcha", "#cleanbeauty"
enrichNofollowers + public contact for the top 3 on TikTok and on Instagram (default true, 1 credit each)
queriesNoquery variants per platform, 1–4 (default 3); each is a paid search call
platformsNodefault all three
marketplaceNoalso search Instagram’s creator marketplace (Meta’s own creator directory, free) for the same niche; needs the Meta connector
minAvgViewsNominimum average views per post; creators below this floor are dropped
minEngagementNointeractions per view, 0–1 (0.05 = 5%)
find_soundFind a soundAInspect

A sound for an edit by name ('the FAAAA sound'), by the moment ('bad news reaction') or by link (TikTok sound, meme-sound page, post, audio file): a durable mp3 and where it starts and lands. Named/described sounds come from what TikTok uses now; pick takes another candidate. A sound that does not exist yet is generate_sfx.

NameRequiredDescriptionDefault
urlNolink to a TikTok sound, a post, a meme-sound page or an audio file
pickNocandidate number from the previous list to take instead of the first
queryNosound by name, moment or meaning, e.g. 'bad news reaction'
find_toolsFind a Hermoso tool by name or taskA
Read-onlyIdempotent
Inspect

Search EVERY Hermoso tool — your starting list is deliberately short, and everything else in the product is here — by name, task or group. Each row gives the tool's PARAMETERS in one line, its CREDIT COST (free means free on every plan; a tool that runs a model quotes the live per-model figure) and its recent HEALTH on this server (failure rate and typical duration, or "no recent calls", which means unseen and not broken). Use it the moment the user asks for something you do not see a tool for (a campaign, an ad set, a lead form, a click-to-WhatsApp ad, a report, keywords, audiences): a tool missing from your list is NEVER proof the feature is missing. Then run the tool with call_tool. A tool that is failing or needs a connector this workspace has not made is ranked last and marked, never hidden — pass onlyHealthy:true if you want those left out. Free, read-only.

NameRequiredDescriptionDefault
groupNolimit to one group: core, research, create, channels, channel_admin, analytics, ads, files, workspace
limitNohow many to return (default 12, max 40)
queryNowords from the task or the tool name, e.g. "lead form", "whatsapp", "google ads keyword", "meta insights"
onlyHealthyNoleave out tools that are failing their recent calls or that need a connector this workspace has not made. Default false — nothing is hidden unless you ask, because a missing row reads as a missing capability.
finish_videoFinish videoAInspect

Post-process an EXISTING rendered video (its served mp4 URL) with the proven direct-response 'reviewer' finish and/or a film-grain pass — no AI model, ~30s, billed for its compute only. pills=true composites a header pill (e.g. '10/10 would buy again'), a brand-accent sub-pill, and 3-4 green-check proof pills cascading in on the beat (YOU author the copy: header ≤40 chars, sub ≤34, each point ≤44 — concrete real benefits, never fabricated stats). grain=true applies a subtle camera-grain finish that makes photoreal AI renders look phone-shot ('less AI') — works alone or with pills. Returns a NEW video; the original is untouched.

NameRequiredDescriptionDefault
subNoaccent sub-pill copy, ≤34 chars (usually the product/brand)
grainNodefault false — anti-AI film-grain finish
pillsNodefault true — set false for a grain-only pass
accentNobrand accent hex for the sub-pill
headerNoheader pill copy, ≤40 chars (required when pills is on)
pointsNo3-4 proof points, ≤44 chars each
videoUrlYesthe served URL of the video to finish (from a previous render/job); a URL, not a local file path; use upload_file for local files
fix_beatFix a video beatAInspect

Surgically re-render ONE time window (1.5-8s) of an existing rendered video and splice it back on the VIDEO TRACK ONLY — the rest of the video and ALL audio stay byte-identical. Use when one beat/shot is broken ('the shot at 8 seconds glitches') and a full re-render would waste the parts that worked; bills only the replacement clip's seconds (~1/3 of a full render). Do NOT pick a window covering spoken dialogue (a video-only splice under speech breaks lip-sync) — pass speechWindows to enforce this. A FINISHED VIDEO-BLOCKS FILM (make_explainer's explainer or faceless channel; get_job says it is editable) is edited ONE BLOCK at a time instead: pass job (its job id) and block (1-based) or atSeconds, with prompt for a different shot and/or line for new narration words. Only that block is re-rendered and/or re-read; every other block, take, the music and the captions are the film's own and stay aligned. It is priced as that block alone and charged exactly what it quotes (dryRun). The original film is kept; a new film comes back, and it can be edited again. Feedback maps to one named move, checked by reading the parameters back, never by re-rendering: quieter/louder (music db, audio_gain), faster/slower (speed; slower = zooms and pans at 0.7x), tighten (trim heads, tails, silences), retime (to a word anchor), recut (order, in-out points), regrade (timeline exposure/contrast/saturation keys).

NameRequiredDescriptionDefault
jobNoa finished video-blocks film (explainer / faceless channel): its job id. Edits one block of it instead of splicing a window
lineNowith `job`: the new narration for that block, the exact words to say (said as written, never rewritten; refused for free when it cannot fit the block)
blockNowith `job`: which block, 1-based (block 3 = 20-30s of a film of 10s blocks)
dryRunNotrue = return the estimated credits this fix reserves and render nothing
promptNowhat the replacement footage should show — describe the shot, matching the master's style. With `job`: the change to that block's picture, in words
refImageNooptional product/style anchor image URL
videoUrlNothe served URL of the master video to fix (not needed with `job`)
atSecondsNowith `job`: the moment in the film instead of `block` ("the line at 0:12" = 12)
endSecondsNowindow end in seconds (window 1.5-8s; not needed with `job`)
startSecondsNowindow start in seconds (not needed with `job`)
speechWindowsNo[[start,end],...] windows with spoken lines — the fix window must not overlap these
forgetForget a memoryA
DestructiveIdempotent
Inspect

Delete a saved Memory item by its id (from list_memory). Records a cross-device delete so it doesn’t come back. Minor + re-creatable (you can remember it again), so no confirm needed.

NameRequiredDescriptionDefault
idYesthe memory item id (from list_memory)
brandNoprofile id/name from list_brands, this call only
format_sheetFormat a Google SheetA
DestructiveIdempotent
Inspect

Make an exported sheet readable: bold the header row, FREEZE it so it stays visible while scrolling, and auto-size the columns so nothing is cut off. Worth calling right after create_sheet — a raw export with unsized columns and a header that scrolls away is the difference between a spreadsheet someone reads and one they close. Changes no cell VALUE, so it is never gated. The defaults do all three on the first tab; pass tab to pick another, freezeRows:0 to skip freezing, boldHeader:false or autoResize:false to skip those.

NameRequiredDescriptionDefault
tabNotab title or numeric sheetId (default: the first tab)
sheetUrlNoGoogle Sheets link, instead of spreadsheetId
autoResizeNoauto-size columns to fit their content; default true, false skips it
boldHeaderNobold the header row; default true, false skips it
freezeRowsNohow many top rows to freeze (default 1, 0 = none)
spreadsheetIdNoGoogle spreadsheet id; use this or sheetUrl
generate_avatarGenerate talking avatarAInspect

ANIMATE A PHOTO so it talks: the presenter in image says script word for word, a separate voice lip-synced onto the still. Use it ONLY when the user asks for an animated photo / talking photo / lip-sync look by name; for any other talk-to-camera or spokesperson ask use generate_video with speak (the person filmed saying it, which reads as real footage). About 1-3 min, holds the pose steady, 480p/720p. The per-second credit price is in hermoso_capabilities (avatarEngines); dryRun:true returns this exact job's hold without rendering. Blocks until done where the host allows, else returns a job id to poll with get_job. Requires canAvatar. Spends credits; a refusal before rendering costs nothing.

NameRequiredDescriptionDefault
seedNofixed seed, only for an engine other than standard
imageYeslocal path or URL of the presenter portrait. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded.
voiceNovoice name (Aria / Sarah / George / Adam). Leave it out and the voice matches the person in `image`; if nobody can be read, the call is refused free asking for one
dryRunNoreturn the credits this exact job would hold, without rendering
engineNoleave out for the standard engine; only an engine listed in hermoso_capabilities avatarEngines is accepted
promptNomotion direction, only for an engine other than standard that hermoso_capabilities lists
scriptYesthe words the avatar speaks
resolutionNo'720p' (default) or '480p'
acceptQueueNoonly for an engine hermoso_capabilities marks oneAtATime: wait in line
generate_imageGenerate ad imageAInspect

Use this when the user asks for an image ad, ad creative, product photo or social post graphic for their business or product. It renders a finished image on a real image model, on their saved brand (product photos, logo), and returns a URL ready to post. For a video, use render_ad. refImages (paths or URLs) composite a real product into the scene. MULTI-BRAND: hydration uses the SAVED brand; for a different brand (a fresh draft_brand) pass its own productImages/logo as refImages with useBrand:false, or the wrong product lands in the shot. Product photos also pick the ENGINE (the compositing model), so a named model is honoured only when no references ride; raw:true or useBrand:false keeps it. model = a catalog id from hermoso_capabilities (omit for the default). A PRODUCT IN A PERSON’S HANDS, or a garment on them: name an id marked needsRefs with a refsMax; person photo first, then up to three product/garment photos. It EDITS THE PHOTOGRAPH: the person is RE-POSED (stance and hands change; face, clothing, setting and light are kept), so it is the wrong tool when the rest must stay untouched. AN ANIMATED GIF OR AN SVG: format "gif" (a short looping animation, frames + frameMs) or "svg" (an editable vector) on gpt-image-2.6 / gpt-image-2.6-flare, from the prompt alone (no refImages). RAW MODEL ACCESS: raw:true dispatches your prompt to the model BYTE-IDENTICAL — no rewriting, no appended guidance, no negative prompt, no brand references attached on your behalf. Credits, the durable delivery of the finished asset and the per-model validation are unchanged. Seconds. Spends credits.

NameRequiredDescriptionDefault
jobNowhat the picture is for. With no `model` named it renders on the engine judged best for that job (hermoso_capabilities `jobEngines.image` lists them): static_ad (the default when neither `model` nor `job` is given) = a finished image ad with its headline, copy and button on the picture; product_photo = a studio, lifestyle, flat-lay or spotlight photo of the real product; marketplace = a listing card; thumbnail = a video cover (make_thumbnail is the full thumbnail pipeline); character_sheet = a multi-view sheet of one person; vector = a logo, icon or flat illustration as an editable SVG (it takes no reference pictures, so the saved brand's photos stay off it). A named `model` always wins
rawNoRAW MODEL ACCESS: run the caller’s prompt on the named model with no Hermoso adjustments at all — the prompt reaches the provider byte-identical (no hex-to-colour-name rewrite, no prepended fidelity preamble) and NO saved-brand product photos are attached, so the model you name is the model that renders. Use it to drive the raw catalog; leave it off for an on-brand ad. Billing, the durable Library landing and per-model validation are unchanged.
maskNoMASKED EDIT — change ONE region and keep the rest: a mask (path or URL, any size) for refImages[0]. TRANSPARENT = change, or on a mask with no transparency WHITE = change, black = keep; the reply says which it read. The mask guides rather than stencils, so the change can blend a little past its edge. Runs on gpt-image-2.6 (`refs.mask` in hermoso_capabilities): leave `model` empty or name it; another named model is refused, free. Needs refImages; keeps the source image's frame, so aspectRatio is not applied.
modelNoimage model id from hermoso_capabilities. A model whose `refs.mode` is "edit" there (gpt-image-2.6, gpt-image-2.5) takes your refImages on ITS OWN editor, up to its `refs.max`, instead of the default compositor
formatNothe file to make (default png). gif = an ANIMATED GIF, a short loop with no sound (describe the subject, the motion and the loop); svg = an editable VECTOR (logo, icon, flat illustration). Both run on gpt-image-2.6 (default) or gpt-image-2.6-flare and take no refImages, mask or markup; the saved brand's photos stay off them. hermoso_capabilities prints each one's credits under `formats`
framesNoformat gif only: frames in the loop, 2 to 16 (default 9)
promptNoREQUIRED on every model EXCEPT the pose rows below. the full image prompt — subject, composition, lighting, and any on-image ad text. ON A POSE MODEL (product-in-hand / try-on) THIS IS EXTRA DIRECTION AND IT IS OPTIONAL — leave it out and the pose is built for you. If you do write one, DESCRIBE THE POSE ("she holds the bottle upright in her right hand at chest height, label to camera"); do NOT phrase it as a swap ("replace the mug with the bottle"), which is REFUSED for free, because the product then comes out the size of whatever it replaced — a 30ml bottle rendered mug-sized in testing.
frameMsNoformat gif only: milliseconds per frame, 20 to 2000, rounded to 10 (default 200; 9 frames x 200 = a 1.8 s loop)
fixLabelNodefault true: when the saved brand's product photo rides in this render, the product's label on the finished image is READ and compared with the photo, and re-printed from the photo at close range ONLY if it came out wrong (a label that is already right costs only the check; a re-print costs more, and dryRun:true returns the up-to price). The reply says whether the label was checked, fixed or left as rendered (`labelPass`). Pass false when the user wants the packaging left exactly as generated: nothing is checked or re-printed.
useBrandNodefault true: with no refImages, the server hydrates the SAVED brand’s product/logo references so the output lands on-brand; pass false for a pure prompt-only render
imageSizeNopixel-size preset for models that support it: 1K/2K, and 4K on the models hermoso_capabilities lists with a 4K imageSize price (a 4K ask on any other model is refused, free) — omit for the default
refImagesNolocal file paths or URLs of product/logo references to composite in. ON THE POSE MODELS — any row hermoso_capabilities marks `needsRefs` with a `refsMax`, such as putting your product in someone’s hands or a virtual try-on — THE ORDER IS THE CONTRACT AND IT IS NOT A COMPOSITE: refImages[0] is the PERSON photo, and the rest (up to `refsMax` minus one) are the product or garment photos. Reversed, you get the product wearing the person. A 4th product is dropped and the reply says so. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded.
complexityNogpt-image-2.6 / 2.6-flare only: how much the model plans before drawing (default high, best for dense text and complex layouts; low suits a small focused change). No separate charge
aspectRatioNoe.g. '1:1', '9:16', '16:9', '4:5'. Each model draws its own list (hermoso_capabilities prints it per model, e.g. Nano Banana 2 goes to 1:8 and 8:1); a ratio the chosen model cannot draw is refused before anything is charged
logoPlacementNooverlay: real logo file laid flat. in_scene: the scene leaves a blank patch on the object and the real file is printed onto it (perspective, light). Never drawn by the model. auto (default): from the prompt
generate_musicGenerate musicAInspect

RAW music: describe it (genre, mood, instruments, tempo), get an instrumental MP3. No length: a full track at a flat fee (explainerMusicCredits in hermoso_capabilities). With seconds: by the second up to about 38 s. A named model always runs.

NameRequiredDescriptionDefault
modelNominimax-music-3 or lyria-3.5
promptYesthe music in words, e.g. 'lo-fi jazz, brushed drums, 80 bpm'
secondsNotrack length, up to 300
generate_textGenerate textAInspect

Text generation against the writing-model catalog (Claude, Gemini, GPT, Llama, DeepSeek…) — ad copy, hooks, scripts, rewrites, brainstorms. Prompt-only, no ad assembly (for a finished on-brand creative use plan_ad -> render_ad). BY DEFAULT the model answers as a marketing copywriter (a short house system prompt is applied, which is what you want for ad copy); pass raw:true for a plain, unstyled answer from the model itself with NO system prompt at all. model = a writing-model id from hermoso_capabilities (omit for the default Claude orchestrator). Paid (billed at the writing model's token cost, by length).

NameRequiredDescriptionDefault
rawNoRAW MODEL ACCESS: send the prompt with NO Hermoso system prompt — the model answers as itself rather than as an ad copywriter. Use it whenever the ask is not marketing copy (analysis, code, extraction, a plain question). Default false: the copywriter framing is applied.
modelNoa writing-model id from hermoso_capabilities (a Claude / Gemini / GPT / Llama / DeepSeek id) — omit for the default
promptYesthe writing task / question
generate_videoGenerate videoAInspect

Use this when the user asks for a video clip, b-roll or a shot from a description, or names a video model. For a finished video ad, Reel, TikTok ad or UGC video for their business, use render_ad. It renders one continuous clip on a real video model and returns its mp4 URL. A clip runs up to the model’s longest listed duration (the longest takes a multi-beat 30s spot in ONE take with native audio), so never assume an 8–10s cap or stitch what fits one clip; durationSeconds must be one of the model’s durations in hermoso_capabilities. NAME A MODEL in model to get it: an unnamed render uses a narrower auto-pool. 1–3 min. refImage anchors the first frame; ttsScript adds a voiceover. AUDIO IS NOT FREE BY DEFAULT: a clip with no audio of its own gets a music bed composed and CHARGED on top (see musicMood and audio), which can cost as much as a cheap draft clip. refVideo (a clip URL) EDITS that clip instead, keeping its canvas and length. RAW MODEL ACCESS: by default small guards are added (label safety with no reference image, a negative prompt where the model takes one, reference lines) and hex colours become names; raw:true dispatches your prompt to the model BYTE-IDENTICAL — no rewriting, no appended guidance, no negative prompt, no brand references attached on your behalf. Credits, the durable delivery of the finished asset and the per-model validation are unchanged. Spends credits (blocked on the Starter plan).

NameRequiredDescriptionDefault
rawNoSend the prompt to the model exactly as written, without the wording Hermoso usually adds (packaging and label guidance, reference lines, colour names for hex codes, quality hints). Use it when you want the model's own interpretation rather than Hermoso's render craft. Two vendor-required fixes still apply: extra @ImageN tokens are dropped and an over-long prompt is trimmed at a sentence (on Veo it is refused with the length and the cap instead). Billing, durable delivery and per-model validation are unchanged.
loopNotrue = a seamless loop whose last frame flows back into its first. Only models with loop true in hermoso_capabilities; needs refImage and cannot be combined with endImage.
audioNodefault true. false = render SILENT: no native model audio, no music bed, and no bed charge held or billed. This is the ONLY way to decline the automatic bed (see musicMood) — leave it alone for anything that should have sound, and do not combine it with ttsScript.
modelNovideo model id from hermoso_capabilities; a named model is never swapped without asking. Omit to let the router pick; its default per job (jobEngines in hermoso_capabilities, from watched side-by-side tests) is seedance-2.5 for product, lifestyle, a person talking to camera and 30s single takes (cheaper engines broke the product there). A model the user picks is kept; never switch it
shotsNoMULTI-SHOT: one clip cut into these shots, in order; the seconds add up to a length the model renders. Replaces `prompt`. Only models with multiShot true in hermoso_capabilities; a shot prompt over that model’s shotPromptMaxChars (Kling 3.0: 512) is refused free, never cut.
speakNoA PERSON SAYING THESE EXACT WORDS TO CAMERA — the default for any "make my photo talk" / spokesperson / talk-to-camera ask. Pass with `creator` or a portrait as `refImage`: the video model films them saying it in their own voice, matched to who they are, and the length follows the words (leave durationSeconds out). `prompt` is then the staging (e.g. "natural", "walking in a park"). Real footage, not an animated photo; generate_avatar is the animated-photo look, only when asked for by name
anglesNoOPTIONAL, OFF BY DEFAULT: with `creator`, also send the extra views that creator already has saved (their pose plates, up to 2) beside their portrait. A test showed no visible improvement over the portrait alone, and each extra view adds about 25 s before the render starts, so leave it off unless asked. Views are skipped when the face library is busy, and the render always goes ahead on the portrait.
dryRunNotrue = render NOTHING and return the credits this exact clip would hold (an estimate; the final charge is its actual cost). Free. Call this to price a clip before running it: without it the call renders and bills.
extendNotrue = EXTEND refVideo by durationSeconds (default 5) with what your prompt says happens next, as ONE clip. By default it CUTS to a NEW SHOT: new angle, same scene, room sound carried, no music at the cut. A named model (any i2v) renders it.
promptNothe video prompt / shot description (for a refVideo edit, this is the transformation instruction); optional with `shots`
creatorNoSTAR A SAVED CREATOR in this clip — their id from list_creators, or the name you know them by, or a PRESET AI creator from list_creators presets (exact name or id; free, no generation). Their saved portrait rides first among the references as the on-camera person, with their saved consent, exactly as render_ad casts them; a real person saved from a photo keeps their real face on camera. An unknown name is refused by name, nothing charged.
endImageNolocal path or URL of the LAST frame: the clip travels from refImage (required with it) to this image. Only models with endFrame true in hermoso_capabilities take it; any other named model is refused by name, nothing charged.
refImageNolocal path or URL to anchor the first frame. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded.
refVideoNoURL of an existing video to EDIT rather than generate from scratch — the clip is transformed per your prompt, keeping the SOURCE clip’s aspect ratio and, on models hermoso_capabilities marks `sourceLength`, its LENGTH (their `durations` are the per-second price ladder; a durationSeconds sent is reported as unused). Trim the source to change the length. Omit `model` for the default editor, or name a model whose videoEdit is true in hermoso_capabilities (a named model that cannot edit is refused, nothing charged); refImage rides along as the look of what the edit adds. With extend:true this is instead the clip to EXTEND. Omit to generate a fresh clip.
ttsVoiceNovoice name, e.g. Rachel / George
faceRouteNo'face_lane' records the user's statement "I own the rights to this face" for the real person in their reference (paid plans; the likeness terms apply). Set it only when the user picked that option after a render said it could not use that face, and send the same request again; it then renders through Hermoso's face library at the normal price. Never set it on your own. The other options are a different `model` or one of the user's saved `creator`s.
musicMoodNothe mood of the music bed (upbeat / calm / warm / epic / tense / playful / elegant / hype / chill / dramatic; default warm). It does not decide WHETHER there is one: a clip with no audio track (a "silent" model in hermoso_capabilities, or one that came back mute) gets a bed composed and CHARGED automatically at explainerMusicCredits. audio:false = silent, no bed, no charge.
refImagesNoSEVERAL reference images (paths or URLs: a person, products, a place) that must all appear. A model uses up to its `refs.max` in hermoso_capabilities (one-image models take the first); with `refs.promptAddressed`, name them Image 1, Image 2… in order. minimax-h3-max-ref takes 9 and keeps each as a reference, not a first frame.
ttsScriptNovoiceover script to speak
cameraMoveNoA named camera move around the still in refImage (orbit = a quarter turn, the default; orbit_half / orbit_full = a half turn / a turntable). Only minimax-h3-max-camera renders one: with model omitted it is picked; any other named model is refused, nothing charged. Needs refImage.
extendModeNowith extend: 'continue' = the SAME take past its last frame, only when asked seamless (models with extend in hermoso_capabilities).
musicModelNocompose the bed on minimax-music-3 or lyria-3.5, skipping the free library
resolutionNoOmit = the default tier, hermoso_capabilities defaultResolution (720p), the tier its `credits` prices are for. 1080p is ~2.25x, 4k more, 480p less: quote creditsByRes and pass the tier you quoted. A tier a model's `resolutions` lacks renders at its best one.
aspectRatioNodefault '9:16'
interactionIdNowith extend:true on an Omni model: the interactionId returned by an earlier render on that model — continues it from its own stored context instead of re-uploading refVideo.
durationSecondsNolength of THIS ONE clip in seconds: one of the CHOSEN model’s listed durations in hermoso_capabilities (they differ per model, 4–8s on the short ones up to 30s). One continuous generation, so a longer ask than the model’s longest clip is REFUSED, nothing charged (never quietly truncated). Past ~15s NAME a long-clip model in `model`. For a longer spot use plan_ad with durationSeconds then render_ad, which stitches acts (on a 15s model, 40s = 15+15+10).
cameraTrajectoryNoYour own ordered camera path, 2 to 12 keyframes, for the camera-controls model only (same rule as cameraMove; overrides it). The first pose is held until its time and the last pose is held to the end. A value outside these bounds is refused by name, nothing charged.
generate_voiceGenerate voiceover or dialogueAInspect

RAW speech as an MP3: one voice reading a script (text), or a DIALOGUE of two or more people (lines), each in their own voice. Not for adding audio to a video (render_ad and generate_video voice their own spots; change_voice re-voices a finished clip). ONE VOICE: text + engine (default 'seed-audio'; also 'eleven-v4', 'minimax-speech', 'kokoro'…) + voice, a preset from that engine (hermoso_capabilities -> voice engines); a name the engine does not have is REFUSED for free with its real list, and a few engines make their own voice (the reply says which voice spoke). DIALOGUE: lines [{speaker, voice, text}] in speaking order. Each speaker keeps ONE voice, no two speakers share one, a speaker with no voice gets one nobody has; up to 10 voices and 10,000 characters, refused for free past that. Default engine: the dialogue model (Eleven v3 Dialogue, voices Aria, George, Sarah, Roger, Laura, Charlie, Callum, River, Liam, Charlotte, Alice, Matilda, Will, Jessica, Eric, Chris, Brian, Daniel, Lily, Bill, or an ElevenLabs voice id; '[laughs]'-style cues work); or name any voice engine (e.g. 'eleven-v4') to read each line on it and join them in order. dryRun:true returns the exact credits and makes nothing; the price quoted is the charge. Paid, priced by characters (hermoso_capabilities lists credits per 1,000).

NameRequiredDescriptionDefault
textNoONE VOICE: the script to speak (≤900 characters)
linesNoDIALOGUE: the turns in speaking order
voiceNoONE VOICE: a preset from the chosen engine (e.g. 'Aria'/'George' on eleven-v4, 'stokie_en' on seed-audio); omit for the engine default
dryRunNotrue = the exact credits only, nothing made or charged
engineNovoice-engine id: one voice defaults to 'seed-audio'; a dialogue defaults to the dialogue model ('eleven-dialogue'); or 'eleven-v4', 'minimax-speech', 'kokoro'… (hermoso_capabilities)
get_brandGet saved profileA
Read-onlyIdempotent
Inspect

What Hermoso ALREADY KNOWS for the active profile: the same saved profile (products, logos, palette, positioning) + learned memory the web Studio uses. Call it when you need to know whether a brand is on file: if hasBrand is true you can omit brand everywhere; if false, onboard with draft_brand. Not a required first step before a render: the create tools read the saved brand by themselves. 0 credits.

NameRequiredDescriptionDefault
brandNoprofile id/name from list_brands, this call only
get_drive_fileGet a Drive file’s detailsA
Read-onlyIdempotent
Inspect

Fetch one Drive file’s metadata — name, type, size, modified time, a webViewLink to open it and a webContentLink to download it. Pass fileId (from list_drive_files). Read-only.

NameRequiredDescriptionDefault
fileIdYesthe Drive file id (from list_drive_files)
get_jobGet render jobA
Read-onlyIdempotent
Inspect

Poll a render job by id. Returns status (queued|running|done|error), progress, and on done the served media URL. Renders take 1–3 minutes: keep calling this until done/error without asking the user — several calls is normal, not a stall. An id that does not exist on this account answers status "not_found" — that is FINAL: stop polling it, and do not re-fire the render (that double-charges).

NameRequiredDescriptionDefault
idYesthe job id: starts with job_, from a render tool's jobId or from list_jobs (generate_voice and generate_sfx return their file directly and have no job)
get_linkedin_leadRead one LinkedIn leadA
Read-onlyIdempotent
Inspect

One LinkedIn lead by id (from list_linkedin_leads), with every answer named by field. Personal data — show, never republish. Read-only, free.

NameRequiredDescriptionDefault
leadIdYeslead id from list_linkedin_leads
pageIdNothe company Page that owns the form — from list_linkedin_pages; omit when one Page is shared
adAccountIdNoread forms owned by an AD ACCOUNT instead of a Page
get_onedrive_fileGet a OneDrive file’s detailsA
Read-onlyIdempotent
Inspect

Fetch one OneDrive item’s metadata — name, type, size, modified time, a webViewLink to open it and a webContentLink to download it. Pass fileId (from list_onedrive_files). Read-only.

NameRequiredDescriptionDefault
fileIdYesthe OneDrive item id (from list_onedrive_files)
get_post_refillAutopilot posting statusA
Read-onlyIdempotent
Inspect

Show AUTOPILOT POSTING (autoposting, the posting refill) for this profile — "is autopilot on?", "what is waiting for review?": whether it is on, its mode (review = each batch of fresh posts waits as drafts for approval; auto = fresh posts are scheduled straight away), WHICH CHANNELS it posts to with each one’s posts per day and posting times, roughly what it costs a day, when it next runs, how many posts are queued, the DRAFTS waiting for approval (id, time, channels, caption, the new image or video), and the notes it learned from reviews. It also names the channels that CANNOT be posted to and why. On the Free plan, paidOnly is true and note explains the status. Read-only, free.

NameRequiredDescriptionDefault

No parameters

get_settingsRead app settingsA
Read-onlyIdempotent
Inspect

Read this account's app settings — the LANGUAGE Hermoso writes ads and copy in (chat replies follow the language the user writes in), the app appearance (theme), and whether the weekly competitor-watch email is on. Same settings as the web app's Settings pane. Read-only, free.

NameRequiredDescriptionDefault

No parameters

get_skillGet skillA
Read-onlyIdempotent
Inspect

Load a bundled skill’s full SKILL.md workflow instructions by name (from list_skills), or a built-in in-app skill by its id (e.g. youtube_script, retention_skeleton). Follow the loaded instructions to run that workflow with the other tools. Read-only, free.

NameRequiredDescriptionDefault
nameYesbundle name from list_skills, e.g. hermoso-generate
headline_variantsHeadline variants of a static adAInspect

Turn ONE finished static ad into several copies that differ ONLY in the headline, for an A/B test: same picture, product, layout, colours and every other line. Pass image, and either headlines (your own, up to 10) or count (default 5, max 10) to have distinct angles written for you in the saved brand's voice (never inventing numbers, prices, ratings or claims the ad or brand does not state); brief steers what to test. The ad's text is read first (3 credits), then one image edit per headline; each output is proofread and flagged (textCheck) if the rendered words do not match, never silently re-rendered. When the saved brand has a product photo and the ad shows that product, the photo rides with the edit and the product's label is read and checked against it: re-printed from the photo only when it came out wrong (the check alone is charged when it is right; one small extra charge finds the product first). fixLabel:false turns that off. The whole batch is priced before anything runs. Returns each headline, its angle and its image URL.

NameRequiredDescriptionDefault
briefNowhat to test, e.g. "price-led vs outcome-led" or "speak to busy parents"
countNohow many headlines to write when `headlines` is omitted (default 5)
imageYesthe finished static ad: URL, Library item URL, upload_file URL or local path
fixLabelNofalse = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product)
headlinesNoyour own headlines to test (up to 10); omit to have them written
hermoso_capabilitiesHermoso models, options and credit costsA
Read-onlyIdempotent
Inspect

Probe what this Hermoso account can do RIGHT NOW: available image/video model ids + their credit costs (estimates: a render is charged its actual cost), aspect ratios, video durations, the recipe ids, and the canEdit/canAvatar flags. Call it when you need a specific model id, a credit cost, or a capability you are not sure of. It is NOT a prerequisite for rendering: generate_image, generate_video and render_ad all run with model omitted and route to the server’s own default. Read-only, free.

NameRequiredDescriptionDefault

No parameters

hermoso_creditsCredit balanceA
Read-onlyIdempotent
Inspect

Return the credit balance, recent priced calls with their credits, and costModel (the one-sentence rule of what costs credits). THE RULE: only AI model runs and Ad Spy research spend credits; publishing, scheduling, ads management, analytics, comments, DMs and connectors cost no credits on any plan (X is the single per-call exception; xCost gives its exact credits per post, with and without a link). Check before kicking off paid generation; answer "does posting cost credits?" with NO. HOW MANY posts is set by the plan, not credits: the Free plan includes 10 posts a month on up to 3 channels, and a post counts once however many channels it goes to (posting says how many are used); every paid plan posts without a limit and runs autopilot (Basic $9.99/mo: unlimited posting and Autopilot, no monthly credits; it includes the same 250+ free credits as the Free plan, monthly credits start on Starter, and Autopilot spends credits). Starter, Pro and Studio: media models at cost + 10% (memberRate); Basic and Free pay standard prices.

NameRequiredDescriptionDefault

No parameters

hook_variantsNew opening hooks for a videoAInspect

HOOK MULTIPLIER: give ONE finished video ad N NEW OPENING HOOKS, N complete versions to A/B test. A planned hook replaces the first ~1.5-4 s (ends at the source's first cut there, else 3 s; hookSeconds overrides) with a new silent shot on a DIFFERENT named mechanic (list_hooks); the rest of the footage and the WHOLE original soundtrack stay, so every version keeps the source's length, and nobody in the hook talks or shows text. hooks[] adds your own openings: {url} a FOUND viral hook (post link or file) joined in front with the approved bridge (cut before its payoff, its own payoff sound carried across; post_edit price), {prompt} an opening described in words, {mechanic} a named one; with hooks and no count only those are made. To make the found hook's subject your product or creator first: recast_hook. Pass the video's FILE URL (a render, job result, list_library or upload_file). 1-5 versions, default 3. Refused free before billing: a source over 120 s, too short for a 1.5 s hook plus 2 s after it, unreadable, or a social post as the SOURCE (clone_video remakes someone else's ad). COST: quoteOnly:true returns the exact credits free: state that first. A paid planning read (planCredits) runs next, then each planned version bills like fix_beat for the hook's seconds. dryRun:true pays for that read; pass its plan back to render exactly those. Returns ONE JOB PER VERSION; call get_job on each until done, never describe a version before its URL arrives. Hooks that show the product use the brand's product photo (productImage overrides; useBrand:false sends none).

NameRequiredDescriptionDefault
planNothe `plan` object a previous dryRun returned, to render exactly those hooks without planning again
countNohow many hook versions, 1-5 (default 3)
hooksNoyour own openings, one version each
notesNoanything the hooks must respect, e.g. "keep it calm", "show the product in every hook"
videoYesthe finished video to give new hooks: its served file URL
dryRunNotrue = the paid plan and the price, no render
useBrandNofalse = send no brand name or product photo (for a video that is not this workspace brand’s)
quoteOnlyNotrue = the exact credits, free; nothing runs
resolutionNorender tier for the new opening. Defaults to the source’s OWN tier so the hook matches the rest of the ad; a lower tier costs a lot less and is scaled into the source’s canvas (visibly softer for the first seconds). dryRun quotes whichever you pick.
hookSecondsNowhere the CURRENT hook ends, in seconds (1.5-4). Omit to use the first shot cut.
productImageNoproduct photo URL used as a reference in hooks that show the product (defaults to the workspace brand’s first product photo)
import_from_cloudImport a Drive / OneDrive folder into the LibraryAInspect

Pull the files in a Google Drive or OneDrive FOLDER into this profile's Library, so they can be used like anything rendered here — published, scheduled, cloned, used as a product photo or a reference. Hermoso downloads each file with the user's own connected account (a Drive/OneDrive file is not public) and stores a durable Hermoso url for each. Give folderId from list_drive_files / list_onedrive_files with onlyFolders — omit it for the root. GOOGLE DRIVE ONLY SHOWS WHAT THE USER HANDED OVER: our Drive scope is drive.file, so Hermoso can see the files and folders it created plus the ones the user picked with the Google picker in the app, and NEVER their whole Drive. Picking a FOLDER hands over the folder alone, not the files in it, so if a folder comes back empty the reply carries a link that opens the picker inside it, where the user selects every file at once (Cmd+A / Ctrl+A); give them that link. OneDrive has no such limit. SUBFOLDERS ARE NOT WALKED and Google-native docs (Docs/Sheets/Slides) have no file to download: both are reported back BY NAME rather than silently dropped, along with anything too large or unreadable, so you can tell the user exactly what did and did not come across.

NameRequiredDescriptionDefault
limitNohow many files to bring across in this call (default 10, max 25). Anything over the limit is listed as skipped so you know what is left.
folderIdNothe folder to import, from list_drive_files / list_onedrive_files (onlyFolders:true). Omit for the root of the drive.
providerYeswhich cloud — `drive` is Google Drive, `onedrive` is Microsoft OneDrive
insert_sceneInsert a new scene into a videoAInspect

INSERT a newly generated scene into a clip you already have: the video plays up to startTime, the new scene (5 to 13 seconds) plays, then the original picks up again at resumeTime. Use for a product close-up, a reaction, a reveal or a missing beat in the middle of an ad without re-rendering the rest. resumeTime = startTime keeps every second of the original; a later resumeTime also cuts the footage in between. Describe the scene in prompt and/or attach up to 4 reference pictures (refImages) or 1 reference clip (refVideos). Clips up to 60 seconds; both times between 1.625 and 60 seconds. Paid render (billed per second of NEW scene); returns the whole video's URL.

NameRequiredDescriptionDefault
videoYesthe source video URL (a render, job result or list_library item)
promptNowhat happens in the new scene: the action, the subject, the camera
secondsNolength of the new scene, 5-13 seconds (default 5)
refImagesNoup to 4 picture URLs of what should appear (the product, a person, a place)
refVideosNoup to 1 clip URL whose look or motion the scene follows
startTimeYesseconds into the clip where the new scene starts (1.625-60)
resolutionNodefault 720p (rendered at 768p); '480p' is cheaper
resumeTimeYesseconds into the ORIGINAL clip where it picks up again after the new scene (2.625-60, at or after startTime)
invite_memberInvite a teammateA
Destructive
Inspect

Invite someone to this profile by email (role: member = read-only on billing, admin = full). This SENDS a real email invite / share link — an account change. Confirm the exact email + role with the user, then call with confirm:true.

NameRequiredDescriptionDefault
roleNodefault member
emailYesthe invitee’s email
confirmNoREQUIRED true — this invites a real person
leave_connectorRemove my own account from a shared connectionA
DestructiveIdempotent
Inspect

On a connector that several teammates can contribute their OWN account to (see list_connectors — the row reports multiContributor), remove YOURS from this profile: your stored credential is dropped and the accounts you shared stop being shared. Your teammates' accounts on the same connection keep working, and nothing changes at the provider — reconnecting in a browser shares again. Use this instead of disconnect_connector when the connection is not yours to remove: disconnect_connector revokes the grant at the provider and only the person who created the connection may call it.

NameRequiredDescriptionDefault
providerYesprovider id exactly as list_connectors reports it, e.g. "linkedin", "tiktok_ads", "meta"
list_brandsList profilesA
Read-onlyIdempotent
Inspect

List every profile on this account (id + name; a profile is a brand, a creator or a personal workspace) and which one this connection acts on, PLUS any profile another account shared with you. Call this, then use_brand to switch; a SHARED profile switches the same way (pass its name or the profile id printed here). If a profile looks empty when the app shows it full, you are acting on a different one: call this first. Read-only, free.

NameRequiredDescriptionDefault

No parameters

list_business_locationsList Google business listingsA
Read-onlyIdempotent
Inspect

List the Google Business Profile listings SHARED WITH THIS PROFILE — id, title, address, website and Maps link. These are the only listings anything here can post to or read: one Google login often manages several businesses (an agency manages its clients’), and the user ticks which of them belong to this profile. Call this before posting whenever more than one is shared and let the USER pick: a Post on the wrong storefront is a public mistake Hermoso will not make for them. If nothing is shared, ask the user to choose — list_connector_accounts("google_business") then set_connector_accounts — and never name or guess a listing. Read-only, 0 credits. Needs Google Business Profile connected (Settings > Connectors > Google Business Profile).

NameRequiredDescriptionDefault

No parameters

list_connector_accountsList a connector’s accountsA
Read-onlyIdempotent
Inspect

Show every identity a connected account can act as, and which ones this PROFILE is currently allowed to use — Facebook Pages + Instagram + Meta ad accounts, Google Ads customers, LinkedIn company Pages (and the personal profile), Pinterest ad accounts, Microsoft Advertising accounts. One person often administers several; only the ticked ones can be posted to or spent from. Call this before set_connector_accounts, and let the USER pick — never guess. Providers: tiktok, x, youtube, threads, bluesky, telegram, pinterest, instagram, meta, google_ads, linkedin, pinterest_ads, linkedin_ads, reddit_ads, apple_ads, microsoft_ads, google_business, google_analytics, snapchat_ads, x_ads, tiktok_ads, google_tag_manager, google_search_console, bing_webmaster. Read-only, free.

NameRequiredDescriptionDefault
providerYeswhich connector’s accounts to list
list_connectorsList connectorsA
Read-onlyIdempotent
Inspect

List the third-party accounts connected to this workspace (Meta, Google Ads, Google Drive/Sheets/Docs, YouTube, LinkedIn, OneDrive, Slack, …) — provider, status and the connected account label — PLUS which providers are available to connect. IT ALSO FLAGS A CONNECTION WHOSE PERMISSIONS ARE OUT OF DATE: a provider writes its granted permission set into the token at consent time, so a connection authorized before a permission was approved does not carry it and never will — those calls are refused by the provider and no retry or wait can change it. Check this FIRST when a connected provider starts refusing things. The fix is a browser: the user reconnects under Workspace > Connectors. Read-only, free.

NameRequiredDescriptionDefault

No parameters

list_creatorsList saved creatorsA
Read-onlyIdempotent
Inspect

List this workspace’s SAVED CREATORS — the reusable on-camera cast (AI creators made here, a person pulled from a social profile, a consented photo upload). Read-only, FREE. Each entry gives the name, the PORTRAIT URL, where the portrait came from and whether a real person’s likeness consent is on file, how many extra pose plates exist, and any chosen or cloned voice. TO PUT ONE IN A FINISHED AD, pass their id or name as render_ad’s creator — that casts them for the whole spot (and skips the character-portrait render, so it costs less than not casting anyone). THE PORTRAIT URL IS THE REUSE HANDLE for the raw lanes — pass it as generate_avatar’s image (a talking clip of them), generate_video’s refImage (they star in the scene), recast_motion’s image (they perform a reference clip’s motion), or generate_image’s refImages. CALL THIS BEFORE OFFERING TO GENERATE A NEW PERSON: re-casting somebody the workspace already has keeps the SAME face across every ad, while a fresh person costs credits and breaks that continuity. An empty answer means the workspace genuinely has no cast yet — say so and offer generate_avatar / save_creator, never invent a roster.

NameRequiredDescriptionDefault
limitNomax creators to return (default 24)
genderNofilter the PRESET creators by gender (the saved cast is never filtered)
list_drive_filesList Google Drive filesA
Read-onlyIdempotent
Inspect

List the Google Drive files & folders Hermoso can reach: the ones it created plus any the user picked with the Google file picker in the app (drive.file, never their whole Drive). This is how you find a picked file's id. Filter by query (name contains …), folderId, or onlyFolders:true; paginate with pageToken. Read-only. A FOLDER: picking a folder hands over the folder alone, not its files, so a folder can list empty or partly; the reply then carries pickUrl, an app link that opens the picker inside that folder (Cmd+A / Ctrl+A selects every file and subfolder). Give the user that exact link, then list again. recursive:true walks every subfolder Hermoso can see (each row has its path) and reports picked files whose folder it cannot see.

NameRequiredDescriptionDefault
queryNoonly files whose name contains this
folderIdNolist the contents of this folder id
pageSizeNorows per page (1–200, default 50)
pageTokenNocursor from a previous call
recursiveNowith folderId: the whole tree
onlyFoldersNolist folders only
includeTrashedNoinclude trashed files (default false)
list_errorsList errors users hitA
Read-onlyIdempotent
Inspect

The errors actually recorded against this workspace, GROUPED by fingerprint — the same failure at the same call site is one row with a hit count and first/last seen, sorted defects-first. Each row says whose side it is: ours (a defect worth fixing), user (a refusal we deliberately authored, e.g. not-connected or out-of-credits), or unknown (a vendor 4xx we cannot attribute — never guessed). Free text, tokens, emails and creative are redacted before anything is stored, so an input echo shows shapes and lengths, not content. Filter by surface (http/mcp/agent/job/client) or kind. Read-only, 0 credits. Scoped to your own workspace; an operator whose client carries the admin key sees every account.

NameRequiredDescriptionDefault
kindNo'ours' = a defect; 'user' = a refusal we authored; 'unknown' = we could not tell
limitNohow many groups to return (default 50, max 200)
sinceNoISO timestamp — only groups last seen at or after this
surfaceNowhere it happened: http (an API route), mcp (an agent tool), agent (the in-app Studio agent), job (an async render/publish), client (a browser crash)
list_hooksThe hook + setting libraries, and which hooks are workingA
Read-onlyIdempotent
Inspect

The curated menu of VISUAL scroll-stop HOOKS (how an ad opens) and SETTINGS (where it is staged) that plan_ad and render_ad accept, PLUS this brand's measured traction per hook. Call it before planning an ad to pick a hook deliberately instead of letting the model improvise one, and call it after publishing to see which ones are actually landing. Three things it will not do: it never recommends a hook from thin data — a verdict is SUPPRESSED below 5 measured posts and the reason is stated; it never compares across channels; and it reports hooks you have NEVER TRIED as a fact, not as advice, because 'you haven't tried this' is an observation and 'you should' would be a verdict drawn from zero data. A hook marked unusable in this brief says WHY (an on-screen-text hook cannot ride an authentic/UGC render, which carries zero on-screen text). Read-only, 0 credits.

NameRequiredDescriptionDefault
tierNoproduct tier, used with category — changes the FINISH of the room, never the room. Default premium.
channelNorestrict the performance half to one channel (facebook, instagram, threads, x, linkedin, youtube, tiktok, pinterest, bluesky)
categoryNothe product category (e.g. 'skincare serum', 'protein powder', 'sunglasses') — returns the setting our Location x Tier matrix puts that category in, with the reason
authenticNotrue if the planned ad is an authentic/UGC/creator-register render — on-screen-text hooks are then reported unusable, with the reason
list_jobsList render jobsA
Read-onlyIdempotent
Inspect

List the most recent render jobs + how many are currently running, so you can report on or resume in-flight work.

NameRequiredDescriptionDefault

No parameters

list_libraryList libraryA
Read-onlyIdempotent
Inspect

Browse this workspace's Library — every image/video generated in the Studio, and uploads (source:'upload'), newest first (the same Library the web app shows). Returns served URLs you can open directly or hand to fetch_asset for a download link, plus each asset's id (L0, L1…, which plan_ad and render_ad inserts take), kind, model, and age. Free, read-only.

NameRequiredDescriptionDefault
kindNofilter by asset kind (default 'all')
limitNomax assets to return (default 20, max 60)
uploadsNo
list_linkedin_lead_eventsLinkedIn lead events received in real timeA
Read-onlyIdempotent
Inspect

The lead events LinkedIn has PUSHED to Hermoso for this brand (new lead / deleted lead, with the form and the lead id), newest first. Empty means none have arrived, not that none exist — list_linkedin_leads reads every lead regardless, and subscribe_linkedin_leads is what starts delivery. Read a lead’s answers with get_linkedin_lead. Read-only, free.

NameRequiredDescriptionDefault
limitNoevents to return, newest first; default 50, max 200
list_linkedin_lead_formsList LinkedIn lead gen formsA
Read-onlyIdempotent
Inspect

The LEAD GEN FORMS a LinkedIn company Page or ad account owns — id, name, state, version and the fields each one asks for (firstName, email, company …). Make a new one with create_linkedin_lead_form; edit or archive one with update_linkedin_lead_form. If it answers that the connection must be reconnected, say exactly that: the lead-sync permission is granted at authorise time. Read-only, free.

NameRequiredDescriptionDefault
pageIdNothe company Page that owns the form — from list_linkedin_pages; omit when one Page is shared
adAccountIdNoread forms owned by an AD ACCOUNT instead of a Page
list_linkedin_leadsList LinkedIn leads (form responses)A
Read-onlyIdempotent
Inspect

The LEADS a LinkedIn lead gen form collected — every response with its answers keyed by field (firstName, lastName, email, company, …), the campaign and creative that produced it, the consents ticked, and whether it was a test lead. Newest first. Filter by formId, a since/until window (ISO date or epoch ms — LinkedIn takes epoch), or testLeadsOnly. THIS IS PERSONAL DATA: show it to the user, hand it to the CRM they name, never repeat it into a post or an unrelated tool. Pass start for the next page. Read-only, free.

NameRequiredDescriptionDefault
limitNoper page, max 100
sinceNoISO date or epoch milliseconds
startNooffset for the next page
untilNoend of the time window, ISO date or epoch milliseconds; omit for now
formIdNoonly this form (from list_linkedin_lead_forms)
pageIdNothe company Page that owns the form — from list_linkedin_pages; omit when one Page is shared
leadTypeNodefaults by owner: SPONSORED for an ad account, COMPANY (organic Page form) for a Page; EVENT for event forms. LinkedIn refuses SPONSORED on a Page owner
adAccountIdNoread forms owned by an AD ACCOUNT instead of a Page
formVersionNodefault 1
testLeadsOnlyNotrue returns ONLY test submissions
list_linkedin_lead_subscriptionsList LinkedIn lead webhooksB
Read-onlyIdempotent
Inspect

The lead notification webhooks registered on a LinkedIn Page or ad account, with the id delete_linkedin_lead_subscription takes. Read-only, free.

NameRequiredDescriptionDefault
pageIdNothe company Page that owns the form — from list_linkedin_pages; omit when one Page is shared
leadTypeNoSPONSORED for an ad account; COMPANY, EVENT or ORGANIZATION_PRODUCT for a Page; defaults by owner
adAccountIdNoread forms owned by an AD ACCOUNT instead of a Page
list_linkedin_pagesList the LinkedIn company Pages this account administersA
Read-onlyIdempotent
Inspect

List the LinkedIn COMPANY PAGES the connected account administers — id, name and the role held on each. ALWAYS call this before post_to_linkedin_page when there is more than one Page: publishing to the wrong company Page is a public mistake and Hermoso never chooses for the user. If it comes back empty, the account holds no Page admin role, or LinkedIn has not granted this app the organization scopes — say that plainly rather than guessing an id. Read-only, free.

NameRequiredDescriptionDefault

No parameters

list_memoryList memoryA
Read-onlyIdempotent
Inspect

List the durable facts & preferences saved in this workspace’s Memory (what the studio remembers about the brand, audience, taste, and do/don’t rules) — the same Memory the web app shows. These shape every future ad. Read-only, free.

NameRequiredDescriptionDefault
limitNomax items (default 50, max 200)
categoryNofilter to one bucket (Brand/Audience/Taste/Do/Don’t/Preference)
list_meta_pagesList Meta pages & ad accountsA
Read-onlyIdempotent
Inspect

List the Facebook Pages (with any linked Instagram business account) and ad accounts on the connected Meta account — use before post_to_meta / create_meta_campaign to pick the target. Requires the user to have connected Meta (Settings > Connectors > Meta); returns a connect hint if not.

NameRequiredDescriptionDefault

No parameters

list_meta_postsList the Page’s / Instagram account’s own postsA
Read-onlyIdempotent
Inspect

List the connected Facebook Page's or Instagram account's OWN existing posts — id, caption, permalink, publish date and format. THIS IS THE TOOL THAT GETS YOU THE postId every other Meta read needs: meta_post_insights, list_meta_comments and manage_meta_post all require one. Use it for "how did our last few posts do", to find a post the user describes loosely, or before backfill_posts. Pass target:'instagram' for the linked IG account (Stories are excluded — Meta's media edge does not return them); Facebook hides unpublished drafts unless you ask for them. Only ever reads a Page the brand has connected. Read-only, 0 credits.

NameRequiredDescriptionDefault
brandNoWHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done.
limitNohow many posts (default 25, max 100)
cursorNopaging cursor returned by a previous call
pageIdNowhich connected Page — omit when the profile has only one
taggedNoINSTAGRAM — list posts OTHER accounts tagged the brand in (UGC, creator posts, partner mentions), with each author’s @handle, instead of the brand’s own posts. Free.
targetNodefault facebook; 'instagram' reads the Page's linked IG business account
accountNowhich Instagram account — an @handle or id from list_connector_accounts("instagram"). Needed when several are linked, and the way an Instagram Login (standalone) account is reached; omit for the one Page-linked account.
includeUnpublishedNoFacebook only — also return unpublished drafts (hidden by default)
list_notion_pagesList Notion pagesA
Read-onlyIdempotent
Inspect

Find the Notion pages and databases the user shared with Hermoso, by title, newest edited first. Gives the page id that save_to_notion, append_to_notion_page and read_notion_page take, or the database id to save rows into. Read-only, free. Needs Notion connected (Settings > Connectors > Notion).

NameRequiredDescriptionDefault
typeNolimit results to pages or to databases; omit for both
limitNo1-100, default 25
queryNowords in the title
cursorNofrom the previous page
list_onedrive_filesList OneDrive filesA
Read-onlyIdempotent
Inspect

List files & folders in the user’s OneDrive — the root by default, a folder’s contents (folderId), or a name search (query). onlyFolders:true lists folders only. Paginate with pageToken (the cursor from a previous call). Read-only.

NameRequiredDescriptionDefault
queryNosearch — only items whose name matches this
folderIdNolist the contents of this folder id
pageSizeNorows per page (1–200, default 50)
pageTokenNocursor from a previous call
onlyFoldersNolist folders only
list_pinterest_boardsList Pinterest boardsA
Read-onlyIdempotent
Inspect

List the boards on the user’s connected Pinterest account — id, name, privacy and pin count. ALWAYS call this before post_to_pinterest and let the USER pick: Pinterest requires a board and Hermoso never chooses one for them. Read-only, 0 credits. Needs Pinterest connected (Settings > Connectors > Pinterest).

NameRequiredDescriptionDefault
accountNowhich connected account: its @handle or id from list_connector_accounts; needed only when several are connected
privacyNofilter by board privacy; default is everything the connection can see
list_playbooksList playbooksA
Read-onlyIdempotent
Inspect

List the PLAYBOOKS saved in this workspace — the reusable strategy cards (winning hooks, angles, formats and the concrete plays to run) kept from teardowns, angle mining and creatives worth repeating. The same Playbooks the web app's Playbooks tab lists. Read one before planning an ad so you re-run what already worked instead of starting cold. Read-only, free.

NameRequiredDescriptionDefault
fullNotrue to return every hook/angle/play in the text, not just the headline counts
limitNomax playbooks to return (default 25, max 100)
list_product_photosList product photosA
Idempotent
Inspect

List the product photos ALREADY saved in your workspace — the brand's product library plus any app-store screens (also surfaces photos locked in your OTHER creations, since a set product lands in the shared library). FREE — returns each photo's url + the product it shows (the shop's own title when one is on file). These are already saved: never ask the user to upload them again. Call it before set_product_image to see the existing photos you can reuse. Reads YOUR saved brand (pass brandId to target a specific profile — that switches this key's active profile like use_brand).

NameRequiredDescriptionDefault
brandIdNoa profile id/name from list_brands whose product library to list; omit to use the active profile
list_published_postsList what this brand has publishedA
Read-onlyIdempotent
Inspect

List every post Hermoso has recorded publishing for this brand, newest first, across all channels — channel, permalink, caption, format, the HOOK and SUBJECT it was written to, and its measured engagement. The WHOLE history, no cap: pass the reply's nextCursor as cursor for older posts. Each row says how it was recorded: 'captured' (written at publish time — the hook is what the author intended) or 'backfilled' (reconstructed from the platform afterwards). A dash for engagement means the platform reported no number — NOT zero. Read-only, 0 credits.

NameRequiredDescriptionDefault
brandNoWHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done.
limitNomax posts (default 50, max 200), newest first
cursorNonextCursor from a previous reply: the next, older page
channelNofilter to one channel: facebook, instagram, threads, x, linkedin, youtube, tiktok, pinterest, bluesky, google_business
list_scheduledList scheduled and past postsA
Read-onlyIdempotent
Inspect

Use this when the user asks what is scheduled, what already posted, or whether a post went out. Show what is queued to post and what already went out. Each fired item reports PER-CHANNEL outcomes, so you can see that (say) Instagram published and TikTok failed on the same item rather than a single misleading verdict. Past items are rebuilt from published-post records, one row per channel; they are not job runs (use list_jobs for those). THE LIST IS COMPACT so it fits in one reply: the next 25 queued and the last 15 fired, captions shortened. Pass id for ONE post in full (every caption and setting, which you need before reschedule_post replaces a caption map), channel to filter, or upcoming / fired for more rows. Read-only, 0 credits.

NameRequiredDescriptionDefault
idNoone post id from this list: returns that post in full, every caption and setting included
brandNoWHICH PROFILE to list — the id or exact name from list_brands. Needed when the post lives in a profile this connection is not pinned to: a post you can CREATE in a profile must be manageable there too, without switching the whole connection. A name that matches no profile, or two, is REFUSED.
firedNohow many already-fired posts to list, most recent last (default 15, max 200)
channelNoonly posts that include this channel, e.g. "pinterest" or "x"
upcomingNohow many queued posts to list, soonest first (default 25, max 200)
list_sheet_tabsList the tabs in a Google SheetA
Read-onlyIdempotent
Inspect

The tabs in a Google Spreadsheet, each with its name, numeric sheetId, row/column count and position. Call this BEFORE naming a tab in update_sheet / clear_sheet_range / manage_sheet_tabs / format_sheet, and before proposing to delete one — it is how you learn what the file actually contains instead of guessing at a name. Read-only, free.

NameRequiredDescriptionDefault
sheetUrlNoa Google Sheets URL — the id is extracted from it
spreadsheetIdNothe spreadsheet id (from create_sheet, or list_drive_files for one the user picked)
list_skillsList skillsA
Read-onlyIdempotent
Inspect

List the bundled Hermoso SKILLS — multi-step workflow instructions (SKILL.md) that orchestrate the other tools (research an ad space, plan+render a finished ad, product photoshoot, raw generation) — plus the in-app strategy skills and creative recipes. Call get_skill to load a bundle. Read-only, free.

NameRequiredDescriptionDefault

No parameters

list_swipefileList the swipefileA
Read-onlyIdempotent
Inspect

List this workspace's SWIPEFILE — the saved-ad research board: every named collection and the ads/creatives kept in it (advertiser, headline, body copy, media URL, platform, when it was saved, and any taste tags). The SAME board the web app's Swipefile tab shows. Use it to answer "what have we saved?", to mine the user's own taste before planning an ad, or to find a reference to remix. Read-only, free.

NameRequiredDescriptionDefault
limitNomax ads to return (default 50, max 500)
collectionNoonly list ads in this collection (by name or id) — omit for every collection
list_teamList team membersA
Read-onlyIdempotent
Inspect

List the members of the current profile — email, role (admin/member) and status. Read-only, free.

NameRequiredDescriptionDefault

No parameters

list_telegram_chatsFind Telegram chat idsA
Read-onlyIdempotent
Inspect

Find the chat ids this Telegram bot can be addressed by. STATE THE LIMIT WHENEVER YOU USE IT: this is NOT the list of chats the bot belongs to — the Bot API publishes no such method — it is every chat that SENT the bot an update in the last 24 hours, which is as long as Telegram keeps an update. A channel the bot posts to every day but nobody messages will NOT appear here, and its absence means nothing at all: post to it by @username or numeric id anyway. If the bot has an outgoing WEBHOOK configured the list is empty for that reason alone (Telegram: getUpdates "will not work if an outgoing webhook is set up"), and the reply says so rather than reading as an empty account. Nothing is consumed — no update offset is confirmed, so this cannot eat the bot’s pending updates. Free, 0 credits.

NameRequiredDescriptionDefault
limitNohow many recent updates to scan, 1–100 (default 100)
accountNowhich connected account: its @handle or id from list_connector_accounts; needed only when several are connected
list_watch_findingsRead the market radarA
Read-onlyIdempotent
Inspect

Read what a brand's MARKET RADAR (set_competitor_watch) has found: each watched brand's new ads, the winners (ads past 30 days running, with a ready brief to make your own version), the trending formats and top posts for its topics, and the ideas to make, plus its state (lastRun, nextRunAt, lastRunOk, paused) and the quote for its next check. Findings marked seed:true are what a brand was already running at its first check, not new launches. Read-only and free; it never runs a check.

NameRequiredDescriptionDefault
brandNoprofile id/name from list_brands, this call only
limitNomax ads (default 25, max 75)
competitorNoonly this watched brand's ads (exact name as returned)
list_whatsapp_accountsWhatsApp Business accounts and numbersA
Read-onlyIdempotent
Inspect

The WhatsApp Business Accounts SHARED WITH THIS PROFILE and the phone numbers registered on each — the ids every other WhatsApp tool needs, plus each number’s QUALITY RATING, which is what decides how many messages Meta will let it send. Start here. A WABA with no number cannot send anything and the reply says so: Hermoso does not register or verify numbers, that is WhatsApp Manager. A business portfolio that could not be read is REPORTED rather than dropped — an account missing from this list would read as "the brand has no WhatsApp", which is a claim about their business and not about our read. TWO ACCOUNTS CAN CARRY THE SAME DISPLAY NAME — always quote the display field or the id when naming one to a user, never the bare name. ONLY THE ACCOUNTS SHARED WITH THIS PROFILE ARE REACHABLE. One Meta login often administers WhatsApp for several businesses, so a human ticks which belong to this profile under Settings > Connectors > Meta > Manage accounts (or set_connector_accounts(provider:"meta")); nothing else here can see or touch the rest. With none ticked every WhatsApp tool refuses and names that as the way out. With exactly ONE ticked, wabaId is optional — pass it only to disambiguate. Read-only, 0 credits (WhatsApp conversations are billed by META to the business directly, never in Hermoso credits). Needs Meta connected.

NameRequiredDescriptionDefault

No parameters

localize_adLocalize a static ad into other languagesAInspect

Translate the on-image text of ONE finished static ad into other languages and keep everything else: same picture, layout, typeface, colours, logo and product. Pass image and languages (up to 5, e.g. ["Spanish", "German", "French (Canada)"]). The ad's text is read (3 credits), translated the way a native copywriter in each market would write it (brand and product names, URLs and prices kept as written), then one image edit per language; each output is proofread and flagged (textCheck) if the words do not match, never silently re-rendered. When the saved brand has a product photo and the ad shows that product, the photo rides with the edit and the product's label is read and checked against it: re-printed from the photo only when it came out wrong (the check alone is charged when it is right; one small extra charge finds the product first). fixLabel:false turns that off. Priced before it runs. For a VIDEO use dub_video.

NameRequiredDescriptionDefault
imageYesthe finished static ad: URL, Library item URL, upload_file URL or local path
fixLabelNofalse = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product)
languagesYestarget languages, by name
make_effectRun a one-click effect on a photoAInspect

ONE-CLICK EFFECTS, the same ones as the /tools pages: a photo goes in, a styled picture comes out, and most can then become a 5-second clip. Plush toy, 3D figure, mascot, comic book, pixel game, billboard ad, giant product, bullet time, packshot, outfit swap, headshot, relight and many more. Name no effect for the free list. effect alone: what it takes. effect + images: a free quote; add confirm:true to make the picture. Then effect + still (that picture's URL): a free quote for the clip; confirm:true makes it. describe adds anything of your own. For a look not on the list use generate_image. A real person's photo follows the same likeness rules as every render.

NameRequiredDescriptionDefault
textNoa line printed exactly, on an effect that takes one
stillNoCLIP STEP: the finished picture to animate
effectNoeffect id from the list
imagesNoits pictures, in the order listed: URL, local path or Library id
speechNoCLIP STEP: a line said in the clip, on an effect that speaks
confirmNotrue = run it (real spend); without it, a free quote
optionsNoits listed options, e.g. {material:"knit"}; "custom" means use `describe`
describeNoyour own words: added to the look, or the custom choice
aspectRatioNoone of its listed shapes
make_explainerMake an explainer videoAInspect

AI HOST EPISODE: format 'host_episode' = one presenter talking to camera, the camera changing every piece (frontal, three-quarter, close), the same host and set throughout, native voice, 16:9. Host = creator (saved or preset) or hostImage; words = topic (written for you) or script (verbatim). It returns a 480p DRAFT; HD (720p) is a SEPARATE call with fromDraft (quote it with dryRun, run it only when the user asks). Otherwise: turn a TOPIC into a finished narrated explainer video, in one of TWO LANES (lane). 'blocks' (the default) = 10-second VIDEO blocks, one narrated line per block, hard cuts, a music bed under the voice: an EXPLAINER renders on Gemini Omni at 720p (9:16 by default, or 16:9), a FACELESS CHANNEL video (format:'faceless_channel', or channel history / kids / fairytale) on MiniMax H3 at 2K (16:9 by default) with five cuts per block. 'stills' = a picture film: writes a sectioned script, paints a BURST of pictures per section (about one every 1.5s — most of them one-detail edits of the frame before, so it reads as movement rather than a slideshow), narrates each section with TTS, holds each picture PERFECTLY STILL for its own slice of the narration (the motion is the CUT RATE — a slow move on a still shimmers), then composites the end card (and any on-screen text you asked for) with the Chrome+ffmpeg engine the ads use (text is never model-painted, so it never garbles). BURNED ON-SCREEN TEXT IS OFF BY DEFAULT — the narration carries the point and the pictures carry the story, so the film ships clean unless the user asks otherwise; captions:true adds held key points and subtitles:true adds subtitles timed to the narration (see both). The stills lane is an image film WITH motion, not N video-model renders — that's what keeps it affordable. style picks the visual family: the default 'cinematic' is photoreal editorial; every other id is a STYLED, strictly non-photoreal look (illustrated / collage / clay / pixel …) that first renders ONE style-key image and then locks every scene to it, so the whole film holds one look. Cost at the default frame density: a ~130-credit hold for a 60s explainer on the default style, ~100 styled; frameDensity:'lean' roughly halves it and 'minimal' (one picture per section) is ~30. All settle to the exact per-frame image + narration spend (a longer target = more sections = more). Takes SEVERAL minutes — one image render per frame; independent frames are painted concurrently, so it is far faster than the frame count suggests. Needs the writing model and a narration voice engine connected. NOT the tool for a short product ad — use render_ad or generate_video for those, and make_template_ad for the deterministic native formats.

NameRequiredDescriptionDefault
laneNo'blocks' (default) = 10-second video blocks, real motion, one narrated line per block; 'stills' = the picture film (a still about every 1.5s, narrated, no video model; cheaper). Quote either with dryRun.
musicNomusic bed under the narration, measured to sit about 14 dB under the voice and sidechain-ducked beneath it. Omit and the KIDS and FAIRYTALE channels get their recommended bed COMPOSED for this film — those two are the only channels a bed is due on unasked, and it costs a small flat fee; every other channel ships dry. 'off' forces silence. 'library' takes a free curated track only, and ships dry when none is on file. NAME A MOOD (upbeat / calm / warm / epic / tense / playful / elegant / hype / chill / dramatic) or DESCRIBE it in words to compose one on ANY channel, at the same fee. hermoso_capabilities reports the exact figure as explainerMusicCredits; quote it before you turn a bed on or pick a mood.
styleNovisual style: 'cinematic' (default, photoreal); styled shortcuts editorial_collage, flat_vector, stickman, whiteboard, ink_marker, silhouette, storybook, paper_diorama, isometric, claymation, pixel_art, watercolor, fluffy_toy, low_poly, stylized_3d, studio_3d (the Kids default), mannequin; or ANY look described in words ('80s anime cel animation'), locked across every frame. Ask rather than pick silently; a styled look costs more.
topicNowhat the explainer should teach or explain — a topic or a short brief (host_episode: or pass `script`)
voiceNonarration voice name — omit for the default warm read
dryRunNotrue = return the estimated credits this explainer reserves (its own pricing, stopped at the hold) and render nothing. Quote it before running one; try frameDensity lean or minimal when the balance is short.
formatNoblocks lane: 'explainer' (default; Gemini Omni 720p, 16:9 or 9:16, runs exactly the length asked) or 'faceless_channel' (a YouTube/TikTok faceless channel video; MiniMax H3 at 2K, 16:9 by default, five hard cuts per 10s block, runs exactly the length asked: a last part-block is rendered whole and cut). Omit and a history / kids / fairytale channel is a faceless channel video. 'host_episode' = the AI host episode (see the top).
scriptNohost_episode: the exact words, said verbatim and split at natural breaks into 4-30s pieces
camerasNohost_episode: the rotation, ids frontal / three_quarter / close or framings in words; one entry = one fixed camera
channelNothe CHANNEL TYPE — it sets the pacing, the narration register and the default look, and is orthogonal to `style` (a named style always wins): explainer (casual second-person, fast cuts), history (witty chronological retelling / documentary), kids (fastest, question-first, warm teacher), fairytale (slow, atmospheric myth or folklore). Default 'explainer'.
creatorNohost_episode: the host, a saved creator or a preset by name or id (list_creators)
endCardNoappend the branded end card. DEFAULT FALSE — set true ONLY when the user asks for one
settingNohost_episode: the set in words (default: written to fit the topic)
upscaleNooptional FINAL upscale — 2 doubles each side, 4 quadruples. Captions and the end card are burned BEFORE it so they upscale with the frame. It is priced BY LENGTH and it is the expensive part — several times the cost of rendering the film itself. hermoso_capabilities reports the exact figures per length as explainerUpscaleCredits. Never turn it on unasked: quote the number and let the user choose.
captionsNoturn ON-SCREEN TEXT on. DEFAULT FALSE, and leave it false unless the user asks — the narration already says the point and the pictures carry it, so the clean film is the better default. `captions:true` on its own burns SUBTITLES (see below), because that is what a caption is for: showing what is being said when the phone is on mute. Slim white text (CAPS in the stills lane), thin black outline, bottom safe band, no plate, no box.
fromFilmNoA PRESENTER ON A FINISHED FILM (only when the user asks for one): the job id of a finished video-blocks explainer or faceless-channel film. With `presenter`, that film comes back with a presenter speaking each block's line on screen; every block keeps its own picture and the film without the presenter is kept (cleanVideo). Never add a presenter unasked.
brandNameNobrand name for the end card — omit to leave it unbranded
fromDraftNohost_episode: a finished draft's job id, to render it in HD (720p) with the same script, cameras, set and host
hostImageNohost_episode: a photo URL of the host instead (upload_file for a local file); a real person's face needs a paid plan
presenterNowith fromFilm: who presents and how. A face the video model flags stops it, nothing charged, with the choices to offer.
subtitlesNowhich on-screen text, once `captions` is on. LEAVE IT UNSET (or true) for SUBTITLES — every spoken word, in order, timed to the narration; no extra render, and there is NO cue limit, so the whole film is subtitled however long it runs (at most 4 words and one line a cue; 5 words / 32 characters in stills). Set it FALSE only if the user explicitly wants section HEADINGS instead: one short summary label held over each ~7-15s section. That is NOT what is being said — it is a label about it — so it is the wrong answer to "add captions" and to anyone watching on mute. `subtitles:true` also implies `captions:true`. TIMING: in the default blocks lane each cue is timed to its own spoken words (one listen to the narration, a tiny charge inside the quote; per-block timing, said, if it fails). In the stills lane cues are spread over each section’s measured narration by character count, exact at section edges, a few tenths off within one: it is not a word-level speech clock, so never promise frame-accurate sync there.
thumbnailNohost_episode: one thumbnail of the host (default true)
aspectRatioNo'9:16' default (a faceless channel video and a host episode default to 16:9)
frameDensityNohow many pictures per second of narration, and therefore what it costs. 'standard' (default) is a frame about every 1.5s — the density a stills film needs to read as a film rather than a slideshow; 'lean' is one about every 2.5s (the longest hold that still reads as a film, ~40% of the frames and ~40% of the cost); 'minimal' is one picture about every 3.5s, the cheapest and the longest any still is ever held, and it reads close to a slideshow. Only drop below the default if the user asked for something cheaper.
durationSecondsNotarget length 20-120s (default 60); drives the section count — ~10s of narration each, 3-8 sections
make_insertMake an insert clipBInspect

A reaction picture (image, or video from videoStart) with its sound, cut to when the sound lands (or seconds): a 1080x1920 clip for post_edit join.

NameRequiredDescriptionDefault
imageNostill picture URL; use this or video
soundYessound URL (find_sound returns one), or a video whose own audio is used
videoNovideo URL to take the picture from, starting at videoStart
secondsNofixed insert length, 0.2 to 6 seconds, instead of when the sound lands
soundEndNoseconds into the sound where it ends
soundStartNoseconds into the sound where the insert's sound starts
videoStartNoseconds into the video where the insert starts
make_motion_filmMake a motion graphics videoAInspect

MOTION GRAPHICS and ANIMATED VIDEO in one call: kinetic typography (animated text), a logo animation or intro, a product launch film, an animated social cut. MOVING PICTURES are the default for launch, product and social: in each scene with a picture, one thing physically happens (a seed splits open, an eye opens, the view travels through a forest), filmed by an image-to-video model, with the type, transitions and the music's beat put on top; a few minutes, priced per shot, and with no pictures given each scene paints its own. moving:false, and always for type and logo, makes it in code with no video model: type, pictures and drawn graphics filmed by a moving virtual camera, a 1080p MP4 in about a minute, billed for its compute only. Pick it for 'animated video', 'motion graphics', 'kinetic text', 'text animation', 'logo animation', 'logo intro', 'animated explainer' asks. recipe: 'type' (every line its own animated card, no picture needed), 'logo' (the logo assembles out of dots and settles, the name and tagline land under it), 'launch' (30s 16:9), 'product' (15s), 'social' (15s 9:16). Or spec, the whole film as JSON (help:true returns the language). STEPS: without confirm it is a FREE quote that gives both prices (length, the music's tempo; the spec in structuredContent): say both; preview:true is a 1-credit contact sheet of the still version; confirm:true renders. You may write moves yourself, or the film's own planner writes them for a few credits. Only the user's or the brand's words appear: never invent a claim, number or review. projectUrl = a past film's url returns its saved spec to edit and render again.

NameRequiredDescriptionDefault
helpNotrue returns the spec language and recipes; renders nothing
hookNoopening line; '*word*' = italic accent
logoNologo image: https, upload_file URL or Library id; default the brand's
moodNomusic and pace
nameNodefault: the saved brand's
sizeNoframe shape: 16:9, 9:16, 1:1 or 4:5
specNothe whole film as JSON, from a quote or a saved film; help:true shows the spec language
briefNowhat the film is about, for the planner
linesNomore short lines, in order
modelNoimage-to-video model id from hermoso_capabilities; default is the best value for short shots
movesNoper scene, what happens in its picture; omit and the planner writes them
musicNo'off' = silent
themeNologo: dark for a light logo
energyNohow much the film keeps moving, 0 to 1; 0 films exactly what is written
imagesNopictures, hero first (https, upload_file URL or Library id)
movingNotrue: one thing physically happens in each picture, filmed by a video model. false: stills moved by a virtual camera, far cheaper. Default true for launch, product, social
recipeNotype, logo, launch, product or social; help:true lists the recipes
confirmNotrue renders the film for real credits; without it you get a free quote
previewNotrue renders a 1-credit contact sheet to approve before the full render
secondsNologo 4-10, others 6-60
taglineNoline shown under the headline or logo
projectUrlNoa past film's url; returns its saved spec to edit and render again
resolutionNothe moving shots: 720p or 1080p (default) where the model has them
make_seriesMake a series of faceless short videos (Content Factory)AInspect

CONTENT FACTORY: turn ONE topic into a SERIES of faceless short videos (narration over pictures, no presenter), optionally in the STYLE of a trending reference video. Give topic ("make a video about…"), episodes (1-10, default 1) or your own episodeTitles, durationSeconds (20-120 per episode, default 30) and optionally reference (a TikTok / Reel / YouTube Short / Facebook / X link or a video file URL whose look, pacing and opening hook every episode copies, with plain subtitles only if the reference burns its own; never its words or characters). Find a trending reference with search_tiktok / search_instagram / search_youtube when the user wants one. THREE STEPS, ALWAYS IN THIS ORDER: (1) call with no confirm for a FREE quote of the whole series (per episode and total); (2) optionally planOnly:true to see the episode titles and the style card first (a small planning charge); (3) confirm:true (pass the plan from step 2 to reuse it) queues every episode and returns their job ids; poll get_job. To make MORE of a series later, pass its returned plan as moreOf: same style, no episode repeated, the reference not watched again. Each finished episode can be scheduled with this tool again: pass jobIds plus schedule {channels, at, everyHours, timezone} and it books one post per finished episode, spaced everyHours apart (default 24). Captions follow the reference (on if it burns words on screen) unless captions says otherwise.

NameRequiredDescriptionDefault
planNothe `plan` a planOnly call returned, to run it without planning again
styleNooverride the look in words (e.g. "flat 2D cartoon") instead of the reference's
topicNowhat the series is about, in the user's words
voiceNonarrator: 'female' or 'male' (default follows the reference), or any narration voice name make_explainer takes
formatNorenderer lane: 'explainer' (default; exact length) or 'faceless_channel' (2K, whole 10s blocks)
jobIdsNoSCHEDULE MODE: the episode job ids to schedule (finished ones are booked)
moreOfNoMORE EPISODES: the `plan` an earlier call returned. Keeps its style and length, writes new episodes only
confirmNoREQUIRED true to start rendering (real spend). Without it you get a free quote
captionsNoburn subtitles of every spoken word. Default: on only when the reference itself burns its spoken words as subtitles, otherwise off; nothing else is ever put on screen (no app screens, URLs, cards or stickers) unless the user asks
episodesNohow many episodes, 1-10 (default 1)
planOnlyNowrite the episode plan and style card, render nothing (a small planning charge)
scheduleNoSCHEDULE MODE: where and when the first episode posts; the rest follow every `everyHours` (default 24)
referenceNoa video link whose style every episode copies
aspectRatioNodefault: the reference's shape, else 9:16
postCaptionsNoSCHEDULE MODE: one post caption per episode, in order (default: the episode title)
episodeTitlesNoyour own episode titles, in order (sets the episode count)
durationSecondsNolength of EACH episode, 20-120 seconds (default 30)
make_template_adMake template adAInspect

Use this when the user asks for a quick, low-cost ad or post built from their words and pictures: a slideshow, carousel, iMessage or ChatGPT chat, Notes, kinetic type, myth vs fact, an app tour, or their own HTML design. Rendered from HTML, no AI model, ~30s, billed for its compute only, ready to post. Presets are SHORTCUTS; 'custom' is YOUR OWN design as config.html (+ css), so no layout, type, colour or motion is unsupported. custom: { html, css?, size? ('9:16' default | '4:5' | '1:1' | '16:9' | any 'W:H' | {w,h} px), durationSeconds? (1-60 = VIDEO; CSS/SVG animation and are frame-stepped, scripts stripped), slides?:[{html, css?}] (2-35 = carousel) }; {{logo}} {{brandName}} {{domain}} {{accent}} fill from the brand; images and fonts load by public https URL; notes[] lists what failed to load. YOU write preset copy: short, casual, believable, finished phrases within budget. Preset ids and their fields are listed on config. config.music on a VIDEO: omit for a free library bed matched to its mood when the library is stocked (hermoso_capabilities hasMusic; else only its own sound effects), 'off' for silence, or words (a mood or description) to compose one for a flat fee.

NameRequiredDescriptionDefault
configYesMUST include config.template: 'custom' or a preset id, plus its fields; language ('Portuguese') for its own words. PRESETS: 'slideshow' (IMAGES, TikTok photo mode / Reels 1080x1920, or size:'4:5' feed carousels; no branding): { slides:[{text, sub?, image?, blur?, background?, position?}] (2-35; words never rewritten), style? ('tiktok-classic'|'clean-minimal'|'note-style' or a look in words), textStyle?, video?:true (+ an MP4, which takes config.music like any video) }; 2 credits, +1 per slide past 5, +2 for the MP4. 'imessage-chat' (VIDEO ~15s): { thread:{contactName, messages:[{from:'them'|'me', text?, product?:{image,title,domain}}]}, theme?, endCard }. 'chatgpt-chat' (VIDEO): { question, answer (may **bold** the brand), productImage?, endCard }. 'apple-notes' (VIDEO): { title, lines[], theme?, endCard }. 'value-prop' (VIDEO ~17s): { hook ≤40ch, claims[3-5 ≤34ch], productImages[2-3], palette[], endCard }. 'static-mockup' (IMAGE): { style:'imessage'|'notes'|'card', size?:{w,h}, ...fields }. 'airdrop-carousel' (VIDEO): { brandName, products:[{image, title?}] (3-16), endCard }. 'app-ui-tour' (VIDEO): { hook?, appName, iconImage?, beats:[{screenImage, caption}] (2-6), endCard }. 'imessage-cascade' (VIDEO): { notifications:[{sender, text}] (4-8), backgroundImage?, endCard }. 'photo-grid' (VIDEO): { title?, photos:[{image, label?}] (4-9), endCard }. 'vignette' (VIDEO): { hook, lines[2-4 ≤40ch], heroImage, endCard }. 'kinetic-type' (VIDEO, own SFX): { phrases[3-6 ≤34ch], productImages?[≤4], endCard }. 'myth-vs-fact' (VIDEO with a real VOICEOVER, small extra charge): { pairs:[{myth ≤50ch, fact ≤60ch}] (2-4; [brackets] accent), endCard }, real truths only. 'carousel' (IMAGES, 5-10 branded 1080x1080): { cover:{hook?, title}, slides:[{headline, support?, stat?:{value, label}}] (3-8), cta:{headline, cta?, domain?}, productImage?, logo? }. endCard = { headline, cta, domain?, logo?, color? } is OPTIONAL on every video preset: include it ONLY when the user asked for an end card (a closing card with their logo); without it the video ends on its own last frame. palette and fontStack optional.
make_thumbnailMake video thumbnailAInspect

Render a click-driving YOUTUBE / Shorts / Instagram THUMBNAIL or video cover through the full production pipeline (concept, casting, scene, render, tweaks, text), not a bare image prompt. Use it for any "thumbnail", "video cover" or MrBeast-style packaging ask INSTEAD of generate_image. About 9 credits per variant; the headline overlay is free.

CONCEPT — open an INFORMATION GAP (the image raises a question the title answers) while staying truthful to the video. Brainstorm ≥5 concepts across the 16 frameworks (ids on framework; combining two is fine) before you pick; hermoso_capabilities has each one's 'realize it with' note and the emotion, overlay, font and rim-colour catalogs.

THREE GATES, all BEFORE you render:

  1. WHO IS IN FRAME — never assume or silently substitute a stranger. A framework with a person and no face photo is refused (nothing charged): ask the user once — themselves (a face photo, identity-locked), a generated person (castGenericPerson:true), or a people-free framework.

  2. TEXT — default is a CLEAN render with the headline TYPESET over it (free, legible, correctly spelled): pass headline. bakeText:true only on an explicit ask for words painted INTO the image. Never infer text intent from the topic.

  3. HOW MANY — ask once: one, or a SET (offer 4: one concept at different emotions / camera takes). Default 1; variants caps at 16.

emotion is the biggest CTR lever on a face (identity lock is automatic for every face photo). To fix a finished one, re-call with tweak + sourceImage for a surgical edit (emotion / background / background_color / rim_light) — tweaks chain. ALWAYS check the returned postRenderCheck against the image before presenting it.

PROMPT LANGUAGE — write every DESCRIPTIVE field (sceneBrief, keyElements, location, composition, background, topic, each person's describe, every reference) in ENGLISH, translating the user's words: the models render English better. headline, headlineLines and bakedUiText stay verbatim in the user's language.

NameRequiredDescriptionDefault
fontNoheadline font: Anton (default) or any Google Fonts family
logoNoa brand logo URL or path to place into the composition
splitNosplit/panel LAYOUT — only when the user asks for one ("split", "before/after", "versus screen"). "X vs Y" as a SCENE stays one unified frame
takesNocamera takes per emotion, 1–4: designed framing / low-angle hero / extreme close-up / wide dutch tilt
topicNothe video's topic — used to pick the hero object when you don't name keyElements
tweakNosurgical pixel-faithful edit of a FINISHED thumbnail (needs sourceImage): kind emotion / background / background_color / rim_light, or any other kind with the edit in words as value
logo3dNofirst turn the flat logo into a volumetric 3D render (one extra billed image), then composite that
peopleNopeople described in prose instead of by photo (each still gets the chosen expression)
emotionNothe expression on the face (default 'shock') — shock · hype · fear · confusion · determination · smug · charisma · disgust · awe · rage · laugh, or your own phrase
bakeTextNodefault false. true paints the headline INTO the generation — only on an explicit user ask; it leaks garbled text elsewhere in the frame
emotionsNorender one variant per emotion (variants = emotions × takes, max 16)
headlineNo2–4 word headline. Typeset OVER the finished render by default (free, always legible); newlines split it into stacked lines
locationNoplace, time of day, weather, atmosphere
rimColorNocolored back+hair light — ONLY when the user names one: 'ice-blue' / 'neon-magenta' / 'toxic-lime' / 'amber-gold' / 'pure-white'
variantsNohow many thumbnails to render (default 1, max 16). Each is its own billed render — offer a set of 4 rather than assuming
frameworkNoconcept framework id (default 'posed_portrait') — before_after · social_ui · three_step · screenshot · posed_portrait · posed_action · specific_day · graphical · landscape · map_aerial · product · adding_text · repetition · size_difference · news_clip · amplified_reality — or your own concept in words
referenceNofields YOU extracted by eye from a reference thumbnail. Extract ALL of: brief (one dense sentence on the concept), subject (pose/action generically, NEVER a specific identity), elements, location, composition, background, split (boolean), split_count, person_count (0-3), emotion (one of the 11 presets or 'other'), emotion_detail (one vivid sentence covering eyes, brows, mouth, head angle). emotion + emotion_detail carry the reference's actual facial performance, which is the single biggest CTR lever on a face; split/split_count reproduce its panel structure. The reference image itself is never sent to the model
backgroundNooverride the default bold saturated colour-field background
faceImagesNoup to 3 face photos (URLs or local paths) — each becomes a locked CHARACTER identity, in order
sceneBriefNowhat the thumbnail depicts — the concept in one dense sentence, rendered exactly
aspectRatioNo'16:9' (YouTube, default) / '9:16' (Shorts) / '4:5' (Instagram) / '4:3' / '1:1'
bakedUiTextNoshort label for a text-carrying framework (a chat bubble, a DAY N badge, a news lower-third, a map callout) — needs frameworkRequested:true
compositionNooverride the default large-foreground-subject composition
keyElementsNosignature props / effects that make it pop — oversized, flying toward camera
sourceImageNothe finished thumbnail URL a `tweak` edits; tweaks chain, so feed each accepted output into the next
overlayStyleNoheadline style: beast (default), fire, neon-lime, clean-glass, marker, or your own CSS declarations
forceGenerateNorender the 'screenshot' framework anyway (it is normally a real video frame, not a generation)
headlineLinesNoexplicit headline lines (up to 3) — overrides splitting `headline` on newlines
headlinePlaceNobottom (default), top, center, or a 0-1 fraction from the top; never over the face
logoPlacementNooverlay (exact, flat) | in_scene (the real file printed onto a blank panel in the shot; with logo3d, a 3D render of it, checked); auto: in_scene with logo3d
restrainedGradeNotrue for a calm / premium / muted look instead of the default punchy poster grade
castGenericPersonNopass true only after the user has explicitly chosen a generated stranger over their own face
frameworkRequestedNotrue ONLY when the USER named this framework — it is what authorizes a text-carrying framework (social_ui / news_clip / specific_day / map_aerial) to bake its short UI label
manage_sheet_tabsAdd, rename or delete a sheet tabA
Destructive
Inspect

Add, rename or delete a tab in a Google Spreadsheet. action:"add" + title · action:"rename" + tab + newTitle · action:"delete" + tab. Name the tab by its TITLE or its numeric sheetId (list_sheet_tabs gives both); an unknown tab is refused with the real list rather than a Google error nobody can map back. DELETING a tab destroys everything on it: call it without confirm first to get the filled-cell count, then confirm:true + confirmCells. Google does not allow removing the LAST remaining tab in a file, and that is refused by name with the way out (clear it, or delete the whole file with delete_drive_file). Every action is read back from the spreadsheet before it is reported as done.

NameRequiredDescriptionDefault
tabNowhich tab — its title or numeric sheetId (rename / delete)
titleNothe name for the new tab (action:"add")
actionYesadd, rename or delete a tab
confirmNotrue deletes a tab that holds data; needs confirmCells
newTitleNowhat to rename the tab to (action:"rename")
sheetUrlNoGoogle Sheets link, instead of spreadsheetId
confirmCellsNothe filled-cell count an unconfirmed delete reported
spreadsheetIdNoGoogle spreadsheet id; use this or sheetUrl
mine_anglesMine customer anglesAInspect

Mine ad ANGLES from real customer language: gathers the customer's own words (Reddit, TikTok, the brand's review page + review-site results) and returns a RANKED angle bank — each angle tagged (pain / outcome / identity / fear / competitive-displacement / social-proof / contrast), 2-5 VERBATIM proof quotes, a 0-100 score with breakdown, and a ready-to-run hook in the customer's own voice. Reads YOUR saved brand (pass brandId to target a specific profile — that switches this key's active profile like use_brand). To tear down a COMPETITOR use competitor_teardown instead. YOUR OWN REVIEWS: pass reviews (a list of review texts, or one pasted block: one per line, numbered, blank-line separated, or a CSV with a review column) and/or reviewsUrl (a CSV, TXT or JSON file from upload_file, or a review page; on a local CLI a file path works too). They are first-class evidence: every quote from them is checked word for word against what you sent and labelled 'your reviews', and a quote that is not verbatim is dropped and counted. useOwnReviewsOnly:true mines only your reviews and gathers nothing public. Limits: 300 reviews, 2,000 characters each, 40,000 in total; over that it is refused at no cost, so send fewer or split into batches. Each angle comes back with next: the exact plan_variations and generate_image arguments that turn it into finished statics (one generate_image per angle = statics with distinct angles). Spends credits on the reads and the model's token cost.

NameRequiredDescriptionDefault
brandIdNoa profile id/name from list_brands to mine for; omit to use the active profile
reviewsNoyour own customer reviews: a list of review texts, or one pasted block (one per line, numbered, blank-line separated, or CSV with a review column)
reviewsUrlNoa URL of your reviews: an uploaded CSV, TXT or JSON file (from upload_file) or a review page. On a local CLI a file path also works.
useOwnReviewsOnlyNotrue = mine only your reviews, no public search (no search credits). Default false = merge with public customer language.
multiply_adMultiply an adAInspect

MULTIPLY a winning video ad into N variants: each gets a NEW character, outfit, location and/or objects while the cut, the camera motion, the pacing and the ORIGINAL AUDIO stay exactly as they were (that is what made the ad work), and any burned-in captions are removed. Pass the source video URL (a previous render, a job result, list_library, or the top performer from post_performance / meta_insights). HOW: the source video itself DRIVES each variant (motion transfer from one image-edited opening frame), so every variant comes back the SAME LENGTH as the source with the same cut, the same performance and the original audio — only the person, outfit, set and props change. Sources up to 30 seconds work as they are; longer ones are refused for free with the way out (trim it first: post_edit with ops [{op:'trim', start:0, end:30}] — clip_video is the AI highlight clipper, not a trim). Returns the plan and ONE JOB PER VARIANT — call get_job on each until it reports done; do not describe a variant before its URL arrives. COST: quoteOnly:true returns the exact credits free: state that first. A paid planning read (planCredits) runs next, then each variant bills perVariantCredits. dryRun:true pays for that read; pass its plan back to render without paying again. Your own pictures: images takes up to 9 of the user's product, people, clothes or places, and change names them @Image1, @Image2 in order ('she holds the bottle in @Image1'); the saved brand's product photo is read as a reference to its real look (useBrand:false leaves it out). A TikTok, Reel, Facebook, X or YouTube post link works as the source: one paid lookup reads it into a file. quality: 1080p (default) or 720p; quoteOnly prices both. Regions: pass regions:['Berlin','Tokyo'] to restyle variants per market; translation is a separate, explicit step — dub_video on a finished variant. change is what should change, in the user's own words ('older women', 'winter streets', 'swap the mug for our bottle'): every variant applies it and the variants still differ around it; a part of it about the voice, words, language, music, length or captions cannot change here, and the reply says so and names the tool that can.

NameRequiredDescriptionDefault
axesNowhat to vary: character, outfit, location, objects (default all four), or your own, e.g. "season"
planNothe `plan` a dryRun returned, rendered without planning again
countNohow many variants, 1-12 (default 6)
notesNoanything the variants must respect, e.g. "keep it women 25-40", "no gyms"
videoYesthe source video: a file URL, or a TikTok, Reel, Facebook, X or YouTube post link
changeNowhat should change, in the user's own words, e.g. 'older women, winter streets'; every variant applies it (omit to let the variants vary freely)
dryRunNotrue = the paid plan and the price, no render
imagesNoup to 9 pictures to put in the variants, in the order @Image1, @Image2 name them: URLs or local paths
qualityNo1080p is the default; 720p costs less, and hands holding a product are less reliable at 720p
regionsNomarkets to restyle for, one or more variants each, e.g. ["Berlin","Tokyo","São Paulo"] — visuals only; audio is never translated here
useBrandNofalse = send no brand product photo (for a video that is not this workspace brand)
quoteOnlyNotrue = the exact credits, free; nothing runs
onedrive_file_permissionsWho can open a OneDrive fileA
DestructiveIdempotent
Inspect

action list (default): every sharing link and person with access to a OneDrive file or folder: link URL, view or edit, scope, expiry, password, whether it is inherited from a parent folder. action remove: revoke one (permissionId from the list); anyone relying on it loses access at once and the link cannot be restored, so WITHOUT confirm:true nothing is removed and you get what would go. An inherited permission is removed on the parent folder, and the owner's own access cannot be removed. Removal is verified by reading the list back. 0 credits. Needs OneDrive connected.

NameRequiredDescriptionDefault
actionNolist (default) or remove
fileIdYesOneDrive file or folder id
confirmNorequired true for remove
permissionIdNofor remove
plan_adPlan an ad conceptAInspect

Plan an ad: turns a brand and a product or brief into an ad concept, with copy variants (headline, primary text, call to action) and either an image prompt or a timed video storyboard, plus the recipe and the model ids to render with. It renders nothing; pass its output to render_ad, generate_image or generate_video. When the user names a length ("a 30 second ad", "make it 45s"), pass it as durationSeconds: the storyboard is written to that length and render_ad renders it as one clip or as joined clips. Without it the planner chooses a length that suits the format, so a length the user asked for belongs in durationSeconds rather than only in the brief. The user’s own screenshots or pictures to show inside the video go in inserts. Uses credits for the planning step.

NameRequiredDescriptionDefault
hookNoforce the VISUAL scroll-stop mechanic the opening beat is built on — a hook id from list_hooks (e.g. "direct_callout", "mid_problem", "macro_asmr"). Omit to let the planner pick. A hook that cannot be delivered in this brief is DROPPED with the reason rather than rendered wrongly — an on-screen-text hook on an authentic/UGC ad is the one that bites, because that register carries zero on-screen text.
brandNobrand name, or a brand profile object {name,domain,category,palette,products,…}. OMIT to use the workspace’s SAVED brand + memory automatically (see get_brand); use draft_brand to onboard a new one
draftNoOnly after a video refusal that offered a lighter draft: the {model, durationSeconds} that refusal named. The plan is then written to that length and priced on that model. Use only the values the refusal gave; when an account cannot cover a video, the refusal comes before planning with three options (an image, adding credits, or this draft when one fits) and the user chooses.
formatNo'image', 'video', or 'auto' when unspecified
recipeNoa recipe id from hermoso_capabilities to force an archetype
talentNowho is on camera; omit to follow the format. Say 'someone new' in product to skip reusing a saved creator.
insertsNoVIDEO ONLY: the user's own SCREENS cut in at these seconds (each item's look, kind, start and end say how), the voice running underneath. How many: at most 8, about one per 2.5 s of film (6 s: 2, 10 s: 4, 15 s: 6, 20 s: 8, 30 s: 8), 1.2-2 s each, on the line that talks about it. When there are more, the most crowded are left out first, never the last (the payoff) or one marked key, and the reply names them. The plan keeps those shots free of screens and returns the pictures as creative.inserts for render_ad to lay in (about 2 credits). One that does not fit is left out and the reply says so.
productYeswhat to advertise + any angle/offer the user specified
settingNoforce the WHERE — a setting id from list_hooks (e.g. "kitchen", "gym", or a surreal one like "volcano_rim" / "airplane_wing", which are played 100% straight and never acknowledged). Omit for a neutral setting.
conceptsNoseveral variants: one line per concept (a different angle each); built exactly
languageNooutput language for the ad copy (e.g. Spanish) — default English
referenceNoa reference to clone: an ad-library link (Facebook Ad Library, LinkedIn Ad Library, Google Ads Transparency — its real copy/advertiser are fetched) OR a VIDEO link — a TikTok, Instagram Reel, Facebook video, X post, YouTube Short/video or a direct video file — which is WATCHED first (frames + voiceover/on-screen-text transcript) so the concept keeps its hook, structure and pacing. To remake one video for this brand at its own length, clone_video is the direct tool
durationSecondsNoFor video: the total length the user asked for, in seconds (30 for "a 30 second ad"). The storyboard’s scenes add up to it and the script is sized for it. Supported range 4 to 180 seconds; a value outside the range is clamped and the reply says so. A length that fits one clip of the render model renders as one continuous clip; a longer one is joined from clips filled to that model’s clip maximum, remainder last (on a 15s-clip model, 40 becomes 15+15+10 and 17 becomes 13+4), and is never sped up. The clip maximum depends on the model: 15s on most, 30s on the longest-clip model, so a 30s ad can be one take. Leave it out when the user named no length; the planner then picks one that suits the format.
plan_variationsPlan ad variationsAInspect

Fan a brief into N DISTINCT ad angles (different hooks/mechanics/audiences), each with its own headline + visual brief — then render each with generate_image and rank with score_ad. LLM planning only; renders nothing itself. To plan AND render a batch of finished static ads for one or several products in one call (up to 20 per product), use make_static_ads.

NameRequiredDescriptionDefault
brandNobrand name or profile object; OMIT to use the workspace’s saved brand
countNohow many distinct variants (default 6)
productYeswhat to advertise
languageNooutput language for the variant copy (e.g. Spanish) — default English
plan_weekPlan a week of postsAInspect

Use this when the user wants a week of social posts planned, or answers yes or no to planning their week. It drafts one batch of 5 posts for the active brand: a new image, a caption and a suggested day and time for each. Without confirm it returns the price and whether the batch fits the account’s posting allowance, and spends nothing. confirm:true starts the batch and bills credits; read the posts with get_job. The posts stay drafts until the user approves them with run_post_refill, and this tool publishes nothing. decline:true records that the user said no.

NameRequiredDescriptionDefault
confirmNotrue only after the user agreed to the price
declineNotrue when the user said no
timezoneNothe user’s IANA timezone when known, e.g. America/New_York; the suggested times use it
post_editPost-production editAInspect

MECHANICAL post-production on an EXISTING video (URL): ordered primitives run by ffmpeg in seconds, ~2 credits flat, NO AI model, as a NEW video. Ops: a branded end card (adds its seconds), trim, speed (0.5-2x), mute (whole or a window), audio_gain (-20..+6 dB), fade_out, music (a bed UNDER the clip, its own sound kept and ducked under: omit track = a free library track picked by mood, no attribution; or track = an audio link: an upload_file URL, a find_sound or generate_sfx link, a video post's sound; track 'generate' composes it, paid, ONLY when the user asks; start/end, db, replace), voiceover (text spoken OVER it, its sound ducked; ~2.5 words/s; voice female|male; +~1 credit), watermark (brand logo), grain (anti-AI), text (timed words in a native look: style 'tiktok-classic' default / 'clean-minimal' / 'note-style' or a textStyle; start/end, or at:{word} and until:{word} to time them to a spoken phrase, refused when it is never said), join (this video FOLLOWED BY clips[]: Library URLs, direct files or public post links, as one 1080x1920 video, loudness matched). Presets are shortcuts: text/watermark take any x/y, grain any amount, join any ffmpeg transition, a bridge any sound link. 'A viral hook, then our clip' = videoUrl: the hook's post link + [{op:'join', clips:[{url: ours}], bridge}], and it ALWAYS gets a bridge unless the user asks for a bare cut: {kind:'impact'} cuts the hook just before its payoff (found from the footage; cutAt overrides) and lands our clip on a punch-in and flash, with the payoff sound FROM THE HOOK ITSELF: its own audio carries across the cut, else one generated from its frames (up to 8 credits), else a neutral impact. {kind:'text', text, then?} only when the user asks for words over the cut. No voiceover bridge: have our clip's host say the connecting line. matchCut = where our clip starts. ANY OTHER EDIT, op or bridge (whip, zoom, freeze, wipe, split screen, generated shot) is edit_timeline: free keyframes, any size. NEVER generate_video/render_ad for these. Feedback maps to one named move, checked by reading the parameters back, never by re-rendering: quieter/louder (music db, audio_gain), faster/slower (speed; slower = zooms and pans at 0.7x), tighten (trim heads, tails, silences), retime (to a word anchor), recut (order, in-out points), regrade (timeline exposure/contrast/saturation keys).

NameRequiredDescriptionDefault
opsYesthe ordered edit plan (max 6 ops)
accentNooverride the brand accent hex
domainNooverride the brand website
dryRunNotrue = return the estimated credits this edit reserves and run nothing
videoUrlYesthe video to edit: a render / Library URL, a direct file, or a public post link; a URL, not a local file path; use upload_file for local files
brandNameNooverride the workspace brand name
post_performanceWhich hooks and subjects are getting tractionA
Read-onlyIdempotent
Inspect

Aggregate this brand's published posts to answer WHICH HOOKS AND SUBJECTS WORK. Groups by hook (default), subject, format (recipe), channel, media format or posting hour, reports the engagement RATE within each channel, and ranks the best and worst POSTS in each channel. Describe a post by the creative it carried (what it shows, its format, its link), not by its caption — the caption is the least important part of a post. THREE THINGS IT DELIBERATELY WILL NOT DO, and you should repeat them rather than paper over them: (1) it never sums metrics across channels — a LinkedIn impression and a TikTok view are different units, so every comparison is within one channel; (2) it SUPPRESSES a verdict below 5 measured posts and says so, because a confident recommendation from 3 posts is worse than none; (3) a post with no recorded hook (published outside Hermoso, or backfilled without a creation match) counts toward channel and format totals but never votes on which hook works. Present the finding verbatim if there is one, and the reason if there is not. Read-only, 0 credits.

NameRequiredDescriptionDefault
axisNowhat to group by — default hook; recipe = the format of the creative
daysNolook back N days (1-730) over the whole history; omit for the recent posts only
brandNoWHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done.
channelNorestrict to one channel
post_to_blueskyPost to BlueskyA
Destructive
Inspect

Use this when the user wants to post to Bluesky. Publishing costs no credits. Publish a post to Bluesky as the connected account. Text up to 300 characters — Bluesky ALSO caps a post at 3000 UTF-8 bytes, so an emoji-heavy post can be under 300 characters and still be refused; Hermoso checks both before spending the round trip and says which limit and by how much. @MENTIONS written as the full handle (@alice.bsky.social) are looked up and tagged as Bluesky mentions; one that does not resolve stays plain text and the reply names it. MEDIA: either up to 10 images (imageUrls + altText; 5–10 post as a Bluesky gallery, more than 10 is refused, never cut; a picture over Bluesky's 2 MB limit is re-encoded to fit and the reply says so) OR one MP4 video (videoUrl + videoAlt), never both — a Bluesky post record carries a single embed and images and video are two different embed types. Video is MP4 only, up to 300MB at Bluesky's end (Hermoso can fetch up to 150MB from a URL), with optional WebVTT caption tracks; the aspect ratio is measured from the file. Bluesky requires a CONFIRMED EMAIL on the account before it will process any video — if it is unconfirmed you get a refusal saying so, and reconnecting will not help. Links in the text are made clickable automatically. LINK CARDS: Bluesky does NOT scrape links, so a URL posted bare renders as plain blue text — the client composing the post has to build the card. Hermoso builds one AUTOMATICALLY when the post has a URL and NO media: it fetches the page, uses its title/description and uploads its image as the card thumbnail. Pass linkCard:false to suppress it, or linkCard:{uri,title,description,thumbUrl} to control it (give both title and description and the page is not fetched at all). A POST CARRIES ONE EMBED, so a card and images/video cannot both ride: if you pass linkCard explicitly ALONGSIDE media the call is REFUSED by name rather than silently dropping one, and if the URL was merely in the text the MEDIA WINS and the reply says the card was skipped (the link stays clickable either way). Returns the post's public bsky.app URL. Connect at Settings > Connectors > Bluesky.

NameRequiredDescriptionDefault
hookNothe post's angle — a list_hooks id or your own wording, reused exactly
textYesThe post, up to 300 characters / 3000 UTF-8 bytes.
brandNoWHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted.
langsNoBCP-47 language tags, e.g. ['en'].
ideaIdNoshort id of the content-plan idea this post came from
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
accountNoWHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the profile has more than one bluesky account connected (several and none named is refused by name, never guessed); omit when there is one.
altTextNoAlt text — an ARRAY, one per image in the same order, or a single STRING to describe every image with it. WRITE ONE: Bluesky’s own lexicon makes `alt` a REQUIRED property of every image, so a post without it is undescribed by design rather than by omission, and Bluesky users expect it. No maximum length is published, so nothing is truncated.
subjectNowhat the post is about
captionsNoUp to 20 WebVTT caption tracks: [{lang:'en', url:'https://…/en.vtt'}] or [{lang:'en', content:'WEBVTT\n\n00:00…'}]. Each file is capped at 20000 bytes.
linkCardNoRich link card (`app.bsky.embed.external`). OMIT for the default (a card is built automatically when the post has a URL and no media). `false` never builds one. `true` builds one from the first URL in the text. An object {uri,title,description,thumbUrl} overrides any field — supply BOTH title and description and the page is never fetched. Cannot be combined with imageUrls/videoUrl: a post has ONE embed, so an explicit linkCard beside media is refused.
videoAltNoAlt text describing the video, for accessibility.
videoUrlNoOne public MP4 URL. Cannot be combined with imageUrls. Bluesky transcodes it, which takes a minute or two; a URL, not a local file path; use upload_file for local files
imageUrlsNoUp to 10 public image URLs to attach (1–4 a standard image post, 5–10 a Bluesky gallery; more than 10 is refused, never cut). Cannot be combined with videoUrl.
platformCoverNoVIDEO COVER. Bluesky has no cover setting and shows the video’s FIRST frame, so by default Hermoso checks it and, only when that frame is blank (a template ad’s empty opening card), sends Bluesky a copy with the first frame replaced by the video’s best frame. Your Library file is never changed. true = send the file exactly as it is.
allowDuplicateNopost it even though an identical post was just made
idempotencyKeyNoany stable string: a repeat within 24h returns the original post instead of posting again
post_to_google_businessPost to Google Business ProfileA
Destructive
Inspect

Publish a Post to the brand’s Google Business Profile — the panel that appears on Google Search and Maps for the business. Text, optionally ONE PHOTO, and a call-to-action button. Google’s Posts API accepts NO VIDEO, so pass a still image. This PUBLISHES immediately and publicly on the business listing — show the user the exact text, photo and button and get an explicit yes BEFORE calling. If the account manages several listings, call list_business_locations first and pass locationId. EVENT and OFFER posts both REQUIRE a title and a start date (Google’s rule). On an OFFER, Google IGNORES the button’s link — pass redeemOnlineUrl instead. A CALL button dials the number on the listing and takes no link. Needs Google Business Profile connected (Settings > Connectors > Google Business Profile).

NameRequiredDescriptionDefault
hookNoWHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. "direct_callout", "before_after") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works.
linkNothe URL the button opens — not for CALL, and ignored on an OFFER
brandNoWHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted.
titleNoheadline — REQUIRED for EVENT and OFFER
ideaIdNoshort id of the content-plan idea this post came from
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
endDateNoYYYY-MM-DD, defaults to startDate
subjectNoWHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. "winter coat", "free trial"). post_performance's second grouping axis: reuse the exact wording, as with hook.
summaryNothe body text of the Post
imageUrlNoa Hermoso render image URL (or an upload_file url) to show on the Post
startDateNoYYYY-MM-DD — REQUIRED for EVENT and OFFER
topicTypeNodefault STANDARD
actionTypeNothe button on the Post
couponCodeNoOFFER only
locationIdNowhich listing, e.g. 'locations/123' from list_business_locations — only needed when the account manages more than one
languageCodeNoBCP-47 language of the Post, default 'en'
redeemOnlineUrlNoOFFER only — this is the link Google actually uses on an offer
termsConditionsNoOFFER only
post_to_linkedinPublish to LinkedInA
Destructive
Inspect

Use this when the user wants to post to their personal LinkedIn profile. Publishing costs no credits. Publish a post to the user’s connected LinkedIn profile — text, and optionally an image (imageUrl), a VIDEO (videoUrl, with captionsSrt and a videoThumbnailUrl cover), a 2–20 image MULTI-IMAGE post (imageUrls), a LINK post with a real preview card (linkUrl + linkTitle / linkDescription / linkThumbnailUrl — LinkedIn never scrapes the page), a DOCUMENT post (documentUrl: a PDF / PPTX readers swipe through), and @mentions of companies (mentions). The image does NOT have to be something Hermoso generated: LinkedIn is served the bytes from us, so the URL must be Hermoso-hosted, and upload_file turns ANY file the user already has into exactly that. This PUBLISHES immediately and PUBLICLY — ALWAYS show the user the exact text and get an explicit yes BEFORE calling. Needs a connected LinkedIn account (Settings > Connectors > LinkedIn).

NameRequiredDescriptionDefault
hookNoWHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. "direct_callout", "before_after") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works.
textYesthe post text
brandNoWHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted.
titleNovideo title
ideaIdNoshort id of the content-plan idea this post came from
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
altTextNoimage alt text — a string for every image, or an array per slide
linkUrlNoa LINK post with a real preview card. Not with media.
subjectNoWHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. "winter coat", "free trial"). post_performance's second grouping axis: reuse the exact wording, as with hook.
imageUrlNoa Hermoso-hosted image URL to attach (≤12MB) — a Hermoso render, or ANY file of the user’s own put through upload_file first. An arbitrary external host is refused (we fetch the bytes ourselves); a URL, not a local file path; use upload_file for local files
mentionsNoTAG COMPANIES: write "@Name" in the text where each tag goes and list each company here — its vanity name (the part after linkedin.com/company/), its company page URL, or {organizationId, name}. Hermoso resolves it on LinkedIn and LinkedIn renders a real clickable mention; a company with no "@" spot in the text is refused rather than appended.
videoUrlNoa Hermoso-hosted video (a render, or the user’s footage via upload_file). LinkedIn processes it before publishing, which takes a minute; a URL, not a local file path; use upload_file for local files
imageUrlsNoa MULTI-IMAGE post: 2–20 Hermoso-hosted images in order, one post the reader swipes through (LinkedIn’s MultiImage API takes a member author on w_member_social). Images only.
linkTitleNothe card headline — LinkedIn never scrapes the page, so without it the card is unlabelled
visibilityNodefault PUBLIC
captionsSrtNoSRT caption CONTENT for a videoUrl post (English only, attached during upload)
documentUrlNoa DOCUMENT post — a Hermoso-hosted PDF / PPTX / DOCX (upload_file first) that readers swipe through page by page in the feed; LinkedIn takes up to 100MB and 300 pages. One kind of media per post: not with an image, video, slides or linkUrl.
documentTitleNothe title shown on the document (defaults to the file name)
allowDuplicateNopost it even though an identical post was just made or attempted. Only pass this when the user genuinely wants the same thing posted twice, or when you have LOOKED at the account and confirmed a timed-out attempt did not land.
idempotencyKeyNoSAFE RETRIES — any stable string: a repeat of the SAME publish within 24h returns the ORIGINAL post id instead of posting again (an identical publish is also auto-recognised for 10 minutes). On a timeout or an ambiguous error, CALL AGAIN WITH THE SAME KEY: it reports the original post or publishes it once. It never posts twice.
linkDescriptionNolink card description; LinkedIn does not read the page, so set it here
linkThumbnailUrlNoHermoso-hosted image for the card
videoThumbnailUrlNocover image for a videoUrl post (Hermoso-hosted)
post_to_linkedin_pagePublish to a LinkedIn company PageA
Destructive
Inspect

Use this when the user wants to post to a LinkedIn company Page. Publishing costs no credits. Publish a post to one of the user’s LinkedIn COMPANY PAGES — text, plus optionally an image, a video, a 2–20 image CAROUSEL (LinkedIn calls it a MultiImage post; pass the slides in order as imageUrls[]), or a LINK POST with a real preview card (linkUrl). USE linkUrl WHENEVER THE POINT OF THE POST IS A LINK: LinkedIn disables URL scraping for API partners, so a url sitting in the text renders as plain text with no card, and the card’s title, description and image only exist if you pass linkTitle / linkDescription / linkThumbnailUrl — read them off the page and supply them. The media need not be a Hermoso render — it must be Hermoso-HOSTED because we upload the bytes to LinkedIn ourselves, and upload_file turns ANY file the user already has into such a URL. A DOCUMENT post (documentUrl — a PDF/PPTX deck readers swipe through) is LinkedIn’s strongest organic B2B format. Tag companies with @Name in the text plus mentions. This is a DIFFERENT thing from post_to_linkedin, which publishes to the person’s own profile: pick the one the user actually asked for and never substitute. organizationId comes from list_linkedin_pages; omit it only when the account administers exactly one Page. This PUBLISHES immediately and PUBLICLY — ALWAYS show the user the exact text and get an explicit yes BEFORE calling. A VIDEO POST CAN CARRY CAPTIONS AND ITS OWN COVER, and both are attached only during the upload: pass captionsSrt (SubRip content — LinkedIn is watched with the sound off) and videoThumbnailUrl (otherwise LinkedIn picks a frame for you). LinkedIn does NOT allow the image, video, captions or thumbnail of a published post to be swapped afterwards, so get all of that right first (the copy can still be edited with manage_linkedin_post).

NameRequiredDescriptionDefault
hookNoWHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. "direct_callout", "before_after") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works.
textYesthe post text
brandNoWHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted.
titleNovideo title
ideaIdNoshort id of the content-plan idea this post came from
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
altTextNoaccessibility alt text (max 4086 characters, ~120 recommended). A STRING describes every image; an ARRAY describes each slide of a multi-image post separately, in slide order — LinkedIn stores altText per image, and their own sample request carries a different one on each. Also works on a personal-profile post (post_to_linkedin).
linkUrlNopublish a LINK POST — LinkedIn renders a real preview card for this URL instead of leaving a bare link in the text. Mutually exclusive with imageUrl / videoUrl / imageUrls: LinkedIn’s content field is a union, so combining them is refused by name rather than one being dropped.
subjectNoWHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. "winter coat", "free trial"). post_performance's second grouping axis: reuse the exact wording, as with hook.
imageUrlNoa Hermoso-hosted image URL — a render (list_library), or ANY image of the user’s own passed through upload_file first. An arbitrary external host is refused.
mentionsNoTAG COMPANIES: write "@Name" in the text where each tag goes and list each company here — its vanity name (the part after linkedin.com/company/), its company page URL, or {organizationId, name}. Hermoso resolves it on LinkedIn and LinkedIn renders a real clickable mention; a company with no "@" spot in the text is refused rather than appended.
videoUrlNoa Hermoso-hosted video URL — a render, or the user’s own footage via upload_file. LinkedIn processes it before publishing, which takes a minute.
coverAtMsNoTHE VIDEO COVER of a videoUrl post, as ONE frame: milliseconds from the start (7000 = the frame at 7s). That frame is uploaded as LinkedIn’s video thumbnail (only possible while the video uploads). videoThumbnailUrl wins.
imageUrlsNoCAROUSEL — an ORDERED list of image (and, where the channel allows, video) URLs published as ONE post the viewer swipes through. THIS IS NOT “post several” — it is a single post with several slides, which is what a multi-slide creative (a listicle, a “1/6 · SWIPE” deck) actually needs; publishing only its first slide tells the viewer to swipe at something that cannot. The ORDER is the product. Limits per channel: Instagram 2–20 (images, videos or a mix), Threads 2–20 (mix allowed), Facebook 2+ (Meta publishes no documented maximum; Hermoso caps the upload fan-out at 30 and says so), LinkedIn company Pages 2–20 (images only), Pinterest 2–5 (images only), TikTok up to 35. One url here is simply an ordinary single post. Anything a channel cannot do is REFUSED with the real reason — nothing is ever quietly downgraded to one slide.
linkTitleNothe headline ON the preview card. LINKEDIN NEVER SCRAPES THE PAGE — their Posts API disables URL scraping for API partners outright — so if you do not pass this the card renders UNLABELLED. Fetch the page’s own title and pass it.
visibilityNodefault PUBLIC
captionsSrtNoCLOSED CAPTIONS for a videoUrl post — the SubRip (.srt) CONTENT itself, cue numbers and `00:00:00,000 --> 00:00:02,000` timing lines included, NOT a URL and NOT the plain script (a file with no timings is refused, because LinkedIn would accept it and then silently never show it). Most of LinkedIn is watched with the sound off, so an uncaptioned video is one most of the feed never hears. LinkedIn allows ONE caption file per video and ENGLISH ONLY; it can be attached only WHILE the video is uploaded, never added to a published post; and it is processed asynchronously, so the reply confirms it was UPLOADED and never that it is visible yet. Requires videoUrl — passing it on an image, carousel or link post is refused by name.
documentUrlNoa DOCUMENT post — a Hermoso-hosted PDF / PPTX / DOCX (upload_file first) that readers swipe through page by page in the feed; LinkedIn takes up to 100MB and 300 pages. One kind of media per post: not with an image, video, slides or linkUrl.
firstCommentNoFIRST COMMENT — posted on the new post AS THE PAGE right after it publishes. Needs the LinkedIn comments authorization on this brand (provider linkedin_community). Up to 2,000 characters, refused rather than cut. If the comment fails the post stays live and firstComment in the result says why.
documentTitleNothe title shown on the document (defaults to the file name)
platformCoverNoVIDEO COVER. Omit it (the default) and Hermoso sets the video’s best frame — the same frame as its Library thumbnail — as the cover (LinkedIn’s video thumbnail upload). true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.
allowDuplicateNopost it even though an identical post was just made or attempted. Only pass this when the user genuinely wants the same thing posted twice, or when you have LOOKED at the account and confirmed a timed-out attempt did not land.
idempotencyKeyNoSAFE RETRIES — any stable string: a repeat of the SAME publish within 24h returns the ORIGINAL post id instead of posting again (an identical publish is also auto-recognised for 10 minutes). On a timeout or an ambiguous error, CALL AGAIN WITH THE SAME KEY: it reports the original post or publishes it once. It never posts twice.
organizationIdNonumeric Page id from list_linkedin_pages
targetAudienceNoLINKEDIN COMPANY PAGE POST ONLY — show the post only to Page followers matching these facets (URNs or bare numeric ids; search_linkedin_ads_targeting finds them). LinkedIn requires the matching audience to be over 300 followers and refuses a smaller one. Personal-profile posts cannot be targeted.
linkDescriptionNothe sub-line on the preview card. Same rule as linkTitle: absent means blank, because LinkedIn will not fetch it.
linkThumbnailUrlNoa Hermoso-hosted image used as the card’s picture (uploaded to LinkedIn for you). Without it the card has no image.
videoThumbnailUrlNothe COVER IMAGE for a videoUrl post — a Hermoso-hosted image (a render, or any picture of the user’s via upload_file). Without it LinkedIn adds a system-generated thumbnail, which on an ad is usually whatever the first frame happens to be. Like captions this can only be set WHILE the video is uploaded, never afterwards. Requires videoUrl. This is NOT linkThumbnailUrl, which is the picture on a link-preview card.
post_to_metaPost to Facebook, Instagram or ThreadsA
Destructive
Inspect

Use this when the user wants to post now to Facebook, Instagram or Threads. Publish to a connected Facebook Page, its linked Instagram, OR the brand’s Threads account — text/link/image/VIDEO/CAROUSEL. A MULTI-SLIDE creative is a CAROUSEL, not several posts: pass the slides in order as imageUrls[] and they publish as ONE swipeable post (Instagram album, Threads carousel, Facebook multi-photo post). Never publish slide 1 of a deck on its own — the creative tells the viewer to swipe. target:"facebook" (default) posts to the Page; target:"instagram" publishes a photo or Reel to the linked IG business account (needs an image or video); target:"threads" posts to the connected Threads account (text, image, or video). Works with any media, an ad made here or the user’s own file: imageUrl/videoUrl accept a public https URL, a data: URI, or a Hermoso /generated path; for a LOCAL file (e.g. on the user’s desktop) call upload_file first and pass the url it returns. INSTAGRAM COLLAB: pass collaborators (up to 3 usernames) to invite other accounts to CO-AUTHOR the post — it then shows on their profile too once they accept. This PUBLISHES immediately — confirm the copy + media with the user first. Needs a connected Meta account (Settings > Connectors > Meta) with posting permission; Threads needs its own connection.

NameRequiredDescriptionDefault
hookNoWHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. "direct_callout", "before_after") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works.
linkNoa URL to attach (FB text post only)
asyncNopublish in the BACKGROUND and return a job id to poll with get_job, instead of waiting. USE THIS FOR VIDEO: a Facebook or Instagram video publish routinely outlives an agent transport, and a timeout on the synchronous path leaves you unable to tell whether the post is live. With async:true nothing can time out — the job reports the post id and url when it lands.
brandNoWHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted.
placeNoFACEBOOK — tag a location on the Page post. The pageId from search_meta_places: the NUMERIC ID OF THE FACEBOOK PAGE for that place, not a place name and not coordinates.
storyNoINSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. Link stickers are not available on a Story posted here. An Instagram Story keeps its caption: people see the start under the Story in the mobile app and tap for the rest. It takes NO collaborators, product tags or trialReel — any of those is refused by name and nothing is posted. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page.
ideaIdNoshort id of the content-plan idea this post came from
pageIdNotarget Page id (from list_meta_pages). Omit when the brand has one Page. With several and none named the call refuses and lists them by name and id: ask the user which Page, never pick one yourself.
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
targetNodefault facebook; instagram -> the Page’s linked IG; threads -> the brand’s connected Threads account
accountNoWHICH Instagram account when target is instagram and the brand has several — Page-linked and Instagram Login accounts alike; an @username or id from list_connector_accounts("instagram"). Several and none named is refused by name; omit when there is one.
altTextNoACCESSIBILITY — the description screen readers announce, and what the platform otherwise auto-generates badly or not at all. Describe what is actually IN the picture, never the caption. ONE STRING describes the picture; on a CAROUSEL it describes EVERY slide. Pass an ARRAY of strings instead to describe each slide separately, aligned to the slide order — use that on a multi-slide post, because one sentence read out over six different pictures is wrong for five of them. More descriptions than pictures is refused rather than dropped. WHERE IT LANDS, per Meta’s own docs: INSTAGRAM image posts and the IMAGE slides of an Instagram carousel (up to 1000 characters; dropped rather than sent on a Reel or a video slide, which Meta do not support); FACEBOOK Page photos including every photo of an album, via alt_text_custom (Meta publish no length for it, so nothing is truncated); THREADS on a single-image or single-video post ONLY — Meta document no way to attach alt text to a Threads CAROUSEL slide, so a Threads carousel publishes undescribed and the reply says so rather than risking the whole post on a guess. (Meta’s AI disclosure defaults from PROVENANCE: a Hermoso render is declared is_ai_generated; media that came through upload_file or an external URL, i.e. the user’s own photos or footage, is NOT. Pass `aiGenerated` to force it either way.)
audioIdNoINSTAGRAM REEL — put one of Instagram’s OWN licensed music tracks under the Reel: the `id` search_instagram_audio returns. Instagram mixes it in when the Reel is published; nothing is downloaded or re-rendered. REELS ONLY, and only on an Instagram account connected through Meta (a Facebook Page with a linked Instagram), which is Instagram’s own rule — the Instagram connector cannot take it and is refused by name.
messageNopost text / caption
subjectNoWHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. "winter coat", "free trial"). post_performance's second grouping axis: reuse the exact wording, as with hook.
audienceNoFACEBOOK PAGE POST ONLY — limit who can see this organic post to people in these places and/or over this age (Facebook allows 13, 15, 18, 21 or 25). Not on Instagram, Threads or a Facebook Reel (a vertical clip becomes a Reel): those are refused. Region and city keys come from the Meta ads location search.
coverUrlNoINSTAGRAM REEL COVER — a public image url Instagram fetches and uses as the cover in the Reels tab. REELS ONLY, and the alternative to `thumbOffset`: passing both is refused, since they are two answers to the same question. Run a local file through upload_file first.
imageUrlNopublic https URL, a data: URI, or a Hermoso /generated path (upload_file gives you one for a local file); a URL, not a local file path; use upload_file for local files
linkNameNoFACEBOOK — override the HEADLINE of the link preview. Without it the post shows whatever Open Graph title the destination carries, which on a bare landing page is often nothing. Needs `link`. MEASURED 2026-09-17: Facebook only honours these when the Page’s business has VERIFIED the domain (Business Manager > Brand safety > Domains) — without that Meta refuses the post outright with “Only owners of the URL…”, so leave them out unless the brand owns and has verified that link’s domain.
topicTagNoTHREADS ONLY — one topic tag for discovery, 1–50 characters. A leading # is stripped for you; Threads refuses "." and "&".
userTagsNoINSTAGRAM USER TAGS — tag people (public accounts) on the post; each is notified. Usernames, or { username, x, y } with x/y from 0 to 1 = where the tag sits on the IMAGE (a tag without a position sits at the centre). A Reel tag takes no position; a story position is optional. Not on a carousel (refused by name).
videoUrlNopublic https URL, data: URI, or /generated path — FB video post / IG Reel; a URL, not a local file path; use upload_file for local files
audioNameNoINSTAGRAM REEL — the name of the Reel’s audio track, which is what viewers tap through to. REELS ONLY.
coverAtMsNoTHE VIDEO COVER for every target of this post, as ONE frame: milliseconds from the start (7000 = the frame at 7s). Instagram gets it as thumb_offset, Facebook as its uploaded cover (a Reel’s preferred thumbnail). Instagram’s own thumbOffset, or a Hermoso-hosted coverUrl, also becomes the Facebook cover.
imageUrlsNoCAROUSEL — an ORDERED list of image (and, where the channel allows, video) URLs published as ONE post the viewer swipes through. THIS IS NOT “post several” — it is a single post with several slides, which is what a multi-slide creative (a listicle, a “1/6 · SWIPE” deck) actually needs; publishing only its first slide tells the viewer to swipe at something that cannot. The ORDER is the product. Limits per channel: Instagram 2–20 (images, videos or a mix), Threads 2–20 (mix allowed), Facebook 2+ (Meta publishes no documented maximum; Hermoso caps the upload fan-out at 30 and says so), LinkedIn company Pages 2–20 (images only), Pinterest 2–5 (images only), TikTok up to 35. One url here is simply an ordinary single post. Anything a channel cannot do is REFUSED with the real reason — nothing is ever quietly downgraded to one slide.
placeNameNothe name the user saw for that place; a different business is refused
trialReelNoINSTAGRAM TRIAL REEL — publish this Reel to NON-FOLLOWERS ONLY at first (Instagram allows trials only on accounts above its follower threshold — about 1,000 followers; an ineligible account is refused by name and nothing is posted), so a hook can be tested on a cold audience without spending it on the people who already follow the brand. Instagram then shows it to followers only if it graduates. MANUAL = the creator graduates it by hand in the Instagram app; SS_PERFORMANCE = Instagram graduates it automatically if it performs well. REELS ONLY and INSTAGRAM ONLY: an image, a carousel, a Facebook post or a Threads post is REFUSED BY NAME rather than quietly published as an ordinary post — a trial that silently goes to every follower is the exact opposite of what was asked for. Omit it for a normal Reel.
locationIdNoTAG A PLACE. THREADS: a place id from search_threads_locations. INSTAGRAM: the pageId from search_meta_places, the NUMERIC ID OF A FACEBOOK PAGE associated with that location, not a place name and not coordinates; a non-numeric value is refused rather than sent.
scheduleAtNoFACEBOOK ONLY — schedule instead of posting now. ISO timestamp (2026-08-01T09:00:00Z) or unix seconds; must be 10 minutes to 30 days ahead. Facebook holds the post and publishes it at that time, so nothing has to stay running on our side. Instagram and Threads have NO scheduling in Meta’s API — passing this for them is refused rather than silently posted immediately.
threadsGifNoTHREADS ONLY — a GIPHY GIF id (the id in a giphy.com/gifs/… link) attached to a TEXT Threads post. GIPHY is the only provider Threads takes.
aiGeneratedNoINSTAGRAM / FACEBOOK REEL / THREADS — Meta’s is_ai_generated self-disclosure (Threads: is_made_with_ai, the AI info label; a text-only Threads post is declared only when you pass true). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, media that came through upload_file or from an external URL (the user’s own photographs or footage) is NOT — a real photo must never carry Instagram’s “AI info” label. Pass true or false only to override: false strips the label from something Hermoso would otherwise declare, true declares a render the user uploaded themselves.
audioVolumeNoINSTAGRAM REEL — how loud that track plays, 0–100 (Instagram’s default 100). Needs audioId.
linkPictureNoFACEBOOK — override the IMAGE of the link preview: a public http(s) url Facebook fetches itself. Needs `link`. MEASURED 2026-09-17: Facebook only honours these when the Page’s business has VERIFIED the domain (Business Manager > Brand safety > Domains) — without that Meta refuses the post outright with “Only owners of the URL…”, so leave them out unless the brand owns and has verified that link’s domain.
productTagsNoINSTAGRAM SHOPPING — make the post SHOPPABLE by tagging products from the brand’s own catalog; tapping a tag opens the product’s price sheet inside Instagram. Instagram only. ON A PHOTO each tag is {product_id, x, y}, where x and y are FRACTIONS of the image from 0.0 (left/top) to 1.0 (right/bottom) — 0.5,0.5 is the middle — and BOTH are required, max 20. ON A REEL it is {product_id} ALONE with no coordinates, max 30. ON A CAROUSEL it is an array PER SLIDE ([[{…}], [], [{…}]]) because Instagram tags each slide’s own container, max 5 per slide and 20 across the post. Ids come from search_instagram_shopping_products; call list_instagram_shopping_catalogs FIRST, because tagging needs an APPROVED Instagram Shop and without one this fails after the media is already uploaded. A tag whose product is not “approved” is stored and shown to nobody.
quotePostIdNoTHREADS ONLY — the id of a post to QUOTE. A quote post is a distinct post type, not a formatting option.
shareToFeedNoINSTAGRAM REEL — true puts the Reel in the Feed grid as well as the Reels tab. REELS ONLY. Left unset it follows Instagram’s own default; Hermoso does not flip it either way on the user’s behalf.
thumbOffsetNoINSTAGRAM REEL COVER, the other way — which frame becomes the cover, in MILLISECONDS from the start of the video. REELS ONLY. Use it instead of `coverUrl` when the right cover is already a frame of the clip.
videoVolumeNoINSTAGRAM REEL — how loud the video’s own sound plays under the track, 0–100 (Instagram’s default 100; 0 = the track alone). Needs audioId.
callToActionNoFACEBOOK — a real call-to-action BUTTON on the Page post. The types that open a destination (SHOP_NOW, LEARN_MORE, SIGN_UP, BUY_NOW, DOWNLOAD, OPEN_LINK, WATCH_MORE, BOOK_TRAVEL) need somewhere to go: the post’s own `link`, or `callToActionLink`. CALL_NOW, MESSAGE_PAGE, LIKE_PAGE and GET_DIRECTIONS act on the Page itself and take none.
countryCodesNoTHREADS ONLY — restrict the post to these ISO 3166-1 alpha-2 countries. Warning: This is an ALLOWLIST, so the post becomes INVISIBLE in every country not named — it narrows reach, it does not target.
firstCommentNoFIRST COMMENT — posted on the new post as the same account right after it publishes (Facebook or Instagram; not Threads, not a story, not an unpublished post). Up to 2,000 characters, refused rather than cut. If the comment fails the post stays live and firstComment in the result says why.
replyControlNoTHREADS ONLY — who may reply. Default is everyone.
collaboratorsNoINSTAGRAM COLLAB — up to 3 Instagram usernames invited to CO-AUTHOR this post. Once one accepts, the post appears on THEIR profile too, with both handles in the header and the likes and comments shared. Pass handles only ("hermosoai"), not profile links; a leading @ is fine. INSTAGRAM ONLY — Facebook and Threads have no collab post at all and are refused BY NAME rather than silently dropping the co-authors — and never on a Story. AN INVITE IS NOT A CO-POST: publishing SENDS a request the other account must accept in their Instagram notifications, and until they do the post is on this brand’s profile ALONE; they may also decline, and Instagram sends no notification either way. So never report the post as live on both accounts — read the invite status back out of the reply, and use instagram_collaborators later to find out whether they accepted.
instagramPollNoINSTAGRAM — attach a POLL: 2 to 4 answers of 1–25 characters each, and the caption IS the question (so a caption is required). Feed photos, carousels and Reels only (never a story), one caption add-on per post (not with instagramCommentPrompt), and only on an account connected through Meta (a Facebook Page with a linked Instagram). WRITE-ONCE: Instagram cannot add, change or remove it after publishing, so show the user the answers first.
platformCoverNoVIDEO COVER. Omit it (the default) and Hermoso sets the video’s best frame — the same frame as its Library thumbnail — as the cover (Instagram Reel: thumb_offset; Facebook video/Reel: an uploaded cover image) — and on Threads, which has no cover setting, a blank first frame is replaced on a copy sent to Threads only. true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.
allowDuplicateNopost it even though an identical post was just made or attempted. Only pass this when the user genuinely wants the same thing posted twice, or when you have LOOKED at the account and confirmed a timed-out attempt did not land.
idempotencyKeyNoSAFE RETRIES — any stable string: a repeat of the SAME publish within 24h returns the ORIGINAL post id instead of posting again (an identical publish is also auto-recognised for 10 minutes). On a timeout or an ambiguous error, CALL AGAIN WITH THE SAME KEY: it reports the original post or publishes it once. It never posts twice.
linkAttachmentNoTHREADS TEXT POSTS ONLY — a full http(s) URL rendered as a clickable link card. This is how a Threads post carries a destination at all; without it a link is just text. Meta allows at most 5 links per post and refuses a link attachment on a post that also carries an image or video, so Hermoso refuses that combination up front rather than letting Meta silently drop it.
linkDescriptionNoFACEBOOK — override the DESCRIPTION of the link preview. Needs `link`. MEASURED 2026-09-17: Facebook only honours these when the Page’s business has VERIFIED the domain (Business Manager > Brand safety > Domains) — without that Meta refuses the post outright with “Only owners of the URL…”, so leave them out unless the brand owns and has verified that link’s domain.
paidPartnershipNoINSTAGRAM — the PAID PARTNERSHIP label. A COMPLIANCE DECLARATION: set it whenever the post is sponsored, gifted or otherwise paid for. Opt-in and never inferred — it is the poster’s own statement about their commercial relationship.
threadsLongTextNoTHREADS ONLY — LONG-FORM text, up to 10,000 characters, attached under a TEXT-only Threads post (Threads' text attachment). The way to publish more than Threads' 500-character post: keep a short lead in the message and put the full text here. Not with an image, video or carousel.
callToActionLinkNoFACEBOOK — where the button goes, when that is not the post’s own `link`.
crossreshareToIgNoTHREADS ONLY — ALSO share this Threads post to the linked Instagram account AS A STORY (not a feed post), in the same publish. NOT available on a Threads CAROUSEL, which is refused by name rather than silently dropped. THERE IS NO CONFIRMATION: Threads returns no field saying whether the Story was created, so report it as REQUESTED and tell the user to check their Instagram Stories — never that it is live.
threadsGhostPostNoTHREADS ONLY — a GHOST post: text only, Threads archives it automatically after 24 hours (a flash offer, a 24h-only note). No other Threads option can ride it.
crossreshareDarkModeNoTHREADS ONLY — render that Instagram Story in dark mode. Only meaningful alongside crossreshareToIg; on its own it is refused rather than silently ignored, because a parameter that never reaches the wire must not look accepted.
instagramPollExtendedNoINSTAGRAM — give that poll Instagram’s extended voting duration instead of its default 3 days (Instagram’s changelog says it then stays open indefinitely). Only with instagramPoll.
instagramCommentPromptNoINSTAGRAM — make the caption a COMMENT PROMPT that people answer in the comments. Same limits as instagramPoll, and never together with it.
brandedContentSponsorIdsNoINSTAGRAM — the numeric Instagram USER IDS of the brands behind that label (at most 2, and ids rather than @handles). Naming sponsors IS asking for the label, so setting these with `paidPartnership:false` is refused instead of publishing brand credits with no disclosure.
post_to_pinterestCreate a PinA
Destructive
Inspect

Use this when the user wants to publish a Pin to Pinterest. Publishing costs no credits. Create a Pin on one of the user’s Pinterest boards from any finished visual they have — image, video, or a 2–5 slide CAROUSEL (pass the slides in order as imageUrls[] and Pinterest publishes one swipeable Pin). Title, description and destination link ride along. The link is what makes a Pin drive traffic, so ask for it rather than omitting it, and the description is the text Pinterest search actually reads. boardId is REQUIRED: call list_pinterest_boards first and let the user choose. This PUBLISHES to their public profile — confirm the board, title and link before calling. Video Pins take a minute or two while Pinterest ingests the file. Needs Pinterest connected (Settings > Connectors > Pinterest).

NameRequiredDescriptionDefault
hookNoWHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. "direct_callout", "before_after") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works.
linkNodestination URL the Pin clicks through to
brandNoWHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted.
titleNoPin title, max 100 characters
ideaIdNoshort id of the content-plan idea this post came from
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
accountNoWHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the profile has more than one pinterest account connected (several and none named is refused by name, never guessed); omit when there is one.
altTextNoaccessibility alt text, max 500 characters. PIN-LEVEL: Pinterest’s API has no per-item alt text at all, so on a CAROUSEL the FIRST description is used for the whole Pin and the reply states that the others were not sent.
boardIdYesnumeric board id from list_pinterest_boards — the user picks it, never guess
subjectNoWHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. "winter coat", "free trial"). post_performance's second grouping axis: reuse the exact wording, as with hook.
imageUrlNoa Hermoso render image URL (or an upload_file url); a URL, not a local file path; use upload_file for local files
videoUrlNoa Hermoso render video URL — takes 1–2 minutes to ingest; a URL, not a local file path; use upload_file for local files
coverAtMsNoTHE VIDEO COVER of a video Pin, as ONE frame: milliseconds from the start (7000 = the frame at 7s). That frame is cut and sent as the Pin cover image. coverImageUrl wins.
imageUrlsNoCAROUSEL — an ORDERED list of image (and, where the channel allows, video) URLs published as ONE post the viewer swipes through. THIS IS NOT “post several” — it is a single post with several slides, which is what a multi-slide creative (a listicle, a “1/6 · SWIPE” deck) actually needs; publishing only its first slide tells the viewer to swipe at something that cannot. The ORDER is the product. Limits per channel: Instagram 2–20 (images, videos or a mix), Threads 2–20 (mix allowed), Facebook 2+ (Meta publishes no documented maximum; Hermoso caps the upload fan-out at 30 and says so), LinkedIn company Pages 2–20 (images only), Pinterest 2–5 (images only), TikTok up to 35. One url here is simply an ordinary single post. Anything a channel cannot do is REFUSED with the real reason — nothing is ever quietly downgraded to one slide.
slideTextNoPINTEREST CAROUSEL ONLY — per-slide title, description and destination LINK, one object per slide in slide order. This is the ONLY genuine per-slide caption on any channel Hermoso publishes to: slide 4 can send people to the product ON slide 4, where every other platform gives a carousel one shared caption. Omit any field to leave it unset; the Pin’s own title/description/link still describe the Pin as a whole. Pinterest publishes no length limit on these, so nothing is truncated. More entries than slides is refused rather than dropped.
aiGeneratedNoPinterest’s AI disclosure (ai_disclosures AI_MODIFIED). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, an image or video that came through upload_file or an external URL (the user’s own) is NOT. Pass true or false only to override.
descriptionNoPin description, max 800 characters — this is what Pinterest search reads
coverImageUrlNovideo Pins only — a render to use as the cover frame
platformCoverNoVIDEO COVER. Omit it (the default) and Hermoso sets the video’s best frame — the same frame as its Library thumbnail — as the cover (on Pinterest the frame rides as the cover image). true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.
allowDuplicateNopost it even though an identical post was just made or attempted. Only pass this when the user genuinely wants the same thing posted twice, or when you have LOOKED at the account and confirmed a timed-out attempt did not land.
boardSectionIdNooptional section within the board
idempotencyKeyNoSAFE RETRIES — any stable string: a repeat of the SAME publish within 24h returns the ORIGINAL post id instead of posting again (an identical publish is also auto-recognised for 10 minutes). On a timeout or an ambiguous error, CALL AGAIN WITH THE SAME KEY: it reports the original post or publishes it once. It never posts twice.
syntheticPerformerNoALSO declare SYNTHETIC_PERFORMER — the Pin shows a realistic AI-generated person. Only with an AI disclosure; never guessed, so pass true only when the user says the person is AI-made.
post_to_telegramPost to TelegramA
Destructive
Inspect

Use this when the user wants to post to a Telegram channel or group. Publishing costs no credits. Publish to a Telegram channel, group or chat as the brand's own bot. WHICH CHAT IS ALWAYS REQUIRED AND IS NEVER GUESSED: pass chatId as the public channel's @username (e.g. @yourchannel) or its numeric id (a group is negative; a supergroup or channel starts with -100). There is no default and there cannot be one — the Telegram Bot API publishes no method that lists the chats a bot belongs to, so Hermoso genuinely cannot know them. list_telegram_chats reports the chats that have MESSAGED the bot in the last 24 hours, which is a shortcut for finding an id and is NOT a roster: a chat missing from it can still be posted to. TEXT: up to 4096 characters on a text-only message, but only 1024 the moment ANY photo or video is attached — a caption is not a message, and Hermoso refuses the over-long one before spending the round trip and says which budget applied. MEDIA: one image (imageUrl), one video (videoUrl), or an ALBUM of 2–10 (imageUrls, in order) in which photos and videos may be MIXED — pass a videoUrl alongside imageUrls and it joins the album as one more item. Hermoso uploads the bytes rather than handing Telegram a link, which is what buys the larger ceilings: 10MB per photo and 50MB per video, where a link would be 5MB and 20MB. THE BOT MUST BE IN THE CHAT — an administrator with Post Messages for a channel, an unrestricted member for a group; if it is not, Telegram refuses and the error says to add it rather than to reconnect. Returns the message id and, for a PUBLIC chat, its t.me link — a private group has no public web link, so url comes back null rather than as a link that would 404 for whoever you hand it to. 0 credits. Connect at Settings > Connectors > Telegram.

NameRequiredDescriptionDefault
hookNothe post's angle — a list_hooks id or your own wording, reused exactly
textNothe message. ≤4096 characters on its own; ≤1024 once any image or video is attached.
brandNoWHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted.
chatIdYesREQUIRED — the destination: a public channel's @username, or the numeric chat id. Never guessed; ask the user, or use list_telegram_chats.
ideaIdNoshort id of the content-plan idea this post came from
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
silentNodeliver without a notification sound (Telegram’s disable_notification). This is NOT a visibility setting — the message is just as visible.
accountNoWHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the profile has more than one telegram account connected (several and none named is refused by name, never guessed); omit when there is one.
buttonsNoURL BUTTONS under the post, Telegram's call to action: [{text:'Shop now', url:'https://…'}] one per row, or rows [[a,b],[c]]. URL buttons only (https://, http:// or tg://), optional style danger|success|primary. A single photo, a video or a text post can carry them; an ALBUM cannot (Telegram's sendMediaGroup takes no buttons), so that combination is refused.
subjectNowhat the post is about
imageUrlNoone image (≤10MB after upload); a URL, not a local file path; use upload_file for local files
videoUrlNoone video (≤50MB). Passed alongside imageUrls it joins the album as one more item; a URL, not a local file path; use upload_file for local files
coverAtMsNoTHE VIDEO COVER in the chat, as ONE frame: milliseconds from the start (7000 = the frame at 7s). Sent as Telegram’s cover image; beats platformCover.
imageUrlsNoan ALBUM of 2–10 media, in order. Photos and videos may be mixed — Telegram allows it.
parseModeNo'HTML' to format the text with Telegram's HTML: <b>, <i>, <u>, <s>, <a href="…">, <code>, <pre>, <blockquote>, <tg-spoiler>. Omit for plain text (the default). In HTML mode a literal <, > or & in the copy must be written &lt; &gt; &amp;, which is why it is never turned on by itself. Markdown (**bold**) is NOT parsed; convert it to HTML tags.
platformCoverNoVIDEO COVER. Omit it (the default) and Hermoso sets the video’s best frame — the same frame as its Library thumbnail — as the cover (Telegram’s in-chat video cover). true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.
allowDuplicateNopost it even though an identical post was just made
disablePreviewNosuppress the link-preview card on a text-only post. Default is Telegram’s own behaviour (previews on).
idempotencyKeyNoany stable string: a repeat within 24h returns the original post instead of posting again
post_to_tiktokPost a video or photo post to TikTokA
Destructive
Inspect

Use this when the user wants to post a video or photo post to TikTok now. Publishing costs no credits. Publish to the user’s connected TikTok account — a finished VIDEO, or a PHOTO POST (TikTok’s photo/slideshow format). A photo post carries 1 to 35 images and ONE image is simply a one-slide photo post, so there is nothing special to do for a single picture: pass imageUrls, in the order the slides should appear, and optionally coverIndex. Pass videoUrl for a video. Never pass both — TikTok has no mixed post. TWO destinations. destination:"post" (THE DEFAULT) publishes it LIVE on their profile: TikTok requires the user to CHOOSE the privacy themselves (no default is allowed), so call tiktok_creator_info, show them their real privacy options, and get their choice and an explicit yes before calling. destination:"draft" is ONLY for when the user asks for a draft, or wants to add a TikTok sound or trending audio (TikTok’s API takes no sound for a VIDEO): BEFORE sending, tell them plainly it lands in their TikTok inbox as a DRAFT, that they add the sound in TikTok’s editor, and that THEY must publish it from the TikTok app — nothing goes live until they do. Never pick draft on your own. A photo post published with destination:"post" gets a TikTok-recommended track automatically (autoAddMusic, default on) that they can change in the app. Pass Hermoso render URLs (or upload_file urls for local/external files). Needs TikTok connected (Settings > Connectors > TikTok).

NameRequiredDescriptionDefault
hookNoWHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. "direct_callout", "before_after") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works.
brandNoWHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted.
titleNothe caption — hashtags go here (video ≤2200 chars, photo post ≤4000)
ideaIdNoshort id of the content-plan idea this post came from
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
accountNoWHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the profile has more than one tiktok account connected (several and none named is refused by name, never guessed); omit when there is one.
privacyNoREQUIRED for destination:"post", for photos and video alike. Must be one the creator actually allows — read them from tiktok_creator_info, never guess.
subjectNoWHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. "winter coat", "free trial"). post_performance's second grouping axis: reuse the exact wording, as with hook.
videoUrlNothe video to post — a Hermoso render URL or an upload_file url. Omit for a photo post; a URL, not a local file path; use upload_file for local files
imageUrlsNoa PHOTO POST: 1–35 image URLs in slide order. One url = a single-image photo post. Do not combine with videoUrl.
yourBrandNodiscloses that this promotes the creator’s own brand
coverIndexNophoto posts: which slide is the cover, 0-based. Default 0 (the first slide).
photoTitleNophoto posts only: a short title above the caption (≤90 chars). Defaults to the caption’s first line.
aiGeneratedNoTikTok’s is_aigc AI-generated-content label (video posts). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, a video that came through upload_file or an external URL (the user’s own footage) is NOT. true/false overrides.
destinationNo"post" (default) = live on the profile now (needs the privacy the user chose + their explicit yes); "draft" = to their TikTok inbox for them to finish and publish in the app — only when they ask for a draft or want to add a TikTok sound, and only after telling them so.
disableDuetNovideo only — TikTok has no duet on a photo post
autoAddMusicNophoto posts only: let TikTok add a recommended track (default true — a silent slideshow reads as broken)
disableStitchNovideo only — TikTok has no stitch on a photo post
platformCoverNoVIDEO COVER. Omit it (the default) and Hermoso sets the video’s best frame — the same frame as its Library thumbnail — as the cover (TikTok video_cover_timestamp_ms, on a direct post — a draft takes no cover, you pick it in the TikTok app). true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.
brandedContentNodiscloses a paid partnership — cannot be combined with SELF_ONLY privacy
disableCommentNotrue = turn comments off on this post
coverTimestampMsNovideo only: which frame to use as the cover, in ms
post_to_xPublish a post to X (Twitter)A
Destructive
Inspect

Use this when the user wants to post to X (Twitter) now. Publish to the user’s connected X (Twitter) account — a single post, a post with an image or video render attached, a reply to an existing post, or a whole THREAD (pass thread as an array and each part is posted as a reply to the one before). This PUBLISHES immediately and PUBLICLY — ALWAYS show the user the exact text and get an explicit yes BEFORE calling. Can also run a POLL (2-4 options) instead of media, and restrict who may reply. LENGTH IS THE POSTING ACCOUNT’S, NOT A FLAT 280: 280 characters on an ordinary account, but UP TO 25,000 if that account has X Premium. Hermoso reads the account’s own subscription from X and sends the post rather than refusing something it may be entitled to; if X declines it on length, the reply says so and names the subscription X reported. Over 25,000 is refused for free — that is X’s own ceiling on every account. Text is NEVER truncated. AND A LONG POST IS CHEAPER THAN A THREAD: it is ONE billed X call where the same words split across five posts are five, so prefer one long post over threading when the account has Premium. Only the first ~280 characters show in the timeline; the rest sits behind “Show more”. Write altText whenever you attach a render. COSTS CREDITS at X’s per-post price, every post in a thread billed: a post that contains a link (any web address, and every quote post) is X’s link rate, far higher than one without (hermoso_credits gives both figures). When the user asks what a post will cost, call this with dryRun:true: it posts NOTHING and returns that exact post’s costCredits. Each profile also has a rolling 24-hour ceiling on what it can spend at X, and a request that would cross it is refused WHOLE before anything publishes, so a batch of posts is bounded rather than open-ended. THIS TOOL POSTS ORGANICALLY — it does not create an ad campaign. X ads are built with the X Ads campaign tools (campaign, then line item, then promoted post), and a promoted post needs a post id, so publish here first and promote that post. Needs X connected (Settings > Connectors > X).

NameRequiredDescriptionDefault
hookNoWHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. "direct_callout", "before_after") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works.
pollNorun a poll on the post. X does not allow a poll and media on the same post, and a poll cannot ride on a thread.
textNothe post text. 280 characters without X Premium, up to 25,000 with it — write the full thing, it is never truncated. With `thread` also set, this is the FIRST post and the thread entries follow it (a thread whose first entry is already this text is not posted twice).
brandNoWHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted.
dryRunNotrue = post NOTHING and return what this exact post would cost (costCredits). Free.
ideaIdNoshort id of the content-plan idea this post came from
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
threadNoa thread: each string is one post, published in order, each replying to the previous (if `text` is also set it is posted first and these follow). Max 25 including `text`. Each part follows the same length rule as `text`, and on an X Premium account ONE long post is usually both better reading and cheaper than a thread.
accountNoWHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the profile has more than one x account connected (several and none named is refused by name, never guessed); omit when there is one.
altTextNoaccessibility description of the attached media, max 1000 characters — write one whenever you attach a render. Costs a small extra amount: X bills one metadata write PER media. With SEVERAL media, pass an ARRAY aligned to their order — X attaches alt text per media id, and a single string describes only the FIRST one (X renders up to four media as a GRID, not a carousel, so one sentence would be wrong for the other three).
subjectNoWHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. "winter coat", "free trial"). post_performance's second grouping axis: reuse the exact wording, as with hook.
imageUrlNoalias of mediaUrl for an IMAGE — same as passing it as mediaUrl
mediaUrlNoa Hermoso render (image or video) to attach to the first post — pass its served URL, or an upload_file url for external media; a URL, not a local file path; use upload_file for local files
videoCtaNoVIDEO posts only — a button on the video; needs videoCtaUrl (https)
videoUrlNoalias of mediaUrl for a VIDEO — same as passing it as mediaUrl
mediaUrlsNoUP TO FOUR Hermoso-hosted media attached to ONE post — X’s own schema caps media_ids at 4. X renders them as a GRID: every image visible at once, nothing to swipe to. That is NOT a carousel, and a numbered "1/6 · SWIPE" slide deck must still not be sent here — it would publish as a grid and the "swipe" instruction would make no sense. Order decides the layout. On a thread the media rides the FIRST post; give each later part its own post to attach more. Mutually exclusive with mediaUrl, and more than 4 is refused before anything is uploaded; a URL, not a local file path; use upload_file for local files
replyToIdNonumeric id of an existing X post to reply to. X ONLY ACCEPTS AN API REPLY TO A POST THAT MENTIONS THIS ACCOUNT OR THAT THIS ACCOUNT WROTE (X policy since Feb 2026, every tier below Enterprise): a cold reply under a stranger's post is refused by X with "You can only reply to or quote posts where you are mentioned or are the author" and nothing is posted. Reply to those from x.com itself; use this for replies in your own threads and to people who tagged you.
dmDeepLinkNoa "Message us" button: true for the connected account, or https://x.com/messages/compose?recipient_id=<id>. TEXT posts only — X refuses it alongside media.
videoTitleNoVIDEO posts only — a headline under the video, max 70 (measured: X drops it on an image post, so it is refused there)
communityIdNopublish into an X COMMUNITY instead of the main timeline — the number in the community’s own URL (x.com/i/communities/<id>). The connected account must be a MEMBER of it; X answers a non-member and a non-existent id with the same refusal and does not separate them.
quotePostIdNonumeric id of a post to QUOTE — X renders the quoted post inside yours. This is NOT replyToId: a reply sits under the original in its thread, a quote stands alone on your own timeline with the original embedded, which is the one you want for commentary. X makes a quote mutually exclusive with media and with a poll (their own schema), and a quote is billed at the higher LINK rate because X appends the quoted post’s t.co URL whatever your text says. Same X rule as replyToId: a quote of a post that does not mention this account is refused by X.
videoCtaUrlNohttps link the video button opens; used with videoCta
platformCoverNoVIDEO COVER. X has no cover setting and shows the video’s FIRST frame, so by default Hermoso checks it and, only when that frame is blank (a template ad’s empty opening card), sends X a copy with the first frame replaced by the video’s best frame. Your Library file is never changed. true = send the file exactly as it is.
replySettingsNorestrict who can reply — omit for everyone, which is the right default for a brand post
taggedUserIdsNotag up to 10 people (numeric X user ids, from x_user) in the post’s media
paidPartnershipNolabel the post a PAID PARTNERSHIP on X — the same disclosure Hermoso already ships for TikTok. OPT-IN ONLY: set it when the post is sponsored, gifted or otherwise paid for, and never assume it on the user’s behalf.
videoDescriptionNoVIDEO posts only — a line under the title, max 200
post_to_youtubePost a video to YouTubeA
Destructive
Inspect

Use this when the user wants to upload a video or Short to YouTube. Publishing costs no credits. Publish a finished video to the brand’s connected YouTube channel. Pass a Hermoso render URL (or an upload_file url for a local/external file). PUBLISHES PUBLICLY BY DEFAULT: a plain "post this to YouTube" puts it ON the channel (confirm the title with the user, as for any publish) and notifies subscribers as YouTube does. Pass the privacy the user states instead: "unlisted" (link-only) or "private" (eyes-only). A video meant to run as a YouTube/Google AD should go up "unlisted" — private videos CANNOT be used as ads. SCHEDULE it with publishAt, FILE it under the right categoryId (the default 22 "People & Blogs" is wrong for most ads), SUBSCRIBER NOTIFICATIONS FOLLOW PRIVACY — a public publish announces the video to the channel’s subscribers (YouTube’s own default), while unlisted/private uploads stay quiet; pass notifySubscribers explicitly to override either way. THUMBNAIL: pass thumbnailUrl, or a frame of the video is set for free; a thumbnail YouTube refuses never fails the upload, and thumbnailNote says why. YOUTUBE MUSIC: YouTube’s API takes no music track. Only when the user wants a track from YouTube’s own Audio Library: BEFORE uploading, tell them plainly it will go up UNLISTED (not on their channel, nobody sees it) so they can add the track in YouTube Studio on desktop (Content > the video > Editor > Audio), and that THEY must then switch it to Public there themselves; get their yes, then pass privacy:"unlisted". Never choose unlisted for music on your own. Needs a connected YouTube channel (Settings > Connectors > YouTube).

NameRequiredDescriptionDefault
hookNoWHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. "direct_callout", "before_after") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works.
tagsNoup to 30 tags
brandNoWHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted.
titleNoREQUIRED in practice — the headline every search result and the Shorts feed shows (≤100 chars); an upload with no title is refused
ideaIdNoshort id of the content-plan idea this post came from
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
accountNoWHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the profile has more than one youtube account connected (several and none named is refused by name, never guessed); omit when there is one.
licenseNoYouTube licence: youtube (standard) or creativeCommon
privacyNodefault public (live + searchable on the channel); unlisted = link-only (the ad-ready setting, or to add YouTube music in Studio first — only when the user asks); private = eyes-only (cannot run as an ad)
subjectNoWHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. "winter coat", "free trial"). post_performance's second grouping axis: reuse the exact wording, as with hook.
videoUrlYesthe video to post — a Hermoso render URL or an upload_file url; a URL, not a local file path; use upload_file for local files
coverAtMsNoTHE VIDEO COVER (the custom thumbnail), as ONE frame: milliseconds from the start (7000 = the frame at 7s). That frame is cut from the upload and set with thumbnails.set. thumbnailUrl wins; this beats platformCover.
publishAtNoSCHEDULE the publish — ISO 8601, e.g. "2026-09-01T15:00:00Z", and it must be in the future. YouTube only allows this on a PRIVATE video and makes it PUBLIC at that moment, so pass privacy:"private" (or leave privacy unset) — asking for a scheduled "unlisted" or "public" post is refused rather than half-honoured.
categoryIdNoYouTube category id, NUMERIC — default "22" (People & Blogs), which is wrong for most ads. 1 Film & Animation · 2 Autos & Vehicles · 10 Music · 15 Pets & Animals · 17 Sports · 19 Travel & Events · 20 Gaming · 22 People & Blogs · 23 Comedy · 24 Entertainment · 25 News & Politics · 26 Howto & Style · 27 Education · 28 Science & Technology · 29 Nonprofits & Activism. The assignable set is region-specific, so the value is forwarded as given and YouTube has the last word.
embeddableNotrue = allow embedding on other sites
aiGeneratedNoYouTube’s “altered or synthetic content” declaration (containsSyntheticMedia). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, a video the user uploaded through upload_file or from an external URL is NOT — real footage must not carry the label. true/false overrides.
descriptionNoREQUIRED in practice — what the video shows, a line about the brand, a link and a few #hashtags (≤5000 chars); YouTube search, suggested videos and the Shorts feed rank on it, so an upload with no description (and no caption) is refused
brandPartnerNosponsoring brand's channel id (UC…) or @handle — gives that brand access to run the video as a partnership ad; "" unlinks (update only)
thumbnailUrlNothe custom thumbnail: a Hermoso-hosted image (make_thumbnail, or upload_file for the user’s own). Omit it and a representative frame of the video is set, free; "auto" keeps YouTube’s pick. Custom thumbnails need a verified channel.
localizationsNotranslated title/description per language, e.g. {"es":{"title":"…"}}; needs defaultLanguage
platformCoverNoVIDEO COVER. Omit it (the default) and Hermoso sets the video’s best frame — the same frame as its Library thumbnail — as the cover (YouTube custom thumbnail; the same as thumbnailUrl:"auto" when true). true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.
recordingDateNoYYYY-MM-DD
defaultLanguageNolanguage of title/description, e.g. "en"
notifySubscribersNoTHE DEFAULT FOLLOWS PRIVACY. privacy:"public" NOTIFIES the channel's subscribers — that is YouTube's own default and normally what someone publishing publicly wants. privacy:"unlisted" and "private" do NOT: the video is not on the channel, so announcing it is nonsense, and a blast to somebody's whole subscriber list cannot be undone. A scheduled publish (publishAt) is PRIVATE at upload, so it does not notify either — pass true to announce one. An explicit value ALWAYS wins in both directions: true announces an unlisted/private upload, false publishes publicly and quietly. The reply reports which way it went and why.
publicStatsViewableNotrue = show public view and like counts
defaultAudioLanguageNospoken language, e.g. "es"
paidProductPlacementNotrue = contains PAID product placement (sponsored) — YouTube shows the disclosure; legally required for sponsored content
post_x_articlePublish a long-form Article to XA
Destructive
Inspect

Publish a long-form ARTICLE to the user’s connected X (Twitter) account — X’s own long-form format, which is a different thing from a long POST. Give it a title and a body written in MARKDOWN (or plain prose) and Hermoso converts it into the DraftJS content_state structure X requires: headings, paragraphs, bulleted and numbered lists, blockquotes, bold / italic / strikethrough, links, horizontal rules, fenced code blocks and pipe tables all carry across. FORMATTING IS NEVER SILENTLY DROPPED — anything X Articles cannot represent (inline code, an inline image) REFUSES the article for free and names exactly what and why, and allowLossy: true is the explicit way to publish it as plain text anyway. THE HARD LIMIT TO PLAN AROUND: X allows only about 10 Article DRAFTS and 5 Article PUBLISHES per account per DAY, it publishes that cap nowhere, and it counts a FAILED attempt against them — so never iterate on an article by republishing it, and use publish: false to save a draft for the user to read in X’s own composer when they want to review before it goes out. This PUBLISHES immediately and PUBLICLY: show the user the full text and get an explicit yes first, because a published Article can NEVER be edited (X’s own rule — edit_x_post will refuse it) and the only remedy is to delete and republish, which costs another of the five. An Article appears in the timeline as a title card, not as body text; readers open it. Costs credits (X bills per API request, and this is three of them). Needs X connected (Settings > Connectors > X).

NameRequiredDescriptionDefault
bodyYesthe article body, as markdown or plain prose. Markdown headings, lists, quotes, links, emphasis, ``` code fences and | pipe | tables | are all converted to X’s own Article structure.
hookNoWHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. "direct_callout", "before_after") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works.
brandNoWHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted.
titleYesthe Article title — X requires one and refuses a draft without it. This is what shows on the timeline card.
ideaIdNoshort id of the content-plan idea this post came from
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
accountNowhich connected account: its @handle or id from list_connector_accounts; needed only when several are connected
publishNodefault true. Pass false to save it as a DRAFT in the account’s X Articles composer instead — nothing becomes public, the user can review and publish it from X, and it does not spend one of the five daily publishes.
subjectNoWHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. "winter coat", "free trial"). post_performance's second grouping axis: reuse the exact wording, as with hook.
headingsNohow headings are rendered. “blocks” (default) uses X’s own heading block types for # and ## headings; ### and deeper become bold lines, because X Articles refuse a third-level heading (the reply says so). “text” renders every heading as a BOLD standalone paragraph instead: the words survive and the heading structure does not, and the reply says so.
allowLossyNopublish even though part of the source cannot be represented on X, rendering those parts as plain text. OFF by default and it should usually stay off — silently publishing a user’s copy with formatting missing is worse than refusing and telling them.
coverImageUrlNooptional cover picture for the Article — a Hermoso render URL or an upload_file url. Must be a STILL image; X Article covers are not videos.
product_sizzleProduct sizzle (music-led)AInspect

Render an 18-30s music-led PRODUCT SIZZLE: ONE 15s Seedance 2.5 hero clip of the product, diced into fast cuts and intercut with typeset spec/CTA cards on a brand-coloured grain background, mixed to a music bed. Faceless by design — no people, no voiceover, no spoken lines; the cards carry every word, so nothing is left to a video model's spelling. Pass a real packshot as refImage or the label will not be yours. EXPENSIVE — the hero clip is the only paid leg and it is a full 15s Seedance 2.5 render: ≈870 credits at the DEFAULT 1080p, ≈360 at 720p, ≈170 at 480p; a 4k ask renders on Seedance 2.0, the only Seedance tier with 4k, ≈2,360 (call hermoso_capabilities for the live seedance-2.5 per-duration numbers; the dicing and the cards are free, and the music bed is included in these figures). Confirm the spend with the user before calling. For a talking/UGC ad use render_ad or generate_avatar; for a cheap deterministic format use make_template_ad.

NameRequiredDescriptionDefault
ctaNoclosing CTA line, ≤30 chars
specsNoup to 4 spec lines for the typeset cards, ≤26 chars each
styleNothe LOOK of the sizzle (same price for every look): hypermotion (Fast cuts, crash zooms and speed ramps), product_2d (The real product with playful hand-drawn 2D animation around it), typography (Product framed by huge bold letterforms in the brand colours), dark_minimal (Moody, premium, a single hard light on black), color_pop (Bright saturated colour, glossy, joyful), mixed_media (Editorial collage: paper, cut-outs and texture around the product), saas (For apps and software: floating screens, light trails, particles), product_showcase (Hero commercial: ingredients, splashes and a clean pack shot). Default hypermotion.
promptYeswhat the sizzle should show — the product, the setting, the look
secondsNofinished length, clamped to 18-30s (default 25). The PAID hero render is always 15s regardless — this only changes how the cuts and cards are packed
refImageNoproduct packshot URL that anchors the real label — strongly recommended
brandNameNobrand name on the cards — defaults to the workspace brand
musicMoodNomusic-bed mood, e.g. driving / cinematic / upbeat
resolutionNohero-clip resolution and therefore the whole cost — DEFAULT '1080p' (≈1,040 credits); '720p' ≈470, '480p' ≈220, '4k' ≈4,130
aspectRatioNo'9:16' default; anything the Seedance row that renders it does not list falls back to 9:16
publish_to_shopify_productPublish an image or video onto a Shopify productA
Destructive
Inspect

Attach a finished IMAGE or VIDEO to one of the merchant's Shopify product listings, as product media. Pass productId (from list_shopify_products, the gid://shopify/Product/… form) and a PUBLIC https mediaUrl, which is what every Hermoso render returns. Whether it is a video is read from the file (.mp4 / .mov / .webm, or the link's content type); pass kind to say so outright. An image is fetched by Shopify directly; a video is uploaded to Shopify for you (MP4, MOV or WEBM, up to Shopify's 1 GB and 10-minute product-video limits; a file over a limit is refused by name, never cut). Shopify processes media asynchronously: the reply is the status read back from Shopify, and READY means it is live on the listing while PROCESSING means it will appear when Shopify finishes. That is success, not a failure. Only works for accounts created by installing Hermoso from the Shopify App Store. Free: the render was already paid for.

NameRequiredDescriptionDefault
altNoalt text for accessibility and SEO; defaults to a generic credit
kindNoonly when the link does not say (no file extension and no content type)
imageUrlNoolder name for mediaUrl; still accepted
mediaUrlNoany public https image or video URL: a Hermoso render works, and so does ANY file of your own brought in with upload_file
productIdYesgid://shopify/Product/… from list_shopify_products
pull_competitor_adsPull competitor adsAInspect

Pull one named brand’s live ads from the Meta (Facebook and Instagram) Ad Library: deduplicated, sorted, with the brand’s own page resolved. One call, back in a few seconds. Use it when the user names a brand and asks what ads it is running ("show me the ads is running"). Covers the Meta Ad Library only; for a broader search across brands or platforms use research_ads. Spends credits.

NameRequiredDescriptionDefault
urlNosame as domain
nameNosame as companyName
sortNo'longest_running' (default) etc.
brandNosame as companyName
limitNomax ads per platform (default 30)
domainNothe advertiser domain, e.g. liquiddeath.com — a full website URL works (url / website are read as this too). Pass companyName OR domain
companyNosame as companyName
countryNo2-letter, default 'US'
websiteNosame as domain
companyNameNothe advertiser name, e.g. "Liquid Death" (company / brand / name are read as this too)
query_notion_databaseRead a Notion databaseA
Read-onlyIdempotent
Inspect

Read the ROWS of a Notion database the user shared with Hermoso — e.g. the team's content calendar: each row's title, id, link and every property as a plain value (status, date, channels, caption, links), plus the database's columns with their types and options. Filter with match {"Status": "Scheduled"} (typed from the database's own columns) or a raw Notion filter object; sort with sortBy (a column, or created_time / last_edited_time) and direction. Up to 100 rows per call, cursor for more; the answer says when Notion stopped at its 10,000-result limit. Use it to import a calendar and schedule its posts, or to find the row update_notion_row changes. Read-only, free. Needs Notion connected (Settings > Connectors > Notion).

NameRequiredDescriptionDefault
limitNo1-100, default 50
matchNo{"Column": value} — equality for status/select/checkbox/number/date, contains for text/multi-select
cursorNonextCursor from the previous page of rows
filterNoa raw Notion filter object
sortByNocolumn name to sort by, or created_time or last_edited_time
archivedNoread archived rows instead
directionNoascending (default) or descending
databaseIdNodatabase id or link (list_notion_pages type:"database")
dataSourceIdNodata source id from an earlier read, as an alternative to databaseId
read_docRead a Google DocA
Read-onlyIdempotent
Inspect

Read the text of a Google Doc Hermoso can reach — one it created, or one the user handed over with the Google file picker in the app (that is how an EXISTING doc becomes readable; find its id with list_drive_files). Pass documentId (from create_doc) OR paste a Google Docs URL as docUrl. Under the drive.file scope it reaches nothing else in the user’s Drive; if Google answers that the file was not found, the user has not picked it yet — ask them to pick it in the app rather than retrying. Returns the plain text of EVERY tab (each headed by its tab name when there are several), with smart chips (dropdowns, people, dates, rich links) shown as the text Docs displays, plus dropdowns[] — each chip with its title, current value and options, which is what update_doc dropdowns sets. Read-only, free.

NameRequiredDescriptionDefault
docUrlNoa Google Docs URL to read — the document id is extracted from it
documentIdNothe document id (from create_doc)
read_notion_pageRead a Notion pageA
Read-onlyIdempotent
Inspect

Read a Notion page the user shared with Hermoso, as markdown (pageId or a Notion link). A database id answers with its data sources instead. Read-only, free.

NameRequiredDescriptionDefault
pageIdYespage id or Notion link
read_sheetRead a Google Sheet rangeA
Read-onlyIdempotent
Inspect

Read cells from a Google Sheet Hermoso can reach — one it created, or one the user handed over with the Google file picker in the app (that is how an EXISTING spreadsheet becomes readable; find its id with list_drive_files). Pass the spreadsheetId (from create_sheet) OR paste a Google Sheets URL as sheetUrl. If Google answers that the file was not found, the user has not picked it yet — ask them to pick it in the app rather than retrying. Returns a 2-D array of values.

NameRequiredDescriptionDefault
rangeNoA1 range, e.g. "A1:D50" (default A1:Z1000)
sheetUrlNoa Google Sheets URL to read — the spreadsheet id is extracted from it
spreadsheetIdNothe spreadsheet id (from create_sheet)
recast_motionRecast motionAInspect

Copy the motion of any video (motion transfer): re-perform a clip's motion, camera and timing with YOUR character, product, clothes or place; sound kept. image (+ images, @Image1, @Image2… in prompt) = who and what. engine auto (default): a person clip + ONE photo of a person (saved, real or AI), 4-30s -> seedance (Seedance 2.5 through our face library, 720p: the clip's scene, beats and sound kept, the WHOLE person swapped) when the clip is one of our renders or faceRoute is set; otherwise wan (Wan 3.0, scene kept, 3-15s, 720p), past 15s kling-edit (Kling 4.0 Edit, 3-30s, 1080p by asking); several photos (one per person) -> h3 (H3 Max Recast, ≤4 photos, 5-30s); no person in the clip -> seedance. By name: kling-edit; wan = Wan 3.0 (≤15s, keeps the set); wan-prime; h3; kling = Kling Motion Control (ONE picture, its background becomes the set). voice/language: the new performer gets a new voice (same timing) or speaks another language (lip-synced), in the same job and price. Billed per output second, ~5 min per 5s; dryRun quotes.

NameRequiredDescriptionDefault
tierNokling only: 'pro' (default): 1080p and real hand-object interaction. 'standard': about 25% fewer credits and faster, but 720p, and it tends to mime a held object with empty hands. hermoso_capabilities lists the exact credits for both
imageNowho performs it: the actor/character image URL (@Image1). Required on kling. No one yet: generate_image a portrait of an AI person from words (no reference photo) and pass its url, or a saved / preset creator’s portrait (list_creators). Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded.
videoYesthe reference video whose motion to re-perform
voiceNoa NEW voice for the new performer: 'match' (a stock voice that fits them), 'female'/'male', or Aria, Sarah, Charlotte, George, Brian, Charlie. Omit = the clip's own sound. Never the original speaker's or a cloned voice
dryRunNoreturn the credits this exact job would hold and the engine auto picks, without rendering
engineNodefault auto
imagesNoseedance/wan: more pictures, @Image2…: a character, product, outfit or place
promptNooptional, e.g. 'same moves, new location: Tokyo at night'
languageNothe new voice speaks this language (e.g. 'Spanish'), lip-synced to the new words; voice defaults to 'match'
faceRouteNo'face_lane' = the user's "I own the rights to this face" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own.
resolutionNoa person swap defaults to 720p (seedance or kling-edit); otherwise seedance and wan default 1080p; h3 720p or 1080p, default the clip’s own
orientationNokling only: which aspect to keep: the video's (default) or the image's
reframe_videoReframe videoAInspect

Reframe a video to a different aspect ratio (e.g. 16:9 master -> 9:16 vertical): the whole original frame is kept and the new canvas around it is generated, with the sound and timing unchanged. Clips up to 300 seconds. Paid render; returns the served URL of the reframed video. Every placement at once (a free blur-pad by default): repurpose_video. A presenter on any video: add_presenter.

NameRequiredDescriptionDefault
videoYesthe source video URL; a URL, not a local file path; use upload_file for local files
aspectRatioYesthe target aspect ratio
rememberRemember a factAInspect

Save a durable fact or PREFERENCE about the brand, audience, or the user’s creative TASTE (e.g. “audience is first-time homebuyers”, “prefers bold lime accents”, “always captions off”) into the workspace Memory so it shapes FUTURE ads. For lasting things, not one-off requests. Merges into the existing Memory (never overwrites); de-dupes on identical text. NEVER how Hermoso, a tool, a connector or a platform API behaves (what a call returns, errors, permissions, limits, ids) and never a phone number or email — those are refused.

NameRequiredDescriptionDefault
textYesthe fact/preference, concise
brandNoprofile id/name from list_brands, this call only
categoryNoshort bucket: Brand, Audience, Taste, Do, Don’t, or Preference (default General)
remix_staticClone a static ad (old name)AInspect

The OLD NAME of clone_static, kept so agents that already call it keep working. It is the same tool with the same inputs, result and cost; prefer clone_static.

NameRequiredDescriptionDefault
brandIdNoa profile id/name from list_brands to clone for; omit to use the active profile
imageUrlYesthe URL of the static ad image to clone. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded.
remove_memberRemove a teammateA
DestructiveIdempotent
Inspect

Remove a member from this profile by email — they lose access (you can re-invite them later), AND every connection THEY made on this profile is disconnected with them: their own X, LinkedIn, TikTok, YouTube, Pinterest, Threads… connections (on a channel holding several accounts, only the accounts they added), and any extra login or authorization they added. Connections the owner or anyone else made are never touched, and one with no record of who connected it is treated as the owner's. Queued posts that would publish through their accounts, or that they scheduled themselves, will not go out. The unconfirmed call reports exactly which connections and how many scheduled posts — relay that to the user, then call with confirm:true.

NameRequiredDescriptionDefault
emailYesthe member’s email
confirmNoREQUIRED true
render_adRender ad videoAInspect

Use this when the user asks for a video ad, Reel, TikTok ad, UGC video or commercial for their business or product. It renders a finished ad on a real video model, on their brand (real product photos, brand voice), as an mp4 ready to post: timed shots, clean speech, text set in post, no music, captions or end card unless asked. Pass plan_ad’s full output as creative (plan_ad turns a one-line ask into the concept). UGC and cinematic plans render from a painted STORYBOARD (a cinematic spot paints a location anchor first, and a hero when someone is on camera); a product-only spot renders from the real product photo, and board:true or foundations:'choose' adds an identity sheet, a moodboard and a board. A plan that fits one clip of the render model renders as one continuous pass; a longer one as STITCHED ACTS (the fewest balanced clips), never time-compressed. Most models cap a clip at 15s and the longest at 30s: dryRun:true shows the act split free, before spending. CAST A SAVED CREATOR with creator (list_creators) so the same person stars in every ad; otherwise each render casts a new face. The user’s own pictures (creative.inserts, or inserts) are laid in full screen at their seconds. An app shown on a device screen with no real screens to lay there returns needsAppScreens, nothing charged: ask, then pass screenChoice or screens. 1–3 min; poll get_job while it renders. Spends credits.

NameRequiredDescriptionDefault
boardNopaint the storyboard board first (default: on for UGC and cinematic spots, off for a product-only commercial); false = render from the text shot list
modelNovideo model id from hermoso_capabilities (default: the engine the router picks for the plan: seedance-2.5 for every ad, a person talking to camera included). Naming one is a DELIBERATE pick — the server asks before ever swapping it (no silent fallback)
musicNomusic bed: OFF unless asked. true = the plan's own music line; or the bed in words ('lo-fi jazz, brushed drums'); false = none
dryRunNoreturn the routing decision (single pass vs stitched acts, resolved model + act lengths) and the estimated credits the real render reserves, WITHOUT submitting a render — free, nothing charged
lockupNobrand wordmark + tagline composited over the closing seconds. DEFAULT FALSE — set true ONLY when the user asks for branding on the close
creatorNoCAST A SAVED CREATOR in this ad — their id from list_creators, or the name you know them by (“Sarah”), or a PRESET AI creator from list_creators presets by exact name or id (free, no generation). Their saved portrait becomes the on-camera identity for the whole spot, so the same face carries across every act and across every ad you render for this brand (casting skips the portrait we would otherwise generate). Omit to let the ad cast a fresh person — EXCEPT for a CREATOR account (onboarded from their own @handle): their own saved likeness is cast by default when the plan has a person on camera and the account is on a paid Hermoso plan (a free account gets a fresh AI person), and the read-back says `default:true`; pass "none" to render without them. Refused for free, with nothing rendered, if the name matches nobody or more than one creator, if an explicitly named creator is cast on a plan with nobody on camera, or if a REAL person is cast on a free plan. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded.
endCardNoappend the branded end card. DEFAULT FALSE on every recipe — set true ONLY when the user asks for an end card (a clone of a video that had none should not grow one)
insertsNothe user's own screens laid over the finished video at these seconds (each item's look says how), sound running underneath (about 2 credits). The film always opens and ends on moving footage. Omit to use plan_ad's creative.inserts; [] renders without them. plan_ad with inserts writes the board around them.
screensNoreal screenshots of the app or site for the shots that film it on a screen (URLs, upload_file URLs or Library ids), laid over them as cards
captionsNoburn the plan's per-scene on-screen words as caption pills. DEFAULT FALSE on every recipe — set true ONLY when the user asks for on-screen text or captions; no recipe turns them on by itself
creativeYesthe FULL structured output of plan_ad (must contain video_storyboard)
ttsVoiceNovoiceover voice name (e.g. Rachel / George) when the plan voices over
faceRouteNo'face_lane' records the user's statement "I own the rights to this face" for the real person in their reference (paid plans; the likeness terms apply). Set it only when the user picked that option after a render said it could not use that face, and send the same request again; it then renders through Hermoso's face library at the normal price. Never set it on your own. The other options are a different `model` or one of the user's saved `creator`s.
textStyleNoTHE LOOK of captions and the end card, only with captions or endCard and only when the user described one: the look in WORDS ("chunky yellow comic letters, purple outline"), a preset (editorial: big serif title + small italic line; bold: condensed caps, outline; minimal; handwritten; boxed; pill, the default), or fields. "TITLE · small line" puts the part after the dot on a second line.
musicModelNocompose the bed on minimax-music-3 or lyria-3.5, skipping the free library
resolutionNoOmit = the default tier, hermoso_capabilities defaultResolution (720p), the tier its `credits` prices are for. 1080p is ~2.25x, 4k more, 480p less: quote creditsByRes and pass the tier you quoted. A tier a model's `resolutions` lacks renders at its best one.
aspectRatioNooutput aspect ratio, e.g. 9:16 (default) / 1:1 / 16:9
foundationsNoproduct commercial / cinematic: 'choose' paints ONLY the foundations (identity sheet + four-up moodboard, or hero + location) and returns them, no video — then call again with moodboardPick + foundationImages
screenChoiceNothe user's answer to needsAppScreens: brand = their saved app screens, site = found on their website (saved to the brand), without = none, device kept turned away
moodboardPickNowhich moodboard panel (1-4, left to right, top to bottom) the storyboard follows; default 1
durationSecondsNototal ad length in seconds, 4–180 (clamped). Omit to keep the plan’s own length, almost always right: this only RE-TIMES the authored board; to change the ad’s length, re-run plan_ad with durationSeconds. A length that fits ONE clip renders in one pass; longer is stitched from acts of the model’s clip maximum (15s on most, 30s on the longest-clip one), remainder last. dryRun:true shows the split free.
foundationImagesNoimages a foundations:'choose' call returned, reused as-is (not repainted)
allowGenericProductNoproceed even though this brand has NO product photo on file and the ad features a product — the packaging will be INVENTED. Only pass true after telling the user that and hearing they are fine with a generic stand-in
report_bugReport a bugAInspect

Report a bug in Hermoso to the team. Use this when something in Hermoso genuinely misbehaves — a tool errors unexpectedly, returns a wrong or malformed result, a render comes back broken, or documented behaviour doesn't match what happened. Include what you were trying to do, the exact tool call and arguments, and what came back. Do NOT use it for out-of-credits, a policy refusal, or a missing capability (use request_feature for that). Free, no credits.

NameRequiredDescriptionDefault
detailsYeswhat you were doing, the tool + arguments you called, what you expected, and what actually happened (paste the exact error)
summaryYesone-line summary of the bug
severityNohigh = blocks the task or loses paid work; medium = wrong output but workable; low = cosmetic
repurpose_videoRepurpose a video for every placementAInspect

ONE video in, EVERY placement out, in one job, as a named file list ([_].mp4) with a manifest. Placements: 'everywhere' (default: 9:16, 4:5, 1:1, 16:9), any W:H, a pixel size (1200x628) or a platform word (tiktok, reels, stories, feed, square, youtube, linkedin, pinterest). Each is reframed by a BLUR-PAD by default (the whole frame over a blurred copy of itself; free, no AI), reframe 'crop' (fill, edges cut; free) or 'ai' (the new canvas generated around the frame; paid, for a canvas the engine makes, else padded and said). A placement may be {placement, reframe} to mix. Optional captions (word-timed, ~4 words a cue, placed for each canvas) and languages (a lip-synced dub per language; every placement of each). A pack of pads and crops only is billed 0. dryRun:true = the exact price (the sum of every leg's own price), which is what it charges. For ONE canvas use post_edit op pad.

NameRequiredDescriptionDefault
padNothe pad's look: fill 'blur' (default) or a #rrggbb colour, blur 0-1, dim 0-0.6, position top/center/bottom or 0-1
nameNothe asset name the files are named after
videoYesthe video: an https / upload_file URL, a Library id (L0…), or a local path on the CLI
dryRunNoprice only: nothing made, nothing charged
reframeNodefault 'pad' (free); 'ai' is paid and only when the user asks
captionsNotrue = word-timed captions on every file (only when the user asks); a textStyle preset name or object sets the look
languagesNodub into these languages (paid), e.g. ['Spanish','de']; the original is kept too
placementsNodefault ['everywhere']; a canvas, a WxH size, a platform word, or {placement, reframe}
wordsPerCueNocaptions: 1-8 words a cue (default 4)
request_featureRequest a featureAInspect

Ask the Hermoso team for a capability that doesn't exist yet. Use this when you need something Hermoso genuinely can't do — an unsupported platform or channel, a missing model, an export format, a tool that would have completed the user's task but isn't available. Say what the user was trying to achieve, not just the feature name — the use case is what gets built. Free, no credits.

NameRequiredDescriptionDefault
detailsYeswhat the user was actually trying to achieve, why the current tools couldn't do it, and what you'd expect the capability to do
summaryYesone line: the capability you need
reschedule_postChange a scheduled postA
DestructiveIdempotent
Inspect

Change a post that is still QUEUED — move it to a different time, rewrite the caption, swap the media, add or drop a channel, or change which board / Page / company Page / listing it goes to. PASS ONLY WHAT CHANGES: an omitted field is left exactly as it was, and an explicit empty string CLEARS one (linkedinOrganizationId:"" moves a company-Page post back to the person’s own profile). The edited item is re-checked against the identical rules its create passed — visibility the channel can honour, per-channel length, media the channel can carry — so an edit can never slip past a refusal that a create would have caught. Get the id from list_scheduled. Something that already went out cannot be changed: a published post is edited or removed with manage_meta_post / manage_linkedin_post / delete_x_post, not rescheduled.

NameRequiredDescriptionDefault
atNothe new time: local wall clock (2026-10-30T10:00, read in `timezone` or the brand’s zone on that date), UTC (…Z) or epoch ms. Future, at most 365 days out.
idYesthe scheduled post id from list_scheduled
linkNodestination URL for a Pinterest Pin, Facebook text post or Google Business post
pollNoX — replaces the poll; an empty options list removes it.
tagsNoYOUTUBE — replaces the WHOLE tag list; an empty array [] clears the tags.
brandNoWHICH PROFILE this post is in — id or exact name from list_brands; needed when it is not the profile this connection is pinned to. A name that matches no profile, or two, is REFUSED.
eventNoGOOGLE BUSINESS — replaces the whole event record {title, startDate, startTime, endDate, endTime}.
offerNoGOOGLE BUSINESS — replaces the whole offer record {couponCode, redeemOnlineUrl, termsConditions}.
placeNoFACEBOOK — replaces the tagged place (the numeric id of its Facebook Page); an empty string removes it.
storyNoINSTAGRAM — true makes it a 24-hour Story, false an ordinary feed post. One image or one video, no carousel.
titleNoPINTEREST / YOUTUBE — replace the headline; "" clears it and goes back to deriving one from the caption
chatIdNoTELEGRAM — send it to a different chat, group or channel (@username or numeric id). It can be changed but never cleared: telegram cannot publish without one.
ideaIdNoshort id of the content-plan idea this post came from
pageIdNoFACEBOOK / INSTAGRAM / THREADS — publish from a different connected Page (list_meta_pages)
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
silentNoTELEGRAM / DISCORD — no notification.
threadNoX — replaces the WHOLE thread; an explicit [] drops back to a single post using the caption.
wpSlugNoWORDPRESS — the post slug
wpTagsNoWORDPRESS — tags (names or ids)
altTextNoACCESSIBILITY — replace the screen-reader description(s). A STRING describes every slide; an ARRAY describes them one at a time in slide order and REPLACES the whole list. "" clears it.
audioIdNoINSTAGRAM REEL — put one of Instagram’s OWN licensed music tracks under the Reel: the `id` search_instagram_audio returns. Instagram mixes it in when the Reel is published; nothing is downloaded or re-rendered. REELS ONLY; an empty string removes it, and only on an Instagram account connected through Meta (a Facebook Page with a linked Instagram), which is Instagram’s own rule — the Instagram connector cannot take it and is refused by name.
boardIdNoPINTEREST — move the Pin to a different board (list_pinterest_boards)
buttonsNoTELEGRAM ONLY — replace the URL buttons ([] removes them).
excerptNoWORDPRESS — the excerpt under the title
messageNoreplace the caption used for every channel that has no override
audienceNoFACEBOOK — replaces who can see the Page post {countries, regions, cities, minAge}; {} removes the limit.
captionsNoreplaces the WHOLE per-channel caption map — send every override you want to keep, not just the new one
channelsNoreplaces the channel list
coverUrlNoINSTAGRAM REEL — replaces the cover image url; an empty string removes it.
imageUrlNoswap the image; "" removes it
linkNameNoFACEBOOK — replaces the link preview headline; an empty string removes the override.
timezoneNoIANA zone for a wall-clock `at`
topicTagNoTHREADS ONLY — one topic tag, without the leading #.
userTagsNoINSTAGRAM USER TAGS — tag people (public accounts) on the post; each is notified. Usernames, or { username, x, y } with x/y from 0 to 1 = where the tag sits on the IMAGE (a tag without a position sits at the centre). A Reel tag takes no position; a story position is optional. Not on a carousel (refused by name).
videoUrlNoswap the video; "" removes it
whopPollNoWHOP: poll options, 2 to 10
xArticleNoX: replaces the X Article (title, headings); {} makes it an ordinary X post again.
audioNameNoINSTAGRAM REEL — replaces the audio track name; an empty string removes it.
coverAtMsNoTHE VIDEO COVER on every channel, as one frame in milliseconds (see schedule_post).
imageUrlsNoreplace the CAROUSEL slides, in order; an empty array [] drops the carousel and goes back to a single image. Omit to leave the slides exactly as they are. The edited item is re-checked against the same carousel rules the create passed, so adding a channel that cannot swipe is refused now rather than posting slide 1 later.
parseModeNoTELEGRAM ONLY — 'HTML' for Telegram's HTML tags.
placeNameNothe name the user saw for that place; a different business is refused
slideTextNoPINTEREST CAROUSEL ONLY — per-slide title / description / link, aligned to slide order.
topicTypeNoGOOGLE BUSINESS — the kind of Post; EVENT and OFFER both require `event`.
trialReelNoINSTAGRAM — replaces the trial-reel setting on a queued Reel (MANUAL or SS_PERFORMANCE); an explicit "" turns the trial off and it goes out as an ordinary Reel. Only takes effect while the post is still queued — a Reel already published cannot be converted into a trial.
yourBrandNoTIKTOK — the own-brand disclosure; false turns it off.
actionTypeNoGOOGLE BUSINESS — the call-to-action button; "" clears it.
locationIdNoGOOGLE BUSINESS — a different listing (list_business_locations)
madeWithAiNoX — the AI-media label; false turns it off.
threadNameNoDISCORD: forum thread title
threadsGifNoTHREADS ONLY — a GIPHY GIF id (the id in a giphy.com/gifs/… link) attached to a TEXT Threads post. GIPHY is the only provider Threads takes.
visibilityNoNOTE: changing this without also naming visibilityByChannel clears any per-channel overrides, so "make it all draft" is not a no-op
whopPinnedNoWHOP: pin the forum post
aiGeneratedNoAI-CONTENT DISCLOSURE (Instagram / Facebook Reel is_ai_generated, TikTok is_aigc, YouTube containsSyntheticMedia, Pinterest ai_disclosures, Threads is_made_with_ai). OMIT IT and the value already on the post stays; a post that never had one is decided from provenance at publish: a Hermoso render is declared AI-generated, media that came through upload_file or from an external URL (the user’s own photos or footage) is NOT. Pass true or false only to override.
audioVolumeNoINSTAGRAM REEL — how loud that track plays, 0–100 (Instagram’s default 100). Needs audioId.
communityIdNoX — the community to publish into; an empty string goes back to the main timeline.
descriptionNoYOUTUBE — replace the video description; "" clears it and the caption is used.
disableDuetNoTIKTOK VIDEO ONLY — block Duets.
linkPictureNoFACEBOOK — replaces the link preview image url; an empty string removes the override.
quotePostIdNoTHREADS ONLY — the id of the Threads post this one quotes.
shareToFeedNoINSTAGRAM REEL — whether the Reel also shows in the Feed grid.
thumbOffsetNoINSTAGRAM REEL — replaces the cover frame, in milliseconds; 0 removes it. Never together with coverUrl.
videoVolumeNoINSTAGRAM REEL — how loud the video’s own sound plays under the track, 0–100 (Instagram’s default 100; 0 = the track alone). Needs audioId.
whopMentionNoWHOP: notify every member
brandPartnerNoYOUTUBE: sponsoring brand channel (UC… or @handle); "" removes it
callToActionNoFACEBOOK — replaces the button on the Page post; "" removes it.
countryCodesNoTHREADS ONLY — two-letter country codes limiting who can see the post.
firstCommentNoFIRST COMMENT — posted on each new post as the same account right after it publishes, on Facebook, Instagram and LinkedIn company Pages (LinkedIn needs its comments authorization, provider linkedin_community). Other channels publish without it and their result row says so. Up to 2,000 characters, refused rather than cut; never on a story. A failed comment never undoes the post.
optimizeCopyNofit the shared caption to each channel’s own rules when you schedule it (the fitted caption is stored on the scheduled post, so what you scheduled is what publishes) wherever no per-channel caption was written; a channel with its own caption is left exactly as written. Send false to switch it off on this item.
privacyLevelNoTIKTOK — WHICH PRIVACY LEVEL the post goes out at, in TikTok’s own vocabulary. TikTok requires the USER to choose this from the levels their own account allows: call tiktok_creator_info, show them the real options, and pass back the one they picked — never a default and never a guess, which TikTok refuses at init. Omit it and the post falls back to the coarse `visibility` (private -> SELF_ONLY, otherwise PUBLIC_TO_EVERYONE), which cannot express MUTUAL_FOLLOW_FRIENDS or FOLLOWER_OF_CREATOR at all. It must agree with the TikTok visibility (SELF_ONLY is the private one) and it is refused on a TikTok DRAFT, which carries no post info.
replyControlNoTHREADS ONLY — who may reply.
slackChannelNoSLACK app: channel id or #name (default: the brand's)
thumbnailUrlNoYOUTUBE: replace the custom thumbnail; "" goes back to a frame of the video, "auto" to YouTube’s pick.
wpCategoriesNoWORDPRESS — categories (names or ids)
xQuotePostIdNoX — the post this one QUOTES; an empty string removes the quote. Named apart from the Threads `quotePostId` on this same schedule.
collaboratorsNoINSTAGRAM — replaces the WHOLE collab list (up to 3 usernames); an explicit [] removes the co-authors and the post goes out as an ordinary single-author post. Only takes effect if the post has not fired yet — an invite already sent cannot be withdrawn from here.
coverImageUrlNoTHE VIDEO COVER on every channel, as a Hermoso-hosted picture (see schedule_post).
disableStitchNoTIKTOK VIDEO ONLY — block Stitches.
discordServerNoDISCORD bot: which server
instagramPollNoINSTAGRAM — attach a POLL: 2 to 4 answers of 1–25 characters each, and the caption IS the question (so a caption is required). Feed photos, carousels and Reels only (never a story), one caption add-on per post (not with instagramCommentPrompt), and only on an account connected through Meta (a Facebook Page with a linked Instagram). WRITE-ONCE: Instagram cannot add, change or remove it after publishing, so show the user the answers first.
platformCoverNoVIDEO COVER. Omit it (the default) and Hermoso sets the video’s best frame — the same frame as its Library thumbnail — as the cover on every channel that allows one — and on X, Threads and Bluesky, which have none, a blank first frame is replaced on a copy sent there only. true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.
replySettingsNoX — who may reply; "" goes back to everyone.
slackThreadTsNoSLACK app: reply in this thread (parent ts)
brandedContentNoTIKTOK — the paid-partnership disclosure; false turns it off.
disableCommentNoTIKTOK — comments off on this post.
disablePreviewNoTELEGRAM ONLY — no link preview.
discordChannelNoDISCORD bot: channel id or #name (default: the brand's)
discordReplyToNoDISCORD bot: reply to this message id
linkAttachmentNoTHREADS ONLY — a full http(s) URL rendered as a link card on a TEXT-ONLY post. The only way a Threads post carries a destination.
slackWorkspaceNoSLACK app: which workspace
targetAudienceNoLINKEDIN COMPANY PAGE — replaces who sees the post; {} removes the limit. The matching audience must be over 300 followers.
whopVisibilityNoWHOP: who can see the forum post
discordMentionsNoDISCORD: let mentions ping
linkDescriptionNoFACEBOOK — replaces the link preview description; an empty string removes the override.
paidPartnershipNoINSTAGRAM AND X — the paid-partnership label; false turns it off.
threadsLongTextNoTHREADS ONLY — LONG-FORM text, up to 10,000 characters, attached under a TEXT-only Threads post (Threads' text attachment). The way to publish more than Threads' 500-character post: keep a short lead in the message and put the full text here. Not with an image, video or carousel.
callToActionLinkNoFACEBOOK — replaces where the button goes; an empty string falls back to the post link.
coverTimestampMsNoTIKTOK VIDEO ONLY — cover frame in milliseconds.
crossreshareToIgNoTHREADS ONLY — also share to Instagram as a Story when it fires; false turns it off. Refused on a carousel.
threadsGhostPostNoTHREADS ONLY — a GHOST post: text only, Threads archives it automatically after 24 hours (a flash offer, a 24h-only note). No other Threads option can ride it.
whopExperienceIdNoWHOP — move it to a different forum or chat (exp_…, from list_whop_destinations).
whopPaywallCentsNoWHOP: forum-post paywall in cents (needs a title)
commercialContentNoTIKTOK — the COMMERCIAL CONTENT DISCLOSURE toggle: true when this post promotes a brand, product or service at all. TikTok requires at least one of yourBrand / brandedContent once it is on, and refuses a post that declares itself commercial without naming which kind. Either disclosure already implies it.
discordStartThreadNoDISCORD bot: open a thread under the post, this title
instagramLocationIdNoINSTAGRAM — replaces the tagged place (the numeric id of its Facebook Page); an empty string removes it. Not locationId, which is the Google Business listing.
visibilityByChannelNoper-channel visibility override, e.g. {tiktok: draft}; values public, unlisted, private or draft
whopPaywallCurrencyNoWHOP: paywall currency
crossreshareDarkModeNoTHREADS ONLY — dark-mode that Instagram Story. Needs crossreshareToIg.
paidProductPlacementNoYOUTUBE: paid product placement (shows the disclosure) — false removes it
instagramPollExtendedNoINSTAGRAM — give that poll Instagram’s extended voting duration instead of its default 3 days (Instagram’s changelog says it then stays open indefinitely). Only with instagramPoll.
instagramCommentPromptNoINSTAGRAM — make the caption a COMMENT PROMPT that people answer in the comments. Same limits as instagramPoll, and never together with it.
linkedinOrganizationIdNoLINKEDIN — target a different company Page, or "" to post as the connected person instead
brandedContentSponsorIdsNoINSTAGRAM — replaces the sponsor user ids behind the paid-partnership label (at most 2); [] removes them.
research_adsResearch adsAInspect

Use this when the user asks an open-ended ad question across several brands or platforms, such as comparisons, which angles are working in a market, or ad examples for a category. Do not use for one named brand’s current ads: pull_competitor_ads covers that. Open-ended ad research that needs JUDGMENT across platforms — comparisons, "what angle is working", "who else is doing this", anything where the right sources are not known up front. It is an agentic loop (several rounds of library pulls plus a written synthesis) and typically takes 30-60 seconds, so it is the WRONG tool for a question that names its own answer. For one named brand’s live ads use pull_competitor_ads; for one keyword or one advertiser on Meta use search_meta_ads — both are a single call and return in a few seconds. Spends credits — an agentic loop, so a handful rather than the one-call cost of a targeted search.

NameRequiredDescriptionDefault
brandNobrand name or profile object to tailor the research to; omit to use the workspace’s saved brand
queryYeswhat to research, e.g. "the longest-running protein-pancake ads on Meta"
resize_adResize a static ad for other placementsAInspect

Re-lay out ONE finished static ad for other placements: the same ad, product, copy (word for word), logo and style, recomposed natively for each canvas rather than cropped. Pass image and optionally aspectRatios from 1:1, 4:5, 9:16, 16:9, 3:4, 4:3 (default 1:1, 4:5 and 9:16; the ad's own ratio is skipped). Reads the ad's text first (3 credits) so every line survives, then one image edit per canvas. When the saved brand has a product photo and the ad shows that product, the photo rides with the edit and the product's label is read and checked against it: re-printed from the photo only when it came out wrong (the check alone is charged when it is right; one small extra charge finds the product first). fixLabel:false turns that off. Priced before it runs. Several sizes AND versions (headlines, languages, offers) at once: repurpose_image. For VIDEO use reframe_video.

NameRequiredDescriptionDefault
imageYesthe finished static ad: URL, Library item URL, upload_file URL or local path
fixLabelNofalse = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product)
aspectRatiosNotarget canvases (default 1:1, 4:5, 9:16)
restyle_videoRestyle a videoAInspect

RESTYLE a clip into a new LOOK: every frame redrawn, shots, motion, camera, framing, timing and sound kept. Looks: Restyle looks in hermoso_capabilities (claymation, knitted yarn, cel-shaded CG anime…), your own words, or a look BUILT FROM YOUR IMAGES (styleImages); characters draws your own characters in. engine auto (default) picks by what is in the clip: a real-looking person -> kling (Kling 4.0 Edit, 3-15s); none -> wan (Wan 3.0, recommended, ≤15s) else seedance (Seedance 2.5, 4-30s); wan-prime = Wan 3.0 Prime by name (costs more). Paid; dryRun quotes it. One change only: edit_video.

NameRequiredDescriptionDefault
styleNoa look id from hermoso_capabilities (e.g. claymation, knitted-yarn, cel-shaded-anime-cg)
videoYesthe source video URL (a render, job result or list_library)
engineNodefault auto
describeNoa look in the user’s own words, when no preset fits, or extra detail on top of a preset
faceRouteNo'face_lane' = the user's "I own the rights to this face" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own.
charactersNocharacters to draw the people as. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded.
resolutionNoSeedance and Wan; default 1080p
styleImagesNopictures that ARE the look (their style, never their subject)
resume_jobResume a stopped filmAInspect

Finish a video-blocks explainer or faceless-channel film (make_explainer) that STOPPED partway: a block the video model would not render, or a server restart. What it already made is kept (the script, the style key, the narration and every block that rendered), so this renders ONLY the missing blocks and re-assembles the same film, with the same topic, look, voice, length and options. It reserves credits only for what is missing and never charges again for what was kept. get_job and list_jobs show which jobs can be resumed and which blocks are kept. Quote it with dryRun first and tell the user the number. A finished film is not resumed: to redo one block of a finished film, use fix_beat with its job id.

NameRequiredDescriptionDefault
jobIdYesthe id of the film that stopped (job_...), as get_job or list_jobs shows it
dryRunNotrue = return the credits the resume reserves and render nothing
retry_scheduledRetry a failed scheduled postA
Destructive
Inspect

Send a post that FAILED again. A scheduled post fans out across its channels INDEPENDENTLY, so a failure is usually PARTIAL — LinkedIn 401s while Instagram published fine — and this re-fires ONLY the channels that did not succeed by default (list_scheduled reports them as retryable). It re-queues the same content as a NEW post that goes out RIGHT AWAY — the queue picks it up on its next pass, within seconds — and the original keeps its failure record so the history still shows what went wrong. Naming a channel that already published is REFUSED rather than quietly posting a second time. Two independent belts stop a double-post: a channel that genuinely published can only REPLAY (nothing is posted), and a channel whose outcome is UNRESOLVED — the platform timed out and may be holding the post — refuses with that reason instead of guessing. Retry after fixing the cause — and you can fix it IN THIS CALL: pass boardId, pageId, linkedinOrganizationId, locationId, message or captions to correct the value that failed, and the corrected post is re-validated exactly like a fresh schedule. That matters because the commonest cause is a field, not an outage: a Pin aimed at the wrong board fails identically however many times it is re-sent. Anything you do not name is copied from the original. To send the same thing again ON PURPOSE, use duplicate_scheduled.

NameRequiredDescriptionDefault
atNohold the retry until a later time — ISO timestamp or epoch milliseconds. Leave it out to retry immediately, which is almost always what you want. A time you name here must be at least a minute from now, exactly like any other scheduled post.
idYesthe scheduled post id from list_scheduled
brandNoWHICH PROFILE this post is in — id or exact name from list_brands; needed when it is not the profile this connection is pinned to. A name that matches no profile, or two, is REFUSED.
chatIdNoCORRECT THE TELEGRAM DESTINATION on retry — the @username or numeric id of the chat. A post aimed at a chat the bot is not in fails every time it is retried until this changes.
pageIdNoCORRECT THE PAGE on retry — which connected Facebook Page (and its linked Instagram/Threads) publishes, from list_meta_pages.
boardIdNoCORRECT THE BOARD on retry — the Pinterest board the Pin goes on, from list_pinterest_boards. A Pin aimed at a board Pinterest refuses fails the same way on every retry until this is changed.
messageNoCORRECT THE CAPTION on retry — use this when the original was refused for length or content. Anything not named here is copied from the original post.
captionsNoCORRECT ONE CHANNEL’S CAPTION on retry, e.g. { "x": "..." } when only that channel refused the text.
channelsNoretry only these channels (default: every channel that did not publish)
locationIdNoCORRECT THE LISTING on retry — which Google Business Profile location, e.g. "locations/123" from list_business_locations.
allowDuplicateNoONLY for a channel you have checked by hand and confirmed the post is genuinely NOT there. It bypasses the double-post protection and can publish a second public copy, so never set it to work around a refusal you have not investigated.
whopExperienceIdNoWHOP — correct the forum or chat on retry (exp_…, from list_whop_destinations).
linkedinOrganizationIdNoCORRECT THE LINKEDIN AUTHOR on retry — the company Page id from list_linkedin_pages. Set it to an empty string to fall back to the personal profile.
run_post_refillRun autopilot now / approve its draftsA
Destructive
Inspect

Run AUTOPILOT POSTING (autoposting, paid plans only) NOW instead of waiting for its daily turn, or review the drafts it made: schedule them, delete one, change a caption, or save a note such as "too salesy". By default it is a preview: it returns the posts it WOULD make — each new picture’s scene, the caption, the channels and the price — rendering nothing and queueing nothing. The preview still WRITES the scenes and copy with a model, which bills its model cost (the reply says exactly how many credits). dryRun:false actually renders a fresh image or video for each post within the budget (credits are spent) and then, by mode, schedules them ("auto") or keeps them as drafts ("review"). It never re-posts the Library. REVIEWING A BATCH: pass approve (draft ids, or ["all"]) to schedule drafts, discard to delete them, edit ([{id, message?, captions?, title?, at?, channels?}]) to change one first (a new at reschedules it); get_post_refill lists them. learn saves notes future batches follow (free). redo remakes ONE draft from a note and is paid; without confirm:true it returns only the price. A scheduled post can still be pulled before it goes out with cancel_scheduled. Every caption is screened against the voice rules and a failing one is dropped, so a plan can come back shorter than the cadence — the reason is in the notes.

NameRequiredDescriptionDefault
editNoREVIEW: change drafts before approving them. Edited copy is screened by the voice rules.
redoNoremake one draft from feedback; paid, so without confirm:true it returns only the price
forceNoplan a preview even while autoposting is switched off — useful for showing someone what it would do before they turn it on.
learnNonotes for future batches, e.g. "too salesy". Free.
dryRunNodefault TRUE (a preview: nothing rendered, nothing queued; the copy it writes bills its model cost). false renders fresh creative and schedules or drafts it per the mode.
approveNoREVIEW: draft ids to schedule, or ["all"]. A draft whose time has passed moves to the next free posting slot.
discardNoREVIEW: draft ids to delete, or ["all"]. Nothing is posted for them.
save_creatorSave a creatorAInspect

Add a portrait to this workspace’s reusable CAST so the SAME person can star in future ads — the headless twin of the app’s + > Pick a creator > save. Pass the portrait’s public url (a generate_image render of an AI person, or a photo of a real person you have permission to use, or of yourself; never a photo just because it is public) plus a name to call them by; from then on list_creators returns them and their url can be re-passed to generate_avatar / generate_video / recast_motion. Saving is FREE and renders nothing. LIKENESS: leave source "generated" for an AI-made person (free on every plan) and use "upload"/"social" for a REAL person. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded.

NameRequiredDescriptionDefault
lookNotheir canonical wardrobe/appearance in words — reused to hold the look steady across ads
nameYeswhat to call this creator (e.g. “Sarah”) — list_creators and the app’s picker match on it
brandNoprofile id/name from list_brands, this call only
imageNoREQUIRED except with useAnyway. public https url of the portrait (an existing render’s url, or any public photo). Not a local file path — upload it with upload_file first and save the url that returns
posesNoup to 4 extra full-body / angle plates of the SAME person (public urls) — they make a wider shot hold the identity
voiceNoa default voice name for this persona (engines + voices are in hermoso_capabilities)
sourceNo"generated" (default) = an AI-made person; "upload" / "social" = a REAL person
useAnywayNoonly for a creator whose saved photo was flagged too unclear to cast (render_ad says so): true casts the current photo as it is, no new image needed
save_playbookSave a playbookAInspect

Save a reusable PLAYBOOK — the strategy takeaways worth re-running: the hooks that work, the angles, the formats, and the concrete plays. Use it to keep what a competitor_teardown or mine_angles just found, or to bank a creative you want to repeat. Lands in the same Playbooks library the web app lists, runs and manages. Distinct from save_skill (a directive applied to every ad) and from the swipefile (raw saved creative). Free.

NameRequiredDescriptionDefault
nameYesthe playbook headline — what it is, in a few words
brandNoprofile id/name from list_brands, this call only
hooksNothe opening hooks worth reusing, verbatim
playsNothe concrete plays to run ({title, detail}) — the actionable half
anglesNothe persuasion angles ({title, detail})
sourceNowhere it came from, e.g. “teardown · Ridge”
formatsNothe formats/recipes this plays best in (e.g. ugc_selfie, cinematic, static)
save_skillSave a skillAInspect

Save a reusable custom SKILL — a named creative directive/playbook applied to future ads (a hook formula, a UGC recipe, a compliance rule, a named specialist persona like “our founder-story style” or “short-form ad strategist”). Distill an imperative, self-contained directive. Merges into the workspace Skills library (list_skills shows built-ins + your custom skills).

NameRequiredDescriptionDefault
nameYesshort skill name, e.g. “Founder-story hook”
brandNoprofile id/name from list_brands, this call only
directiveYesthe full instruction the skill applies when used (1–6 sentences, imperative)
save_to_driveSave file(s) to Google DriveAInspect

Save a Hermoso render — or ANY file — into the user’s connected Google Drive. Pass a Hermoso render URL as url (or urls[] for several); for a local/external file, call upload_file first and pass the url it returns. Optional folder (created if new) + name. Returns the Drive file(s) with a webViewLink. Needs Google Drive connected (Settings > Connectors > Google Drive — one connection covers Drive, Sheets and Docs). NOTE: Hermoso uses the drive.file scope, so it reaches ONLY the files it created plus any the user explicitly handed over with the Google file picker in the app — never their whole Drive.

NameRequiredDescriptionDefault
urlNoa single Hermoso render URL to save
nameNofile name (single save)
urlsNoseveral render URLs (up to 20) to save in one call
folderNoDrive folder name to save into (created if new)
save_to_notionSave to NotionAInspect

Save to the user's Notion as a NEW page: a research report, competitor teardown or campaign report as content (markdown: headings, lists, links, and an image or video URL on its own line becomes the media), a swipefile collection (swipefileCollection: name or id; each ad with its creative, copy, run dates and link), the content calendar (includeSchedule:true), or a mix. Put it under parentPageId, as a row in databaseId, or omit both for a private page in their workspace. Long text is split across blocks, never cut. The answer is read back from Notion. Free; never overwrites anything.

NameRequiredDescriptionDefault
iconNoone emoji
titleNotitle of the new Notion page
contentNomarkdown body
databaseIdNodatabase id or link: saves as a new row
dataSourceIdNodata source id to save into as a new row; pass only one of parentPageId, databaseId, dataSourceId
parentPageIdNopage id or link to save under
includeScheduleNoadd the scheduled posts
swipefileCollectionNoswipefile collection name or id
save_to_onedriveSave file(s) to OneDriveAInspect

Save a Hermoso render — or ANY file — into the user’s connected Microsoft OneDrive. Pass a Hermoso render URL as url (or urls[] for several); for a local/external file, call upload_file first and pass the url it returns. Optional folder (created if new) + name. Returns the OneDrive file(s) with a webViewLink. Needs OneDrive connected (Settings > Connectors > OneDrive).

NameRequiredDescriptionDefault
urlNoa single Hermoso render URL to save
nameNofile name (single save)
urlsNoseveral render URLs (up to 20) to save in one call
folderNoOneDrive folder name to save into (created if new)
save_to_swipefileSave ads to the swipefileA
Idempotent
Inspect

Save one or more ads/creatives to a named SWIPEFILE collection, creating the collection if it does not exist — the headless twin of the heart on every ad card in the web app. Use it whenever research turns up something worth keeping: a competitor ad from search_meta_ads / pull_competitor_ads, an organic post, or one of your own renders. Saved ads persist to the workspace board the web Swipefile tab shows, and feed the taste signal every future ad is planned against. De-dupes: re-saving the same ad (same key/link/media) MOVES it into the named collection instead of duplicating it. Free.

NameRequiredDescriptionDefault
brandNoprofile id/name from list_brands, this call only
itemsYesthe ads to save
collectionYesthe collection name — an existing one, or a new one to create
schedule_postSchedule a post for laterA
Destructive
Inspect

Use this when the user wants to schedule a social media post for later, on one channel or several at once. Scheduling and publishing cost no credits on any Hermoso plan (X alone bills per call: hermoso_credits gives the exact credits per X post). THE FREE PLAN INCLUDES 10 POSTS A MONTH ON UP TO 3 CHANNELS (a post counts once, however many channels it goes to; counted in the UTC month the post goes out); every paid plan posts without a limit. A post over that allowance is refused here and nothing is queued; the refusal says how many are used and when the count resets. Relay it as written, never retry it and never split the post to fit. billing_status and list_scheduled show how many are used. Queue a post for a future time on one or more connected channels at once: facebook, instagram, threads, tiktok, youtube, linkedin, x, pinterest, bluesky, telegram, wordpress, discord, slack — every one live. whop works where its connector is offered (list_connectors says) and is held back elsewhere: there a whop post is refused, not queued. A MULTI-SLIDE creative goes in imageUrls[] as a CAROUSEL, in order — never schedule just its first slide. Name the time in at, or pass useQueue:true for the brand’s next free POSTING SLOT (what “just queue it” means). imageUrl/videoUrl take a Hermoso render URL or an upload_file URL. captions gives a channel its own wording; the rest use message. PINTEREST AND YOUTUBE SHOW A TITLE: title (max 100 chars), derived from the caption when omitted; YOUTUBE also takes description, tags, thumbnailUrl. Every PER-CHANNEL SETTING is a parameter below, carried straight to the real publisher — TikTok’s brandedContent / yourBrand disclosures (set them whenever the post is commercial), Google Business topicType / event / offer / actionType, an X thread / poll, Instagram collaborators, and the rest. Channels are attempted INDEPENDENTLY: one failing never blocks the others. SOME CHANNELS MUST BE TOLD WHICH ACCOUNT, and Hermoso never guesses: Pinterest needs boardId (list_pinterest_boards) or is refused; a LinkedIn COMPANY PAGE post needs linkedinOrganizationId (list_linkedin_pages) or it goes to the person’s own profile. Several Facebook Pages need pageId (list_meta_pages): a Facebook post on a brand with several Pages and none named is refused when you schedule it, with the Pages listed by name and id, so ask the user which one. Several Google Business listings need locationId (list_business_locations), and without it that post is refused when it fires. Resolve those FIRST and let the user pick. A scheduled post GOES LIVE PUBLICLY by default on every channel, never quietly downgraded. Only if the user asks, set visibility (or visibilityByChannel): ‘unlisted’ (YouTube) · ‘private’ (YouTube, or TikTok SELF_ONLY) · ‘draft’ (TikTok, or an unpublished Facebook Page post). A visibility a channel cannot do is REFUSED now, never posted weaker later.

NameRequiredDescriptionDefault
atNowhen to post: local wall clock, no offset (2026-10-30T10:00), read in `timezone` or the brand’s zone ON THAT DATE so a series survives a clock change; or UTC (…Z) / epoch ms. Future, at most 365 days out. Give this OR useQueue, never both.
hookNoWHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. "direct_callout", "before_after") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works.
linkNoa link to attach (Facebook)
pollNoX — attach a poll: {options:["…","…"], durationMinutes}. 2–4 options of at most 25 characters each; voting runs 5–10080 minutes (7 days), default 1440. X makes a poll MUTUALLY EXCLUSIVE with media, so an item carrying an image or video is refused — schedule the poll as its own X-only post.
tagsNoYOUTUBE — up to 30 search tags for the video (plain words, no #).
brandNoWHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted.
eventNoGOOGLE BUSINESS — required for an EVENT or OFFER post: {title, startDate:"YYYY-MM-DD", endDate, startTime:"HH:MM", endTime}. `title` is the EVENT’s headline, a different thing from the post `title` (which is the Pinterest/YouTube one). Google documents its TimeInterval as needing all four date/time parts to be valid, so send the times whenever you know them.
offerNoGOOGLE BUSINESS — OFFER posts only: {couponCode, redeemOnlineUrl, termsConditions}. redeemOnlineUrl is where an offer actually sends people, since the button link is ignored on an Offer.
placeNoFACEBOOK — tag a location on the Page post. The pageId from search_meta_places: the NUMERIC ID OF THE FACEBOOK PAGE for that place, not a place name and not coordinates.
storyNoINSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. Link stickers are not available on a Story posted here. An Instagram Story keeps its caption: people see the start under the Story in the mobile app and tap for the rest. It takes NO collaborators, product tags or trialReel — any of those is refused by name and nothing is posted. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page.
titleNoPINTEREST / YOUTUBE — the headline, max 100 characters. Pinterest shows it in search and under the pin; YouTube requires one. Leave it out and Hermoso derives one from that channel’s caption (first sentence, cut on a word boundary, trailing hashtags dropped) — set a real one whenever the caption does not open with a usable headline.
chatIdNoTELEGRAM — REQUIRED whenever telegram is a channel: WHICH chat, group or channel the bot posts to. A public channel’s @username (@yourchannel) or the numeric id (a group is negative; a supergroup or channel starts with -100). There is no default and there cannot be one — the Telegram Bot API publishes no method that lists a bot’s chats — so scheduling telegram without one is refused up front. list_telegram_chats finds ids for chats that have messaged the bot in the last 24 hours.
ideaIdNoshort id of the content-plan idea this post came from
pageIdNoFACEBOOK / INSTAGRAM / THREADS — which connected Facebook Page (and its linked Instagram) publishes, from list_meta_pages. Needed when the profile has more than one Page connected: a Facebook post with several and no id is refused WHEN YOU SCHEDULE IT, with the Pages listed by name and id, so ask the user which Page and schedule again with its id. It is never sent from a guessed Page.
recipeNothe post's FORMAT id, e.g. "slideshow" or "imessage_chat" — post_performance groups by it, so reuse one id per format
silentNoTELEGRAM / DISCORD — no notification.
threadNoX — publish a THREAD, one entry per post, each replying to the one before (at most 25). 280 characters per part without X Premium, up to 25,000 with it; nothing is truncated. It REPLACES the X caption: with a thread set, `message`/`captions.x` is not sent to X at all. A thread cannot carry a poll.
wpSlugNoWORDPRESS — the post slug
wpTagsNoWORDPRESS — tags (names or ids)
altTextNoACCESSIBILITY — the screen-reader description of the attached image. ONE STRING describes the picture; on a CAROUSEL it describes EVERY slide. Pass an ARRAY of strings instead to describe each slide separately, aligned to the slide order — use that on a multi-slide post, because one sentence read out over six different pictures is wrong for five of them. More descriptions than pictures is refused rather than dropped. Write one whenever the post carries an image: describe what is IN the picture, never a repeat of the caption, which a screen reader already reads. CARRIED BY: X (max 1000, one per media), Pinterest (max 500 — PIN-LEVEL only, since its API has no per-item alt text, so slide 1’s description is used for the whole Pin and the result says the others were not sent), LinkedIn COMPANY PAGES (max 4086, one per slide), INSTAGRAM image posts and image slides (max 1000), FACEBOOK photos and albums, and BLUESKY, whose lexicon makes it REQUIRED on every image. The schedule is REFUSED if the LONGEST description exceeds the tightest of the channels on it, rather than truncated on the way out. NOT CARRIED, and none of these is a refusal — the post still publishes, just undescribed there, and the per-channel result says which: TikTok (its photo post has no alt field at any level), a THREADS CAROUSEL, an INSTAGRAM Reel or video slide, and a LinkedIn PERSONAL-profile post.
audioIdNoINSTAGRAM REEL — put one of Instagram’s OWN licensed music tracks under the Reel: the `id` search_instagram_audio returns. Instagram mixes it in when the Reel is published; nothing is downloaded or re-rendered. REELS ONLY, and only on an Instagram account connected through Meta (a Facebook Page with a linked Instagram), which is Instagram’s own rule — the Instagram connector cannot take it and is refused by name.
boardIdNoPINTEREST — REQUIRED whenever pinterest is a channel: the board the Pin goes on, from list_pinterest_boards. The user picks it; a Pin on the wrong board is a public mistake. Scheduling pinterest without one is refused immediately.
buttonsNoTELEGRAM ONLY — URL buttons under the post, as post_to_telegram takes them; never on an album.
excerptNoWORDPRESS — the excerpt under the title
messageNothe caption/text used for every channel unless overridden in captions
subjectNoWHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. "winter coat", "free trial"). post_performance's second grouping axis: reuse the exact wording, as with hook.
accountsNoWHICH accounts of a multi-account channel to post to, e.g. { "tiktok": ["@a", "@b"] } or { "tiktok": "all" } — one row per account at fire time, each with its own result. Omit for channels with one account (several and none named is refused by name).
audienceNoFACEBOOK PAGE POST ONLY — limit who can see this organic post to people in these places and/or over this age (Facebook allows 13, 15, 18, 21 or 25). Not on Instagram, Threads or a Facebook Reel (a vertical clip becomes a Reel): those are refused. Region and city keys come from the Meta ads location search.
captionsNoper-channel caption overrides, e.g. { "instagram": "…", "threads": "…" } — platforms want different lengths and hashtag conventions
channelsYesone or more channels to post to at that time
coverUrlNoINSTAGRAM REEL COVER — a public image url Instagram fetches and uses as the cover in the Reels tab. REELS ONLY, and the alternative to `thumbOffset`: passing both is refused, since they are two answers to the same question. Run a local file through upload_file first.
imageUrlNoa Hermoso render URL (a /generated path, or what upload_file returned), a data: URI, or a public https URL. NOTE: only Facebook/Instagram/Threads accept an arbitrary public URL — X, TikTok, YouTube, LinkedIn, Pinterest and Google Business re-host the bytes and REFUSE anything that is not a Hermoso render, so run an external file through upload_file first and schedule that url; a URL, not a local file path; use upload_file for local files
linkNameNoFACEBOOK — override the HEADLINE of the link preview. Without it the post shows whatever Open Graph title the destination carries, which on a bare landing page is often nothing. Needs `link`. MEASURED 2026-09-17: Facebook only honours these when the Page’s business has VERIFIED the domain (Business Manager > Brand safety > Domains) — without that Meta refuses the post outright with “Only owners of the URL…”, so leave them out unless the brand owns and has verified that link’s domain.
timezoneNoIANA zone (e.g. "Africa/Cairo") a wall-clock `at` or the queue is read in; overrides the brand’s saved zone for this one post. Without either, UTC.
topicTagNoTHREADS ONLY — one topic tag for the post, without the leading #.
useQueueNoinstead of naming a minute, drop this into the brand’s POSTING QUEUE: Hermoso takes the earliest of its saved posting times that is still free (skipping any slot another queued post already holds). This is the natural answer to “just queue it” / “post it at my next opening”. Mutually exclusive with `at` — passing both is refused rather than one being silently preferred. If the profile has no posting times set, or every slot for the next 90 days is taken, it is refused by name and nothing is scheduled.
userTagsNoINSTAGRAM USER TAGS — tag people (public accounts) on the post; each is notified. Usernames, or { username, x, y } with x/y from 0 to 1 = where the tag sits on the IMAGE (a tag without a position sits at the centre). A Reel tag takes no position; a story position is optional. Not on a carousel (refused by name).
videoUrlNoa Hermoso render URL (a /generated path, or what upload_file returned), a data: URI, or a public https URL — required for youtube, and for tiktok unless you pass an imageUrl (TikTok takes a photo post too). Same origin rule as imageUrl: everything except Facebook/Instagram/Threads REFUSES a non-Hermoso URL, so pass external video through upload_file first; a URL, not a local file path; use upload_file for local files
whopPollNoWHOP FORUM POSTS ONLY — a poll: 2 to 10 options, in order.
xArticleNoX: publish the X item as a long-form X ARTICLE. title is its headline, the X text (message or captions.x) its markdown body, the image its cover. Refused now if the markdown has formatting X cannot hold. X allows about 5 Articles a day; one that fires into that cap fails with the reset time and can be retried.
audioNameNoINSTAGRAM REEL — the name of the Reel’s audio track, which is what viewers tap through to. REELS ONLY.
coverAtMsNoTHE VIDEO COVER on EVERY channel of this post, as ONE frame: milliseconds from the start (7000 = the frame at 7s). Instagram, TikTok, Facebook, LinkedIn Page, Pinterest, Telegram and YouTube all get that frame; X, Threads and Bluesky have no cover setting. A channel’s own field (thumbOffset, coverTimestampMs, coverUrl) wins there and otherwise counts as this.
imageUrlsNoCAROUSEL — an ORDERED list of image URLs to publish as ONE swipeable post on every channel that supports it (Instagram 2–20, Threads 2–20, Facebook, LinkedIn company Pages 2–20, Pinterest 2–5, TikTok up to 35 as a photo post). Use this whenever the creative is a multi-slide deck: a scheduled post carrying only slide 1 of a “1/6 · SWIPE” set is a broken ad that nobody is watching when it fires. THE ORDER IS THE PRODUCT. A channel on this schedule that cannot do carousels — X, YouTube, Google Business Profile — is REFUSED NOW, with the reason, so you can drop it or give it its own single image; it is never quietly downgraded hours later.
parseModeNoTELEGRAM ONLY — 'HTML' for Telegram's HTML tags; omit for plain text.
placeNameNothe name the user saw for that place; a different business is refused
slideTextNoPINTEREST CAROUSEL ONLY — per-slide title / description / link, aligned to slide order. Every other platform takes ONE caption for the whole carousel.
topicTypeNoGOOGLE BUSINESS — the KIND of Post. STANDARD is the default; EVENT and OFFER both REQUIRE `event` (title + start date), and OFFER also takes `offer`.
trialReelNoINSTAGRAM TRIAL REEL — publish this Reel to NON-FOLLOWERS ONLY at first (Instagram allows trials only on accounts above its follower threshold — about 1,000 followers; an ineligible account is refused by name and nothing is posted), so a hook can be tested on a cold audience without spending it on the people who already follow the brand; Instagram shows it to followers only if it graduates. MANUAL = the creator graduates it by hand in the Instagram app; SS_PERFORMANCE = Instagram graduates it automatically if it performs. REELS ONLY and INSTAGRAM ONLY: an image, a carousel, or a Facebook/Threads channel is REFUSED BY NAME rather than quietly published as an ordinary post — a trial that silently goes to every follower is the exact opposite of what was asked for, so Instagram must be one of the `channels` and the item must carry a video. Omit it for a normal Reel.
yourBrandNoTIKTOK — the OWN-BRAND disclosure (brand_organic_toggle): true when the post promotes the creator’s own business. TikTok asks for at least one of this and brandedContent once a post is commercial.
actionTypeNoGOOGLE BUSINESS — the call-to-action button. Every button except CALL needs `link` (CALL dials the number on the listing and takes none). Google IGNORES the button link on an OFFER post — put the destination in offer.redeemOnlineUrl. Omit and a post carrying a link gets LEARN_MORE.
locationIdNoGOOGLE BUSINESS PROFILE — which listing, e.g. 'locations/123' from list_business_locations. Needed when the account manages more than one storefront; it is never chosen for the user.
madeWithAiNoX — X’s AI-media label on this post. Opt-in: X treats it as the poster’s own claim about their media, so it is never set on the user’s behalf.
threadNameNoDISCORD: forum thread title
threadsGifNoTHREADS ONLY — a GIPHY GIF id (the id in a giphy.com/gifs/… link) attached to a TEXT Threads post. GIPHY is the only provider Threads takes.
visibilityNohow it should be published — DEFAULT 'public' (live). Only pass something else if the user explicitly asked to stage/hide it. Not every channel supports every value; an impossible combination is refused when you schedule it, with the reason.
whopPinnedNoWHOP FORUM POSTS ONLY — pin it to the top of the forum.
aiGeneratedNoAI-CONTENT DISCLOSURE (Instagram / Facebook Reel is_ai_generated, TikTok is_aigc, YouTube containsSyntheticMedia, Pinterest ai_disclosures, Threads is_made_with_ai). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared AI-generated, media that came through upload_file or from an external URL (the user’s own photos or footage) is NOT. Pass true or false only to override.
audioVolumeNoINSTAGRAM REEL — how loud that track plays, 0–100 (Instagram’s default 100). Needs audioId.
communityIdNoX — publish into an X COMMUNITY instead of the main timeline: the number in the community’s own URL (x.com/i/communities/<id>). The connected account must be a MEMBER of it.
descriptionNoYOUTUBE: the video DESCRIPTION, max 5000 characters, carrying the links, the CTA and what YouTube search reads. Omit it and the caption is used.
disableDuetNoTIKTOK VIDEO ONLY — block Duets. TikTok’s photo-post API has no Duets, so this is refused on a photo/slideshow item rather than silently dropped.
linkPictureNoFACEBOOK — override the IMAGE of the link preview: a public http(s) url Facebook fetches itself. Needs `link`. MEASURED 2026-09-17: Facebook only honours these when the Page’s business has VERIFIED the domain (Business Manager > Brand safety > Domains) — without that Meta refuses the post outright with “Only owners of the URL…”, so leave them out unless the brand owns and has verified that link’s domain.
quotePostIdNoTHREADS ONLY — the id of the Threads post this one quotes.
shareToFeedNoINSTAGRAM REEL — true puts the Reel in the Feed grid as well as the Reels tab. REELS ONLY. Left unset it follows Instagram’s own default; Hermoso does not flip it either way on the user’s behalf.
thumbOffsetNoINSTAGRAM REEL COVER, the other way — which frame becomes the cover, in MILLISECONDS from the start of the video. REELS ONLY. Use it instead of `coverUrl` when the right cover is already a frame of the clip.
videoVolumeNoINSTAGRAM REEL — how loud the video’s own sound plays under the track, 0–100 (Instagram’s default 100; 0 = the track alone). Needs audioId.
whopMentionNoWHOP FORUM POSTS ONLY — notify every member (off by default).
brandPartnerNoYOUTUBE: sponsoring brand channel (UC… or @handle)
callToActionNoFACEBOOK — a real call-to-action BUTTON on the Page post. The types that open a destination (SHOP_NOW, LEARN_MORE, SIGN_UP, BUY_NOW, DOWNLOAD, OPEN_LINK, WATCH_MORE, BOOK_TRAVEL) need somewhere to go: the post’s own `link`, or `callToActionLink`. CALL_NOW, MESSAGE_PAGE, LIKE_PAGE and GET_DIRECTIONS act on the Page itself and take none.
countryCodesNoTHREADS ONLY — two-letter country codes to limit who can see the post. Omit to show it everywhere.
firstCommentNoFIRST COMMENT — posted on each new post as the same account right after it publishes, on Facebook, Instagram and LinkedIn company Pages (LinkedIn needs its comments authorization, provider linkedin_community). Other channels publish without it and their result row says so. Up to 2,000 characters, refused rather than cut; never on a story. A failed comment never undoes the post.
optimizeCopyNoRECOMMENDED when one caption goes to several channels: fit the shared caption to each channel’s own rules when you schedule it (the fitted caption is stored on the scheduled post, so what you scheduled is what publishes) wherever no per-channel caption was written. The angle and every claim stay the author’s; a channel with its own caption is left exactly as written. Off by default so nobody’s words are rewritten unasked.
privacyLevelNoTIKTOK — WHICH PRIVACY LEVEL the post goes out at, in TikTok’s own vocabulary. TikTok requires the USER to choose this from the levels their own account allows: call tiktok_creator_info, show them the real options, and pass back the one they picked — never a default and never a guess, which TikTok refuses at init. Omit it and the post falls back to the coarse `visibility` (private -> SELF_ONLY, otherwise PUBLIC_TO_EVERYONE), which cannot express MUTUAL_FOLLOW_FRIENDS or FOLLOWER_OF_CREATOR at all. It must agree with the TikTok visibility (SELF_ONLY is the private one) and it is refused on a TikTok DRAFT, which carries no post info.
replyControlNoTHREADS ONLY — who may reply. Omit for Threads' own default (everyone).
slackChannelNoSLACK app: channel id or #name (default: the brand's)
thumbnailUrlNoYOUTUBE: the custom thumbnail, a Hermoso-hosted image (make_thumbnail, or upload_file for the user’s own). Omit it and a frame of the video is used; "auto" keeps YouTube’s pick.
wpCategoriesNoWORDPRESS — categories (names or ids)
xQuotePostIdNoX — the numeric id of an X post this one QUOTES: the last part of its URL. NAMED xQuotePostId, NOT quotePostId, because `quotePostId` on this same schedule belongs to THREADS. Billed at X’s higher LINK rate.
collaboratorsNoINSTAGRAM — a COLLAB post: up to 3 Instagram usernames invited to CO-AUTHOR it, so it appears on their profile too once they accept, with both handles in the header and the engagement shared. Handles only ("hermosoai"); a leading @ is fine. Instagram must be one of the `channels` — asking for collaborators on a schedule Instagram is not on is REFUSED now rather than discovered when it fires, and the other channels in a mixed schedule simply publish without co-authors. THE INVITE IS SENT WHEN THE POST FIRES, not when you schedule it, and it is PENDING until the other account accepts in their notifications; check with instagram_collaborators afterwards rather than telling the user it is live on both profiles.
coverImageUrlNoTHE VIDEO COVER as a picture instead of a frame — a Hermoso-hosted image (upload_file). Every channel that takes a cover image gets it (Instagram, Facebook, LinkedIn Page, Pinterest, Telegram, YouTube); TikTok takes only a frame. Never together with coverAtMs.
disableStitchNoTIKTOK VIDEO ONLY — block Stitches. Same photo-post rule as disableDuet.
discordServerNoDISCORD bot: which server
instagramPollNoINSTAGRAM — attach a POLL: 2 to 4 answers of 1–25 characters each, and the caption IS the question (so a caption is required). Feed photos, carousels and Reels only (never a story), one caption add-on per post (not with instagramCommentPrompt), and only on an account connected through Meta (a Facebook Page with a linked Instagram). WRITE-ONCE: Instagram cannot add, change or remove it after publishing, so show the user the answers first.
platformCoverNoVIDEO COVER. Omit it (the default) and Hermoso sets the video’s best frame — the same frame as its Library thumbnail — as the cover on every channel that allows one (Instagram, Facebook, TikTok direct posts, LinkedIn Pages, Pinterest, Telegram, YouTube) — and on X, Threads and Bluesky, which have none, a blank first frame is replaced on a copy sent there only. true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.
replySettingsNoX — who may reply. Omit for everyone, which is the right default for a brand post.
slackThreadTsNoSLACK app: reply in this thread (parent ts)
brandedContentNoTIKTOK — the PAID-PARTNERSHIP disclosure (TikTok’s brand_content_toggle): true when this post promotes a THIRD-PARTY business. It is a compliance declaration, not a preference — set it whenever the post is sponsored. TikTok only accepts branded content on a public or friends-only post, so it cannot ride a private/SELF_ONLY or draft TikTok item and the schedule is refused with the reason.
disableCommentNoTIKTOK — turn comments off on this post.
disablePreviewNoTELEGRAM ONLY — no link preview.
discordChannelNoDISCORD bot: channel id or #name (default: the brand's)
discordReplyToNoDISCORD bot: reply to this message id
linkAttachmentNoTHREADS ONLY — a full http(s) URL rendered as a link card. This is the ONLY way a Threads post carries a destination, and Threads attaches it to TEXT-ONLY posts (a post with media cannot also carry a card).
slackWorkspaceNoSLACK app: which workspace
targetAudienceNoLINKEDIN COMPANY PAGE POST ONLY — show the post only to Page followers matching these facets (URNs or bare numeric ids; search_linkedin_ads_targeting finds them). LinkedIn requires the matching audience to be over 300 followers and refuses a smaller one. Personal-profile posts cannot be targeted.
whopVisibilityNoWHOP FORUM POSTS ONLY — who can see it; unset is the forum’s own default.
discordMentionsNoDISCORD: let mentions ping
linkDescriptionNoFACEBOOK — override the DESCRIPTION of the link preview. Needs `link`. MEASURED 2026-09-17: Facebook only honours these when the Page’s business has VERIFIED the domain (Business Manager > Brand safety > Domains) — without that Meta refuses the post outright with “Only owners of the URL…”, so leave them out unless the brand owns and has verified that link’s domain.
paidPartnershipNoINSTAGRAM AND X — the PAID PARTNERSHIP label, a compliance declaration: set it when the post is sponsored, gifted or otherwise paid for. OPT-IN ONLY, never assume it on the user’s behalf. On Instagram, brandedContentSponsorIds names the brands behind it.
threadsLongTextNoTHREADS ONLY — LONG-FORM text, up to 10,000 characters, attached under a TEXT-only Threads post (Threads' text attachment). The way to publish more than Threads' 500-character post: keep a short lead in the message and put the full text here. Not with an image, video or carousel.
callToActionLinkNoFACEBOOK — where the button goes, when that is not the post’s own `link`.
coverTimestampMsNoTIKTOK VIDEO ONLY — which frame TikTok uses as the cover, in milliseconds from the start. Omit and Hermoso uses the video’s best frame (platformCover:true leaves it to TikTok, which uses the first frame).
crossreshareToIgNoTHREADS ONLY — when this fires, ALSO share it to the linked Instagram account as a STORY. Refused on a Threads carousel. No confirmation exists that the Story was created, so the result says it was requested.
threadsGhostPostNoTHREADS ONLY — a GHOST post: text only, Threads archives it automatically after 24 hours (a flash offer, a 24h-only note). No other Threads option can ride it.
whopExperienceIdNoWHOP — which forum or chat in the whop (exp_…, from list_whop_destinations). Omit it when the whop has only one; with several and none named the post is refused at fire time, never guessed.
whopPaywallCentsNoWHOP FORUM POSTS ONLY — a paywall in cents (500 = $5.00): readers pay to see the body; needs a title.
commercialContentNoTIKTOK — the COMMERCIAL CONTENT DISCLOSURE toggle: true when the post promotes a brand, product or service at all. TikTok requires at least one of yourBrand / brandedContent alongside it, and a post declaring itself commercial without naming which kind is refused. Setting either disclosure already implies this, so it is only needed to be explicit.
discordStartThreadNoDISCORD bot: open a thread under the post, this title
instagramLocationIdNoINSTAGRAM — tag a place (called locationId on post_to_meta; locationId here is the Google Business listing). It is the pageId from search_meta_places, the NUMERIC ID OF A FACEBOOK PAGE associated with that location, not a place name and not coordinates; a non-numeric value is refused rather than sent.
visibilityByChannelNooverride visibility for one channel, e.g. { "tiktok": "draft" } to go live everywhere but stage TikTok for review
whopPaywallCurrencyNoWHOP — the paywall currency, a 3-letter code (default usd).
crossreshareDarkModeNoTHREADS ONLY — render that Instagram Story in dark mode. Needs crossreshareToIg.
paidProductPlacementNoYOUTUBE: paid product placement (shows the disclosure)
instagramPollExtendedNoINSTAGRAM — give that poll Instagram’s extended voting duration instead of its default 3 days (Instagram’s changelog says it then stays open indefinitely). Only with instagramPoll.
instagramCommentPromptNoINSTAGRAM — make the caption a COMMENT PROMPT that people answer in the comments. Same limits as instagramPoll, and never together with it.
linkedinOrganizationIdNoLINKEDIN — publish as a COMPANY PAGE instead of the connected personal profile. The organization id from list_linkedin_pages. Omit and it posts as the person: an unset id means the profile, never “probably the company”. A Page can also carry VIDEO, which a personal profile cannot.
brandedContentSponsorIdsNoINSTAGRAM — the numeric Instagram USER IDS of the brands behind that label (at most 2, and ids rather than @handles). Naming sponsors IS asking for the label, so setting these with `paidPartnership:false` is refused instead of publishing brand credits with no disclosure.
score_adScore adAInspect

Virality/performance prediction for a finished ad (image or video URL): overall score, per-dimension breakdown (scroll-stop, hook, clarity, brand/product, CTA, retention, goal fit, own: could another brand run it unchanged), strengths, and the single biggest fix. Use BEFORE spending on distribution, or to rank variants.

NameRequiredDescriptionDefault
urlYesthe ad asset URL (a /generated/ path or public URL)
kindNo'image' (default) or 'video'
intentNowhat the ad is trying to achieve, for goal-fit scoring
search_google_adsSearch Google adsAInspect

Structured Google Ads Transparency pull for ONE advertiser (by domain or advertiserId) — use when you know the brand; use research_ads for open-ended research. Deliberately fetches the cheap BASIC listing (get_ad_details=false, ~1 credit — the detailed variant with per-ad headlines costs 25 credits/call and is not exposed here). Returns compact JSON {advertiser, format, adUrl, image, firstShown, lastShown} per ad.

NameRequiredDescriptionDefault
limitNomax ads returned (1–25, default 8)
domainNothe advertiser's domain, e.g. nike.com
regionNo2-letter region, default US
advertiserIdNoGoogle advertiser id (AR…) when the domain is ambiguous
search_instagramSearch InstagramAInspect

Organic Instagram REELS keyword search (/v2/instagram/reels/search — our only IG keyword surface; profile/hashtag pulls go through fetch_social_data with a handle). Returns compact JSON {desc, author, handle, plays, likes, link, cover} per reel, ranked by plays. It matches captions worldwide and cannot be limited to a country or language: for one market, search in the words people there use, or read named local accounts' Reels through fetch_social_data's Instagram profile endpoint, which is the better first step for a local market. Spends about a credit.

NameRequiredDescriptionDefault
limitNomax reels returned (1–25, default 8)
queryYeskeyword to search reels for
search_linkedin_adsSearch LinkedIn adsAInspect

Structured LinkedIn Ad Library search by company name, keyword, or companyId — use for a targeted B2B pull; use research_ads for open-ended research. Returns compact JSON {advertiser, headline, description, cta, link, media, dates, impressions} per ad — LinkedIn's library includes impression counts. Spends about a credit.

NameRequiredDescriptionDefault
limitNomax ads returned (1–25, default 8)
companyNoadvertiser company name
keywordNokeyword across all advertisers
companyIdNoLinkedIn company id (numeric) when the name is ambiguous
countriesNoCSV of 2-letter codes like 'US,CA'; omit or 'ALL' = worldwide
search_meta_adsSearch Meta adsAInspect

Structured Meta (Facebook/Instagram) Ad Library pull — use when you know exactly WHAT to fetch: a keyword (query) OR one advertiser (companyName / pageId). Returns compact JSON {page_name, body, cta, link, dates, media} per ad. For open-ended research that needs judgment across platforms, use research_ads instead. Spends credits on each ad-library read it makes: one for a keyword; for a brand name, the page lookup and its ads (plus a keyword search when that page has none).

NameRequiredDescriptionDefault
limitNomax ads returned (1–25, default 8)
queryNokeyword search across ALL advertisers (use INSTEAD of companyName/pageId)
domainNothe advertiser’s website, e.g. buffer.com — with companyName, used to confirm which Meta page is really theirs
pageIdNoone advertiser’s ads by Facebook page id (most precise)
statusNoACTIVE = currently running; default ALL (includes proven past winners)
countryNo2-letter code or 'ALL' (default ALL)
mediaTypeNofilter by creative type (default ALL)
companyNameNoone advertiser’s ads by brand name — resolved to the brand’s own page only on evidence (its Instagram, its domain, or ads that land on its domain); pass domain too for the best match
search_meta_placesFind a place to tag on Instagram or FacebookAInspect

Find a PLACE to tag on an Instagram or Facebook post: a shop, cafe, restaurant, venue or landmark. Give its name with the city ("Blue Bottle Coffee Oakland"), or paste its Facebook Page link, an Instagram place link (instagram.com/explore/locations/...) or its numeric id. A pasted link or id is read and comes back as that Page's taggable pageId with its name and address, or a plain reason it cannot be tagged. Returns a handful of matching Facebook Pages that have an address: pageId, name, address, category, link. Pass the chosen pageId as locationId (Instagram) or place (Facebook) on post_to_meta, or instagramLocationId on schedule_post. These are matches to CONFIRM with the user (check the name and address; a place tag is public), and Instagram checks the place when the post is made. Needs no connected account. Spends about 1 credit per Page it checks (a name search checks up to 5 Pages, so about 5 to 7 credits); a pasted Facebook Page link or numeric id is about 1 credit; an Instagram place link costs nothing.

NameRequiredDescriptionDefault
nearNooptional city or area to narrow a name search, e.g. "Paris"
limitNohow many Pages to check on a name search, 1 to 8 (default 5); each is about 1 credit
queryYesthe place name with its city (e.g. "Blue Bottle Coffee Oakland"), or a Facebook Page link, an Instagram place link, or a numeric id
search_postsTop posts about any topic, brand or productAInspect

The POSTS people make ABOUT a subject — a brand ("liquid death"), a product, a hobby ("coffee"), a hashtag ("#homecafe") — from whoever posted them, across organic TikTok, Instagram Reels and YouTube in ONE call, ranked by views. Not the brand's own ads (search_meta_ads / research_ads) and not the people (find_creators folds these same posts into creators): use it to see what is actually being posted and watched about a subject, to find clips worth cloning (clone_video), and to read the hooks and angles an audience already responds to. About one credit per platform searched (one query each by default; queries adds "best X" / "X review" / #tag variants, each a paid call); repeats inside 20 minutes are free.

NameRequiredDescriptionDefault
limitNoposts per platform, 1–60 (default 24)
topicYessubject, brand, product or hashtag — "liquid death", "coffee", "#homecafe"
queriesNoquery variants per platform, 1–4 (default 1); each is a paid search call
platformsNodefault all three
search_redditSearch RedditAInspect

Reddit keyword search (/v1/reddit/search, top-ranked) — a goldmine for the customer's OWN words (pain points, objections, language) to mine into ad hooks and copy. Returns compact JSON {desc (title+selftext), subreddit, upvotes, comments, link} per post. Spends about a credit.

NameRequiredDescriptionDefault
limitNomax posts returned (1–25, default 8)
queryYeswhat to search Reddit for
search_threadsSearch ThreadsAInspect

Organic Threads keyword search (/v1/threads/search) — short-form text/social posts for trend + voice research. Returns compact JSON {desc, author, handle, likes, link, cover} per post. Spends about a credit.

NameRequiredDescriptionDefault
limitNomax posts returned (1–25, default 8)
queryYeskeyword to search Threads for
search_tiktokSearch TikTokAInspect

Organic TikTok keyword search (there is NO TikTok ad library) — top-performing videos to mine for hooks/trends/remixable creative. Returns compact JSON {desc, author, handle, plays, likes, link, cover} per video, ranked by plays. Use research_ads for open-ended research; for what to POST, tiktok_ideas. Spends about a credit.

NameRequiredDescriptionDefault
limitNomax videos returned (1–25, default 8)
queryYeskeyword or hashtag (no # needed)
search_youtubeSearch YouTubeAInspect

Organic YouTube keyword search (/v1/youtube/search) — videos to mine for hooks/angles/long-form structure. Returns compact JSON {desc (title), author, handle, plays, link, cover} per video, ranked by views. Spends about a credit.

NameRequiredDescriptionDefault
limitNomax videos returned (1–25, default 8)
queryYeskeyword to search videos for
set_auto_reloadSet auto-reloadA
Idempotent
Inspect

Turn automatic credit reloads on or off (admin only): when the balance drops below a threshold, the card on file is charged for a top-up pack — SERVER-SIDE, even with no app open. Requires a saved card, added once in the app at first checkout/top-up; if there's none the tool tells you exactly where to add it. After that one-time card setup, agents can manage auto-reload, top-ups and plan links fully. Members (read-only billing) get an 'ask an admin' message.

NameRequiredDescriptionDefault
enabledYestrue to turn auto-reload on, false to turn it off
reloadCreditsNohow many credits to add each reload — must match a credit pack size (see buy_credits)
thresholdCreditsNoreload when the balance drops below this many credits
set_competitor_watchSet the market radarA
DestructiveIdempotent
Inspect

Set, change or stop a brand's MARKET RADAR (the standing competitor watch; Ad Spy > Watching in the web app; each brand has its own), run weekly or daily: newAds (what watched brands launched), winners (their ad passing 30 days running), trends + topPosts (formats and most-viewed posts for niche topics), ideas (3 to make). Only what you pass changes. competitors / topics REPLACE their lists (max 8 brands, 5 topics); both empty stops it and clears findings. auto:true picks 3-5 competitors, 2-3 similar brands and 2-3 topics from the saved brand or seed (one model call, billed at its token cost). dryRun:true quotes one check, saves nothing. Saving is free; each check holds its quote first and is skipped when the balance is short. runNow is refused while a check runs or just after one finished. Read it with list_watch_findings.

NameRequiredDescriptionDefault
autoNotrue picks competitors, similar brands and topics from the saved brand or seed; bills one model call at its token cost
seedNofor auto: a website, brand or niche
brandNoprofile id/name from list_brands, this call only
emailNothe weekly email
forceNowith runNow: check again right after a check, allowed once what is watched changed
dryRunNotrue quotes one check and saves nothing
runNowNocheck now (spends now)
topicsNoniche search phrases, e.g. "protein breakfast"
cadenceNoweekly (default) or daily check
modulesNoon/off; omitted keep theirs
competitorsNothe COMPLETE brand list
set_connector_accountsChoose which accounts a brand may useA
DestructiveIdempotent
Inspect

Set WHICH of a connector's accounts this profile is allowed to post to and spend from — Facebook Pages / Instagram / Meta ad accounts, Google Ads customers, LinkedIn company Pages (and the personal profile), Pinterest or Microsoft Advertising ad accounts. Pass ids from list_connector_accounts. This REPLACES the current selection: anything you leave out is un-shared, and an EMPTY list shares nothing (publishing then refuses — it fails closed by design, and the server re-verifies every id against the live connection, so an id the account cannot actually reach is rejected rather than saved). Ask the user which accounts they mean; posting as the wrong Page is a public mistake. Providers: tiktok, x, youtube, threads, bluesky, telegram, pinterest, instagram, meta, google_ads, linkedin, pinterest_ads, linkedin_ads, reddit_ads, apple_ads, microsoft_ads, google_business, google_analytics, snapchat_ads, x_ads, tiktok_ads, google_tag_manager, google_search_console, bing_webmaster. Free.

NameRequiredDescriptionDefault
providerYeswhich connector to scope
accountIdsYesthe ids (from list_connector_accounts) this profile may use — an empty array shares nothing
set_post_refillSet up, change or pause autopilot postingA
DestructiveIdempotent
Inspect

Turn AUTOPILOT POSTING (autoposting, the posting refill) on, change it or pause it. Available on paid plans: on the Free plan switching it on is refused, so relay that and do not retry. Examples: "post 2 times a day on Instagram and Bluesky about …, let me review first", "make it 1 a day", "switch to auto-publish", "pause autopilot". PASS ONLY WHAT CHANGES. It always makes NEW posts (a fresh image, carousel or video for each) and never re-posts the Library. WHERE: every connected channel by default (channels narrows it); HOW OFTEN: the brand’s posting times, and channelPostsPerDay sets fewer on a channel (e.g. {"x":1}). There are no credit limits to set — get_post_refill shows roughly what it costs a day. Switching it on is refused when no channel is connected or nothing a day would be made. SWITCHING IT ON ASKS THE BRIEF ONCE: a goal (followers / sales / launch / other) is required; content pillars (get_post_refill suggests some from the brand), formats (mix / video / carousel / image) and an avoid list are optional. Posts are written from the brief, the brand profile, what has worked on each channel (compared only within a channel), the brand’s skills and playbooks and the ads it saved. Ask the user for anything missing (goal, mode, channels) rather than guessing. mode picks what happens to each batch: "review" (the default) keeps the fresh posts as drafts until a human approves them (run_post_refill approve), "auto" schedules them straight away. enabled:false is the PAUSE — it removes the recurring job outright, and posts already queued are left alone (cancel those with cancel_scheduled). Three posting times means up to three posts a day; postsPerDay sets fewer, and to post MORE per day pass postingTimes (one time per post a day) — e.g. "2 a day" on a brand with 3 posting times is postsPerDay:2, "4 a day" needs four postingTimes.

NameRequiredDescriptionDefault
goalNoTHE BRIEF: what the posts are for — grow followers, drive sales, promote a launch, or other (say what in goalNote). Required to switch it on.
modeNo"review" (default): each batch of fresh posts waits as drafts for approval. "auto": fresh posts are scheduled to publish automatically.
avoidNoTHE BRIEF: never mention or show these — topics, claims, competitors, faces.
chatIdNoTELEGRAM — which chat, group or channel posts go to (@username or numeric id). Without one, telegram is skipped: there is no default chat and posting to the wrong one is a public mistake.
pageIdNoFACEBOOK / INSTAGRAM / THREADS — which connected Page to publish from (list_meta_pages). Omit for the brand’s only Page.
boardIdNoPINTEREST — which board Pins go on (list_pinterest_boards). Without one, Pinterest is skipped: a Pin on the wrong board is a public mistake, so it is never guessed.
enabledNoon/off. true needs a connected channel and a goal (refused otherwise). false PAUSES it: the recurring job is deleted and nothing new is made. Already-queued posts are untouched.
formatsNoTHE BRIEF: what to make — mix (images with a short video now and then, the default), video (short videos only), carousel (3 new images per post), image.
pillarsNoTHE BRIEF: content pillars / topics, up to 8 short lines. Every post belongs to one. get_post_refill suggests some from the brand profile.
accountsNoaccounts to post to on a channel holding several: {"tiktok":["@a","@b"]} or "all"; null clears. A new setup must pick; an older one posts to the primary until it does.
channelsNoREPLACES the list; a channel connected later stays out until added. "all" = every channel, later ones too, only if the user asks
goalNoteNoTHE BRIEF: a line about the goal (e.g. the launch date and what is launching).
timezoneNoIANA timezone the posting times are in, e.g. "America/New_York" (default: the brand's, else UTC)
daysAheadNohow far ahead to keep the queue full, 1–30 (default 7)
learningsNonotes learned from reviews; REPLACES the list
addChannelsNoadd these
postsPerDayNoposts a day, up to the number of posting times ("2 times a day" = 2). 0 (default) = one per posting time. To post MORE per day than there are posting times, also pass postingTimes.
postingTimesNothe brand's daily posting times, 24-hour "HH:MM" (e.g. ["09:00","13:00","18:00"]); REPLACES the list. Autopilot makes one post per posting time, and scheduled posts use the same times. get_post_refill shows the current ones.
removeChannelsNostop posting here
excludeChannelsNonever these channels, even ones connected later; [] clears
whopExperienceIdNoWHOP — which forum or chat posts go to (exp_…, from list_whop_destinations). Without one, the whop’s only forum or chat is used; with several, whop is skipped rather than guessed.
channelPostsPerDayNoper channel, how many posts a day (the first N of the posting times), e.g. {"x":1,"instagram":3}. 0 turns a channel off; null resets it to every posting time. Merged into what is saved.
forgetCaptionEditsNotrue = forget the caption edits learned from past reviews
linkedinOrganizationIdNoLINKEDIN — which company Page to post as (list_linkedin_pages). A single shared Page is used automatically; a Page that is not shared with this profile is ignored rather than failing the whole post.
set_product_imageSet product photoA
DestructiveIdempotent
Inspect

Lock an image as the ad's real PRODUCT photo and SAVE it as this brand's default product, so every later plan_ad / render_ad / generate_image grounds on the true packaging without being told again. Pass imageUrl = a product shot's URL — an image from a prior research result (an organic Instagram/TikTok post, a scraped page image), a workspace / list_product_photos url, or any public product photo. The server downloads it and runs a product+safety check: a lifestyle/scene shot with no clear product, or an off-category / unsafe image, is REJECTED and NOTHING is locked or saved (the summary says why). On PASS it persists the photo to a DURABLE url, writes it to the brand's product library as the DEFAULT, and READS THE BRAND BACK to confirm — savedToBrand and the summary report what the brand ACTUALLY holds now, never what was asked for, so if it did not become the default you are told instead of finding out from a paid render. Bills one vision check. Reads YOUR saved brand for the category match (pass brandId to target a specific profile — switches this key's active profile like use_brand).

NameRequiredDescriptionDefault
brandIdNoa profile id/name from list_brands to lock the product for; omit to use the active profile
imageUrlYesthe image URL to lock as the product (from a research result, a workspace / list_product_photos url, or any public product photo)
source_noteNoa short note on where it came from, e.g. "from their IG post"
set_roleChange a teammate’s roleA
DestructiveIdempotent
Inspect

Change a workspace member’s role — admin (full access incl. billing) or member (read-only on billing). A privilege change: confirm the exact person + new role with the user, then call with confirm:true.

NameRequiredDescriptionDefault
roleYesthe new role
emailYesthe member’s email
confirmNoREQUIRED true
share_onedrive_fileShare a OneDrive file as a linkA
DestructiveIdempotent
Inspect

Create a SHARING LINK to a OneDrive file or folder (fileId from list_onedrive_files): type view (read-only, default), edit, or embed (an link, personal OneDrive only); scope anonymous (anyone with the link, default), organization (work or school OneDrive only) or users (only people you invite); optional expiresAt (date or ISO) and password (on a personal OneDrive both need a Microsoft 365 subscription; the refusal says so). An ANONYMOUS EDIT link lets anyone holding it change or delete the file, so it needs confirm:true after you show the user that; every other link is created straight away. If Hermoso already made the same kind of link, OneDrive returns that one and the reply says it was reused. Read back from the item's permission list. 0 credits. Needs OneDrive connected.

NameRequiredDescriptionDefault
typeNoview (default), edit, or embed (personal OneDrive only)
scopeNoanonymous (default, anyone with the link), organization (work or school), or users (invited people only)
fileIdYesOneDrive file or folder id from list_onedrive_files
confirmNorequired for an anonymous edit link
passwordNopersonal OneDrive with Microsoft 365
expiresAtNoe.g. 2026-11-30 or 2026-11-30T17:00:00Z
stitch_videoStitch multi-scene videoAInspect

Render a multi-scene STITCHED video (≥2 scenes) — ONLY for spots LONGER than ONE clip of the chosen model. A multi-beat ad that FITS one clip renders better and cheaper as ONE single-pass generate_video/render_ad (a single generation carries the whole hook->demo->payoff arc) — never stitch those. What fits is the model’s own maximum from hermoso_capabilities, not a fixed number: 15s on most models, 30s on the longest-clip one, so a 30s spot need not be stitched at all if you name that model. Blocks until done. Spends credits.

NameRequiredDescriptionDefault
modelNovideo model id from hermoso_capabilities — omit to let the router pick
voiceNovoiceover voice name, e.g. Rachel / George
scenesYesarray of scene objects (visual + optional voiceover/seconds)
voiceoverNofull voiceover script spoken across the scenes
resolutionNo720p (default), 1080p for full detail, or 480p for a cheaper draft
aspectRatioNooutput aspect ratio, e.g. 9:16 (default) / 1:1 / 16:9
durationSecondsNototal spot length in seconds (defaults to the sum of the scenes’ seconds)
store_getRead a workspace storeA
Read-onlyIdempotent
Inspect

Read one of this workspace’s data stores by key, for visibility into what the app holds — playbooks, swipefile, saved locations, avatars, creations, chats, brand, memory, skills. Read-only, free. Allowed keys: heist.memory.v1, heist.skills.v1, heist.playbooks.v1, heist.avatars.v1, heist.locations.v1, heist.chats.v1, heist.creations.v1, heist.assets.v1, heist.brand.v1, adInspo.swipefile.v1. (The typed tools — list_memory / list_skills / get_brand — are friendlier for those; use store_get for the rest.)

NameRequiredDescriptionDefault
keyYesthe store key to read (one of the allowlisted keys)
limitNomax array items to return (default 50)
subscribe_linkedin_leadsTurn on real-time LinkedIn lead deliveryA
Idempotent
Inspect

Have LinkedIn push every new lead to Hermoso the moment it is submitted, and optionally relay each event on to the user’s own CRM. THE WEBHOOK LINKEDIN VALIDATES IS ALWAYS HERMOSO’S OWN: LinkedIn challenges it with our app secret (and re-challenges every ~2 hours), which no CRM, Zapier or Make endpoint can answer — so never promise a customer URL as the LinkedIn webhook. Pass forwardTo (public HTTPS) to have Hermoso relay each lead event there; leave it off to keep events in Hermoso only (list_linkedin_lead_events). The reply is read back from LinkedIn, not from the 201. Leads stay readable with list_linkedin_leads either way. Free.

NameRequiredDescriptionDefault
pageIdNothe company Page that owns the form — from list_linkedin_pages; omit when one Page is shared
leadTypeNodefaults by owner: SPONSORED for an ad account, COMPANY for a Page
forwardToNooptional public HTTPS URL Hermoso relays each lead event to (a CRM, Zapier, Make)
adAccountIdNoread forms owned by an AD ACCOUNT instead of a Page
tidy_memoryTidy the Memory listA
DestructiveIdempotent
Inspect

Clean up and consolidate the workspace Memory: drops entries that are about how Hermoso, a tool, a connector or a platform API behaves (product behaviour, not the brand), drops phone numbers and emails, and merges near-duplicate facts into one sentence each. Call with no argument to get the PROPOSAL (what would be removed and merged, with reasons) — nothing changes. Call again with confirm:true to apply it through the same typed writers the app uses (deletes carry tombstones so they stay deleted on every device). One model call, billed at its token cost.

NameRequiredDescriptionDefault
brandNoprofile id/name from list_brands, this call only
confirmNotrue to APPLY the proposal; omit to only see it
tiktok_post_statusCheck whether a TikTok post finished publishingA
Read-onlyIdempotent
Inspect

Follow a TikTok post that post_to_tiktok sent but TikTok was still processing (it came back pending:true). Pass the publishId it returned. Answers one of three states: done (PUBLISH_COMPLETE, with the public url once TikTok’s moderation has named it; urlStatus is ready, pending, private or draft), failed (with TikTok’s reason), or still processing, which is normal for a few minutes and is NOT a failure. Never post the video again while it is processing. Free, read-only. Needs TikTok connected (Settings > Connectors > TikTok).

NameRequiredDescriptionDefault
accountNowhich connected TikTok account the post went out on (@handle or id from list_connector_accounts) when the profile has more than one
publishIdYesthe publishId post_to_tiktok returned
update_brandUpdate profile fieldsA
Destructive
Inspect

Patch SPECIFIC fields of the active profile (name, domain, sells, summary, category, audience, positioning, voice, style, goal, logos, brand/product name pronunciations, profileType: explore = kept but unused) WITHOUT overwriting the rest — a read-modify-write on the saved brand. Use for “change our voice to playful”, “we sell to dentists now”. To onboard a brand from scratch, use draft_brand. If no brand is saved yet and you only INFERRED one from what the user is making, ask them to confirm it is their brand before saving it (they may be working for a client or just trying things). Only pass the fields you’re changing.

NameRequiredDescriptionDefault
goalNocurrent marketing goal
logoNologo image: URL or local file path to a PNG, JPEG or WebP (no SVG)
nameNobrand name
brandNoprofile id/name from list_brands, this call only
sellsNowhat the brand sells
styleNovisual style — palette, typography, aesthetic
voiceNobrand voice/tone
domainNowebsite domain
summaryNoone-line description
audienceNowho the brand sells to, one line
categoryNoproduct or business category, one line
logoDarkNofor light pictures
logoLightNofor dark pictures; "none" removes
pronounceNohow the brand NAME is said aloud, as a simple respelling with the stressed syllable in capitals (e.g. "KOH-dee-ak"). Videos use it as a delivery note beside the spoken line; set it when a render mispronounced the name.
positioningNohow the brand is positioned against alternatives, one line
profileTypeNowebsite, social, describe or explore; explore keeps the profile but stops using it
pronunciationsNohow PRODUCT names are said aloud, e.g. {"Power Cakes": "POW-er cakes"}. Merged into the saved ones; an empty string removes one.
update_docEdit a Google Doc in placeA
DestructiveIdempotent
Inspect

EDIT a Google Doc — the correction append_to_doc cannot make. Two shapes: replacements:[{find, replace}] rewrites specific text wherever it appears (call read_doc first and match the text EXACTLY; matchCase:false ignores case), or rewrite:"…" replaces the ENTIRE body (rewrite:"" empties it), or dropdowns:[{title, value}] sets a dropdown chip (e.g. Status -> Approved — read_doc lists every chip with its options; pass dropdownId when two share a title; an unknown title or option is refused with the real list and nothing changes); the same entry with options:[…] (the COMPLETE new list, 2–50) and/or newTitle changes the dropdown itself for every chip built from it, and an option you drop that a chip shows needs replace:{"Old":"New"}. Find/replace runs immediately and REPORTS how many occurrences changed — zero matches is reported as a FAILURE to match, never as a quiet success, because a text edit that silently does nothing is worse than one that visibly fails. A whole-body rewrite is destructive: call it without confirm first to get the character count, then confirm:true + confirmCells. Both are index-free by design — an agent cannot reliably compute Google’s character offsets, and a wrong offset deletes the wrong sentence.

NameRequiredDescriptionDefault
docUrlNoa Google Docs URL — the id is extracted from it
confirmNotrue runs a whole-body rewrite after the character count is shown
rewriteNoreplace the WHOLE body with this text ("" empties the doc)
dropdownsNodropdown chips to set, or dropdowns to change
documentIdNothe document id (from create_doc, or list_drive_files for one the user picked)
confirmCellsNoecho back the character count the unconfirmed call reported (rewrite only)
replacementsNofind/replace pairs, applied in order
update_drive_fileRename / move / trash a Drive fileA
DestructiveIdempotent
Inspect

Update a Drive file: rename (name), move it into a folder (moveToFolderId, optionally removeFromFolderId to move OUT of the old one), or trash / untrash it (trash:true|false). Pass fileId (from list_drive_files). To delete permanently, use delete_drive_file.

NameRequiredDescriptionDefault
nameNonew name
trashNotrue -> move to Trash; false -> restore from Trash
fileIdYesthe Drive file id
moveToFolderIdNofolder id to move the file into (from create_drive_folder / list_drive_files)
removeFromFolderIdNothe old parent folder id to remove (when moving)
update_notion_rowUpdate a Notion database rowA
DestructiveIdempotent
Inspect

Change properties on ONE row of a Notion database (pageId from query_notion_database) — e.g. mark a calendar row Status "Posted" and put the live post link in its URL column. properties is {"Column": value} by column name: text, number, checkbox, select (an existing option or a new one), status (must be an existing option), multi-select (array; replaces the whole value), date (ISO, or {start, end}), url, email, phone; null clears. Validated against the database's own columns before anything is sent and refused by name otherwise, never coerced; columns you do not name keep their values. Read back from Notion. trash:true moves the row to Notion's trash (restorable; returns a preview first and needs confirm:true), trash:false restores it. Free.

NameRequiredDescriptionDefault
trashNotrue moves the row to Notion trash (needs confirm); false restores it
pageIdYesthe row id or link
confirmNotrue confirms moving the row to trash; not needed for other changes
propertiesNo{"Column": value}
update_onedrive_fileRename / move a OneDrive fileA
DestructiveIdempotent
Inspect

Update a OneDrive item: rename (name) and/or move it into a folder (moveToFolderId). Pass fileId (from list_onedrive_files). To remove an item, use delete_onedrive_file.

NameRequiredDescriptionDefault
nameNonew name
fileIdYesthe OneDrive item id
moveToFolderIdNofolder id to move the item into (from create_onedrive_folder / list_onedrive_files)
update_saved_creatorUpdate a saved creator (outreach status, note)A
DestructiveIdempotent
Inspect

Set the OUTREACH STATUS and/or a NOTE on a creator already saved in the swipefile (find_creators -> save_to_swipefile, or the heart on a creator card). Status is one of new | contacted | replied | booked | passed. The note is free text (deal terms, rate, what was sent). Reads back the updated row. Use list_swipefile to find the key. Free.

NameRequiredDescriptionDefault
keyYesthe saved row's key from list_swipefile, e.g. tiktok:handle
noteNoreplaces the existing note; pass "" to clear it
brandNoprofile id/name from list_brands, this call only
statusNooutreach status: new, contacted, replied, booked or passed
update_settingsChange app settingsA
DestructiveIdempotent
Inspect

Change this account's app settings. language = the language EVERY ad, script and plan is written in from now on (chat replies follow the language the user writes in) (say the language in plain English, e.g. "German", "Japanese", "Brazilian Portuguese") — it applies to renders made over MCP as well as in the app. theme = the app's appearance, "dark" or "light". watchEmail = the weekly competitor-watch email on/off. Only pass what you are changing. Account-wide (every profile), and it takes effect on the next call.

NameRequiredDescriptionDefault
themeNoapp appearance
languageNolanguage for generated ads and copy (chat replies follow the language the user writes in) — e.g. "English", "German", "Japanese"
watchEmailNoweekly competitor-watch email on/off
update_sheetWrite to a range in a Google SheetA
DestructiveIdempotent
Inspect

CORRECT cells in a Google Sheet — write values to an exact range, overwriting whatever is there. This is the fix append_to_sheet cannot make: appending only ever adds rows at the bottom, so without this a wrong number stays wrong forever and the only "correction" is a second row contradicting the first. Pass range (e.g. "B2:C5", or "Q3 Report!B2" to name a tab — list_sheet_tabs gives the names) and values as an array of row arrays; an anchor cell like "B2" is fine and the block is written down and right from it. Writing into EMPTY cells goes straight through. Writing OVER cells that already hold values is REFUSED first, naming exactly how many filled cells would be overwritten — show the user that, get a yes, then call again with confirm:true. The result is READ BACK from the sheet, so what you report is what the sheet now holds rather than what Google accepted.

NameRequiredDescriptionDefault
rangeNoA1 range or anchor cell, e.g. "B2:C5", "B2", or "Q3 Report!B2" (default A1)
valuesNoarray of row arrays to write
confirmNorequired only when the target range already holds values
updatesNowrite SEVERAL disjoint ranges in one call, instead of range+values
sheetUrlNoGoogle Sheets link, instead of spreadsheetId
spreadsheetIdNoGoogle spreadsheet id; use this or sheetUrl
valueInputOptionNoUSER_ENTERED (default) parses formulas, dates and numbers the way typing them would; RAW stores every value as literal text
upgrade_planUpgrade planAInspect

Change the SUBSCRIPTION plan (admin only). No argument lists the monthly plans. Basic ($9.99) is the posting plan: unlimited posting and Autopilot with NO monthly credits (it includes the same 250+ free credits as the Free plan, monthly credits start on Starter, and Autopilot spends credits). Starter, Pro and Studio include monthly credits and no markup on models. Every paid plan posts without a limit (Free: 10 posts a month on 3 channels). New subscriber: a Checkout URL for the human. Existing subscriber upgrading: a quote + quote_token; once the human agrees, call again with confirm:true and quote_token to charge the card on file. Downgrade: a billing-page link; it starts at the term's end.

NameRequiredDescriptionDefault
planNothe plan id to move to (e.g. pro) — omit to list the available plans first
periodNomonthly only ('yr' buys monthly)
confirmNotrue (with quote_token) to make a quoted upgrade
quote_tokenNothe quoteToken from the quote step
upload_fileUpload a local file -> durable public URLAInspect

Persist an ARBITRARY user file (image, video, audio, PDF or document, up to 150MB) into Hermoso and get back a durable public URL that EVERY publish, schedule and ad-build tool accepts — post_to_meta / post_to_linkedin / post_to_linkedin_page / post_to_youtube / post_to_tiktok / post_to_pinterest / post_to_x / post_to_google_business / schedule_post / upload_meta_asset / upload_google_ads_asset / create_meta_ad / create_linkedin_ads_creative / set_youtube_thumbnail / save_to_drive / save_to_onedrive. Kept in the Library. THIS IS THE BRING-YOUR-OWN-CREATIVE PATH: it is for files that have NOTHING to do with a Hermoso render (media on the user's desktop, an agency's finished ad, a photo they shot), and it means you can publish, schedule and run ads through Hermoso without generating anything here. Provide exactly ONE source — passing two is an error, never a silent preference: url (ANY public http(s) link — Hermoso fetches it server-side, so nothing crosses this connection and there is no practical size limit; THIS IS THE ONE THAT ALWAYS WORKS, including on the hosted connector), path (a local file, ONLY when Hermoso runs on the user's own machine over stdio/CLI; the hosted connector cannot see their disk), or dataUri (a base64 data: URI — keep it under ~15MB, since the bytes travel over this connection). If the file is already at a public https URL, the Meta, Reddit and ChatGPT-Ads tools take it directly and re-host it safely — but LinkedIn (posts and ad creatives), Pinterest, the YouTube thumbnail and upload_google_ads_asset upload the BYTES themselves and therefore refuse an external host, so run it through here first and pass the URL this returns. When in doubt, use this: its URL works everywhere. (Calling the underlying HTTP route directly? POST /api/upload takes the file's RAW BYTES as the request body with its own content-type — NOT multipart/form-data — or ?url= with no body.) Returns {url, kind, bytes}.

NameRequiredDescriptionDefault
urlNoa PUBLIC http(s) URL Hermoso fetches server-side (private/internal addresses are refused, and every redirect hop is re-checked). Works on every surface including the hosted connector, and the bytes never cross this connection — prefer this whenever the file is reachable on the web.
nameNooriginal file name — helps pick the right extension
pathNolocal filesystem path (stdio/CLI only — refused on the hosted connector)
dataUriNobase64 data: URI of the file bytes (data:<mime>;base64,<…>) — bytes travel over this connection, so keep it small
getUploadUrlNoASK FOR A ONE-TIME UPLOAD URL instead of uploading now — use this whenever the file is on the user’s machine and you can run a shell or an HTTP request. Returns a uploadUrl you PUT the raw bytes to (any HTTP client), which answers with the durable Hermoso url. It beats `dataUri` for anything but a small image: a data: URI spends the whole file as tokens in this conversation. One file per url, and it expires.
upscale_videoUpscale videoAInspect

Upscale a video to higher resolution (2x) for final delivery. Paid render; returns the served URL. A 480p Seedance 2.5 draft: finish_draft (same take, native 1080p). Three engines: the default ('standard') is the safe precision upscaler; engine:'flux' is the FLUX 3 video upscaler (1080p/2K/4K) with an optional mode:'creative' detail-enhancement pass; engine:'detail' rebuilds fine detail such as hair, skin and fabric, the sharpest result for a hero shot and the most expensive. Pick 'flux' or 'detail' when the user asks for added detail rather than a faithful enlargement.

NameRequiredDescriptionDefault
modeNoFLUX only — 'creative' turns on its detail-enhancement pass; default precise
videoYesthe source video URL; a URL, not a local file path; use upload_file for local files
engineNodefault 'standard', the precision upscaler. 'flux' = the FLUX 3 video upscaler. 'detail' = rebuilds fine detail, costs the most
use_brandSwitch profileA
Idempotent
Inspect

Pin which profile this connection acts on. Pass the profile id or exact name from list_brands. Works for your own profiles AND one another account SHARED with you (pass its name or the profile id list_brands prints); access is verified against your real membership before it is pinned. Persists for this API key until changed, on every surface (hosted connector included — no environment variables, no restart).

NameRequiredDescriptionDefault
brandYesprofile id (e.g. default / p_xxx), its exact name from list_brands, or the profile id of one shared with you

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 25 tool updatesv0.1.525
    • Changedadd_subtitles2 fields changed
      • changedInput schema / properties / textStyle / anyOf
        Previous value: -[
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "properties": {
        -      "background": {
        -        "description": "none|pill|a colour",
        -        "type": "string"
        -      },
        -      "cardColor": {
        -        "type": "string"
        -      },
        -      "color": {
        -        "description": "any CSS colour",
        -        "type": "string"
        -      },
        -      "describe": {
        -        "description": "the look in words",
        -        "type": "string"
        -      },
        -      "font": {
        -        "description": "sans|serif|elegant|condensed|hand or any Google Fonts family",
        -        "type": "string"
        -      },
        -      "italic": {
        -        "type": "boolean"
        -      },
        -      "outline": {
        -        "type": "boolean"
        -      },
        -      "outlineColor": {
        -        "type": "string"
        -      },
        -      "position": {
        -        "description": "top|center|lower|bottom or 0.05-0.95 from the top",
        -        "type": [
        -          "string",
        -          "number"
        -        ]
        -      },
        -      "preset": {
        -        "type": "string"
        -      },
        -      "shadow": {
        -        "type": "boolean"
        -      },
        -      "size": {
        -        "description": "s|m|l|xl, '120px', or a 0.015-0.15 frame fraction",
        -        "type": [
        -          "string",
        -          "number"
        -        ]
        -      },
        -      "subFont": {
        -        "type": "string"
        -      },
        -      "subItalic": {
        -        "type": "boolean"
        -      },
        -      "textCase": {
        -        "enum": [
        -          "as-is",
        -          "upper",
        -          "lower",
        -          "title"
        -        ],
        -        "type": "string"
        -      },
        -      "tilt": {
        -        "description": "degrees, ±45",
        -        "type": "number"
        -      },
        -      "weight": {
        -        "type": "number"
        -      }
        -    },
        -    "type": "object"
        -  }
        -]New value: +[
        +  {
        +    "type": "string"
        +  },
        +  {
        +    "properties": {
        +      "background": {
        +        "description": "none|pill|a colour",
        +        "type": "string"
        +      },
        +      "cardColor": {
        +        "type": "string"
        +      },
        +      "color": {
        +        "description": "any CSS colour",
        +        "type": "string"
        +      },
        +      "describe": {
        +        "description": "the look in words",
        +        "type": "string"
        +      },
        +      "font": {
        +        "description": "sans|serif|elegant|condensed|hand or any Google Fonts family",
        +        "type": "string"
        +      },
        +      "highlightColor": {
        +        "description": "word-timed subtitles only: the word being said is lit in this colour (preset highlight sets it)",
        +        "type": "string"
        +      },
        +      "italic": {
        +        "type": "boolean"
        +      },
        +      "outline": {
        +        "type": "boolean"
        +      },
        +      "outlineColor": {
        +        "type": "string"
        +      },
        +      "position": {
        +        "description": "top|center|lower|bottom or 0.05-0.95 from the top",
        +        "type": [
        +          "string",
        +          "number"
        +        ]
        +      },
        +      "preset": {
        +        "type": "string"
        +      },
        +      "shadow": {
        +        "type": "boolean"
        +      },
        +      "size": {
        +        "description": "s|m|l|xl, '120px', or a 0.015-0.15 frame fraction",
        +        "type": [
        +          "string",
        +          "number"
        +        ]
        +      },
        +      "subFont": {
        +        "type": "string"
        +      },
        +      "subItalic": {
        +        "type": "boolean"
        +      },
        +      "textCase": {
        +        "enum": [
        +          "as-is",
        +          "upper",
        +          "lower",
        +          "title"
        +        ],
        +        "type": "string"
        +      },
        +      "tilt": {
        +        "description": "degrees, ±45",
        +        "type": "number"
        +      },
        +      "weight": {
        +        "type": "number"
        +      }
        +    },
        +    "type": "object"
        +  }
        +]
      • changedInput schema / properties / video / description
        Previous value: -"the video to subtitle"New value: +"the video to subtitle; a URL, not a local file path; use upload_file for local files"
    • Changedanalyze_video1 field changed
      • changedInput schema / properties / url / description
        Previous value: -"the video URL (a served /generated/ path or a public http(s) video)"New value: +"the video URL (a served /generated/ path or a public http(s) video); a URL, not a local file path; use upload_file for local files"
    • Changedchange_voice1 field changed
      • changedInput schema / properties / video / description
        Previous value: -"the source video URL"New value: +"the source video URL; a URL, not a local file path; use upload_file for local files"
    • Addedcheck_video
    • Changedclip_video1 field changed
      • changedInput schema / properties / video / description
        Previous value: -"the long video to clip — a YouTube/Vimeo/Loom/Dailymotion/Streamable/Rumble/Wistia/Twitch/TED watch URL, a direct https .mp4/.mov/.webm, or a Hermoso /generated/ URL"New value: +"the long video to clip — a YouTube/Vimeo/Loom/Dailymotion/Streamable/Rumble/Wistia/Twitch/TED watch URL, a direct https .mp4/.mov/.webm, or a Hermoso /generated/ URL; a URL, not a local file path; use upload_file for local files"
    • Changedclone_video1 field changed
      • changedInput schema / properties / url / description
        Previous value: -"the video to clone — a TikTok / Instagram Reel / Facebook / X / YouTube link, or a direct https video file URL"New value: +"the video to clone — a TikTok / Instagram Reel / Facebook / X / YouTube link, or a direct https video file URL; a URL, not a local file path; use upload_file for local files"
    • Changeddraft_brand1 field changed
      • addedInput schema / properties / confirmed
        Added value: +{
        +  "description": "user confirmed it is their brand: read + save site product photos (paid)",
        +  "type": "boolean"
        +}
    • Changeddub_video1 field changed
      • changedInput schema / properties / video / description
        Previous value: -"the source video URL"New value: +"the source video URL; a URL, not a local file path; use upload_file for local files"
    • Changededit_image3 fields changed
      • addedInput schema / properties / complexity
        Added value: +{
        +  "description": "gpt-image-2.6 editors only: how much it plans before drawing (default high; low suits a small focused change)",
        +  "enum": [
        +    "low",
        +    "medium",
        +    "high"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / markup
        Added value: +{
        +  "description": "MARKUP-GUIDED EDIT: drawn annotations (circles, arrows, scribbles) showing what to change, as a URL or local path: a transparent PNG of just the marks, or a copy of the same image with the marks drawn on it. Say the change in `instruction` (\"make the circled vase blue\"); the marks are not in the result. Runs on gpt-image-2.6 (or the editor named in `model`). Never with a mask",
        +  "type": "string"
        +}
      • addedInput schema / properties / model
        Added value: +{
        +  "description": "the editor, only when the user names one: gpt-image-2.6, gpt-image-2.6-flare (its own faster editor) or gpt-image-2.5. Omit for the default",
        +  "type": "string"
        +}
    • Changededit_video1 field changed
      • changedInput schema / properties / video / description
        Previous value: -"the source video URL (a render, job result or list_library)"New value: +"the source video URL (a render, job result or list_library); a URL, not a local file path; use upload_file for local files"
    • Changedfinish_video1 field changed
      • changedInput schema / properties / videoUrl / description
        Previous value: -"the served URL of the video to finish (from a previous render/job)"New value: +"the served URL of the video to finish (from a previous render/job); a URL, not a local file path; use upload_file for local files"
    • Changedgenerate_image6 fields changed
      • addedInput schema / properties / complexity
        Added value: +{
        +  "description": "gpt-image-2.6 / 2.6-flare only: how much the model plans before drawing (default high, best for dense text and complex layouts; low suits a small focused change). No separate charge",
        +  "enum": [
        +    "low",
        +    "medium",
        +    "high"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / format
        Added value: +{
        +  "description": "the file to make (default png). gif = an ANIMATED GIF, a short loop with no sound (describe the subject, the motion and the loop); svg = an editable VECTOR (logo, icon, flat illustration). Both run on gpt-image-2.6 (default) or gpt-image-2.6-flare and take no refImages, mask or markup; the saved brand's photos stay off them. hermoso_capabilities prints each one's credits under `formats`",
        +  "enum": [
        +    "png",
        +    "gif",
        +    "svg"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / frameMs
        Added value: +{
        +  "description": "format gif only: milliseconds per frame, 20 to 2000, rounded to 10 (default 200; 9 frames x 200 = a 1.8 s loop)",
        +  "maximum": 2000,
        +  "minimum": 20,
        +  "type": "integer"
        +}
      • addedInput schema / properties / frames
        Added value: +{
        +  "description": "format gif only: frames in the loop, 2 to 16 (default 9)",
        +  "maximum": 16,
        +  "minimum": 2,
        +  "type": "integer"
        +}
      • changedInput schema / properties / mask / description
        Previous value: -"MASKED EDIT — change ONE region and keep the rest: a mask (path or URL, any size) for refImages[0]. TRANSPARENT = change, or on a mask with no transparency WHITE = change, black = keep; the reply says which it read. The mask guides rather than stencils, so the change can blend a little past its edge. Runs on gpt-image-2.5 (`refs.mask` in hermoso_capabilities): leave `model` empty or name it; another named model is refused, free. Needs refImages; keeps the source image's frame, so aspectRatio is not applied."New value: +"MASKED EDIT — change ONE region and keep the rest: a mask (path or URL, any size) for refImages[0]. TRANSPARENT = change, or on a mask with no transparency WHITE = change, black = keep; the reply says which it read. The mask guides rather than stencils, so the change can blend a little past its edge. Runs on gpt-image-2.6 (`refs.mask` in hermoso_capabilities): leave `model` empty or name it; another named model is refused, free. Needs refImages; keeps the source image's frame, so aspectRatio is not applied."
      • changedInput schema / properties / model / description
        Previous value: -"image model id from hermoso_capabilities. A model whose `refs.mode` is \"edit\" there (gpt-image-2.5) takes your refImages on ITS OWN editor, up to its `refs.max`, instead of the default compositor"New value: +"image model id from hermoso_capabilities. A model whose `refs.mode` is \"edit\" there (gpt-image-2.6, gpt-image-2.5) takes your refImages on ITS OWN editor, up to its `refs.max`, instead of the default compositor"
    • Changedpost_edit4 fields changed
      • addedInput schema / properties / ops / items / properties / at
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "properties": {
        +        "occurrence": {
        +          "description": "1 = first time said (default), 2 = second, -1 = last",
        +          "type": "number"
        +        },
        +        "word": {
        +          "description": "the word or phrase as it is said",
        +          "type": "string"
        +        }
        +      },
        +      "required": [
        +        "word"
        +      ],
        +      "type": "object"
        +    }
        +  ],
        +  "description": "text, instead of start/end: the words appear as this spoken phrase starts (timed from the video's own words; refused if it is never said)"
        +}
      • addedInput schema / properties / ops / items / properties / duration
        Added value: +{
        +  "description": "text with at and no until: seconds the words stay, 0.3-30 (default 2)",
        +  "type": "number"
        +}
      • addedInput schema / properties / ops / items / properties / until
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "properties": {
        +        "occurrence": {
        +          "description": "counted from the at phrase on",
        +          "type": "number"
        +        },
        +        "word": {
        +          "description": "the word or phrase as it is said",
        +          "type": "string"
        +        }
        +      },
        +      "required": [
        +        "word"
        +      ],
        +      "type": "object"
        +    }
        +  ],
        +  "description": "text with at: the words leave as this phrase ends"
        +}
      • changedInput schema / properties / videoUrl / description
        Previous value: -"the video to edit: a render / Library URL, a direct file, or a public post link"New value: +"the video to edit: a render / Library URL, a direct file, or a public post link; a URL, not a local file path; use upload_file for local files"
    • Changedpost_to_bluesky1 field changed
      • changedInput schema / properties / videoUrl / description
        Previous value: -"One public MP4 URL. Cannot be combined with imageUrls. Bluesky transcodes it, which takes a minute or two."New value: +"One public MP4 URL. Cannot be combined with imageUrls. Bluesky transcodes it, which takes a minute or two; a URL, not a local file path; use upload_file for local files"
    • Changedpost_to_linkedin2 fields changed
      • changedInput schema / properties / imageUrl / description
        Previous value: -"a Hermoso-hosted image URL to attach (≤12MB) — a Hermoso render, or ANY file of the user’s own put through upload_file first. An arbitrary external host is refused (we fetch the bytes ourselves)."New value: +"a Hermoso-hosted image URL to attach (≤12MB) — a Hermoso render, or ANY file of the user’s own put through upload_file first. An arbitrary external host is refused (we fetch the bytes ourselves); a URL, not a local file path; use upload_file for local files"
      • changedInput schema / properties / videoUrl / description
        Previous value: -"a Hermoso-hosted video (a render, or the user’s footage via upload_file). LinkedIn processes it before publishing, which takes a minute."New value: +"a Hermoso-hosted video (a render, or the user’s footage via upload_file). LinkedIn processes it before publishing, which takes a minute; a URL, not a local file path; use upload_file for local files"
    • Changedpost_to_meta2 fields changed
      • changedInput schema / properties / imageUrl / description
        Previous value: -"public https URL, a data: URI, or a Hermoso /generated path (upload_file gives you one for a local file)"New value: +"public https URL, a data: URI, or a Hermoso /generated path (upload_file gives you one for a local file); a URL, not a local file path; use upload_file for local files"
      • changedInput schema / properties / videoUrl / description
        Previous value: -"public https URL, data: URI, or /generated path — FB video post / IG Reel"New value: +"public https URL, data: URI, or /generated path — FB video post / IG Reel; a URL, not a local file path; use upload_file for local files"
    • Changedpost_to_pinterest2 fields changed
      • changedInput schema / properties / imageUrl / description
        Previous value: -"a Hermoso render image URL (or an upload_file url)"New value: +"a Hermoso render image URL (or an upload_file url); a URL, not a local file path; use upload_file for local files"
      • changedInput schema / properties / videoUrl / description
        Previous value: -"a Hermoso render video URL — takes 1–2 minutes to ingest"New value: +"a Hermoso render video URL — takes 1–2 minutes to ingest; a URL, not a local file path; use upload_file for local files"
    • Changedpost_to_telegram2 fields changed
      • changedInput schema / properties / imageUrl / description
        Previous value: -"one image (≤10MB after upload)"New value: +"one image (≤10MB after upload); a URL, not a local file path; use upload_file for local files"
      • changedInput schema / properties / videoUrl / description
        Previous value: -"one video (≤50MB). Passed alongside imageUrls it joins the album as one more item."New value: +"one video (≤50MB). Passed alongside imageUrls it joins the album as one more item; a URL, not a local file path; use upload_file for local files"
    • Changedpost_to_tiktok1 field changed
      • changedInput schema / properties / videoUrl / description
        Previous value: -"the video to post — a Hermoso render URL or an upload_file url. Omit for a photo post."New value: +"the video to post — a Hermoso render URL or an upload_file url. Omit for a photo post; a URL, not a local file path; use upload_file for local files"
    • Changedpost_to_x2 fields changed
      • changedInput schema / properties / mediaUrl / description
        Previous value: -"a Hermoso render (image or video) to attach to the first post — pass its served URL, or an upload_file url for external media"New value: +"a Hermoso render (image or video) to attach to the first post — pass its served URL, or an upload_file url for external media; a URL, not a local file path; use upload_file for local files"
      • changedInput schema / properties / mediaUrls / description
        Previous value: -"UP TO FOUR Hermoso-hosted media attached to ONE post — X’s own schema caps media_ids at 4. X renders them as a GRID: every image visible at once, nothing to swipe to. That is NOT a carousel, and a numbered \"1/6 · SWIPE\" slide deck must still not be sent here — it would publish as a grid and the \"swipe\" instruction would make no sense. Order decides the layout. On a thread the media rides the FIRST post; give each later part its own post to attach more. Mutually exclusive with mediaUrl, and more than 4 is refused before anything is uploaded."New value: +"UP TO FOUR Hermoso-hosted media attached to ONE post — X’s own schema caps media_ids at 4. X renders them as a GRID: every image visible at once, nothing to swipe to. That is NOT a carousel, and a numbered \"1/6 · SWIPE\" slide deck must still not be sent here — it would publish as a grid and the \"swipe\" instruction would make no sense. Order decides the layout. On a thread the media rides the FIRST post; give each later part its own post to attach more. Mutually exclusive with mediaUrl, and more than 4 is refused before anything is uploaded; a URL, not a local file path; use upload_file for local files"
    • Changedpost_to_youtube1 field changed
      • changedInput schema / properties / videoUrl / description
        Previous value: -"the video to post — a Hermoso render URL or an upload_file url"New value: +"the video to post — a Hermoso render URL or an upload_file url; a URL, not a local file path; use upload_file for local files"
    • Changedreframe_video1 field changed
      • changedInput schema / properties / video / description
        Previous value: -"the source video URL"New value: +"the source video URL; a URL, not a local file path; use upload_file for local files"
    • Changedrender_ad1 field changed
      • changedInput schema / properties / textStyle / anyOf
        Previous value: -[
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "properties": {
        -      "background": {
        -        "description": "none|pill|a colour",
        -        "type": "string"
        -      },
        -      "cardColor": {
        -        "type": "string"
        -      },
        -      "color": {
        -        "description": "any CSS colour",
        -        "type": "string"
        -      },
        -      "describe": {
        -        "description": "the look in words",
        -        "type": "string"
        -      },
        -      "font": {
        -        "description": "sans|serif|elegant|condensed|hand or any Google Fonts family",
        -        "type": "string"
        -      },
        -      "italic": {
        -        "type": "boolean"
        -      },
        -      "outline": {
        -        "type": "boolean"
        -      },
        -      "outlineColor": {
        -        "type": "string"
        -      },
        -      "position": {
        -        "description": "top|center|lower|bottom or 0.05-0.95 from the top",
        -        "type": [
        -          "string",
        -          "number"
        -        ]
        -      },
        -      "preset": {
        -        "type": "string"
        -      },
        -      "shadow": {
        -        "type": "boolean"
        -      },
        -      "size": {
        -        "description": "s|m|l|xl, '120px', or a 0.015-0.15 frame fraction",
        -        "type": [
        -          "string",
        -          "number"
        -        ]
        -      },
        -      "subFont": {
        -        "type": "string"
        -      },
        -      "subItalic": {
        -        "type": "boolean"
        -      },
        -      "textCase": {
        -        "enum": [
        -          "as-is",
        -          "upper",
        -          "lower",
        -          "title"
        -        ],
        -        "type": "string"
        -      },
        -      "tilt": {
        -        "description": "degrees, ±45",
        -        "type": "number"
        -      },
        -      "weight": {
        -        "type": "number"
        -      }
        -    },
        -    "type": "object"
        -  }
        -]New value: +[
        +  {
        +    "type": "string"
        +  },
        +  {
        +    "properties": {
        +      "background": {
        +        "description": "none|pill|a colour",
        +        "type": "string"
        +      },
        +      "cardColor": {
        +        "type": "string"
        +      },
        +      "color": {
        +        "description": "any CSS colour",
        +        "type": "string"
        +      },
        +      "describe": {
        +        "description": "the look in words",
        +        "type": "string"
        +      },
        +      "font": {
        +        "description": "sans|serif|elegant|condensed|hand or any Google Fonts family",
        +        "type": "string"
        +      },
        +      "highlightColor": {
        +        "description": "word-timed subtitles only: the word being said is lit in this colour (preset highlight sets it)",
        +        "type": "string"
        +      },
        +      "italic": {
        +        "type": "boolean"
        +      },
        +      "outline": {
        +        "type": "boolean"
        +      },
        +      "outlineColor": {
        +        "type": "string"
        +      },
        +      "position": {
        +        "description": "top|center|lower|bottom or 0.05-0.95 from the top",
        +        "type": [
        +          "string",
        +          "number"
        +        ]
        +      },
        +      "preset": {
        +        "type": "string"
        +      },
        +      "shadow": {
        +        "type": "boolean"
        +      },
        +      "size": {
        +        "description": "s|m|l|xl, '120px', or a 0.015-0.15 frame fraction",
        +        "type": [
        +          "string",
        +          "number"
        +        ]
        +      },
        +      "subFont": {
        +        "type": "string"
        +      },
        +      "subItalic": {
        +        "type": "boolean"
        +      },
        +      "textCase": {
        +        "enum": [
        +          "as-is",
        +          "upper",
        +          "lower",
        +          "title"
        +        ],
        +        "type": "string"
        +      },
        +      "tilt": {
        +        "description": "degrees, ±45",
        +        "type": "number"
        +      },
        +      "weight": {
        +        "type": "number"
        +      }
        +    },
        +    "type": "object"
        +  }
        +]
    • Changedschedule_post2 fields changed
      • changedInput schema / properties / imageUrl / description
        Previous value: -"a Hermoso render URL (a /generated path, or what upload_file returned), a data: URI, or a public https URL. NOTE: only Facebook/Instagram/Threads accept an arbitrary public URL — X, TikTok, YouTube, LinkedIn, Pinterest and Google Business re-host the bytes and REFUSE anything that is not a Hermoso render, so run an external file through upload_file first and schedule that url."New value: +"a Hermoso render URL (a /generated path, or what upload_file returned), a data: URI, or a public https URL. NOTE: only Facebook/Instagram/Threads accept an arbitrary public URL — X, TikTok, YouTube, LinkedIn, Pinterest and Google Business re-host the bytes and REFUSE anything that is not a Hermoso render, so run an external file through upload_file first and schedule that url; a URL, not a local file path; use upload_file for local files"
      • changedInput schema / properties / videoUrl / description
        Previous value: -"a Hermoso render URL (a /generated path, or what upload_file returned), a data: URI, or a public https URL — required for youtube, and for tiktok unless you pass an imageUrl (TikTok takes a photo post too). Same origin rule as imageUrl: everything except Facebook/Instagram/Threads REFUSES a non-Hermoso URL, so pass external video through upload_file first."New value: +"a Hermoso render URL (a /generated path, or what upload_file returned), a data: URI, or a public https URL — required for youtube, and for tiktok unless you pass an imageUrl (TikTok takes a photo post too). Same origin rule as imageUrl: everything except Facebook/Instagram/Threads REFUSES a non-Hermoso URL, so pass external video through upload_file first; a URL, not a local file path; use upload_file for local files"
    • Changedupscale_video1 field changed
      • changedInput schema / properties / video / description
        Previous value: -"the source video URL"New value: +"the source video URL; a URL, not a local file path; use upload_file for local files"
  2. 41 tool updatesv0.1.519
    • Removedautofill_canva_template
    • Changedconnect_connector1 field changed
      • changedInput schema / properties / provider / description
        Previous value: -"the connector id: applovin_ads, stripe, intercom, vercel, openai_ads, apple_ads, bluesky, telegram, bing_webmaster, posthog, mixpanel, amplitude, slack, discord, webhook"New value: +"the connector id: applovin_ads, stripe, intercom, mailchimp, klaviyo, vercel, openai_ads, apple_ads, bluesky, telegram, bing_webmaster, posthog, mixpanel, amplitude, wordpress, slack, discord, webhook"
    • Removedcreate_canva_folder
    • Changeddiagnose_posts1 field changed
      • changedInput schema / properties / channel / description
        Previous value: -"restrict to one channel: facebook, instagram, threads, x, linkedin, youtube, tiktok, reddit, pinterest"New value: +"restrict to one channel: facebook, instagram, threads, x, linkedin, youtube, tiktok, pinterest, bluesky"
    • Changeddisconnect_connector1 field changed
      • changedInput schema / properties / account / description
        Previous value: -"on a channel with several connected accounts (TikTok, X, YouTube, Threads, Bluesky, Telegram, Reddit, Pinterest): remove ONLY this account (@handle or id from list_connector_accounts) and keep the others"New value: +"on a channel with several connected accounts (TikTok, X, YouTube, Threads, Bluesky, Telegram, Pinterest): remove ONLY this account (@handle or id from list_connector_accounts) and keep the others"
    • Changedduplicate_scheduled2 fields changed
      • changedInput schema / properties / channels / items / enum
        Previous value: -[
        -  "facebook",
        -  "instagram",
        -  "threads",
        -  "tiktok",
        -  "youtube",
        -  "linkedin",
        -  "x",
        -  "pinterest",
        -  "google_business",
        -  "bluesky",
        -  "telegram"
        -]New value: +[
        +  "facebook",
        +  "instagram",
        +  "threads",
        +  "tiktok",
        +  "youtube",
        +  "linkedin",
        +  "x",
        +  "pinterest",
        +  "google_business",
        +  "bluesky",
        +  "telegram",
        +  "wordpress",
        +  "discord",
        +  "slack",
        +  "whop"
        +]
      • addedInput schema / properties / whopExperienceId
        Added value: +{
        +  "description": "WHOP — which forum or chat the copy goes to (exp_…, from list_whop_destinations).",
        +  "type": "string"
        +}
    • Changededit_image1 field changed
      • addedInput schema / properties / logo
        Added value: +{
        +  "type": "boolean"
        +}
    • Changededit_video1 field changed
      • addedInput schema / properties / textChanges
        Added value: +{
        +  "description": "EVERY change of on-screen words at once ([{from, to}], e.g. each line and its translation). A video laid out from a design is made again with all of them in ONE render, about 2 credits, never a video edit",
        +  "items": {
        +    "properties": {
        +      "from": {
        +        "type": "string"
        +      },
        +      "to": {
        +        "type": "string"
        +      }
        +    },
        +    "required": [
        +      "from",
        +      "to"
        +    ],
        +    "type": "object"
        +  },
        +  "type": "array"
        +}
    • Removedexport_canva_design
    • Changedfix_beat9 fields changed
      • addedInput schema / properties / atSeconds
        Added value: +{
        +  "description": "with `job`: the moment in the film instead of `block` (\"the line at 0:12\" = 12)",
        +  "type": "number"
        +}
      • addedInput schema / properties / block
        Added value: +{
        +  "description": "with `job`: which block, 1-based (block 3 = 20-30s of a film of 10s blocks)",
        +  "type": "number"
        +}
      • changedInput schema / properties / endSeconds / description
        Previous value: -"window end in seconds (window 1.5-8s)"New value: +"window end in seconds (window 1.5-8s; not needed with `job`)"
      • addedInput schema / properties / job
        Added value: +{
        +  "description": "a finished video-blocks film (explainer / faceless channel): its job id. Edits one block of it instead of splicing a window",
        +  "type": "string"
        +}
      • addedInput schema / properties / line
        Added value: +{
        +  "description": "with `job`: the new narration for that block, the exact words to say (said as written, never rewritten; refused for free when it cannot fit the block)",
        +  "type": "string"
        +}
      • changedInput schema / properties / prompt / description
        Previous value: -"what the replacement footage should show — describe the shot, matching the master's style"New value: +"what the replacement footage should show — describe the shot, matching the master's style. With `job`: the change to that block's picture, in words"
      • changedInput schema / properties / startSeconds / description
        Previous value: -"window start in seconds"New value: +"window start in seconds (not needed with `job`)"
      • changedInput schema / properties / videoUrl / description
        Previous value: -"the served URL of the master video to fix"New value: +"the served URL of the master video to fix (not needed with `job`)"
      • removedInput schema / required
        Removed value: -[
        -  "videoUrl",
        -  "startSeconds",
        -  "endSeconds",
        -  "prompt"
        -]
    • Changedgenerate_image2 fields changed
      • addedInput schema / properties / job
        Added value: +{
        +  "description": "what the picture is for. With no `model` named it renders on the engine judged best for that job (hermoso_capabilities `jobEngines.image` lists them): static_ad (the default when neither `model` nor `job` is given) = a finished image ad with its headline, copy and button on the picture; product_photo = a studio, lifestyle, flat-lay or spotlight photo of the real product; marketplace = a listing card; thumbnail = a video cover (make_thumbnail is the full thumbnail pipeline); character_sheet = a multi-view sheet of one person; vector = a logo, icon or flat illustration as an editable SVG (it takes no reference pictures, so the saved brand's photos stay off it). A named `model` always wins",
        +  "enum": [
        +    "static_ad",
        +    "product_photo",
        +    "marketplace",
        +    "thumbnail",
        +    "character_sheet",
        +    "vector"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / logoPlacement / description
        Previous value: -"overlay: real logo file laid flat. in_scene: on something in the scene, from the file, checked. auto (default): from the prompt"New value: +"overlay: real logo file laid flat. in_scene: the scene leaves a blank patch on the object and the real file is printed onto it (perspective, light). Never drawn by the model. auto (default): from the prompt"
    • Changedgenerate_video1 field changed
      • changedInput schema / properties / resolution / description
        Previous value: -"'1080p' default; '480p'/'720p' cheaper drafts, '4k' premium (more credits). A model renders only the tiers in its own `resolutions` list in hermoso_capabilities; a tier it lacks renders at its best one without a word, so check before promising 1080p or 4k."New value: +"Omit = the default tier, hermoso_capabilities defaultResolution (720p), the tier its `credits` prices are for. 1080p is ~2.25x, 4k more, 480p less: quote creditsByRes and pass the tier you quoted. A tier a model's `resolutions` lacks renders at its best one."
    • Changedgenerate_voice6 fields changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "description": "true = the exact credits only, nothing made or charged",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / engine / description
        Previous value: -"voice-engine id: 'seed-audio' (default), 'eleven-v4' (Eleven v4), 'minimax-speech', or 'kokoro' — listed in hermoso_capabilities"New value: +"voice-engine id: one voice defaults to 'seed-audio'; a dialogue defaults to the dialogue model ('eleven-dialogue'); or 'eleven-v4', 'minimax-speech', 'kokoro'… (hermoso_capabilities)"
      • addedInput schema / properties / lines
        Added value: +{
        +  "description": "DIALOGUE: the turns in speaking order",
        +  "items": {
        +    "properties": {
        +      "speaker": {
        +        "description": "who speaks; the same name keeps the same voice",
        +        "type": "string"
        +      },
        +      "text": {
        +        "description": "what they say",
        +        "type": "string"
        +      },
        +      "voice": {
        +        "description": "that speaker's voice from the engine's list (or an ElevenLabs voice id); omit to be given one",
        +        "type": "string"
        +      }
        +    },
        +    "required": [
        +      "text"
        +    ],
        +    "type": "object"
        +  },
        +  "type": "array"
        +}
      • changedInput schema / properties / text / description
        Previous value: -"the script to speak (≤900 characters)"New value: +"ONE VOICE: the script to speak (≤900 characters)"
      • changedInput schema / properties / voice / description
        Previous value: -"a voice preset from the chosen engine (e.g. 'Aria'/'George' on eleven-v4, 'stokie_en' on seed-audio) — omit for the engine default"New value: +"ONE VOICE: a preset from the chosen engine (e.g. 'Aria'/'George' on eleven-v4, 'stokie_en' on seed-audio); omit for the engine default"
      • removedInput schema / required
        Removed value: -[
        -  "text"
        -]
    • Changedget_job1 field changed
      • changedInput schema / properties / id / description
        Previous value: -"the job id, e.g. job_xxx"New value: +"the job id: starts with job_, from a render tool's jobId or from list_jobs (generate_voice and generate_sfx return their file directly and have no job)"
    • Removedlist_canva_brand_templates
    • Removedlist_canva_designs
    • Removedlist_canva_folder
    • Changedlist_connector_accounts1 field changed
      • changedInput schema / properties / provider / enum
        Previous value: -[
        -  "tiktok",
        -  "x",
        -  "youtube",
        -  "threads",
        -  "bluesky",
        -  "telegram",
        -  "reddit",
        -  "pinterest",
        -  "instagram",
        -  "meta",
        -  "google_ads",
        -  "linkedin",
        -  "pinterest_ads",
        -  "linkedin_ads",
        -  "reddit_ads",
        -  "apple_ads",
        -  "microsoft_ads",
        -  "google_business",
        -  "google_analytics",
        -  "snapchat_ads",
        -  "x_ads",
        -  "tiktok_ads",
        -  "google_tag_manager",
        -  "google_search_console",
        -  "bing_webmaster"
        -]New value: +[
        +  "tiktok",
        +  "x",
        +  "youtube",
        +  "threads",
        +  "bluesky",
        +  "telegram",
        +  "pinterest",
        +  "instagram",
        +  "meta",
        +  "google_ads",
        +  "linkedin",
        +  "pinterest_ads",
        +  "linkedin_ads",
        +  "reddit_ads",
        +  "apple_ads",
        +  "microsoft_ads",
        +  "google_business",
        +  "google_analytics",
        +  "snapchat_ads",
        +  "x_ads",
        +  "tiktok_ads",
        +  "google_tag_manager",
        +  "google_search_console",
        +  "bing_webmaster"
        +]
    • Changedlist_hooks1 field changed
      • changedInput schema / properties / channel / description
        Previous value: -"restrict the performance half to one channel (facebook, instagram, threads, x, linkedin, youtube, tiktok, reddit, pinterest)"New value: +"restrict the performance half to one channel (facebook, instagram, threads, x, linkedin, youtube, tiktok, pinterest, bluesky)"
    • Changedlist_library2 fields changed
      • changedInput schema / properties / kind / enum
        Previous value: -[
        -  "image",
        -  "video",
        -  "all"
        -]New value: +[
        +  "image",
        +  "video",
        +  "document",
        +  "all"
        +]
      • addedInput schema / properties / uploads
        Added value: +{
        +  "type": "boolean"
        +}
    • Changedlist_published_posts1 field changed
      • changedInput schema / properties / channel / description
        Previous value: -"filter to one channel: facebook, instagram, threads, x, linkedin, youtube, tiktok, reddit, pinterest, google_business"New value: +"filter to one channel: facebook, instagram, threads, x, linkedin, youtube, tiktok, pinterest, bluesky, google_business"
    • Addedmake_effect
    • Changedmake_explainer5 fields changed
      • changedInput schema / properties / captions / description
        Previous value: -"turn ON-SCREEN TEXT on. DEFAULT FALSE, and leave it false unless the user asks — the narration already says the point and the pictures carry it, so the clean film is the better default. `captions:true` on its own burns SUBTITLES (see below), because that is what a caption is for: showing what is being said when the phone is on mute. Slim white CAPS, thin black outline, bottom safe band, no plate, no box."New value: +"turn ON-SCREEN TEXT on. DEFAULT FALSE, and leave it false unless the user asks — the narration already says the point and the pictures carry it, so the clean film is the better default. `captions:true` on its own burns SUBTITLES (see below), because that is what a caption is for: showing what is being said when the phone is on mute. Slim white text (CAPS in the stills lane), thin black outline, bottom safe band, no plate, no box."
      • changedInput schema / properties / format / description
        Previous value: -"blocks lane: 'explainer' (default; Gemini Omni 720p, 16:9 or 9:16, runs exactly the length asked) or 'faceless_channel' (a YouTube/TikTok faceless channel video; MiniMax H3 at 2K, 16:9 by default, five hard cuts per 10s block, a whole number of blocks). Omit and a history / kids / fairytale channel is a faceless channel video. 'host_episode' = the AI host episode (see the top)."New value: +"blocks lane: 'explainer' (default; Gemini Omni 720p, 16:9 or 9:16, runs exactly the length asked) or 'faceless_channel' (a YouTube/TikTok faceless channel video; MiniMax H3 at 2K, 16:9 by default, five hard cuts per 10s block, runs exactly the length asked: a last part-block is rendered whole and cut). Omit and a history / kids / fairytale channel is a faceless channel video. 'host_episode' = the AI host episode (see the top)."
      • addedInput schema / properties / fromFilm
        Added value: +{
        +  "description": "A PRESENTER ON A FINISHED FILM (only when the user asks for one): the job id of a finished video-blocks explainer or faceless-channel film. With `presenter`, that film comes back with a presenter speaking each block's line on screen; every block keeps its own picture and the film without the presenter is kept (cleanVideo). Never add a presenter unasked.",
        +  "type": "string"
        +}
      • addedInput schema / properties / presenter
        Added value: +{
        +  "description": "with fromFilm: who presents and how. A face the video model flags stops it, nothing charged, with the choices to offer.",
        +  "properties": {
        +    "creator": {
        +      "description": "a saved creator or a preset, by name or id (list_creators)",
        +      "type": "string"
        +    },
        +    "image": {
        +      "description": "or a photo / avatar of the presenter (upload_file for a local file); a real person's face follows the real-face rules",
        +      "type": "string"
        +    },
        +    "look": {
        +      "description": "'corner' (default): cut out of a green screen, standing in a bottom corner; 'badge': a round badge in a corner; 'split': the block on one half, the presenter on the other",
        +      "enum": [
        +        "corner",
        +        "badge",
        +        "split"
        +      ],
        +      "type": "string"
        +    },
        +    "position": {
        +      "description": "'br' (default), 'bl', 'tr' or 'tl' (a corner cutout stays at the bottom); for split, 'bl' puts the presenter first",
        +      "type": "string"
        +    },
        +    "scale": {
        +      "description": "the share of the frame the presenter takes, 0.2-0.8 (default: the look's own size)",
        +      "type": "number"
        +    },
        +    "voice": {
        +      "description": "the presenter's voice (default: the film's own narrator)",
        +      "type": "string"
        +    }
        +  },
        +  "type": "object"
        +}
      • changedInput schema / properties / subtitles / description
        Previous value: -"which on-screen text, once `captions` is on. LEAVE IT UNSET (or true) for SUBTITLES — every spoken word, in order, timed to the narration; free, no extra render, no extra credits, and there is NO cue limit, so the whole film is subtitled however long it runs (at most 5 words / 32 characters a line). Set it FALSE only if the user explicitly wants section HEADINGS instead: one short summary label held over each ~7-15s section. That is NOT what is being said — it is a label about it — so it is the wrong answer to \"add captions\" and to anyone watching on mute. `subtitles:true` also implies `captions:true`. TIMING: each cue is anchored to that section’s REAL measured narration length and distributed inside the section by character count — exact at every section boundary, approximate to a few tenths of a second within one. It is not a word-level speech clock, so never promise frame-accurate sync."New value: +"which on-screen text, once `captions` is on. LEAVE IT UNSET (or true) for SUBTITLES — every spoken word, in order, timed to the narration; no extra render, and there is NO cue limit, so the whole film is subtitled however long it runs (at most 4 words and one line a cue; 5 words / 32 characters in stills). Set it FALSE only if the user explicitly wants section HEADINGS instead: one short summary label held over each ~7-15s section. That is NOT what is being said — it is a label about it — so it is the wrong answer to \"add captions\" and to anyone watching on mute. `subtitles:true` also implies `captions:true`. TIMING: in the default blocks lane each cue is timed to its own spoken words (one listen to the narration, a tiny charge inside the quote; per-block timing, said, if it fails). In the stills lane cues are spread over each section’s measured narration by character count, exact at section edges, a few tenths off within one: it is not a word-level speech clock, so never promise frame-accurate sync there."
    • Changedmake_motion_film6 fields changed
      • addedInput schema / properties / brief
        Added value: +{
        +  "description": "what the film is about, for the planner",
        +  "type": "string"
        +}
      • addedInput schema / properties / energy
        Added value: +{
        +  "description": "how much the film keeps moving, 0 to 1; 0 films exactly what is written",
        +  "type": "number"
        +}
      • addedInput schema / properties / model
        Added value: +{
        +  "description": "image-to-video model id from hermoso_capabilities; default is the best value for short shots",
        +  "type": "string"
        +}
      • addedInput schema / properties / moves
        Added value: +{
        +  "description": "per scene, what happens in its picture; omit and the planner writes them",
        +  "items": {
        +    "properties": {
        +      "action": {
        +        "description": "the one thing that physically happens, in plain words",
        +        "type": "string"
        +      },
        +      "layer": {
        +        "description": "the picture layer's id, when the scene has several",
        +        "type": "string"
        +      },
        +      "matchNext": {
        +        "description": "the shot ends on the next scene's picture",
        +        "type": "boolean"
        +      },
        +      "scene": {
        +        "description": "scene id",
        +        "type": "string"
        +      },
        +      "still": {
        +        "description": "a picture to animate, when not the one in the scene",
        +        "type": "string"
        +      },
        +      "stillPrompt": {
        +        "description": "a picture to paint first, for a scene with none",
        +        "type": "string"
        +      },
        +      "video": {
        +        "description": "a shot already filmed: kept, and not paid for again",
        +        "type": "string"
        +      }
        +    },
        +    "required": [
        +      "scene",
        +      "action"
        +    ],
        +    "type": "object"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / moving
        Added value: +{
        +  "description": "true: one thing physically happens in each picture, filmed by a video model. false: stills moved by a virtual camera, far cheaper. Default true for launch, product, social",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / resolution
        Added value: +{
        +  "description": "the moving shots: 720p or 1080p (default) where the model has them",
        +  "type": "string"
        +}
    • Changedmake_series1 field changed
      • addedInput schema / properties / moreOf
        Added value: +{
        +  "description": "MORE EPISODES: the `plan` an earlier call returned. Keeps its style and length, writes new episodes only"
        +}
    • Changedmake_template_ad1 field changed
      • changedInput schema / properties / config / description
        Previous value: -"MUST include config.template: 'custom' or a preset id, plus its fields. PRESETS: 'slideshow' (IMAGES, TikTok photo mode / Reels 1080x1920, or size:'4:5' feed carousels; no branding): { slides:[{text, sub?, image?, blur?, background?, position?}] (2-35; words never rewritten), style? ('tiktok-classic'|'clean-minimal'|'note-style' or a look in words), textStyle?, video?:true (+ an MP4, which takes config.music like any video) }; 2 credits, +1 per slide past 5, +2 for the MP4. 'imessage-chat' (VIDEO ~15s): { thread:{contactName, messages:[{from:'them'|'me', text?, product?:{image,title,domain}}]}, theme?, endCard }. 'chatgpt-chat' (VIDEO): { question, answer (may **bold** the brand), productImage?, endCard }. 'apple-notes' (VIDEO): { title, lines[], theme?, endCard }. 'value-prop' (VIDEO ~17s): { hook ≤40ch, claims[3-5 ≤34ch], productImages[2-3], palette[], endCard }. 'static-mockup' (IMAGE): { style:'imessage'|'notes'|'card', size?:{w,h}, ...fields }. 'airdrop-carousel' (VIDEO): { brandName, products:[{image, title?}] (3-16), endCard }. 'app-ui-tour' (VIDEO): { hook?, appName, iconImage?, beats:[{screenImage, caption}] (2-6), endCard }. 'imessage-cascade' (VIDEO): { notifications:[{sender, text}] (4-8), backgroundImage?, endCard }. 'photo-grid' (VIDEO): { title?, photos:[{image, label?}] (4-9), endCard }. 'vignette' (VIDEO): { hook, lines[2-4 ≤40ch], heroImage, endCard }. 'kinetic-type' (VIDEO, own SFX): { phrases[3-6 ≤34ch], productImages?[≤4], endCard }. 'myth-vs-fact' (VIDEO with a real VOICEOVER, small extra charge): { pairs:[{myth ≤50ch, fact ≤60ch}] (2-4; [brackets] accent), endCard }, real truths only. 'carousel' (IMAGES, 5-10 branded 1080x1080): { cover:{hook?, title}, slides:[{headline, support?, stat?:{value, label}}] (3-8), cta:{headline, cta?, domain?}, productImage?, logo? }. endCard = { headline, cta, domain?, logo?, color? } is OPTIONAL on every video preset: include it ONLY when the user asked for an end card (a closing card with their logo); without it the video ends on its own last frame. palette and fontStack optional."New value: +"MUST include config.template: 'custom' or a preset id, plus its fields; language ('Portuguese') for its own words. PRESETS: 'slideshow' (IMAGES, TikTok photo mode / Reels 1080x1920, or size:'4:5' feed carousels; no branding): { slides:[{text, sub?, image?, blur?, background?, position?}] (2-35; words never rewritten), style? ('tiktok-classic'|'clean-minimal'|'note-style' or a look in words), textStyle?, video?:true (+ an MP4, which takes config.music like any video) }; 2 credits, +1 per slide past 5, +2 for the MP4. 'imessage-chat' (VIDEO ~15s): { thread:{contactName, messages:[{from:'them'|'me', text?, product?:{image,title,domain}}]}, theme?, endCard }. 'chatgpt-chat' (VIDEO): { question, answer (may **bold** the brand), productImage?, endCard }. 'apple-notes' (VIDEO): { title, lines[], theme?, endCard }. 'value-prop' (VIDEO ~17s): { hook ≤40ch, claims[3-5 ≤34ch], productImages[2-3], palette[], endCard }. 'static-mockup' (IMAGE): { style:'imessage'|'notes'|'card', size?:{w,h}, ...fields }. 'airdrop-carousel' (VIDEO): { brandName, products:[{image, title?}] (3-16), endCard }. 'app-ui-tour' (VIDEO): { hook?, appName, iconImage?, beats:[{screenImage, caption}] (2-6), endCard }. 'imessage-cascade' (VIDEO): { notifications:[{sender, text}] (4-8), backgroundImage?, endCard }. 'photo-grid' (VIDEO): { title?, photos:[{image, label?}] (4-9), endCard }. 'vignette' (VIDEO): { hook, lines[2-4 ≤40ch], heroImage, endCard }. 'kinetic-type' (VIDEO, own SFX): { phrases[3-6 ≤34ch], productImages?[≤4], endCard }. 'myth-vs-fact' (VIDEO with a real VOICEOVER, small extra charge): { pairs:[{myth ≤50ch, fact ≤60ch}] (2-4; [brackets] accent), endCard }, real truths only. 'carousel' (IMAGES, 5-10 branded 1080x1080): { cover:{hook?, title}, slides:[{headline, support?, stat?:{value, label}}] (3-8), cta:{headline, cta?, domain?}, productImage?, logo? }. endCard = { headline, cta, domain?, logo?, color? } is OPTIONAL on every video preset: include it ONLY when the user asked for an end card (a closing card with their logo); without it the video ends on its own last frame. palette and fontStack optional."
    • Changedmake_thumbnail1 field changed
      • changedInput schema / properties / logoPlacement / description
        Previous value: -"overlay (exact, flat) | in_scene (from the file, checked); auto: in_scene with logo3d"New value: +"overlay (exact, flat) | in_scene (the real file printed onto a blank panel in the shot; with logo3d, a 3D render of it, checked); auto: in_scene with logo3d"
    • Changedmultiply_ad4 fields changed
      • addedInput schema / properties / images
        Added value: +{
        +  "description": "up to 9 pictures to put in the variants, in the order @Image1, @Image2 name them: URLs or local paths",
        +  "items": {
        +    "anyOf": [
        +      {
        +        "type": "string"
        +      },
        +      {
        +        "properties": {
        +          "role": {
        +            "description": "what it is, in your words, e.g. \"our founder\"",
        +            "type": "string"
        +          },
        +          "url": {
        +            "type": "string"
        +          }
        +        },
        +        "required": [
        +          "url"
        +        ],
        +        "type": "object"
        +      }
        +    ]
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / quality
        Added value: +{
        +  "description": "1080p is the default; 720p costs less, and hands holding a product are less reliable at 720p",
        +  "enum": [
        +    "1080p",
        +    "720p"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / useBrand
        Added value: +{
        +  "description": "false = send no brand product photo (for a video that is not this workspace brand)",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / video / description
        Previous value: -"the source video URL"New value: +"the source video: a file URL, or a TikTok, Reel, Facebook, X or YouTube post link"
    • Changedplan_ad8 fields changed
      • addedInput schema / properties / concepts
        Added value: +{
        +  "description": "several variants: one line per concept (a different angle each); built exactly",
        +  "items": {
        +    "type": "string"
        +  },
        +  "maxItems": 4,
        +  "type": "array"
        +}
      • changedInput schema / properties / inserts / description
        Previous value: -"VIDEO ONLY: the user's own SCREENS (an app, a page, a dashboard, a document, a review, a chart) shown at these seconds as a rounded card over the upper middle of the footage, which keeps playing around it (look 'full' covers the frame, only when the user asked), the voice running underneath. Fewer and shorter: about one per 4 s of film, 1.2-2 s each, on the line that talks about it; never in the first second or the last 1.5 s. The plan frames those shots for the card, keeps them free of screens, and returns the pictures as creative.inserts for render_ad to lay in (about 2 credits). A photo of a person, product or place (kind 'photo') is not laid in: pass a product photo as the brand's product photo and a person as a saved creator. One that does not fit is left out and the reply says so."New value: +"VIDEO ONLY: the user's own SCREENS cut in at these seconds (each item's look, kind, start and end say how), the voice running underneath. How many: at most 8, about one per 2.5 s of film (6 s: 2, 10 s: 4, 15 s: 6, 20 s: 8, 30 s: 8), 1.2-2 s each, on the line that talks about it. When there are more, the most crowded are left out first, never the last (the payoff) or one marked key, and the reply names them. The plan keeps those shots free of screens and returns the pictures as creative.inserts for render_ad to lay in (about 2 credits). One that does not fit is left out and the reply says so."
      • addedInput schema / properties / inserts / items / properties / focus
        Added value: +{
        +  "description": "the readable part to fill the frame with, as fractions of the picture from its top-left (e.g. the flagged-items table); a crop as tall as wide, or taller, reads best on a phone. Omit for the whole picture, plain margins trimmed",
        +  "properties": {
        +    "h": {
        +      "type": "number"
        +    },
        +    "w": {
        +      "type": "number"
        +    },
        +    "x": {
        +      "type": "number"
        +    },
        +    "y": {
        +      "type": "number"
        +    }
        +  },
        +  "required": [
        +    "x",
        +    "y",
        +    "w",
        +    "h"
        +  ],
        +  "type": "object"
        +}
      • addedInput schema / properties / inserts / items / properties / key
        Added value: +{
        +  "description": "true = the story needs this one (the payoff): never left out when there are more screens than the length takes",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / inserts / items / properties / kind / description
        Previous value: -"screen (default): an app, a page, a dashboard, a document, a review, a chart. photo: a person, a product or a place, which is NOT laid over the video (a product photo goes in as the brand's product photo, a person to star as a saved creator)"New value: +"screen (default): an app, page, dashboard, document, review or chart. photo: a person, product or place, NOT laid over the video (a product photo goes in as the brand's product photo, a person as a saved creator)"
      • changedInput schema / properties / inserts / items / properties / look / description
        Previous value: -"card (default): a rounded card over the upper middle of the footage, which keeps playing. full: covers the whole frame, ONLY when the user asked for full-screen"New value: +"cutaway (default): a hard cut to the screen filling the frame, cropped to focus, held still, never enlarged past 1.25x its pixels (any gap is the screenshot's own background colour; a screen too small to read is left out and said). card: picture-in-picture over the playing footage, clear of the speaker's face. full: the whole picture uncropped on a blurred backdrop. card and full ONLY when the user asked"
      • changedInput schema / properties / inserts / items / properties / look / enum
        Previous value: -[
        -  "card",
        -  "full"
        -]New value: +[
        +  "cutaway",
        +  "card",
        +  "full"
        +]
      • changedInput schema / properties / inserts / items / properties / start / description
        Previous value: -"second it appears (never before 1 s: the film opens on moving footage)"New value: +"second it appears, never before 1 s (the film opens on moving footage)"
    • Addedplan_week
    • Changedpost_to_meta2 fields changed
      • changedInput schema / properties / pageId / description
        Previous value: -"target Page id (from list_meta_pages); omit = first Page"New value: +"target Page id (from list_meta_pages). Omit when the brand has one Page. With several and none named the call refuses and lists them by name and id: ask the user which Page, never pick one yourself."
      • changedInput schema / properties / story / description
        Previous value: -"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A link sticker cannot be added through Meta’s publishing API on either channel: add it in the Instagram app. An Instagram Story keeps its caption: people see the start under the Story in the app and tap for the rest (instagram.com does not show it). It takes NO collaborators, product tags or trialReel — any of those is refused by name and nothing is posted. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."New value: +"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. Link stickers are not available on a Story posted here. An Instagram Story keeps its caption: people see the start under the Story in the mobile app and tap for the rest. It takes NO collaborators, product tags or trialReel — any of those is refused by name and nothing is posted. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."
    • Changedproduct_sizzle1 field changed
      • changedInput schema / properties / aspectRatio / description
        Previous value: -"'9:16' default; anything the seedance-2 catalog entry does not list falls back to 9:16"New value: +"'9:16' default; anything the Seedance row that renders it does not list falls back to 9:16"
    • Changedrender_ad10 fields changed
      • changedInput schema / properties / inserts / description
        Previous value: -"the user's own screens laid over the finished video at these seconds, as a card over the playing footage by default (look 'full' covers the frame, only when asked), sound running underneath (about 2 credits). The film always opens and ends on moving footage. Omit to use plan_ad's creative.inserts; [] renders without them. plan_ad with inserts writes the board around them."New value: +"the user's own screens laid over the finished video at these seconds (each item's look says how), sound running underneath (about 2 credits). The film always opens and ends on moving footage. Omit to use plan_ad's creative.inserts; [] renders without them. plan_ad with inserts writes the board around them."
      • addedInput schema / properties / inserts / items / properties / focus
        Added value: +{
        +  "description": "the readable part to fill the frame with, as fractions of the picture from its top-left (e.g. the flagged-items table); a crop as tall as wide, or taller, reads best on a phone. Omit for the whole picture, plain margins trimmed",
        +  "properties": {
        +    "h": {
        +      "type": "number"
        +    },
        +    "w": {
        +      "type": "number"
        +    },
        +    "x": {
        +      "type": "number"
        +    },
        +    "y": {
        +      "type": "number"
        +    }
        +  },
        +  "required": [
        +    "x",
        +    "y",
        +    "w",
        +    "h"
        +  ],
        +  "type": "object"
        +}
      • addedInput schema / properties / inserts / items / properties / key
        Added value: +{
        +  "description": "true = the story needs this one (the payoff): never left out when there are more screens than the length takes",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / inserts / items / properties / kind / description
        Previous value: -"screen (default): an app, a page, a dashboard, a document, a review, a chart. photo: a person, a product or a place, which is NOT laid over the video (a product photo goes in as the brand's product photo, a person to star as a saved creator)"New value: +"screen (default): an app, page, dashboard, document, review or chart. photo: a person, product or place, NOT laid over the video (a product photo goes in as the brand's product photo, a person as a saved creator)"
      • changedInput schema / properties / inserts / items / properties / look / description
        Previous value: -"card (default): a rounded card over the upper middle of the footage, which keeps playing. full: covers the whole frame, ONLY when the user asked for full-screen"New value: +"cutaway (default): a hard cut to the screen filling the frame, cropped to focus, held still, never enlarged past 1.25x its pixels (any gap is the screenshot's own background colour; a screen too small to read is left out and said). card: picture-in-picture over the playing footage, clear of the speaker's face. full: the whole picture uncropped on a blurred backdrop. card and full ONLY when the user asked"
      • changedInput schema / properties / inserts / items / properties / look / enum
        Previous value: -[
        -  "card",
        -  "full"
        -]New value: +[
        +  "cutaway",
        +  "card",
        +  "full"
        +]
      • changedInput schema / properties / inserts / items / properties / start / description
        Previous value: -"second it appears (never before 1 s: the film opens on moving footage)"New value: +"second it appears, never before 1 s (the film opens on moving footage)"
      • changedInput schema / properties / resolution / description
        Previous value: -"'1080p' default; '480p'/'720p' cheaper drafts, '4k' premium (more credits). A model renders only the tiers in its own `resolutions` list in hermoso_capabilities (the 30s model tops out at 720p); a tier it lacks renders at its best one without a word, so check before promising 1080p or 4k."New value: +"Omit = the default tier, hermoso_capabilities defaultResolution (720p), the tier its `credits` prices are for. 1080p is ~2.25x, 4k more, 480p less: quote creditsByRes and pass the tier you quoted. A tier a model's `resolutions` lacks renders at its best one."
      • addedInput schema / properties / screenChoice
        Added value: +{
        +  "description": "the user's answer to needsAppScreens: brand = their saved app screens, site = found on their website (saved to the brand), without = none, device kept turned away",
        +  "enum": [
        +    "brand",
        +    "site",
        +    "without"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / screens
        Added value: +{
        +  "description": "real screenshots of the app or site for the shots that film it on a screen (URLs, upload_file URLs or Library ids), laid over them as cards",
        +  "items": {
        +    "type": "string"
        +  },
        +  "maxItems": 6,
        +  "type": "array"
        +}
    • Addedrepurpose_video
    • Changedreschedule_post22 fields changed
      • changedInput schema / properties / channels / items / enum
        Previous value: -[
        -  "facebook",
        -  "instagram",
        -  "threads",
        -  "tiktok",
        -  "youtube",
        -  "linkedin",
        -  "x",
        -  "pinterest",
        -  "google_business",
        -  "bluesky",
        -  "telegram"
        -]New value: +[
        +  "facebook",
        +  "instagram",
        +  "threads",
        +  "tiktok",
        +  "youtube",
        +  "linkedin",
        +  "x",
        +  "pinterest",
        +  "google_business",
        +  "bluesky",
        +  "telegram",
        +  "wordpress",
        +  "discord",
        +  "slack",
        +  "whop"
        +]
      • addedInput schema / properties / discordChannel
        Added value: +{
        +  "description": "DISCORD bot: channel id or #name (default: the brand's)",
        +  "type": "string"
        +}
      • addedInput schema / properties / discordMentions
        Added value: +{
        +  "description": "DISCORD: let mentions ping",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / discordReplyTo
        Added value: +{
        +  "description": "DISCORD bot: reply to this message id",
        +  "type": "string"
        +}
      • addedInput schema / properties / discordServer
        Added value: +{
        +  "description": "DISCORD bot: which server",
        +  "type": "string"
        +}
      • addedInput schema / properties / discordStartThread
        Added value: +{
        +  "description": "DISCORD bot: open a thread under the post, this title",
        +  "type": "string"
        +}
      • addedInput schema / properties / excerpt
        Added value: +{
        +  "description": "WORDPRESS — the excerpt under the title",
        +  "type": "string"
        +}
      • changedInput schema / properties / silent / description
        Previous value: -"TELEGRAM ONLY — no notification sound."New value: +"TELEGRAM / DISCORD — no notification."
      • addedInput schema / properties / slackChannel
        Added value: +{
        +  "description": "SLACK app: channel id or #name (default: the brand's)",
        +  "type": "string"
        +}
      • addedInput schema / properties / slackThreadTs
        Added value: +{
        +  "description": "SLACK app: reply in this thread (parent ts)",
        +  "type": "string"
        +}
      • addedInput schema / properties / slackWorkspace
        Added value: +{
        +  "description": "SLACK app: which workspace",
        +  "type": "string"
        +}
      • addedInput schema / properties / threadName
        Added value: +{
        +  "description": "DISCORD: forum thread title",
        +  "type": "string"
        +}
      • addedInput schema / properties / whopExperienceId
        Added value: +{
        +  "description": "WHOP — move it to a different forum or chat (exp_…, from list_whop_destinations).",
        +  "type": "string"
        +}
      • addedInput schema / properties / whopMention
        Added value: +{
        +  "description": "WHOP: notify every member",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / whopPaywallCents
        Added value: +{
        +  "description": "WHOP: forum-post paywall in cents (needs a title)",
        +  "maximum": 9007199254740991,
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedInput schema / properties / whopPaywallCurrency
        Added value: +{
        +  "description": "WHOP: paywall currency",
        +  "type": "string"
        +}
      • addedInput schema / properties / whopPinned
        Added value: +{
        +  "description": "WHOP: pin the forum post",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / whopPoll
        Added value: +{
        +  "description": "WHOP: poll options, 2 to 10",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / whopVisibility
        Added value: +{
        +  "description": "WHOP: who can see the forum post",
        +  "enum": [
        +    "members_only",
        +    "globally_visible"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / wpCategories
        Added value: +{
        +  "description": "WORDPRESS — categories (names or ids)",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / wpSlug
        Added value: +{
        +  "description": "WORDPRESS — the post slug",
        +  "type": "string"
        +}
      • addedInput schema / properties / wpTags
        Added value: +{
        +  "description": "WORDPRESS — tags (names or ids)",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
    • Addedresume_job
    • Changedretry_scheduled2 fields changed
      • changedInput schema / properties / channels / items / enum
        Previous value: -[
        -  "facebook",
        -  "instagram",
        -  "threads",
        -  "tiktok",
        -  "youtube",
        -  "linkedin",
        -  "x",
        -  "pinterest",
        -  "google_business",
        -  "bluesky",
        -  "telegram"
        -]New value: +[
        +  "facebook",
        +  "instagram",
        +  "threads",
        +  "tiktok",
        +  "youtube",
        +  "linkedin",
        +  "x",
        +  "pinterest",
        +  "google_business",
        +  "bluesky",
        +  "telegram",
        +  "wordpress",
        +  "discord",
        +  "slack",
        +  "whop"
        +]
      • addedInput schema / properties / whopExperienceId
        Added value: +{
        +  "description": "WHOP — correct the forum or chat on retry (exp_…, from list_whop_destinations).",
        +  "type": "string"
        +}
    • Removedsave_to_canva
    • Changedschedule_post24 fields changed
      • changedInput schema / properties / channels / items / enum
        Previous value: -[
        -  "facebook",
        -  "instagram",
        -  "threads",
        -  "tiktok",
        -  "youtube",
        -  "linkedin",
        -  "x",
        -  "pinterest",
        -  "google_business",
        -  "bluesky",
        -  "telegram"
        -]New value: +[
        +  "facebook",
        +  "instagram",
        +  "threads",
        +  "tiktok",
        +  "youtube",
        +  "linkedin",
        +  "x",
        +  "pinterest",
        +  "google_business",
        +  "bluesky",
        +  "telegram",
        +  "wordpress",
        +  "discord",
        +  "slack",
        +  "whop"
        +]
      • addedInput schema / properties / discordChannel
        Added value: +{
        +  "description": "DISCORD bot: channel id or #name (default: the brand's)",
        +  "type": "string"
        +}
      • addedInput schema / properties / discordMentions
        Added value: +{
        +  "description": "DISCORD: let mentions ping",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / discordReplyTo
        Added value: +{
        +  "description": "DISCORD bot: reply to this message id",
        +  "type": "string"
        +}
      • addedInput schema / properties / discordServer
        Added value: +{
        +  "description": "DISCORD bot: which server",
        +  "type": "string"
        +}
      • addedInput schema / properties / discordStartThread
        Added value: +{
        +  "description": "DISCORD bot: open a thread under the post, this title",
        +  "type": "string"
        +}
      • addedInput schema / properties / excerpt
        Added value: +{
        +  "description": "WORDPRESS — the excerpt under the title",
        +  "type": "string"
        +}
      • changedInput schema / properties / pageId / description
        Previous value: -"FACEBOOK / INSTAGRAM / THREADS — which connected Facebook Page (and its linked Instagram) publishes, from list_meta_pages. Needed when the profile has more than one Page connected; with several and no id the post is refused at fire time rather than sent from the wrong brand."New value: +"FACEBOOK / INSTAGRAM / THREADS — which connected Facebook Page (and its linked Instagram) publishes, from list_meta_pages. Needed when the profile has more than one Page connected: a Facebook post with several and no id is refused WHEN YOU SCHEDULE IT, with the Pages listed by name and id, so ask the user which Page and schedule again with its id. It is never sent from a guessed Page."
      • changedInput schema / properties / silent / description
        Previous value: -"TELEGRAM ONLY — no notification sound."New value: +"TELEGRAM / DISCORD — no notification."
      • addedInput schema / properties / slackChannel
        Added value: +{
        +  "description": "SLACK app: channel id or #name (default: the brand's)",
        +  "type": "string"
        +}
      • addedInput schema / properties / slackThreadTs
        Added value: +{
        +  "description": "SLACK app: reply in this thread (parent ts)",
        +  "type": "string"
        +}
      • addedInput schema / properties / slackWorkspace
        Added value: +{
        +  "description": "SLACK app: which workspace",
        +  "type": "string"
        +}
      • changedInput schema / properties / story / description
        Previous value: -"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A link sticker cannot be added through Meta’s publishing API on either channel: add it in the Instagram app. An Instagram Story keeps its caption: people see the start under the Story in the app and tap for the rest (instagram.com does not show it). It takes NO collaborators, product tags or trialReel — any of those is refused by name and nothing is posted. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."New value: +"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. Link stickers are not available on a Story posted here. An Instagram Story keeps its caption: people see the start under the Story in the mobile app and tap for the rest. It takes NO collaborators, product tags or trialReel — any of those is refused by name and nothing is posted. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."
      • addedInput schema / properties / threadName
        Added value: +{
        +  "description": "DISCORD: forum thread title",
        +  "type": "string"
        +}
      • addedInput schema / properties / whopExperienceId
        Added value: +{
        +  "description": "WHOP — which forum or chat in the whop (exp_…, from list_whop_destinations). Omit it when the whop has only one; with several and none named the post is refused at fire time, never guessed.",
        +  "type": "string"
        +}
      • addedInput schema / properties / whopMention
        Added value: +{
        +  "description": "WHOP FORUM POSTS ONLY — notify every member (off by default).",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / whopPaywallCents
        Added value: +{
        +  "description": "WHOP FORUM POSTS ONLY — a paywall in cents (500 = $5.00): readers pay to see the body; needs a title.",
        +  "maximum": 9007199254740991,
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedInput schema / properties / whopPaywallCurrency
        Added value: +{
        +  "description": "WHOP — the paywall currency, a 3-letter code (default usd).",
        +  "type": "string"
        +}
      • addedInput schema / properties / whopPinned
        Added value: +{
        +  "description": "WHOP FORUM POSTS ONLY — pin it to the top of the forum.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / whopPoll
        Added value: +{
        +  "description": "WHOP FORUM POSTS ONLY — a poll: 2 to 10 options, in order.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / whopVisibility
        Added value: +{
        +  "description": "WHOP FORUM POSTS ONLY — who can see it; unset is the forum’s own default.",
        +  "enum": [
        +    "members_only",
        +    "globally_visible"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / wpCategories
        Added value: +{
        +  "description": "WORDPRESS — categories (names or ids)",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / wpSlug
        Added value: +{
        +  "description": "WORDPRESS — the post slug",
        +  "type": "string"
        +}
      • addedInput schema / properties / wpTags
        Added value: +{
        +  "description": "WORDPRESS — tags (names or ids)",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
    • Changedset_connector_accounts1 field changed
      • changedInput schema / properties / provider / enum
        Previous value: -[
        -  "tiktok",
        -  "x",
        -  "youtube",
        -  "threads",
        -  "bluesky",
        -  "telegram",
        -  "reddit",
        -  "pinterest",
        -  "instagram",
        -  "meta",
        -  "google_ads",
        -  "linkedin",
        -  "pinterest_ads",
        -  "linkedin_ads",
        -  "reddit_ads",
        -  "apple_ads",
        -  "microsoft_ads",
        -  "google_business",
        -  "google_analytics",
        -  "snapchat_ads",
        -  "x_ads",
        -  "tiktok_ads",
        -  "google_tag_manager",
        -  "google_search_console",
        -  "bing_webmaster"
        -]New value: +[
        +  "tiktok",
        +  "x",
        +  "youtube",
        +  "threads",
        +  "bluesky",
        +  "telegram",
        +  "pinterest",
        +  "instagram",
        +  "meta",
        +  "google_ads",
        +  "linkedin",
        +  "pinterest_ads",
        +  "linkedin_ads",
        +  "reddit_ads",
        +  "apple_ads",
        +  "microsoft_ads",
        +  "google_business",
        +  "google_analytics",
        +  "snapchat_ads",
        +  "x_ads",
        +  "tiktok_ads",
        +  "google_tag_manager",
        +  "google_search_console",
        +  "bing_webmaster"
        +]
    • Changedset_post_refill9 fields changed
      • addedInput schema / properties / accounts
        Added value: +{
        +  "additionalProperties": {
        +    "anyOf": [
        +      {
        +        "items": {
        +          "type": "string"
        +        },
        +        "type": "array"
        +      },
        +      {
        +        "const": "all",
        +        "type": "string"
        +      },
        +      {
        +        "type": "null"
        +      }
        +    ]
        +  },
        +  "description": "accounts to post to on a channel holding several: {\"tiktok\":[\"@a\",\"@b\"]} or \"all\"; null clears. A new setup must pick; an older one posts to the primary until it does.",
        +  "propertyNames": {
        +    "type": "string"
        +  },
        +  "type": "object"
        +}
      • addedInput schema / properties / addChannels
        Added value: +{
        +  "description": "add these",
        +  "items": {
        +    "enum": [
        +      "facebook",
        +      "instagram",
        +      "threads",
        +      "tiktok",
        +      "youtube",
        +      "linkedin",
        +      "x",
        +      "pinterest",
        +      "google_business",
        +      "bluesky",
        +      "telegram",
        +      "wordpress",
        +      "discord",
        +      "slack",
        +      "whop"
        +    ],
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / channels / anyOf
        Added value: +[
        +  {
        +    "items": {
        +      "enum": [
        +        "facebook",
        +        "instagram",
        +        "threads",
        +        "tiktok",
        +        "youtube",
        +        "linkedin",
        +        "x",
        +        "pinterest",
        +        "google_business",
        +        "bluesky",
        +        "telegram",
        +        "wordpress",
        +        "discord",
        +        "slack",
        +        "whop"
        +      ],
        +      "type": "string"
        +    },
        +    "type": "array"
        +  },
        +  {
        +    "const": "all",
        +    "type": "string"
        +  }
        +]
      • changedInput schema / properties / channels / description
        Previous value: -"restrict it to these channels. Omit (or send an empty list) to use every connected channel that can carry each post."New value: +"REPLACES the list; a channel connected later stays out until added. \"all\" = every channel, later ones too, only if the user asks"
      • removedInput schema / properties / channels / items
        Removed value: -{
        -  "enum": [
        -    "facebook",
        -    "instagram",
        -    "threads",
        -    "tiktok",
        -    "youtube",
        -    "linkedin",
        -    "x",
        -    "pinterest",
        -    "google_business",
        -    "bluesky",
        -    "telegram"
        -  ],
        -  "type": "string"
        -}
      • removedInput schema / properties / channels / type
        Removed value: -"array"
      • addedInput schema / properties / excludeChannels
        Added value: +{
        +  "description": "never these channels, even ones connected later; [] clears",
        +  "items": {
        +    "enum": [
        +      "facebook",
        +      "instagram",
        +      "threads",
        +      "tiktok",
        +      "youtube",
        +      "linkedin",
        +      "x",
        +      "pinterest",
        +      "google_business",
        +      "bluesky",
        +      "telegram",
        +      "wordpress",
        +      "discord",
        +      "slack",
        +      "whop"
        +    ],
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / removeChannels
        Added value: +{
        +  "description": "stop posting here",
        +  "items": {
        +    "enum": [
        +      "facebook",
        +      "instagram",
        +      "threads",
        +      "tiktok",
        +      "youtube",
        +      "linkedin",
        +      "x",
        +      "pinterest",
        +      "google_business",
        +      "bluesky",
        +      "telegram",
        +      "wordpress",
        +      "discord",
        +      "slack",
        +      "whop"
        +    ],
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / whopExperienceId
        Added value: +{
        +  "description": "WHOP — which forum or chat posts go to (exp_…, from list_whop_destinations). Without one, the whop’s only forum or chat is used; with several, whop is skipped rather than guessed.",
        +  "type": "string"
        +}
  3. 8 tool updatesv0.1.489
    • Changedgenerate_music2 fields changed
      • addedInput schema / properties / model
        Added value: +{
        +  "description": "minimax-music-3 or lyria-3.5",
        +  "type": "string"
        +}
      • addedInput schema / properties / seconds
        Added value: +{
        +  "description": "track length, up to 300",
        +  "type": "number"
        +}
    • Changedgenerate_video1 field changed
      • addedInput schema / properties / musicModel
        Added value: +{
        +  "description": "compose the bed on minimax-music-3 or lyria-3.5, skipping the free library",
        +  "type": "string"
        +}
    • Changedhook_variants2 fields changed
      • changedInput schema / properties / dryRun / description
        Previous value: -"true = return the plan and the quote, render nothing"New value: +"true = the paid plan and the price, no render"
      • addedInput schema / properties / quoteOnly
        Added value: +{
        +  "description": "true = the exact credits, free; nothing runs",
        +  "type": "boolean"
        +}
    • Changedmultiply_ad3 fields changed
      • changedInput schema / properties / dryRun / description
        Previous value: -"true = return the plan and the quote, render nothing"New value: +"true = the paid plan and the price, no render"
      • addedInput schema / properties / plan
        Added value: +{
        +  "description": "the `plan` a dryRun returned, rendered without planning again"
        +}
      • addedInput schema / properties / quoteOnly
        Added value: +{
        +  "description": "true = the exact credits, free; nothing runs",
        +  "type": "boolean"
        +}
    • Changedpost_edit1 field changed
      • addedInput schema / properties / ops / items / properties / model
        Added value: +{
        +  "description": "music, generate: the model",
        +  "type": "string"
        +}
    • Changedpost_to_meta1 field changed
      • changedInput schema / properties / story / description
        Previous value: -"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A Story takes NO CAPTION, collaborators, product tags or trialReel — any of those is refused by name and nothing is posted (a story must never silently drop your words). FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."New value: +"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A link sticker cannot be added through Meta’s publishing API on either channel: add it in the Instagram app. An Instagram Story keeps its caption: people see the start under the Story in the app and tap for the rest (instagram.com does not show it). It takes NO collaborators, product tags or trialReel — any of those is refused by name and nothing is posted. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."
    • Changedrender_ad1 field changed
      • addedInput schema / properties / musicModel
        Added value: +{
        +  "description": "compose the bed on minimax-music-3 or lyria-3.5, skipping the free library",
        +  "type": "string"
        +}
    • Changedschedule_post1 field changed
      • changedInput schema / properties / story / description
        Previous value: -"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A Story takes NO CAPTION, collaborators, product tags or trialReel — any of those is refused by name and nothing is posted (a story must never silently drop your words). FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."New value: +"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A link sticker cannot be added through Meta’s publishing API on either channel: add it in the Instagram app. An Instagram Story keeps its caption: people see the start under the Story in the app and tap for the rest (instagram.com does not show it). It takes NO collaborators, product tags or trialReel — any of those is refused by name and nothing is posted. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."
  4. 11 tool updatesv0.1.484
    • Changededit_image1 field changed
      • changedInput schema / properties / dryRun / description
        Previous value: -"true = return the estimated credits this edit reserves and render nothing"New value: +"true = return what this edit usually costs in credits and render nothing"
    • Changedgenerate_image1 field changed
      • changedInput schema / properties / fixLabel / description
        Previous value: -"default true: when the saved brand's product photo rides in this render, the product's label on the finished image is READ and compared with the photo, and re-printed from the photo at close range ONLY if it came out wrong (a label that is already right costs only the check, a credit or two; a re-print adds about ten). The reply says whether the label was checked, fixed or left as rendered (`labelPass`). Pass false when the user wants the packaging left exactly as generated: nothing is checked or re-printed."New value: +"default true: when the saved brand's product photo rides in this render, the product's label on the finished image is READ and compared with the photo, and re-printed from the photo at close range ONLY if it came out wrong (a label that is already right costs only the check; a re-print costs more, and dryRun:true returns the up-to price). The reply says whether the label was checked, fixed or left as rendered (`labelPass`). Pass false when the user wants the packaging left exactly as generated: nothing is checked or re-printed."
    • Changedgenerate_video1 field changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "description": "true = render NOTHING and return the credits this exact clip would hold (an estimate; the final charge is its actual cost). Free. Call this to price a clip before running it: without it the call renders and bills.",
        +  "type": "boolean"
        +}
    • Changedmake_series2 fields changed
      • changedInput schema / properties / voice / description
        Previous value: -"narrator; default follows the reference"New value: +"narrator: 'female' or 'male' (default follows the reference), or any narration voice name make_explainer takes"
      • removedInput schema / properties / voice / enum
        Removed value: -[
        -  "female",
        -  "male"
        -]
    • Changedpost_to_meta5 fields changed
      • changedInput schema / properties / aiGenerated / description
        Previous value: -"INSTAGRAM / FACEBOOK REEL — Meta’s is_ai_generated self-disclosure. OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, media that came through upload_file or from an external URL (the user’s own photographs or footage) is NOT — a real photo must never carry Instagram’s “AI info” label. Pass true or false only to override: false strips the label from something Hermoso would otherwise declare, true declares a render the user uploaded themselves."New value: +"INSTAGRAM / FACEBOOK REEL / THREADS — Meta’s is_ai_generated self-disclosure (Threads: is_made_with_ai, the AI info label; a text-only Threads post is declared only when you pass true). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, media that came through upload_file or from an external URL (the user’s own photographs or footage) is NOT — a real photo must never carry Instagram’s “AI info” label. Pass true or false only to override: false strips the label from something Hermoso would otherwise declare, true declares a render the user uploaded themselves."
      • changedInput schema / properties / locationId / description
        Previous value: -"TAG A PLACE. THREADS: a place id from search_threads_locations. INSTAGRAM: the NUMERIC ID OF A FACEBOOK PAGE associated with that location, not a place name and not coordinates; a non-numeric value is refused rather than sent."New value: +"TAG A PLACE. THREADS: a place id from search_threads_locations. INSTAGRAM: the pageId from search_meta_places, the NUMERIC ID OF A FACEBOOK PAGE associated with that location, not a place name and not coordinates; a non-numeric value is refused rather than sent."
      • changedInput schema / properties / place / description
        Previous value: -"FACEBOOK — tag a location on the Page post. The NUMERIC ID OF THE FACEBOOK PAGE for that place, not a place name and not coordinates."New value: +"FACEBOOK — tag a location on the Page post. The pageId from search_meta_places: the NUMERIC ID OF THE FACEBOOK PAGE for that place, not a place name and not coordinates."
      • addedInput schema / properties / placeName
        Added value: +{
        +  "description": "the name the user saw for that place; a different business is refused",
        +  "type": "string"
        +}
      • changedInput schema / properties / story / description
        Previous value: -"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A Story carries NO CAPTION (there is nowhere to show one), no collaborators, no product tags and no trialReel — passing any of those is refused by name and nothing is posted, because a story that silently drops the words is worse than one that never went. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."New value: +"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A Story takes NO CAPTION, collaborators, product tags or trialReel — any of those is refused by name and nothing is posted (a story must never silently drop your words). FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."
    • Changedpost_to_x3 fields changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "description": "true = post NOTHING and return what this exact post would cost (costCredits). Free.",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / text / description
        Previous value: -"the post text. 280 characters without X Premium, up to 25,000 with it — write the full thing, it is never truncated. Use this OR thread, not both."New value: +"the post text. 280 characters without X Premium, up to 25,000 with it — write the full thing, it is never truncated. With `thread` also set, this is the FIRST post and the thread entries follow it (a thread whose first entry is already this text is not posted twice)."
      • changedInput schema / properties / thread / description
        Previous value: -"a thread: each string is one post, published in order, each replying to the previous. Max 25. Each part follows the same length rule as `text`, and on an X Premium account ONE long post is usually both better reading and cheaper than a thread."New value: +"a thread: each string is one post, published in order, each replying to the previous (if `text` is also set it is posted first and these follow). Max 25 including `text`. Each part follows the same length rule as `text`, and on an X Premium account ONE long post is usually both better reading and cheaper than a thread."
    • Changedreschedule_post4 fields changed
      • changedInput schema / properties / aiGenerated / description
        Previous value: -"AI-CONTENT DISCLOSURE (Instagram / Facebook Reel is_ai_generated, TikTok is_aigc, YouTube containsSyntheticMedia, Pinterest ai_disclosures). OMIT IT and the value already on the post stays; a post that never had one is decided from provenance at publish: a Hermoso render is declared AI-generated, media that came through upload_file or from an external URL (the user’s own photos or footage) is NOT. Pass true or false only to override."New value: +"AI-CONTENT DISCLOSURE (Instagram / Facebook Reel is_ai_generated, TikTok is_aigc, YouTube containsSyntheticMedia, Pinterest ai_disclosures, Threads is_made_with_ai). OMIT IT and the value already on the post stays; a post that never had one is decided from provenance at publish: a Hermoso render is declared AI-generated, media that came through upload_file or from an external URL (the user’s own photos or footage) is NOT. Pass true or false only to override."
      • addedInput schema / properties / brandPartner
        Added value: +{
        +  "description": "YOUTUBE: sponsoring brand channel (UC… or @handle); \"\" removes it",
        +  "type": "string"
        +}
      • addedInput schema / properties / paidProductPlacement
        Added value: +{
        +  "description": "YOUTUBE: paid product placement (shows the disclosure) — false removes it",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / placeName
        Added value: +{
        +  "description": "the name the user saw for that place; a different business is refused",
        +  "type": "string"
        +}
    • Changedrun_post_refill1 field changed
      • changedInput schema / properties / dryRun / description
        Previous value: -"default TRUE (a preview: nothing rendered, nothing queued; the copy it writes bills a few credits). false renders fresh creative and schedules or drafts it per the mode."New value: +"default TRUE (a preview: nothing rendered, nothing queued; the copy it writes bills its model cost). false renders fresh creative and schedules or drafts it per the mode."
    • Changedschedule_post5 fields changed
      • changedInput schema / properties / aiGenerated / description
        Previous value: -"AI-CONTENT DISCLOSURE (Instagram / Facebook Reel is_ai_generated, TikTok is_aigc, YouTube containsSyntheticMedia, Pinterest ai_disclosures). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared AI-generated, media that came through upload_file or from an external URL (the user’s own photos or footage) is NOT. Pass true or false only to override."New value: +"AI-CONTENT DISCLOSURE (Instagram / Facebook Reel is_ai_generated, TikTok is_aigc, YouTube containsSyntheticMedia, Pinterest ai_disclosures, Threads is_made_with_ai). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared AI-generated, media that came through upload_file or from an external URL (the user’s own photos or footage) is NOT. Pass true or false only to override."
      • changedInput schema / properties / instagramLocationId / description
        Previous value: -"INSTAGRAM — tag a place (called locationId on post_to_meta; locationId here is the Google Business listing). It is the NUMERIC ID OF A FACEBOOK PAGE associated with that location, not a place name and not coordinates; a non-numeric value is refused rather than sent."New value: +"INSTAGRAM — tag a place (called locationId on post_to_meta; locationId here is the Google Business listing). It is the pageId from search_meta_places, the NUMERIC ID OF A FACEBOOK PAGE associated with that location, not a place name and not coordinates; a non-numeric value is refused rather than sent."
      • changedInput schema / properties / place / description
        Previous value: -"FACEBOOK — tag a location on the Page post. The NUMERIC ID OF THE FACEBOOK PAGE for that place, not a place name and not coordinates."New value: +"FACEBOOK — tag a location on the Page post. The pageId from search_meta_places: the NUMERIC ID OF THE FACEBOOK PAGE for that place, not a place name and not coordinates."
      • addedInput schema / properties / placeName
        Added value: +{
        +  "description": "the name the user saw for that place; a different business is refused",
        +  "type": "string"
        +}
      • changedInput schema / properties / story / description
        Previous value: -"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A Story carries NO CAPTION (there is nowhere to show one), no collaborators, no product tags and no trialReel — passing any of those is refused by name and nothing is posted, because a story that silently drops the words is worse than one that never went. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."New value: +"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A Story takes NO CAPTION, collaborators, product tags or trialReel — any of those is refused by name and nothing is posted (a story must never silently drop your words). FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."
    • Changedset_competitor_watch1 field changed
      • changedInput schema / properties / auto / description
        Previous value: -"true picks competitors, similar brands and topics from the saved brand or seed; costs a few credits"New value: +"true picks competitors, similar brands and topics from the saved brand or seed; bills one model call at its token cost"
    • Changedupscale_video2 fields changed
      • changedInput schema / properties / engine / description
        Previous value: -"default 'standard', the precision upscaler. 'flux' = the FLUX 3 video upscaler"New value: +"default 'standard', the precision upscaler. 'flux' = the FLUX 3 video upscaler. 'detail' = rebuilds fine detail, costs the most"
      • changedInput schema / properties / engine / enum
        Previous value: -[
        -  "standard",
        -  "flux"
        -]New value: +[
        +  "standard",
        +  "flux",
        +  "detail"
        +]
  5. 1 tool updatev0.1.467
    • Changededit_video1 field changed
      • changedInput schema / properties / engine / description
        Previous value: -"default auto: Seedance 2.5 Edit without a real-looking person, else Kling O3 Edit"New value: +"default auto: Seedance 2.5 Edit without a real-looking person, else Kling 4.0 Edit"
  6. 61 tool updatesv0.1.465
    • Changedadd_subtitles5 fields changed
      • addedInput schema / properties / auto / description
        Added value: +"sentences (default) or words: timing unit for transcribed subtitles"
      • addedInput schema / properties / cues / items / properties / end / description
        Added value: +"second the line disappears"
      • addedInput schema / properties / cues / items / properties / start / description
        Added value: +"second the line appears"
      • addedInput schema / properties / cues / items / properties / text / description
        Added value: +"subtitle line text, max 90 characters, no emoji"
      • addedInput schema / properties / wordsPerCue / description
        Added value: +"words per subtitle line when auto is words"
    • Changedappend_to_notion_page2 fields changed
      • addedInput schema / properties / includeSchedule / description
        Added value: +"true appends the scheduled posts too"
      • addedInput schema / properties / swipefileCollection / description
        Added value: +"swipefile collection name or id to append, with each ad's creative, copy, dates and link"
    • Addedautofill_canva_template
    • Changedclear_sheet_range3 fields changed
      • addedInput schema / properties / confirm / description
        Added value: +"true clears the range; call without it first to get the filled-cell count"
      • addedInput schema / properties / sheetUrl / description
        Added value: +"Google Sheets link, instead of spreadsheetId"
      • addedInput schema / properties / spreadsheetId / description
        Added value: +"Google spreadsheet id; use this or sheetUrl"
    • Addedcreate_canva_folder
    • Changeddelete_linkedin_lead_subscription1 field changed
      • addedInput schema / properties / subscriptionId / description
        Added value: +"webhook id from list_linkedin_lead_subscriptions"
    • Changedduplicate_scheduled5 fields changed
      • addedInput schema / properties / imageUrl / description
        Added value: +"replacement image for the copy: a Hermoso render URL or an upload_file URL"
      • addedInput schema / properties / link / description
        Added value: +"destination URL for the copy (Pinterest, Facebook text post or Google Business post)"
      • addedInput schema / properties / title / description
        Added value: +"Pinterest or YouTube headline for the copy, max 100 characters"
      • addedInput schema / properties / videoUrl / description
        Added value: +"replacement video for the copy: a Hermoso render URL or an upload_file URL"
      • addedInput schema / properties / visibility / description
        Added value: +"public (default), unlisted or private (YouTube), or draft (TikTok or an unpublished Facebook Page post)"
    • Addedexport_canva_design
    • Changedexport_schedule_to_notion2 fields changed
      • addedInput schema / properties / dataSourceId / description
        Added value: +"data source id of an existing database to write rows into, matched to columns by name"
      • addedInput schema / properties / parentPageId / description
        Added value: +"Notion page to create the new database under; omit for a private page in the workspace"
    • Changedfind_creators1 field changed
      • addedInput schema / properties / minAvgViews / description
        Added value: +"minimum average views per post; creators below this floor are dropped"
    • Changedfind_sound3 fields changed
      • addedInput schema / properties / pick / description
        Added value: +"candidate number from the previous list to take instead of the first"
      • addedInput schema / properties / query / description
        Added value: +"sound by name, moment or meaning, e.g. 'bad news reaction'"
      • addedInput schema / properties / url / description
        Added value: +"link to a TikTok sound, a post, a meme-sound page or an audio file"
    • Changedformat_sheet4 fields changed
      • addedInput schema / properties / autoResize / description
        Added value: +"auto-size columns to fit their content; default true, false skips it"
      • addedInput schema / properties / boldHeader / description
        Added value: +"bold the header row; default true, false skips it"
      • addedInput schema / properties / sheetUrl / description
        Added value: +"Google Sheets link, instead of spreadsheetId"
      • addedInput schema / properties / spreadsheetId / description
        Added value: +"Google spreadsheet id; use this or sheetUrl"
    • Changedget_linkedin_lead1 field changed
      • addedInput schema / properties / leadId / description
        Added value: +"lead id from list_linkedin_leads"
    • Changedhook_variants3 fields changed
      • addedInput schema / properties / hooks / items / properties / mechanic / description
        Added value: +"a named hook mechanic id from list_hooks"
      • addedInput schema / properties / hooks / items / properties / prompt / description
        Added value: +"an opening described in words"
      • addedInput schema / properties / hooks / items / properties / url / description
        Added value: +"viral hook to join in front: a post link or a video file"
    • Addedlist_canva_brand_templates
    • Addedlist_canva_designs
    • Addedlist_canva_folder
    • Changedlist_linkedin_lead_events1 field changed
      • addedInput schema / properties / limit / description
        Added value: +"events to return, newest first; default 50, max 200"
    • Changedlist_linkedin_lead_subscriptions1 field changed
      • addedInput schema / properties / leadType / description
        Added value: +"SPONSORED for an ad account; COMPANY, EVENT or ORGANIZATION_PRODUCT for a Page; defaults by owner"
    • Changedlist_linkedin_leads1 field changed
      • addedInput schema / properties / until / description
        Added value: +"end of the time window, ISO date or epoch milliseconds; omit for now"
    • Changedlist_meta_posts1 field changed
      • addedInput schema / properties / tagged
        Added value: +{
        +  "description": "INSTAGRAM — list posts OTHER accounts tagged the brand in (UGC, creator posts, partner mentions), with each author’s @handle, instead of the brand’s own posts. Free.",
        +  "type": "boolean"
        +}
    • Changedlist_notion_pages1 field changed
      • addedInput schema / properties / type / description
        Added value: +"limit results to pages or to databases; omit for both"
    • Changedlist_pinterest_boards1 field changed
      • addedInput schema / properties / account
        Added value: +{
        +  "description": "which connected account: its @handle or id from list_connector_accounts; needed only when several are connected",
        +  "type": "string"
        +}
    • Changedlist_telegram_chats1 field changed
      • addedInput schema / properties / account
        Added value: +{
        +  "description": "which connected account: its @handle or id from list_connector_accounts; needed only when several are connected",
        +  "type": "string"
        +}
    • Changedmake_insert7 fields changed
      • addedInput schema / properties / image / description
        Added value: +"still picture URL; use this or video"
      • addedInput schema / properties / seconds / description
        Added value: +"fixed insert length, 0.2 to 6 seconds, instead of when the sound lands"
      • addedInput schema / properties / sound / description
        Added value: +"sound URL (find_sound returns one), or a video whose own audio is used"
      • addedInput schema / properties / soundEnd / description
        Added value: +"seconds into the sound where it ends"
      • addedInput schema / properties / soundStart / description
        Added value: +"seconds into the sound where the insert's sound starts"
      • addedInput schema / properties / video / description
        Added value: +"video URL to take the picture from, starting at videoStart"
      • addedInput schema / properties / videoStart / description
        Added value: +"seconds into the video where the insert starts"
    • Changedmake_motion_film8 fields changed
      • addedInput schema / properties / confirm / description
        Added value: +"true renders the film for real credits; without it you get a free quote"
      • addedInput schema / properties / help / description
        Added value: +"true returns the spec language and recipes; renders nothing"
      • addedInput schema / properties / preview / description
        Added value: +"true renders a 1-credit contact sheet to approve before the full render"
      • addedInput schema / properties / projectUrl / description
        Added value: +"a past film's url; returns its saved spec to edit and render again"
      • addedInput schema / properties / recipe / description
        Added value: +"type, logo, launch, product or social; help:true lists the recipes"
      • addedInput schema / properties / size / description
        Added value: +"frame shape: 16:9, 9:16, 1:1 or 4:5"
      • addedInput schema / properties / spec / description
        Added value: +"the whole film as JSON, from a quote or a saved film; help:true shows the spec language"
      • addedInput schema / properties / tagline / description
        Added value: +"line shown under the headline or logo"
    • Changedmake_series4 fields changed
      • addedInput schema / properties / schedule / properties / at / description
        Added value: +"time of the first post: YYYY-MM-DDTHH:MM wall-clock, or an ISO date-time"
      • addedInput schema / properties / schedule / properties / channels / description
        Added value: +"channels to post each episode to, e.g. tiktok, instagram, youtube (as schedule_post takes them)"
      • addedInput schema / properties / schedule / properties / everyHours / description
        Added value: +"hours between episodes; default 24"
      • addedInput schema / properties / schedule / properties / timezone / description
        Added value: +"timezone name for at, as schedule_post takes it"
    • Changedmake_template_ad1 field changed
      • changedInput schema / properties / config / description
        Previous value: -"MUST include config.template: 'custom' or a preset id, plus its fields. PRESETS: 'slideshow' (IMAGES, TikTok photo mode / Reels 1080x1920, or size:'4:5' feed carousels; no branding): { slides:[{text, sub?, image?, blur?, background?, position?}] (2-35; words never rewritten), style? ('tiktok-classic'|'clean-minimal'|'note-style' or a look in words), textStyle?, video?:true (+ an MP4, which takes config.music like any video) }; 2 credits, +1 per slide past 5, +2 for the MP4. 'imessage-chat' (VIDEO ~15s): { thread:{contactName, messages:[{from:'them'|'me', text?, product?:{image,title,domain}}]}, theme?, endCard }. 'chatgpt-chat' (VIDEO): { question, answer (may **bold** the brand), productImage?, endCard }. 'apple-notes' (VIDEO): { title, lines[], theme?, endCard }. 'value-prop' (VIDEO ~17s): { hook ≤40ch, claims[3-5 ≤34ch], productImages[2-3], palette[], endCard }. 'static-mockup' (IMAGE): { style:'imessage'|'notes'|'card', size?:{w,h}, ...fields }. 'airdrop-carousel' (VIDEO): { brandName, products:[{image, title?}] (3-16), endCard }. 'app-ui-tour' (VIDEO): { hook?, appName, iconImage?, beats:[{screenImage, caption}] (2-6), endCard }. 'imessage-cascade' (VIDEO): { notifications:[{sender, text}] (4-8), backgroundImage?, endCard }. 'photo-grid' (VIDEO): { title?, photos:[{image, label?}] (4-9), endCard }. 'vignette' (VIDEO): { hook, lines[2-4 ≤40ch], heroImage, endCard }. 'kinetic-type' (VIDEO, own SFX): { phrases[3-6 ≤34ch], productImages?[≤4], endCard }. 'myth-vs-fact' (VIDEO with a real VOICEOVER, small extra charge): { pairs:[{myth ≤50ch, fact ≤60ch}] (2-4; [brackets] accent), endCard }, real truths only. 'carousel' (IMAGES, 5-10 branded 1080x1080): { cover:{hook?, title}, slides:[{headline, support?, stat?:{value, label}}] (3-8), cta:{headline, cta?, domain?}, productImage?, logo? }. endCard = { headline, cta, domain?, logo?, color? }; palette and fontStack optional."New value: +"MUST include config.template: 'custom' or a preset id, plus its fields. PRESETS: 'slideshow' (IMAGES, TikTok photo mode / Reels 1080x1920, or size:'4:5' feed carousels; no branding): { slides:[{text, sub?, image?, blur?, background?, position?}] (2-35; words never rewritten), style? ('tiktok-classic'|'clean-minimal'|'note-style' or a look in words), textStyle?, video?:true (+ an MP4, which takes config.music like any video) }; 2 credits, +1 per slide past 5, +2 for the MP4. 'imessage-chat' (VIDEO ~15s): { thread:{contactName, messages:[{from:'them'|'me', text?, product?:{image,title,domain}}]}, theme?, endCard }. 'chatgpt-chat' (VIDEO): { question, answer (may **bold** the brand), productImage?, endCard }. 'apple-notes' (VIDEO): { title, lines[], theme?, endCard }. 'value-prop' (VIDEO ~17s): { hook ≤40ch, claims[3-5 ≤34ch], productImages[2-3], palette[], endCard }. 'static-mockup' (IMAGE): { style:'imessage'|'notes'|'card', size?:{w,h}, ...fields }. 'airdrop-carousel' (VIDEO): { brandName, products:[{image, title?}] (3-16), endCard }. 'app-ui-tour' (VIDEO): { hook?, appName, iconImage?, beats:[{screenImage, caption}] (2-6), endCard }. 'imessage-cascade' (VIDEO): { notifications:[{sender, text}] (4-8), backgroundImage?, endCard }. 'photo-grid' (VIDEO): { title?, photos:[{image, label?}] (4-9), endCard }. 'vignette' (VIDEO): { hook, lines[2-4 ≤40ch], heroImage, endCard }. 'kinetic-type' (VIDEO, own SFX): { phrases[3-6 ≤34ch], productImages?[≤4], endCard }. 'myth-vs-fact' (VIDEO with a real VOICEOVER, small extra charge): { pairs:[{myth ≤50ch, fact ≤60ch}] (2-4; [brackets] accent), endCard }, real truths only. 'carousel' (IMAGES, 5-10 branded 1080x1080): { cover:{hook?, title}, slides:[{headline, support?, stat?:{value, label}}] (3-8), cta:{headline, cta?, domain?}, productImage?, logo? }. endCard = { headline, cta, domain?, logo?, color? } is OPTIONAL on every video preset: include it ONLY when the user asked for an end card (a closing card with their logo); without it the video ends on its own last frame. palette and fontStack optional."
    • Changedmake_thumbnail5 fields changed
      • addedInput schema / properties / people / items / properties / describe / description
        Added value: +"the person's appearance in English prose, used instead of a face photo"
      • addedInput schema / properties / split / properties / mode / description
        Added value: +"layout: plain, before_after, versus or custom"
      • addedInput schema / properties / split / properties / panels / description
        Added value: +"panel descriptions in order, left to right or top to bottom; sets the panel count (minimum 2)"
      • addedInput schema / properties / tweak / properties / kind / description
        Added value: +"emotion, background, background_color or rim_light, or another kind with the edit in words"
      • addedInput schema / properties / tweak / properties / value / description
        Added value: +"new value for the kind: an emotion, a colour, or the edit in words"
    • Changedmanage_sheet_tabs5 fields changed
      • addedInput schema / properties / action / description
        Added value: +"add, rename or delete a tab"
      • addedInput schema / properties / confirm / description
        Added value: +"true deletes a tab that holds data; needs confirmCells"
      • addedInput schema / properties / confirmCells / description
        Added value: +"the filled-cell count an unconfirmed delete reported"
      • addedInput schema / properties / sheetUrl / description
        Added value: +"Google Sheets link, instead of spreadsheetId"
      • addedInput schema / properties / spreadsheetId / description
        Added value: +"Google spreadsheet id; use this or sheetUrl"
    • Addedonedrive_file_permissions
    • Changedplan_ad2 fields changed
      • addedInput schema / properties / draft / properties / durationSeconds / description
        Added value: +"clip length in seconds from that same refusal; use only the value it gave"
      • addedInput schema / properties / draft / properties / model / description
        Added value: +"video model id from a refusal that offered a lighter draft; use only the value it gave"
    • Changedpost_edit16 fields changed
      • addedInput schema / properties / ops / items / properties / bridge / properties / flash / description
        Added value: +"true (default) adds a white flash on the cut; false skips it"
      • addedInput schema / properties / ops / items / properties / bridge / properties / kind / description
        Added value: +"impact (cut before the hook's payoff) or text (words over the cut)"
      • addedInput schema / properties / ops / items / properties / bridge / properties / matchCut / description
        Added value: +"second in our clip where it starts"
      • addedInput schema / properties / ops / items / properties / bridge / properties / shake / description
        Added value: +"true (default) adds a camera shake on the cut; false skips it"
      • addedInput schema / properties / ops / items / properties / bridge / properties / text / description
        Added value: +"text bridge only: words shown over the cut, only when the user asks for them"
      • addedInput schema / properties / ops / items / properties / bridge / properties / then / description
        Added value: +"text bridge only: a line shown after the text"
      • addedInput schema / properties / ops / items / properties / clips / items / properties / end / description
        Added value: +"seconds into this clip to stop at"
      • addedInput schema / properties / ops / items / properties / clips / items / properties / match / description
        Added value: +"seam match for this clip: auto (default), off, or a grade object"
      • addedInput schema / properties / ops / items / properties / clips / items / properties / reframe / description
        Added value: +"shot change at this seam: auto, off, or a step from 1.1 to 1.5"
      • addedInput schema / properties / ops / items / properties / clips / items / properties / start / description
        Added value: +"seconds into this clip to start from"
      • addedInput schema / properties / ops / items / properties / clips / items / properties / url / description
        Added value: +"clip to join after the video: Library URL, direct file or public post link"
      • addedInput schema / properties / ops / items / properties / keepSound / description
        Added value: +"voiceover only: true (default) keeps the clip's sound under the voice; false replaces it"
      • addedInput schema / properties / ops / items / properties / op / description
        Added value: +"the edit to apply, in order; each op takes only the fields listed for it in the tool description"
      • addedInput schema / properties / ops / items / properties / textStyle / description
        Added value: +"text op: a textStyle object or preset name, same as style"
      • addedInput schema / properties / ops / items / properties / voice / description
        Added value: +"voiceover voice: female or male"
      • addedInput schema / properties / ops / items / properties / y / description
        Added value: +"vertical centre for text or watermark: 0 to 1 of frame height, or pixels"
    • Changedpost_to_bluesky1 field changed
      • changedInput schema / properties / imageUrls / description
        Previous value: -"Up to 4 public image URLs to attach. Cannot be combined with videoUrl."New value: +"Up to 10 public image URLs to attach (1–4 a standard image post, 5–10 a Bluesky gallery; more than 10 is refused, never cut). Cannot be combined with videoUrl."
    • Changedpost_to_linkedin13 fields changed
      • addedInput schema / properties / altText
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    }
        +  ],
        +  "description": "image alt text — a string for every image, or an array per slide"
        +}
      • addedInput schema / properties / captionsSrt
        Added value: +{
        +  "description": "SRT caption CONTENT for a videoUrl post (English only, attached during upload)",
        +  "type": "string"
        +}
      • addedInput schema / properties / documentTitle
        Added value: +{
        +  "description": "the title shown on the document (defaults to the file name)",
        +  "type": "string"
        +}
      • addedInput schema / properties / documentUrl
        Added value: +{
        +  "description": "a DOCUMENT post — a Hermoso-hosted PDF / PPTX / DOCX (upload_file first) that readers swipe through page by page in the feed; LinkedIn takes up to 100MB and 300 pages. One kind of media per post: not with an image, video, slides or linkUrl.",
        +  "type": "string"
        +}
      • changedInput schema / properties / imageUrls / description
        Previous value: -"A CAROUSEL IS NOT AVAILABLE ON A PERSONAL PROFILE — LinkedIn's organic multi-image post publishes from a COMPANY PAGE. Passing several here is refused by name rather than posting slide 1; use post_to_linkedin_page instead."New value: +"a MULTI-IMAGE post: 2–20 Hermoso-hosted images in order, one post the reader swipes through (LinkedIn’s MultiImage API takes a member author on w_member_social). Images only."
      • addedInput schema / properties / linkDescription
        Added value: +{
        +  "description": "link card description; LinkedIn does not read the page, so set it here",
        +  "type": "string"
        +}
      • addedInput schema / properties / linkThumbnailUrl
        Added value: +{
        +  "description": "Hermoso-hosted image for the card",
        +  "type": "string"
        +}
      • addedInput schema / properties / linkTitle
        Added value: +{
        +  "description": "the card headline — LinkedIn never scrapes the page, so without it the card is unlabelled",
        +  "type": "string"
        +}
      • addedInput schema / properties / linkUrl
        Added value: +{
        +  "description": "a LINK post with a real preview card. Not with media.",
        +  "type": "string"
        +}
      • addedInput schema / properties / mentions
        Added value: +{
        +  "description": "TAG COMPANIES: write \"@Name\" in the text where each tag goes and list each company here — its vanity name (the part after linkedin.com/company/), its company page URL, or {organizationId, name}. Hermoso resolves it on LinkedIn and LinkedIn renders a real clickable mention; a company with no \"@\" spot in the text is refused rather than appended.",
        +  "items": {},
        +  "type": "array"
        +}
      • addedInput schema / properties / title
        Added value: +{
        +  "description": "video title",
        +  "type": "string"
        +}
      • addedInput schema / properties / videoThumbnailUrl
        Added value: +{
        +  "description": "cover image for a videoUrl post (Hermoso-hosted)",
        +  "type": "string"
        +}
      • addedInput schema / properties / videoUrl
        Added value: +{
        +  "description": "a Hermoso-hosted video (a render, or the user’s footage via upload_file). LinkedIn processes it before publishing, which takes a minute.",
        +  "type": "string"
        +}
    • Changedpost_to_linkedin_page14 fields changed
      • changedInput schema / properties / altText / description
        Previous value: -"accessibility alt text (max 4086 characters, ~120 recommended). A STRING describes every image; an ARRAY describes each slide of a multi-image post separately, in slide order — LinkedIn stores altText per image, and their own sample request carries a different one on each. Not available on a PERSONAL-profile post: LinkedIn’s member posting API has no alt-text field at all."New value: +"accessibility alt text (max 4086 characters, ~120 recommended). A STRING describes every image; an ARRAY describes each slide of a multi-image post separately, in slide order — LinkedIn stores altText per image, and their own sample request carries a different one on each. Also works on a personal-profile post (post_to_linkedin)."
      • addedInput schema / properties / documentTitle
        Added value: +{
        +  "description": "the title shown on the document (defaults to the file name)",
        +  "type": "string"
        +}
      • addedInput schema / properties / documentUrl
        Added value: +{
        +  "description": "a DOCUMENT post — a Hermoso-hosted PDF / PPTX / DOCX (upload_file first) that readers swipe through page by page in the feed; LinkedIn takes up to 100MB and 300 pages. One kind of media per post: not with an image, video, slides or linkUrl.",
        +  "type": "string"
        +}
      • addedInput schema / properties / firstComment
        Added value: +{
        +  "description": "FIRST COMMENT — posted on the new post AS THE PAGE right after it publishes. Needs the LinkedIn comments authorization on this brand (provider linkedin_community). Up to 2,000 characters, refused rather than cut. If the comment fails the post stays live and firstComment in the result says why.",
        +  "type": "string"
        +}
      • changedInput schema / properties / imageUrls / description
        Previous value: -"CAROUSEL — an ORDERED list of image (and, where the channel allows, video) URLs published as ONE post the viewer swipes through. THIS IS NOT “post several” — it is a single post with several slides, which is what a multi-slide creative (a listicle, a “1/6 · SWIPE” deck) actually needs; publishing only its first slide tells the viewer to swipe at something that cannot. The ORDER is the product. Limits per channel: Instagram 2–10 (images, videos or a mix), Threads 2–20 (mix allowed), Facebook 2+ (Meta publishes no documented maximum; Hermoso caps the upload fan-out at 30 and says so), LinkedIn company Pages 2–20 (images only), Pinterest 2–5 (images only), TikTok up to 35. One url here is simply an ordinary single post. Anything a channel cannot do is REFUSED with the real reason — nothing is ever quietly downgraded to one slide."New value: +"CAROUSEL — an ORDERED list of image (and, where the channel allows, video) URLs published as ONE post the viewer swipes through. THIS IS NOT “post several” — it is a single post with several slides, which is what a multi-slide creative (a listicle, a “1/6 · SWIPE” deck) actually needs; publishing only its first slide tells the viewer to swipe at something that cannot. The ORDER is the product. Limits per channel: Instagram 2–20 (images, videos or a mix), Threads 2–20 (mix allowed), Facebook 2+ (Meta publishes no documented maximum; Hermoso caps the upload fan-out at 30 and says so), LinkedIn company Pages 2–20 (images only), Pinterest 2–5 (images only), TikTok up to 35. One url here is simply an ordinary single post. Anything a channel cannot do is REFUSED with the real reason — nothing is ever quietly downgraded to one slide."
      • addedInput schema / properties / mentions
        Added value: +{
        +  "description": "TAG COMPANIES: write \"@Name\" in the text where each tag goes and list each company here — its vanity name (the part after linkedin.com/company/), its company page URL, or {organizationId, name}. Hermoso resolves it on LinkedIn and LinkedIn renders a real clickable mention; a company with no \"@\" spot in the text is refused rather than appended.",
        +  "items": {},
        +  "type": "array"
        +}
      • addedInput schema / properties / targetAudience / properties / degrees / description
        Added value: +"education degree values to narrow the followers shown the post; company Page posts only"
      • addedInput schema / properties / targetAudience / properties / fieldsOfStudy / description
        Added value: +"fields of study to narrow the followers shown the post; company Page posts only"
      • addedInput schema / properties / targetAudience / properties / geoLocations / description
        Added value: +"LinkedIn geo URNs or ids of Page followers to show the post to; company Page posts only"
      • addedInput schema / properties / targetAudience / properties / industries / description
        Added value: +"LinkedIn industry URNs or ids to narrow the followers shown the post; company Page posts only"
      • addedInput schema / properties / targetAudience / properties / jobFunctions / description
        Added value: +"LinkedIn job function URNs or ids to narrow the followers shown the post; company Page posts only"
      • addedInput schema / properties / targetAudience / properties / organizations / description
        Added value: +"organization URNs or ids to narrow the followers shown the post; company Page posts only"
      • addedInput schema / properties / targetAudience / properties / seniorities / description
        Added value: +"LinkedIn seniority URNs or ids to narrow the followers shown the post; company Page posts only"
      • addedInput schema / properties / targetAudience / properties / staffCountRanges / description
        Added value: +"company size bands to narrow the followers shown the post; company Page posts only"
    • Changedpost_to_meta8 fields changed
      • addedInput schema / properties / audience / properties / minAge / description
        Added value: +"Facebook Page post audience: minimum age, one of 13, 15, 18, 21 or 25"
      • addedInput schema / properties / firstComment
        Added value: +{
        +  "description": "FIRST COMMENT — posted on the new post as the same account right after it publishes (Facebook or Instagram; not Threads, not a story, not an unpublished post). Up to 2,000 characters, refused rather than cut. If the comment fails the post stays live and firstComment in the result says why.",
        +  "type": "string"
        +}
      • changedInput schema / properties / imageUrls / description
        Previous value: -"CAROUSEL — an ORDERED list of image (and, where the channel allows, video) URLs published as ONE post the viewer swipes through. THIS IS NOT “post several” — it is a single post with several slides, which is what a multi-slide creative (a listicle, a “1/6 · SWIPE” deck) actually needs; publishing only its first slide tells the viewer to swipe at something that cannot. The ORDER is the product. Limits per channel: Instagram 2–10 (images, videos or a mix), Threads 2–20 (mix allowed), Facebook 2+ (Meta publishes no documented maximum; Hermoso caps the upload fan-out at 30 and says so), LinkedIn company Pages 2–20 (images only), Pinterest 2–5 (images only), TikTok up to 35. One url here is simply an ordinary single post. Anything a channel cannot do is REFUSED with the real reason — nothing is ever quietly downgraded to one slide."New value: +"CAROUSEL — an ORDERED list of image (and, where the channel allows, video) URLs published as ONE post the viewer swipes through. THIS IS NOT “post several” — it is a single post with several slides, which is what a multi-slide creative (a listicle, a “1/6 · SWIPE” deck) actually needs; publishing only its first slide tells the viewer to swipe at something that cannot. The ORDER is the product. Limits per channel: Instagram 2–20 (images, videos or a mix), Threads 2–20 (mix allowed), Facebook 2+ (Meta publishes no documented maximum; Hermoso caps the upload fan-out at 30 and says so), LinkedIn company Pages 2–20 (images only), Pinterest 2–5 (images only), TikTok up to 35. One url here is simply an ordinary single post. Anything a channel cannot do is REFUSED with the real reason — nothing is ever quietly downgraded to one slide."
      • changedInput schema / properties / story / description
        Previous value: -"INSTAGRAM STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A Story carries NO CAPTION (there is nowhere to show one), no collaborators, no product tags and no trialReel — passing any of those is refused by name and nothing is posted, because a story that silently drops the words is worse than one that never went. INSTAGRAM ONLY: a Facebook Page story is a different upload and is not built, so a Facebook channel with `story` set is refused rather than published to the feed."New value: +"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A Story carries NO CAPTION (there is nowhere to show one), no collaborators, no product tags and no trialReel — passing any of those is refused by name and nothing is posted, because a story that silently drops the words is worse than one that never went. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."
      • addedInput schema / properties / threadsGhostPost
        Added value: +{
        +  "description": "THREADS ONLY — a GHOST post: text only, Threads archives it automatically after 24 hours (a flash offer, a 24h-only note). No other Threads option can ride it.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / threadsGif
        Added value: +{
        +  "description": "THREADS ONLY — a GIPHY GIF id (the id in a giphy.com/gifs/… link) attached to a TEXT Threads post. GIPHY is the only provider Threads takes.",
        +  "type": "string"
        +}
      • addedInput schema / properties / threadsLongText
        Added value: +{
        +  "description": "THREADS ONLY — LONG-FORM text, up to 10,000 characters, attached under a TEXT-only Threads post (Threads' text attachment). The way to publish more than Threads' 500-character post: keep a short lead in the message and put the full text here. Not with an image, video or carousel.",
        +  "type": "string"
        +}
      • addedInput schema / properties / userTags
        Added value: +{
        +  "description": "INSTAGRAM USER TAGS — tag people (public accounts) on the post; each is notified. Usernames, or { username, x, y } with x/y from 0 to 1 = where the tag sits on the IMAGE (a tag without a position sits at the centre). A Reel tag takes no position; a story position is optional. Not on a carousel (refused by name).",
        +  "items": {
        +    "anyOf": [
        +      {
        +        "type": "string"
        +      },
        +      {
        +        "properties": {
        +          "username": {
        +            "type": "string"
        +          },
        +          "x": {
        +            "maximum": 1,
        +            "minimum": 0,
        +            "type": "number"
        +          },
        +          "y": {
        +            "maximum": 1,
        +            "minimum": 0,
        +            "type": "number"
        +          }
        +        },
        +        "required": [
        +          "username"
        +        ],
        +        "type": "object"
        +      }
        +    ]
        +  },
        +  "type": "array"
        +}
    • Changedpost_to_pinterest6 fields changed
      • addedInput schema / properties / aiGenerated
        Added value: +{
        +  "description": "Pinterest’s AI disclosure (ai_disclosures AI_MODIFIED). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, an image or video that came through upload_file or an external URL (the user’s own) is NOT. Pass true or false only to override.",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / imageUrls / description
        Previous value: -"CAROUSEL — an ORDERED list of image (and, where the channel allows, video) URLs published as ONE post the viewer swipes through. THIS IS NOT “post several” — it is a single post with several slides, which is what a multi-slide creative (a listicle, a “1/6 · SWIPE” deck) actually needs; publishing only its first slide tells the viewer to swipe at something that cannot. The ORDER is the product. Limits per channel: Instagram 2–10 (images, videos or a mix), Threads 2–20 (mix allowed), Facebook 2+ (Meta publishes no documented maximum; Hermoso caps the upload fan-out at 30 and says so), LinkedIn company Pages 2–20 (images only), Pinterest 2–5 (images only), TikTok up to 35. One url here is simply an ordinary single post. Anything a channel cannot do is REFUSED with the real reason — nothing is ever quietly downgraded to one slide."New value: +"CAROUSEL — an ORDERED list of image (and, where the channel allows, video) URLs published as ONE post the viewer swipes through. THIS IS NOT “post several” — it is a single post with several slides, which is what a multi-slide creative (a listicle, a “1/6 · SWIPE” deck) actually needs; publishing only its first slide tells the viewer to swipe at something that cannot. The ORDER is the product. Limits per channel: Instagram 2–20 (images, videos or a mix), Threads 2–20 (mix allowed), Facebook 2+ (Meta publishes no documented maximum; Hermoso caps the upload fan-out at 30 and says so), LinkedIn company Pages 2–20 (images only), Pinterest 2–5 (images only), TikTok up to 35. One url here is simply an ordinary single post. Anything a channel cannot do is REFUSED with the real reason — nothing is ever quietly downgraded to one slide."
      • addedInput schema / properties / slideText / items / properties / description / description
        Added value: +"carousel slide description for this slide"
      • addedInput schema / properties / slideText / items / properties / link / description
        Added value: +"carousel slide destination URL for this slide"
      • addedInput schema / properties / slideText / items / properties / title / description
        Added value: +"carousel slide title; one object per slide, in slide order"
      • addedInput schema / properties / syntheticPerformer
        Added value: +{
        +  "description": "ALSO declare SYNTHETIC_PERFORMER — the Pin shows a realistic AI-generated person. Only with an AI disclosure; never guessed, so pass true only when the user says the person is AI-made.",
        +  "type": "boolean"
        +}
    • Changedpost_to_telegram2 fields changed
      • addedInput schema / properties / buttons
        Added value: +{
        +  "description": "URL BUTTONS under the post, Telegram's call to action: [{text:'Shop now', url:'https://…'}] one per row, or rows [[a,b],[c]]. URL buttons only (https://, http:// or tg://), optional style danger|success|primary. A single photo, a video or a text post can carry them; an ALBUM cannot (Telegram's sendMediaGroup takes no buttons), so that combination is refused.",
        +  "items": {
        +    "anyOf": [
        +      {
        +        "properties": {
        +          "style": {
        +            "enum": [
        +              "danger",
        +              "success",
        +              "primary"
        +            ],
        +            "type": "string"
        +          },
        +          "text": {
        +            "type": "string"
        +          },
        +          "url": {
        +            "type": "string"
        +          }
        +        },
        +        "required": [
        +          "text",
        +          "url"
        +        ],
        +        "type": "object"
        +      },
        +      {
        +        "items": {
        +          "properties": {
        +            "style": {
        +              "enum": [
        +                "danger",
        +                "success",
        +                "primary"
        +              ],
        +              "type": "string"
        +            },
        +            "text": {
        +              "type": "string"
        +            },
        +            "url": {
        +              "type": "string"
        +            }
        +          },
        +          "required": [
        +            "text",
        +            "url"
        +          ],
        +          "type": "object"
        +        },
        +        "type": "array"
        +      }
        +    ]
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / parseMode
        Added value: +{
        +  "description": "'HTML' to format the text with Telegram's HTML: <b>, <i>, <u>, <s>, <a href=\"…\">, <code>, <pre>, <blockquote>, <tg-spoiler>. Omit for plain text (the default). In HTML mode a literal <, > or & in the copy must be written &lt; &gt; &amp;, which is why it is never turned on by itself. Markdown (**bold**) is NOT parsed; convert it to HTML tags.",
        +  "enum": [
        +    "HTML"
        +  ],
        +  "type": "string"
        +}
    • Changedpost_to_tiktok1 field changed
      • addedInput schema / properties / disableComment / description
        Added value: +"true = turn comments off on this post"
    • Changedpost_to_x6 fields changed
      • addedInput schema / properties / dmDeepLink
        Added value: +{
        +  "description": "a \"Message us\" button: true for the connected account, or https://x.com/messages/compose?recipient_id=<id>. TEXT posts only — X refuses it alongside media.",
        +  "type": [
        +    "string",
        +    "boolean"
        +  ]
        +}
      • addedInput schema / properties / taggedUserIds
        Added value: +{
        +  "description": "tag up to 10 people (numeric X user ids, from x_user) in the post’s media",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / videoCta
        Added value: +{
        +  "description": "VIDEO posts only — a button on the video; needs videoCtaUrl (https)",
        +  "enum": [
        +    "visit_site",
        +    "watch_now"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / videoCtaUrl
        Added value: +{
        +  "description": "https link the video button opens; used with videoCta",
        +  "type": "string"
        +}
      • addedInput schema / properties / videoDescription
        Added value: +{
        +  "description": "VIDEO posts only — a line under the title, max 200",
        +  "type": "string"
        +}
      • addedInput schema / properties / videoTitle
        Added value: +{
        +  "description": "VIDEO posts only — a headline under the video, max 70 (measured: X drops it on an image post, so it is refused there)",
        +  "type": "string"
        +}
    • Changedpost_to_youtube9 fields changed
      • addedInput schema / properties / brandPartner
        Added value: +{
        +  "description": "sponsoring brand's channel id (UC…) or @handle — gives that brand access to run the video as a partnership ad; \"\" unlinks (update only)",
        +  "type": "string"
        +}
      • addedInput schema / properties / defaultAudioLanguage
        Added value: +{
        +  "description": "spoken language, e.g. \"es\"",
        +  "type": "string"
        +}
      • addedInput schema / properties / defaultLanguage
        Added value: +{
        +  "description": "language of title/description, e.g. \"en\"",
        +  "type": "string"
        +}
      • addedInput schema / properties / embeddable
        Added value: +{
        +  "description": "true = allow embedding on other sites",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / license
        Added value: +{
        +  "description": "YouTube licence: youtube (standard) or creativeCommon",
        +  "enum": [
        +    "youtube",
        +    "creativeCommon"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / localizations
        Added value: +{
        +  "additionalProperties": {
        +    "properties": {
        +      "description": {
        +        "type": "string"
        +      },
        +      "title": {
        +        "type": "string"
        +      }
        +    },
        +    "type": "object"
        +  },
        +  "description": "translated title/description per language, e.g. {\"es\":{\"title\":\"…\"}}; needs defaultLanguage",
        +  "propertyNames": {
        +    "type": "string"
        +  },
        +  "type": "object"
        +}
      • addedInput schema / properties / paidProductPlacement
        Added value: +{
        +  "description": "true = contains PAID product placement (sponsored) — YouTube shows the disclosure; legally required for sponsored content",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / publicStatsViewable
        Added value: +{
        +  "description": "true = show public view and like counts",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / recordingDate
        Added value: +{
        +  "description": "YYYY-MM-DD",
        +  "type": "string"
        +}
    • Changedpost_x_article1 field changed
      • addedInput schema / properties / account
        Added value: +{
        +  "description": "which connected account: its @handle or id from list_connector_accounts; needed only when several are connected",
        +  "type": "string"
        +}
    • Changedpublish_to_shopify_product4 fields changed
      • changedInput schema / properties / imageUrl / description
        Previous value: -"any public https image URL — a Hermoso render works, and so does ANY file of your own brought in with upload_file"New value: +"older name for mediaUrl; still accepted"
      • addedInput schema / properties / kind
        Added value: +{
        +  "description": "only when the link does not say (no file extension and no content type)",
        +  "enum": [
        +    "image",
        +    "video"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / mediaUrl
        Added value: +{
        +  "description": "any public https image or video URL: a Hermoso render works, and so does ANY file of your own brought in with upload_file",
        +  "type": "string"
        +}
      • changedInput schema / required
        Previous value: -[
        -  "productId",
        -  "imageUrl"
        -]New value: +[
        +  "productId"
        +]
    • Addedquery_notion_database
    • Changedrender_ad4 fields changed
      • addedInput schema / properties / foundationImages / properties / hero / description
        Added value: +"hero image URL from a foundations:'choose' call; reused as-is"
      • addedInput schema / properties / foundationImages / properties / identitySheet / description
        Added value: +"identity sheet image URL from a foundations:'choose' call; reused as-is"
      • addedInput schema / properties / foundationImages / properties / location / description
        Added value: +"location image URL from a foundations:'choose' call; reused as-is"
      • addedInput schema / properties / foundationImages / properties / moodboard / description
        Added value: +"moodboard image URL from a foundations:'choose' call; reused as-is"
    • Changedreschedule_post39 fields changed
      • changedInput schema / properties / aiGenerated / description
        Previous value: -"AI-CONTENT DISCLOSURE (Instagram / Facebook Reel is_ai_generated, TikTok is_aigc, YouTube containsSyntheticMedia). OMIT IT and the value already on the post stays; a post that never had one is decided from provenance at publish: a Hermoso render is declared AI-generated, media that came through upload_file or from an external URL (the user’s own photos or footage) is NOT. Pass true or false only to override."New value: +"AI-CONTENT DISCLOSURE (Instagram / Facebook Reel is_ai_generated, TikTok is_aigc, YouTube containsSyntheticMedia, Pinterest ai_disclosures). OMIT IT and the value already on the post stays; a post that never had one is decided from provenance at publish: a Hermoso render is declared AI-generated, media that came through upload_file or from an external URL (the user’s own photos or footage) is NOT. Pass true or false only to override."
      • addedInput schema / properties / audience / properties / cities / description
        Added value: +"Facebook Page post audience: Meta numeric city keys (the ads location search returns them)"
      • addedInput schema / properties / audience / properties / countries / description
        Added value: +"Facebook Page post audience: two-letter country codes, e.g. CA, US"
      • addedInput schema / properties / audience / properties / minAge / description
        Added value: +"Facebook Page post audience: minimum age, one of 13, 15, 18, 21 or 25"
      • addedInput schema / properties / audience / properties / regions / description
        Added value: +"Facebook Page post audience: Meta numeric region keys (the ads location search returns them)"
      • addedInput schema / properties / buttons
        Added value: +{
        +  "description": "TELEGRAM ONLY — replace the URL buttons ([] removes them).",
        +  "items": {
        +    "anyOf": [
        +      {
        +        "properties": {
        +          "style": {
        +            "enum": [
        +              "danger",
        +              "success",
        +              "primary"
        +            ],
        +            "type": "string"
        +          },
        +          "text": {
        +            "type": "string"
        +          },
        +          "url": {
        +            "type": "string"
        +          }
        +        },
        +        "required": [
        +          "text",
        +          "url"
        +        ],
        +        "type": "object"
        +      },
        +      {
        +        "items": {
        +          "properties": {
        +            "style": {
        +              "enum": [
        +                "danger",
        +                "success",
        +                "primary"
        +              ],
        +              "type": "string"
        +            },
        +            "text": {
        +              "type": "string"
        +            },
        +            "url": {
        +              "type": "string"
        +            }
        +          },
        +          "required": [
        +            "text",
        +            "url"
        +          ],
        +          "type": "object"
        +        },
        +        "type": "array"
        +      }
        +    ]
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / disablePreview
        Added value: +{
        +  "description": "TELEGRAM ONLY — no link preview.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / event / properties / endDate / description
        Added value: +"event end date, e.g. 2026-11-01"
      • addedInput schema / properties / event / properties / endTime / description
        Added value: +"event end time, e.g. 20:00"
      • addedInput schema / properties / event / properties / startDate / description
        Added value: +"event start date, e.g. 2026-11-01"
      • addedInput schema / properties / event / properties / startTime / description
        Added value: +"event start time, e.g. 18:00"
      • addedInput schema / properties / event / properties / title / description
        Added value: +"Google Business event title; required for an event or offer post"
      • addedInput schema / properties / firstComment
        Added value: +{
        +  "description": "FIRST COMMENT — posted on each new post as the same account right after it publishes, on Facebook, Instagram and LinkedIn company Pages (LinkedIn needs its comments authorization, provider linkedin_community). Other channels publish without it and their result row says so. Up to 2,000 characters, refused rather than cut; never on a story. A failed comment never undoes the post.",
        +  "type": "string"
        +}
      • addedInput schema / properties / link / description
        Added value: +"destination URL for a Pinterest Pin, Facebook text post or Google Business post"
      • addedInput schema / properties / offer / properties / couponCode / description
        Added value: +"Google Business offer coupon code"
      • addedInput schema / properties / offer / properties / redeemOnlineUrl / description
        Added value: +"link where the offer is redeemed online"
      • addedInput schema / properties / offer / properties / termsConditions / description
        Added value: +"terms text shown with the offer"
      • addedInput schema / properties / parseMode
        Added value: +{
        +  "description": "TELEGRAM ONLY — 'HTML' for Telegram's HTML tags.",
        +  "enum": [
        +    "HTML"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / poll / properties / durationMinutes / description
        Added value: +"X poll length in minutes, 5 to 10080; default 1440 (one day)"
      • addedInput schema / properties / poll / properties / options / description
        Added value: +"X poll choices: 2 to 4 options, max 25 characters each"
      • addedInput schema / properties / silent
        Added value: +{
        +  "description": "TELEGRAM ONLY — no notification sound.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / slideText / items / properties / description / description
        Added value: +"Pinterest carousel slide: description for this slide"
      • addedInput schema / properties / slideText / items / properties / link / description
        Added value: +"Pinterest carousel slide: destination URL for this slide"
      • addedInput schema / properties / slideText / items / properties / title / description
        Added value: +"Pinterest carousel slide: title for this slide"
      • addedInput schema / properties / targetAudience / properties / degrees / description
        Added value: +"LinkedIn degree facet: numeric id or urn:li:degree value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / fieldsOfStudy / description
        Added value: +"LinkedIn field of study facet: numeric id or urn:li:fieldOfStudy value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / geoLocations / description
        Added value: +"LinkedIn geo facet: numeric id or urn:li:geo value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / industries / description
        Added value: +"LinkedIn industry facet: numeric id or urn:li:industry value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / jobFunctions / description
        Added value: +"LinkedIn job function facet: numeric id or urn:li:function value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / organizations / description
        Added value: +"LinkedIn school or organization facet: numeric id or urn:li:organization value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / seniorities / description
        Added value: +"LinkedIn seniority facet: numeric id or urn:li:seniority value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / staffCountRanges / description
        Added value: +"LinkedIn company size bands, e.g. SIZE_11_TO_50"
      • addedInput schema / properties / threadsGhostPost
        Added value: +{
        +  "description": "THREADS ONLY — a GHOST post: text only, Threads archives it automatically after 24 hours (a flash offer, a 24h-only note). No other Threads option can ride it.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / threadsGif
        Added value: +{
        +  "description": "THREADS ONLY — a GIPHY GIF id (the id in a giphy.com/gifs/… link) attached to a TEXT Threads post. GIPHY is the only provider Threads takes.",
        +  "type": "string"
        +}
      • addedInput schema / properties / threadsLongText
        Added value: +{
        +  "description": "THREADS ONLY — LONG-FORM text, up to 10,000 characters, attached under a TEXT-only Threads post (Threads' text attachment). The way to publish more than Threads' 500-character post: keep a short lead in the message and put the full text here. Not with an image, video or carousel.",
        +  "type": "string"
        +}
      • addedInput schema / properties / userTags
        Added value: +{
        +  "description": "INSTAGRAM USER TAGS — tag people (public accounts) on the post; each is notified. Usernames, or { username, x, y } with x/y from 0 to 1 = where the tag sits on the IMAGE (a tag without a position sits at the centre). A Reel tag takes no position; a story position is optional. Not on a carousel (refused by name).",
        +  "items": {
        +    "anyOf": [
        +      {
        +        "type": "string"
        +      },
        +      {
        +        "properties": {
        +          "username": {
        +            "type": "string"
        +          },
        +          "x": {
        +            "maximum": 1,
        +            "minimum": 0,
        +            "type": "number"
        +          },
        +          "y": {
        +            "maximum": 1,
        +            "minimum": 0,
        +            "type": "number"
        +          }
        +        },
        +        "required": [
        +          "username"
        +        ],
        +        "type": "object"
        +      }
        +    ]
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / visibilityByChannel / description
        Added value: +"per-channel visibility override, e.g. {tiktok: draft}; values public, unlisted, private or draft"
      • addedInput schema / properties / xArticle / properties / headings / description
        Added value: +"blocks (default) keeps real headings; text turns headings into bold lines"
      • addedInput schema / properties / xArticle / properties / title / description
        Added value: +"X Article headline; the post text becomes the article body in markdown"
    • Changedrun_post_refill7 fields changed
      • addedInput schema / properties / edit / items / properties / captions / description
        Added value: +"per-channel caption overrides for the draft, e.g. {instagram: ...}"
      • addedInput schema / properties / edit / items / properties / id / description
        Added value: +"draft id to change, from get_post_refill"
      • addedInput schema / properties / edit / items / properties / message / description
        Added value: +"replacement caption for the draft"
      • addedInput schema / properties / edit / items / properties / title / description
        Added value: +"Pinterest or YouTube headline for the draft"
      • addedInput schema / properties / redo / description
        Added value: +"remake one draft from feedback; paid, so without confirm:true it returns only the price"
      • addedInput schema / properties / redo / properties / feedback / description
        Added value: +"what to change, e.g. too salesy; saved for later batches when remember is on"
      • addedInput schema / properties / redo / properties / id / description
        Added value: +"draft id to remake, from get_post_refill"
    • Changedsave_playbook4 fields changed
      • addedInput schema / properties / angles / items / properties / detail / description
        Added value: +"what the angle is and why it persuades; optional, up to 600 characters"
      • addedInput schema / properties / angles / items / properties / title / description
        Added value: +"the angle, in a few words"
      • addedInput schema / properties / plays / items / properties / detail / description
        Added value: +"how to run the play; optional, up to 600 characters"
      • addedInput schema / properties / plays / items / properties / title / description
        Added value: +"the play to run, in a few words"
    • Addedsave_to_canva
    • Changedsave_to_notion2 fields changed
      • addedInput schema / properties / dataSourceId / description
        Added value: +"data source id to save into as a new row; pass only one of parentPageId, databaseId, dataSourceId"
      • addedInput schema / properties / title / description
        Added value: +"title of the new Notion page"
    • Changedschedule_post39 fields changed
      • changedInput schema / properties / aiGenerated / description
        Previous value: -"AI-CONTENT DISCLOSURE (Instagram / Facebook Reel is_ai_generated, TikTok is_aigc, YouTube containsSyntheticMedia). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared AI-generated, media that came through upload_file or from an external URL (the user’s own photos or footage) is NOT. Pass true or false only to override."New value: +"AI-CONTENT DISCLOSURE (Instagram / Facebook Reel is_ai_generated, TikTok is_aigc, YouTube containsSyntheticMedia, Pinterest ai_disclosures). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared AI-generated, media that came through upload_file or from an external URL (the user’s own photos or footage) is NOT. Pass true or false only to override."
      • addedInput schema / properties / audience / properties / minAge / description
        Added value: +"Facebook Page post audience: minimum age, one of 13, 15, 18, 21 or 25"
      • addedInput schema / properties / brandPartner
        Added value: +{
        +  "description": "YOUTUBE: sponsoring brand channel (UC… or @handle)",
        +  "type": "string"
        +}
      • addedInput schema / properties / buttons
        Added value: +{
        +  "description": "TELEGRAM ONLY — URL buttons under the post, as post_to_telegram takes them; never on an album.",
        +  "items": {
        +    "anyOf": [
        +      {
        +        "properties": {
        +          "style": {
        +            "enum": [
        +              "danger",
        +              "success",
        +              "primary"
        +            ],
        +            "type": "string"
        +          },
        +          "text": {
        +            "type": "string"
        +          },
        +          "url": {
        +            "type": "string"
        +          }
        +        },
        +        "required": [
        +          "text",
        +          "url"
        +        ],
        +        "type": "object"
        +      },
        +      {
        +        "items": {
        +          "properties": {
        +            "style": {
        +              "enum": [
        +                "danger",
        +                "success",
        +                "primary"
        +              ],
        +              "type": "string"
        +            },
        +            "text": {
        +              "type": "string"
        +            },
        +            "url": {
        +              "type": "string"
        +            }
        +          },
        +          "required": [
        +            "text",
        +            "url"
        +          ],
        +          "type": "object"
        +        },
        +        "type": "array"
        +      }
        +    ]
        +  },
        +  "type": "array"
        +}
      • changedInput schema / properties / chatId / description
        Previous value: -"TELEGRAM — REQUIRED whenever telegram is a channel: WHICH chat, group or channel the bot posts to. A public channel’s @username (@hermosoai) or the numeric id (a group is negative; a supergroup or channel starts with -100). There is no default and there cannot be one — the Telegram Bot API publishes no method that lists a bot’s chats — so scheduling telegram without one is refused up front. list_telegram_chats finds ids for chats that have messaged the bot in the last 24 hours."New value: +"TELEGRAM — REQUIRED whenever telegram is a channel: WHICH chat, group or channel the bot posts to. A public channel’s @username (@yourchannel) or the numeric id (a group is negative; a supergroup or channel starts with -100). There is no default and there cannot be one — the Telegram Bot API publishes no method that lists a bot’s chats — so scheduling telegram without one is refused up front. list_telegram_chats finds ids for chats that have messaged the bot in the last 24 hours."
      • addedInput schema / properties / disablePreview
        Added value: +{
        +  "description": "TELEGRAM ONLY — no link preview.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / event / properties / endDate / description
        Added value: +"event end date, e.g. 2026-11-01"
      • addedInput schema / properties / event / properties / endTime / description
        Added value: +"event end time, e.g. 20:00"
      • addedInput schema / properties / event / properties / startDate / description
        Added value: +"event start date, e.g. 2026-11-01"
      • addedInput schema / properties / event / properties / startTime / description
        Added value: +"event start time, e.g. 18:00"
      • addedInput schema / properties / event / properties / title / description
        Added value: +"Google Business event title; required for an event or offer post"
      • addedInput schema / properties / firstComment
        Added value: +{
        +  "description": "FIRST COMMENT — posted on each new post as the same account right after it publishes, on Facebook, Instagram and LinkedIn company Pages (LinkedIn needs its comments authorization, provider linkedin_community). Other channels publish without it and their result row says so. Up to 2,000 characters, refused rather than cut; never on a story. A failed comment never undoes the post.",
        +  "type": "string"
        +}
      • changedInput schema / properties / imageUrls / description
        Previous value: -"CAROUSEL — an ORDERED list of image URLs to publish as ONE swipeable post on every channel that supports it (Instagram 2–10, Threads 2–20, Facebook, LinkedIn company Pages 2–20, Pinterest 2–5, TikTok up to 35 as a photo post). Use this whenever the creative is a multi-slide deck: a scheduled post carrying only slide 1 of a “1/6 · SWIPE” set is a broken ad that nobody is watching when it fires. THE ORDER IS THE PRODUCT. A channel on this schedule that cannot do carousels — X, YouTube, Google Business Profile — is REFUSED NOW, with the reason, so you can drop it or give it its own single image; it is never quietly downgraded hours later."New value: +"CAROUSEL — an ORDERED list of image URLs to publish as ONE swipeable post on every channel that supports it (Instagram 2–20, Threads 2–20, Facebook, LinkedIn company Pages 2–20, Pinterest 2–5, TikTok up to 35 as a photo post). Use this whenever the creative is a multi-slide deck: a scheduled post carrying only slide 1 of a “1/6 · SWIPE” set is a broken ad that nobody is watching when it fires. THE ORDER IS THE PRODUCT. A channel on this schedule that cannot do carousels — X, YouTube, Google Business Profile — is REFUSED NOW, with the reason, so you can drop it or give it its own single image; it is never quietly downgraded hours later."
      • addedInput schema / properties / offer / properties / couponCode / description
        Added value: +"Google Business offer coupon code"
      • addedInput schema / properties / offer / properties / redeemOnlineUrl / description
        Added value: +"link where the offer is redeemed online"
      • addedInput schema / properties / offer / properties / termsConditions / description
        Added value: +"terms text shown with the offer"
      • addedInput schema / properties / paidProductPlacement
        Added value: +{
        +  "description": "YOUTUBE: paid product placement (shows the disclosure)",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / parseMode
        Added value: +{
        +  "description": "TELEGRAM ONLY — 'HTML' for Telegram's HTML tags; omit for plain text.",
        +  "enum": [
        +    "HTML"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / poll / properties / durationMinutes / description
        Added value: +"X poll length in minutes, 5 to 10080; default 1440 (one day)"
      • addedInput schema / properties / poll / properties / options / description
        Added value: +"X poll choices: 2 to 4 options, max 25 characters each"
      • addedInput schema / properties / silent
        Added value: +{
        +  "description": "TELEGRAM ONLY — no notification sound.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / slideText / items / properties / description / description
        Added value: +"Pinterest carousel slide: description for this slide"
      • addedInput schema / properties / slideText / items / properties / link / description
        Added value: +"Pinterest carousel slide: destination URL for this slide"
      • addedInput schema / properties / slideText / items / properties / title / description
        Added value: +"Pinterest carousel slide: title for this slide"
      • changedInput schema / properties / story / description
        Previous value: -"INSTAGRAM STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A Story carries NO CAPTION (there is nowhere to show one), no collaborators, no product tags and no trialReel — passing any of those is refused by name and nothing is posted, because a story that silently drops the words is worse than one that never went. INSTAGRAM ONLY: a Facebook Page story is a different upload and is not built, so a Facebook channel with `story` set is refused rather than published to the feed."New value: +"INSTAGRAM OR FACEBOOK PAGE STORY — publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A Story carries NO CAPTION (there is nowhere to show one), no collaborators, no product tags and no trialReel — passing any of those is refused by name and nothing is posted, because a story that silently drops the words is worse than one that never went. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page."
      • addedInput schema / properties / targetAudience / properties / degrees / description
        Added value: +"LinkedIn degree facet: numeric id or urn:li:degree value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / fieldsOfStudy / description
        Added value: +"LinkedIn field of study facet: numeric id or urn:li:fieldOfStudy value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / geoLocations / description
        Added value: +"LinkedIn geo facet: numeric id or urn:li:geo value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / industries / description
        Added value: +"LinkedIn industry facet: numeric id or urn:li:industry value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / jobFunctions / description
        Added value: +"LinkedIn job function facet: numeric id or urn:li:function value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / organizations / description
        Added value: +"LinkedIn school or organization facet: numeric id or urn:li:organization value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / seniorities / description
        Added value: +"LinkedIn seniority facet: numeric id or urn:li:seniority value from search_linkedin_ads_targeting"
      • addedInput schema / properties / targetAudience / properties / staffCountRanges / description
        Added value: +"LinkedIn company size bands, e.g. SIZE_11_TO_50"
      • addedInput schema / properties / threadsGhostPost
        Added value: +{
        +  "description": "THREADS ONLY — a GHOST post: text only, Threads archives it automatically after 24 hours (a flash offer, a 24h-only note). No other Threads option can ride it.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / threadsGif
        Added value: +{
        +  "description": "THREADS ONLY — a GIPHY GIF id (the id in a giphy.com/gifs/… link) attached to a TEXT Threads post. GIPHY is the only provider Threads takes.",
        +  "type": "string"
        +}
      • addedInput schema / properties / threadsLongText
        Added value: +{
        +  "description": "THREADS ONLY — LONG-FORM text, up to 10,000 characters, attached under a TEXT-only Threads post (Threads' text attachment). The way to publish more than Threads' 500-character post: keep a short lead in the message and put the full text here. Not with an image, video or carousel.",
        +  "type": "string"
        +}
      • addedInput schema / properties / userTags
        Added value: +{
        +  "description": "INSTAGRAM USER TAGS — tag people (public accounts) on the post; each is notified. Usernames, or { username, x, y } with x/y from 0 to 1 = where the tag sits on the IMAGE (a tag without a position sits at the centre). A Reel tag takes no position; a story position is optional. Not on a carousel (refused by name).",
        +  "items": {
        +    "anyOf": [
        +      {
        +        "type": "string"
        +      },
        +      {
        +        "properties": {
        +          "username": {
        +            "type": "string"
        +          },
        +          "x": {
        +            "maximum": 1,
        +            "minimum": 0,
        +            "type": "number"
        +          },
        +          "y": {
        +            "maximum": 1,
        +            "minimum": 0,
        +            "type": "number"
        +          }
        +        },
        +        "required": [
        +          "username"
        +        ],
        +        "type": "object"
        +      }
        +    ]
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / xArticle / properties / headings / description
        Added value: +"blocks (default) keeps real headings; text turns headings into bold lines"
      • addedInput schema / properties / xArticle / properties / title / description
        Added value: +"X Article headline; the post text becomes the article body in markdown"
    • Addedsearch_meta_places
    • Changedset_competitor_watch10 fields changed
      • addedInput schema / properties / auto / description
        Added value: +"true picks competitors, similar brands and topics from the saved brand or seed; costs a few credits"
      • addedInput schema / properties / cadence / description
        Added value: +"weekly (default) or daily check"
      • addedInput schema / properties / competitors / items / properties / kind / description
        Added value: +"competitor (head-to-head rival) or similar (adjacent brand)"
      • addedInput schema / properties / competitors / items / properties / name / description
        Added value: +"brand name to watch"
      • addedInput schema / properties / dryRun / description
        Added value: +"true quotes one check and saves nothing"
      • addedInput schema / properties / modules / properties / ideas / description
        Added value: +"true includes three ideas to make from the findings"
      • addedInput schema / properties / modules / properties / newAds / description
        Added value: +"true includes new ads the watched brands launched"
      • addedInput schema / properties / modules / properties / topPosts / description
        Added value: +"true includes the most-viewed posts for the topics"
      • addedInput schema / properties / modules / properties / trends / description
        Added value: +"true includes formats trending in the topics"
      • addedInput schema / properties / modules / properties / winners / description
        Added value: +"true includes watched ads that have run 30 days"
    • Changedset_post_refill1 field changed
      • addedInput schema / properties / forgetCaptionEdits / description
        Added value: +"true = forget the caption edits learned from past reviews"
    • Addedshare_onedrive_file
    • Changedupdate_brand6 fields changed
      • addedInput schema / properties / audience / description
        Added value: +"who the brand sells to, one line"
      • addedInput schema / properties / category / description
        Added value: +"product or business category, one line"
      • addedInput schema / properties / logo / description
        Added value: +"logo image: URL or local file path to a PNG, JPEG or WebP (no SVG)"
      • addedInput schema / properties / name / description
        Added value: +"brand name"
      • addedInput schema / properties / positioning / description
        Added value: +"how the brand is positioned against alternatives, one line"
      • addedInput schema / properties / profileType / description
        Added value: +"website, social, describe or explore; explore keeps the profile but stops using it"
    • Changedupdate_doc5 fields changed
      • addedInput schema / properties / confirm / description
        Added value: +"true runs a whole-body rewrite after the character count is shown"
      • addedInput schema / properties / dropdowns / items / properties / tabId / description
        Added value: +"tab id holding the dropdown, when the document has several tabs"
      • addedInput schema / properties / replacements / items / properties / find / description
        Added value: +"exact text to find, as read_doc shows it; zero matches is reported as a failure"
      • addedInput schema / properties / replacements / items / properties / matchCase / description
        Added value: +"false ignores letter case when matching; default matches case"
      • addedInput schema / properties / replacements / items / properties / replace / description
        Added value: +"text to put in place of each match"
    • Addedupdate_notion_row
    • Changedupdate_saved_creator1 field changed
      • addedInput schema / properties / status / description
        Added value: +"outreach status: new, contacted, replied, booked or passed"
    • Changedupdate_sheet4 fields changed
      • addedInput schema / properties / sheetUrl / description
        Added value: +"Google Sheets link, instead of spreadsheetId"
      • addedInput schema / properties / spreadsheetId / description
        Added value: +"Google spreadsheet id; use this or sheetUrl"
      • addedInput schema / properties / updates / items / properties / range / description
        Added value: +"A1 range or anchor cell for this block, e.g. B2:C5 or Q3!B2"
      • addedInput schema / properties / updates / items / properties / values / description
        Added value: +"rows for this block, as an array of row arrays"
  7. 27 tool updatesv0.1.454
    • Changedadd_subtitles1 field changed
      • changedInput schema / properties / textStyle / anyOf
        Previous value: -[
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "properties": {
        -      "background": {
        -        "description": "none|pill|a colour",
        -        "type": "string"
        -      },
        -      "cardColor": {
        -        "type": "string"
        -      },
        -      "color": {
        -        "description": "any CSS colour",
        -        "type": "string"
        -      },
        -      "describe": {
        -        "description": "the look in words",
        -        "type": "string"
        -      },
        -      "font": {
        -        "description": "sans|serif|elegant|condensed|hand or any Google Fonts family",
        -        "type": "string"
        -      },
        -      "italic": {
        -        "type": "boolean"
        -      },
        -      "outline": {
        -        "type": "boolean"
        -      },
        -      "outlineColor": {
        -        "type": "string"
        -      },
        -      "position": {
        -        "anyOf": [
        -          {
        -            "type": "string"
        -          },
        -          {
        -            "type": "number"
        -          }
        -        ],
        -        "description": "top|center|lower|bottom or 0.05-0.95 from the top"
        -      },
        -      "preset": {
        -        "type": "string"
        -      },
        -      "shadow": {
        -        "type": "boolean"
        -      },
        -      "size": {
        -        "anyOf": [
        -          {
        -            "type": "string"
        -          },
        -          {
        -            "type": "number"
        -          }
        -        ],
        -        "description": "s|m|l|xl, '120px', or a 0.015-0.15 frame fraction"
        -      },
        -      "subFont": {
        -        "type": "string"
        -      },
        -      "subItalic": {
        -        "type": "boolean"
        -      },
        -      "textCase": {
        -        "enum": [
        -          "as-is",
        -          "upper",
        -          "lower",
        -          "title"
        -        ],
        -        "type": "string"
        -      },
        -      "tilt": {
        -        "description": "degrees, ±45",
        -        "type": "number"
        -      },
        -      "weight": {
        -        "type": "number"
        -      }
        -    },
        -    "type": "object"
        -  }
        -]New value: +[
        +  {
        +    "type": "string"
        +  },
        +  {
        +    "properties": {
        +      "background": {
        +        "description": "none|pill|a colour",
        +        "type": "string"
        +      },
        +      "cardColor": {
        +        "type": "string"
        +      },
        +      "color": {
        +        "description": "any CSS colour",
        +        "type": "string"
        +      },
        +      "describe": {
        +        "description": "the look in words",
        +        "type": "string"
        +      },
        +      "font": {
        +        "description": "sans|serif|elegant|condensed|hand or any Google Fonts family",
        +        "type": "string"
        +      },
        +      "italic": {
        +        "type": "boolean"
        +      },
        +      "outline": {
        +        "type": "boolean"
        +      },
        +      "outlineColor": {
        +        "type": "string"
        +      },
        +      "position": {
        +        "description": "top|center|lower|bottom or 0.05-0.95 from the top",
        +        "type": [
        +          "string",
        +          "number"
        +        ]
        +      },
        +      "preset": {
        +        "type": "string"
        +      },
        +      "shadow": {
        +        "type": "boolean"
        +      },
        +      "size": {
        +        "description": "s|m|l|xl, '120px', or a 0.015-0.15 frame fraction",
        +        "type": [
        +          "string",
        +          "number"
        +        ]
        +      },
        +      "subFont": {
        +        "type": "string"
        +      },
        +      "subItalic": {
        +        "type": "boolean"
        +      },
        +      "textCase": {
        +        "enum": [
        +          "as-is",
        +          "upper",
        +          "lower",
        +          "title"
        +        ],
        +        "type": "string"
        +      },
        +      "tilt": {
        +        "description": "degrees, ±45",
        +        "type": "number"
        +      },
        +      "weight": {
        +        "type": "number"
        +      }
        +    },
        +    "type": "object"
        +  }
        +]
    • Addedappend_to_notion_page
    • Changedappend_to_sheet2 fields changed
      • removedInput schema / properties / rows / items / items / anyOf
        Removed value: -[
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "type": "number"
        -  },
        -  {
        -    "type": "boolean"
        -  }
        -]
      • addedInput schema / properties / rows / items / items / type
        Added value: +[
        +  "string",
        +  "number",
        +  "boolean"
        +]
    • Addedclick_to_ad
    • Changedcreate_sheet2 fields changed
      • removedInput schema / properties / rows / items / items / anyOf
        Removed value: -[
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "type": "number"
        -  },
        -  {
        -    "type": "boolean"
        -  }
        -]
      • addedInput schema / properties / rows / items / items / type
        Added value: +[
        +  "string",
        +  "number",
        +  "boolean"
        +]
    • Addeddelete_library_item
    • Changedduplicate_scheduled2 fields changed
      • changedInput schema / properties / at / description
        Previous value: -"when the copy goes out — ISO timestamp or epoch milliseconds (default: an hour from now)"New value: +"when the copy goes out: local wall clock (2026-10-30T10:00), UTC or epoch ms (default: an hour from now)"
      • changedInput schema / properties / timezone / description
        Previous value: -"IANA zone for the queue, e.g. \"America/New_York\""New value: +"IANA zone for a wall-clock `at` or the queue"
    • Addedexport_schedule_to_notion
    • Changedfetch_app_screens1 field changed
      • addedInput schema / properties / storeUrl
        Added value: +{
        +  "description": "a Google Play / App Store link or package id",
        +  "type": "string"
        +}
    • Changedfind_creators1 field changed
      • changedInput schema / properties / enrich / description
        Previous value: -"read follower counts for the top 6 (default true, ~1 credit each)"New value: +"followers + public contact for the top 3 on TikTok and on Instagram (default true, 1 credit each)"
    • Addedinsert_scene
    • Addedlist_notion_pages
    • Changedlist_watch_findings3 fields changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "profile id/name from list_brands, this call only",
        +  "type": "string"
        +}
      • changedInput schema / properties / competitor / description
        Previous value: -"only findings for this watched brand (exact name as returned in `watching`) — omit for all of them"New value: +"only this watched brand's ads (exact name as returned)"
      • changedInput schema / properties / limit / description
        Previous value: -"max findings to return (default 25, max 75 — the server keeps at most 75, and at most 15 per brand)"New value: +"max ads (default 25, max 75)"
    • Addedmake_motion_film
    • Addedmake_series
    • Changedplan_ad2 fields changed
      • changedInput schema / properties / draft / description
        Previous value: -"ONLY after a video refusal that offered a light draft: the {model, durationSeconds} it named. The plan is then authored to that length and priced on that model. Never invent one — a video the account cannot cover is refused BEFORE planning with the three options (image / add credits / this draft when one fits), and the user chooses."New value: +"Only after a video refusal that offered a lighter draft: the {model, durationSeconds} that refusal named. The plan is then written to that length and priced on that model. Use only the values the refusal gave; when an account cannot cover a video, the refusal comes before planning with three options (an image, adding credits, or this draft when one fits) and the user chooses."
      • changedInput schema / properties / durationSeconds / description
        Previous value: -"VIDEO ONLY — the total spot length the user explicitly asked for, in seconds, copied verbatim (30 for \"a 30 second ad\"). The planner authors the storyboard TO it: the scenes’ seconds sum to it and the script is word-budgeted for it. Supported range 4–180; anything outside is CLAMPED to it (the reply says so). A length that fits ONE clip of the render model renders as a single continuous pass; anything longer is STITCHED from acts filled to that model’s clip maximum with the remainder last (on a 15s-clip model, 40 -> 15+15+10 and 17 -> 13+4) — never time-compressed. The maximum is the model’s own: 15s on most, 30s on the longest-clip model, so a 30s ad can be one unbroken take rather than two acts. Omit when the user named no length; do NOT pass a guess: when it is omitted the planner picks a length suited to the format, and a length the user names always wins."New value: +"For video: the total length the user asked for, in seconds (30 for \"a 30 second ad\"). The storyboard’s scenes add up to it and the script is sized for it. Supported range 4 to 180 seconds; a value outside the range is clamped and the reply says so. A length that fits one clip of the render model renders as one continuous clip; a longer one is joined from clips filled to that model’s clip maximum, remainder last (on a 15s-clip model, 40 becomes 15+15+10 and 17 becomes 13+4), and is never sped up. The clip maximum depends on the model: 15s on most, 30s on the longest-clip model, so a 30s ad can be one take. Leave it out when the user named no length; the planner then picks one that suits the format."
    • Changedpost_to_meta2 fields changed
      • changedInput schema / properties / altText / description
        Previous value: -"ACCESSIBILITY — the description screen readers announce, and what the platform otherwise auto-generates badly or not at all. Describe what is actually IN the picture, never the caption. ONE STRING describes the picture; on a CAROUSEL it describes EVERY slide. Pass an ARRAY of strings instead to describe each slide separately, aligned to the slide order — that is strictly better on a multi-slide post, because one sentence read out over six different pictures is wrong for five of them. More descriptions than pictures is refused rather than dropped. WHERE IT LANDS, per Meta’s own docs: INSTAGRAM image posts and the IMAGE slides of an Instagram carousel (up to 1000 characters; dropped rather than sent on a Reel or a video slide, which Meta do not support); FACEBOOK Page photos including every photo of an album, via alt_text_custom (Meta publish no length for it, so nothing is truncated); THREADS on a single-image or single-video post ONLY — Meta document no way to attach alt text to a Threads CAROUSEL slide, so a Threads carousel publishes undescribed and the reply says so rather than risking the whole post on a guess. (Meta’s AI disclosure defaults from PROVENANCE: a Hermoso render is declared is_ai_generated; media that came through upload_file or an external URL, i.e. the user’s own photos or footage, is NOT. Pass `aiGenerated` to force it either way.)"New value: +"ACCESSIBILITY — the description screen readers announce, and what the platform otherwise auto-generates badly or not at all. Describe what is actually IN the picture, never the caption. ONE STRING describes the picture; on a CAROUSEL it describes EVERY slide. Pass an ARRAY of strings instead to describe each slide separately, aligned to the slide order — use that on a multi-slide post, because one sentence read out over six different pictures is wrong for five of them. More descriptions than pictures is refused rather than dropped. WHERE IT LANDS, per Meta’s own docs: INSTAGRAM image posts and the IMAGE slides of an Instagram carousel (up to 1000 characters; dropped rather than sent on a Reel or a video slide, which Meta do not support); FACEBOOK Page photos including every photo of an album, via alt_text_custom (Meta publish no length for it, so nothing is truncated); THREADS on a single-image or single-video post ONLY — Meta document no way to attach alt text to a Threads CAROUSEL slide, so a Threads carousel publishes undescribed and the reply says so rather than risking the whole post on a guess. (Meta’s AI disclosure defaults from PROVENANCE: a Hermoso render is declared is_ai_generated; media that came through upload_file or an external URL, i.e. the user’s own photos or footage, is NOT. Pass `aiGenerated` to force it either way.)"
      • changedInput schema / properties / paidPartnership / description
        Previous value: -"INSTAGRAM — the PAID PARTNERSHIP label. A COMPLIANCE DECLARATION, the same kind Hermoso already carries for TikTok and X: set it whenever the post is sponsored, gifted or otherwise paid for. Opt-in and never inferred — it is the poster’s own statement about their commercial relationship."New value: +"INSTAGRAM — the PAID PARTNERSHIP label. A COMPLIANCE DECLARATION: set it whenever the post is sponsored, gifted or otherwise paid for. Opt-in and never inferred — it is the poster’s own statement about their commercial relationship."
    • Changedproduct_sizzle1 field changed
      • addedInput schema / properties / style
        Added value: +{
        +  "description": "the LOOK of the sizzle (same price for every look): hypermotion (Fast cuts, crash zooms and speed ramps), product_2d (The real product with playful hand-drawn 2D animation around it), typography (Product framed by huge bold letterforms in the brand colours), dark_minimal (Moody, premium, a single hard light on black), color_pop (Bright saturated colour, glossy, joyful), mixed_media (Editorial collage: paper, cut-outs and texture around the product), saas (For apps and software: floating screens, light trails, particles), product_showcase (Hero commercial: ingredients, splashes and a clean pack shot). Default hypermotion.",
        +  "enum": [
        +    "hypermotion",
        +    "product_2d",
        +    "typography",
        +    "dark_minimal",
        +    "color_pop",
        +    "mixed_media",
        +    "saas",
        +    "product_showcase"
        +  ],
        +  "type": "string"
        +}
    • Addedread_notion_page
    • Changedrecast_motion2 fields changed
      • changedInput schema / properties / engine / enum
        Previous value: -[
        -  "auto",
        -  "h3",
        -  "seedance",
        -  "wan",
        -  "wan-prime",
        -  "kling"
        -]New value: +[
        +  "auto",
        +  "kling-edit",
        +  "h3",
        +  "seedance",
        +  "wan",
        +  "wan-prime",
        +  "kling"
        +]
      • changedInput schema / properties / resolution / description
        Previous value: -"seedance and wan default 1080p; h3 takes 720p or 1080p, default the clip’s own"New value: +"a person swap defaults to 720p (seedance or kling-edit); otherwise seedance and wan default 1080p; h3 720p or 1080p, default the clip’s own"
    • Changedrender_ad3 fields changed
      • removedInput schema / properties / music / anyOf
        Removed value: -[
        -  {
        -    "type": "boolean"
        -  },
        -  {
        -    "type": "string"
        -  }
        -]
      • addedInput schema / properties / music / type
        Added value: +[
        +  "boolean",
        +  "string"
        +]
      • changedInput schema / properties / textStyle / anyOf
        Previous value: -[
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "properties": {
        -      "background": {
        -        "description": "none|pill|a colour",
        -        "type": "string"
        -      },
        -      "cardColor": {
        -        "type": "string"
        -      },
        -      "color": {
        -        "description": "any CSS colour",
        -        "type": "string"
        -      },
        -      "describe": {
        -        "description": "the look in words",
        -        "type": "string"
        -      },
        -      "font": {
        -        "description": "sans|serif|elegant|condensed|hand or any Google Fonts family",
        -        "type": "string"
        -      },
        -      "italic": {
        -        "type": "boolean"
        -      },
        -      "outline": {
        -        "type": "boolean"
        -      },
        -      "outlineColor": {
        -        "type": "string"
        -      },
        -      "position": {
        -        "anyOf": [
        -          {
        -            "type": "string"
        -          },
        -          {
        -            "type": "number"
        -          }
        -        ],
        -        "description": "top|center|lower|bottom or 0.05-0.95 from the top"
        -      },
        -      "preset": {
        -        "type": "string"
        -      },
        -      "shadow": {
        -        "type": "boolean"
        -      },
        -      "size": {
        -        "anyOf": [
        -          {
        -            "type": "string"
        -          },
        -          {
        -            "type": "number"
        -          }
        -        ],
        -        "description": "s|m|l|xl, '120px', or a 0.015-0.15 frame fraction"
        -      },
        -      "subFont": {
        -        "type": "string"
        -      },
        -      "subItalic": {
        -        "type": "boolean"
        -      },
        -      "textCase": {
        -        "enum": [
        -          "as-is",
        -          "upper",
        -          "lower",
        -          "title"
        -        ],
        -        "type": "string"
        -      },
        -      "tilt": {
        -        "description": "degrees, ±45",
        -        "type": "number"
        -      },
        -      "weight": {
        -        "type": "number"
        -      }
        -    },
        -    "type": "object"
        -  }
        -]New value: +[
        +  {
        +    "type": "string"
        +  },
        +  {
        +    "properties": {
        +      "background": {
        +        "description": "none|pill|a colour",
        +        "type": "string"
        +      },
        +      "cardColor": {
        +        "type": "string"
        +      },
        +      "color": {
        +        "description": "any CSS colour",
        +        "type": "string"
        +      },
        +      "describe": {
        +        "description": "the look in words",
        +        "type": "string"
        +      },
        +      "font": {
        +        "description": "sans|serif|elegant|condensed|hand or any Google Fonts family",
        +        "type": "string"
        +      },
        +      "italic": {
        +        "type": "boolean"
        +      },
        +      "outline": {
        +        "type": "boolean"
        +      },
        +      "outlineColor": {
        +        "type": "string"
        +      },
        +      "position": {
        +        "description": "top|center|lower|bottom or 0.05-0.95 from the top",
        +        "type": [
        +          "string",
        +          "number"
        +        ]
        +      },
        +      "preset": {
        +        "type": "string"
        +      },
        +      "shadow": {
        +        "type": "boolean"
        +      },
        +      "size": {
        +        "description": "s|m|l|xl, '120px', or a 0.015-0.15 frame fraction",
        +        "type": [
        +          "string",
        +          "number"
        +        ]
        +      },
        +      "subFont": {
        +        "type": "string"
        +      },
        +      "subItalic": {
        +        "type": "boolean"
        +      },
        +      "textCase": {
        +        "enum": [
        +          "as-is",
        +          "upper",
        +          "lower",
        +          "title"
        +        ],
        +        "type": "string"
        +      },
        +      "tilt": {
        +        "description": "degrees, ±45",
        +        "type": "number"
        +      },
        +      "weight": {
        +        "type": "number"
        +      }
        +    },
        +    "type": "object"
        +  }
        +]
    • Changedreschedule_post2 fields changed
      • changedInput schema / properties / at / description
        Previous value: -"the new time — ISO timestamp (2026-08-05T09:00:00Z) or epoch milliseconds. Must be in the future, at most 365 days out."New value: +"the new time: local wall clock (2026-10-30T10:00, read in `timezone` or the brand’s zone on that date), UTC (…Z) or epoch ms. Future, at most 365 days out."
      • addedInput schema / properties / timezone
        Added value: +{
        +  "description": "IANA zone for a wall-clock `at`",
        +  "type": "string"
        +}
    • Addedsave_to_notion
    • Changedschedule_post3 fields changed
      • changedInput schema / properties / altText / description
        Previous value: -"ACCESSIBILITY — the screen-reader description of the attached image. ONE STRING describes the picture; on a CAROUSEL it describes EVERY slide. Pass an ARRAY of strings instead to describe each slide separately, aligned to the slide order — that is strictly better on a multi-slide post, because one sentence read out over six different pictures is wrong for five of them. More descriptions than pictures is refused rather than dropped. Write one whenever the post carries an image: describe what is IN the picture, never a repeat of the caption, which a screen reader already reads. CARRIED BY: X (max 1000, one per media), Pinterest (max 500 — PIN-LEVEL only, since its API has no per-item alt text, so slide 1’s description is used for the whole Pin and the result says the others were not sent), LinkedIn COMPANY PAGES (max 4086, one per slide), INSTAGRAM image posts and image slides (max 1000), FACEBOOK photos and albums, and BLUESKY, whose lexicon makes it REQUIRED on every image. The schedule is REFUSED if the LONGEST description exceeds the tightest of the channels on it, rather than truncated on the way out. NOT CARRIED, and none of these is a refusal — the post still publishes, just undescribed there, and the per-channel result says which: TikTok (its photo post has no alt field at any level), a THREADS CAROUSEL, an INSTAGRAM Reel or video slide, and a LinkedIn PERSONAL-profile post."New value: +"ACCESSIBILITY — the screen-reader description of the attached image. ONE STRING describes the picture; on a CAROUSEL it describes EVERY slide. Pass an ARRAY of strings instead to describe each slide separately, aligned to the slide order — use that on a multi-slide post, because one sentence read out over six different pictures is wrong for five of them. More descriptions than pictures is refused rather than dropped. Write one whenever the post carries an image: describe what is IN the picture, never a repeat of the caption, which a screen reader already reads. CARRIED BY: X (max 1000, one per media), Pinterest (max 500 — PIN-LEVEL only, since its API has no per-item alt text, so slide 1’s description is used for the whole Pin and the result says the others were not sent), LinkedIn COMPANY PAGES (max 4086, one per slide), INSTAGRAM image posts and image slides (max 1000), FACEBOOK photos and albums, and BLUESKY, whose lexicon makes it REQUIRED on every image. The schedule is REFUSED if the LONGEST description exceeds the tightest of the channels on it, rather than truncated on the way out. NOT CARRIED, and none of these is a refusal — the post still publishes, just undescribed there, and the per-channel result says which: TikTok (its photo post has no alt field at any level), a THREADS CAROUSEL, an INSTAGRAM Reel or video slide, and a LinkedIn PERSONAL-profile post."
      • changedInput schema / properties / at / description
        Previous value: -"when to post — ISO timestamp (2026-08-01T09:00:00Z) or epoch milliseconds. Must be in the future, at most 365 days out. Give this OR useQueue, never both."New value: +"when to post: local wall clock, no offset (2026-10-30T10:00), read in `timezone` or the brand’s zone ON THAT DATE so a series survives a clock change; or UTC (…Z) / epoch ms. Future, at most 365 days out. Give this OR useQueue, never both."
      • changedInput schema / properties / timezone / description
        Previous value: -"IANA zone for the queue, e.g. \"America/New_York\" — only meaningful with useQueue, and it overrides the brand’s saved zone for this one post. A saved slot of \"09:00\" is a WALL-CLOCK time, so the zone is what turns it into an instant; without either the brand’s saved zone or this, the queue resolves in UTC."New value: +"IANA zone (e.g. \"Africa/Cairo\") a wall-clock `at` or the queue is read in; overrides the brand’s saved zone for this one post. Without either, UTC."
    • Changedset_competitor_watch15 fields changed
      • addedInput schema / properties / auto
        Added value: +{
        +  "type": "boolean"
        +}
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "profile id/name from list_brands, this call only",
        +  "type": "string"
        +}
      • addedInput schema / properties / cadence
        Added value: +{
        +  "enum": [
        +    "weekly",
        +    "daily"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / competitors / description
        Previous value: -"the brands to watch — the COMPLETE list, replacing whatever was set before. Empty array = stop watching."New value: +"the COMPLETE brand list"
      • changedInput schema / properties / competitors / items / properties / domain / description
        Previous value: -"its domain, e.g. ridge.com — required for Google Ads Transparency, and what disambiguates a common brand name on Meta"New value: +"e.g. ridge.com: Google is read by domain, and it finds the right Meta page"
      • addedInput schema / properties / competitors / items / properties / kind
        Added value: +{
        +  "enum": [
        +    "competitor",
        +    "similar"
        +  ],
        +  "type": "string"
        +}
      • removedInput schema / properties / competitors / items / properties / name / description
        Removed value: -"the brand name, as it advertises"
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "type": "boolean"
        +}
      • addedInput schema / properties / email
        Added value: +{
        +  "description": "the weekly email",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / force
        Added value: +{
        +  "description": "with runNow: check again right after a check, allowed once what is watched changed",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / modules
        Added value: +{
        +  "description": "on/off; omitted keep theirs",
        +  "properties": {
        +    "ideas": {
        +      "type": "boolean"
        +    },
        +    "newAds": {
        +      "type": "boolean"
        +    },
        +    "topPosts": {
        +      "type": "boolean"
        +    },
        +    "trends": {
        +      "type": "boolean"
        +    },
        +    "winners": {
        +      "type": "boolean"
        +    }
        +  },
        +  "type": "object"
        +}
      • changedInput schema / properties / runNow / description
        Previous value: -"true to run one check immediately (spends credits now) instead of waiting a week for the first one"New value: +"check now (spends now)"
      • addedInput schema / properties / seed
        Added value: +{
        +  "description": "for auto: a website, brand or niche",
        +  "type": "string"
        +}
      • addedInput schema / properties / topics
        Added value: +{
        +  "description": "niche search phrases, e.g. \"protein breakfast\"",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • removedInput schema / required
        Removed value: -[
        -  "competitors"
        -]
    • Changedupdate_settings1 field changed
      • changedInput schema / properties / language / description
        Previous value: -"language for generated ads, copy and answers — e.g. \"English\", \"German\", \"Japanese\""New value: +"language for generated ads and copy (chat replies follow the language the user writes in) — e.g. \"English\", \"German\", \"Japanese\""
    • Changedupdate_sheet4 fields changed
      • removedInput schema / properties / updates / items / properties / values / items / items / anyOf
        Removed value: -[
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "type": "number"
        -  },
        -  {
        -    "type": "boolean"
        -  }
        -]
      • addedInput schema / properties / updates / items / properties / values / items / items / type
        Added value: +[
        +  "string",
        +  "number",
        +  "boolean"
        +]
      • removedInput schema / properties / values / items / items / anyOf
        Removed value: -[
        -  {
        -    "type": "string"
        -  },
        -  {
        -    "type": "number"
        -  },
        -  {
        -    "type": "boolean"
        -  }
        -]
      • addedInput schema / properties / values / items / items / type
        Added value: +[
        +  "string",
        +  "number",
        +  "boolean"
        +]
  8. 1 tool updatev0.1.436
    • Changeddraft_brand3 fields changed
      • changedInput schema / properties / domain / description
        Previous value: -"a website to scrape"New value: +"a website to scrape, or a social link (profile, YouTube channel or one post) to draft from the account it names"
      • changedInput schema / properties / platform / description
        Previous value: -"platform for socialHandle (instagram/tiktok/…)"New value: +"socialHandle platform: instagram/tiktok/youtube/twitter/threads"
      • changedInput schema / properties / socialHandle / description
        Previous value: -"a social handle to draft from (influencers/creators) — pair with platform"New value: +"a social handle (creators); needs platform, never guessed"
  9. 16 tool updatesv0.1.435
    • Changedconnect_connector1 field changed
      • changedInput schema / properties / provider / description
        Previous value: -"the connector id: applovin_ads, stripe, vercel, openai_ads, apple_ads, bluesky, telegram, bing_webmaster, posthog, mixpanel, amplitude, slack, discord, webhook"New value: +"the connector id: applovin_ads, stripe, intercom, vercel, openai_ads, apple_ads, bluesky, telegram, bing_webmaster, posthog, mixpanel, amplitude, slack, discord, webhook"
    • Changededit_image1 field changed
      • changedInput schema / properties / dryRun / description
        Previous value: -"true = return the exact credits this edit reserves and render nothing"New value: +"true = return the estimated credits this edit reserves and render nothing"
    • Changedfix_beat1 field changed
      • changedInput schema / properties / dryRun / description
        Previous value: -"true = return the exact credits this fix reserves and render nothing"New value: +"true = return the estimated credits this fix reserves and render nothing"
    • Changedgenerate_image1 field changed
      • changedInput schema / properties / mask / description
        Previous value: -"MASKED EDIT — change ONE region of an image and keep the rest: a local path or URL of a mask image for refImages[0] (the image being edited). Either convention works and the reply says which it read: TRANSPARENT pixels = change, or, on a mask with no transparency, WHITE = change and black = keep. Any size; it is scaled to the image. The mask GUIDES the edit rather than stencilling it: the new content can blend a little past its edge. Runs on the model hermoso_capabilities marks `refs.mask` (gpt-image-2.5): leave `model` empty or name that one — any other named model is refused, free. Needs refImages; the result keeps the source image's own frame, so aspectRatio is not applied."New value: +"MASKED EDIT — change ONE region and keep the rest: a mask (path or URL, any size) for refImages[0]. TRANSPARENT = change, or on a mask with no transparency WHITE = change, black = keep; the reply says which it read. The mask guides rather than stencils, so the change can blend a little past its edge. Runs on gpt-image-2.5 (`refs.mask` in hermoso_capabilities): leave `model` empty or name it; another named model is refused, free. Needs refImages; keeps the source image's frame, so aspectRatio is not applied."
    • Changedgenerate_video10 fields changed
      • changedInput schema / properties / cameraMove / description
        Previous value: -"A named camera move around the still in refImage, spelled exactly as the enum gives it: an orbit (a quarter turn, the default), orbiting left, a half turn or a full turntable, a rise, a crane up, a push in, a pull back, or a reveal. Only the camera-controls model renders one (minimax-h3-max-camera, listed with its moves in hermoso_capabilities): pass it with model omitted and that model is picked, or with that model named; any other named model is refused by name with nothing charged. Needs refImage."New value: +"A named camera move around the still in refImage (orbit = a quarter turn, the default; orbit_half / orbit_full = a half turn / a turntable). Only minimax-h3-max-camera renders one: with model omitted it is picked; any other named model is refused, nothing charged. Needs refImage."
      • changedInput schema / properties / durationSeconds / description
        Previous value: -"length of THIS ONE clip in seconds — pick one of the CHOSEN model’s listed durations from hermoso_capabilities (never a generic guess: the lists differ per model, from 4–8s on the short models up to 30s on the longest-clip one). This is a single continuous generation, so it CANNOT exceed that model’s longest clip: a longer ask is REFUSED with nothing rendered and nothing charged (it is never quietly truncated). Past ~15s only the long-clip models qualify, and an unnamed render is routed by the narrower auto-pool — so NAME the model in `model` when you are asking for a long single take. For a spot longer than any one clip, use plan_ad with durationSeconds then render_ad, which stitches acts of at most one model clip each (on a 15s-clip model, 40s = 15+15+10)."New value: +"length of THIS ONE clip in seconds: one of the CHOSEN model’s listed durations in hermoso_capabilities (they differ per model, 4–8s on the short ones up to 30s). One continuous generation, so a longer ask than the model’s longest clip is REFUSED, nothing charged (never quietly truncated). Past ~15s NAME a long-clip model in `model`. For a longer spot use plan_ad with durationSeconds then render_ad, which stitches acts (on a 15s model, 40s = 15+15+10)."
      • changedInput schema / properties / extend / description
        Previous value: -"true = EXTEND refVideo: the same clip continues per your prompt for durationSeconds more (each model’s extend.minSeconds..maxSeconds in hermoso_capabilities), delivered as ONE clip, source then continuation. Needs model named (a model with extend in hermoso_capabilities)."New value: +"true = EXTEND refVideo by durationSeconds (default 5) with what your prompt says happens next, as ONE clip. By default it CUTS to a NEW SHOT: new angle, same scene, room sound carried, no music at the cut. A named model (any i2v) renders it."
      • addedInput schema / properties / extendMode
        Added value: +{
        +  "description": "with extend: 'continue' = the SAME take past its last frame, only when asked seamless (models with extend in hermoso_capabilities).",
        +  "enum": [
        +    "cut",
        +    "continue"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / model / description
        Previous value: -"video model id from hermoso_capabilities; a named model is never swapped without asking. Omit to let the router pick"New value: +"video model id from hermoso_capabilities; a named model is never swapped without asking. Omit to let the router pick; its default per job (jobEngines in hermoso_capabilities, from watched side-by-side tests) is seedance-2.5 for product, lifestyle, a person talking to camera and 30s single takes (cheaper engines broke the product there). A model the user picks is kept; never switch it"
      • changedInput schema / properties / musicMood / description
        Previous value: -"WHICH mood the music bed is composed in (upbeat / calm / warm / epic / tense / playful / elegant / hype / chill / dramatic). It does NOT decide WHETHER there is one: a clip that comes back with no audio track — every model hermoso_capabilities lists as \"silent\", plus any audio model that returned mute — gets a bed composed and CHARGED automatically, at the flat per-track fee hermoso_capabilities reports as explainerMusicCredits, and omitting this field only means the mood defaults to \"warm\". Pass audio:false for a genuinely silent clip with no bed and no bed charge."New value: +"the mood of the music bed (upbeat / calm / warm / epic / tense / playful / elegant / hype / chill / dramatic; default warm). It does not decide WHETHER there is one: a clip with no audio track (a \"silent\" model in hermoso_capabilities, or one that came back mute) gets a bed composed and CHARGED automatically at explainerMusicCredits. audio:false = silent, no bed, no charge."
      • changedInput schema / properties / refImages / description
        Previous value: -"SEVERAL reference images (local paths or URLs) — a person, products, a place — that must all appear in the clip. Only models whose `refs.max` in hermoso_capabilities is above 1 use more than one, and each uses at most that many; with `refs.promptAddressed` true, name them in your prompt as Image 1, Image 2… in this order. minimax-h3-max-ref takes up to 9 and keeps each one as a reference rather than a first frame. On a model that takes one image, only the first is used."New value: +"SEVERAL reference images (paths or URLs: a person, products, a place) that must all appear. A model uses up to its `refs.max` in hermoso_capabilities (one-image models take the first); with `refs.promptAddressed`, name them Image 1, Image 2… in order. minimax-h3-max-ref takes 9 and keeps each as a reference, not a first frame."
      • changedInput schema / properties / refVideo / description
        Previous value: -"URL of an existing video to EDIT rather than generate from scratch — the clip is transformed per your prompt, inheriting the SOURCE clip’s canvas (aspect ratio) and, on every model hermoso_capabilities marks `sourceLength`, its LENGTH too: those endpoints have no duration parameter, their listed `durations` are the per-second price ladder, and a durationSeconds you send is reported back as unused rather than silently dropped. Trim the source to change the length. Omit `model` for the default editor, or name a model whose videoEdit is true in hermoso_capabilities (a named model that cannot edit is refused, nothing charged); refImage rides along as the look of what the edit adds. With extend:true this is instead the clip to EXTEND. Omit to generate a fresh clip."New value: +"URL of an existing video to EDIT rather than generate from scratch — the clip is transformed per your prompt, keeping the SOURCE clip’s aspect ratio and, on models hermoso_capabilities marks `sourceLength`, its LENGTH (their `durations` are the per-second price ladder; a durationSeconds sent is reported as unused). Trim the source to change the length. Omit `model` for the default editor, or name a model whose videoEdit is true in hermoso_capabilities (a named model that cannot edit is refused, nothing charged); refImage rides along as the look of what the edit adds. With extend:true this is instead the clip to EXTEND. Omit to generate a fresh clip."
      • changedInput schema / properties / resolution / description
        Previous value: -"'1080p' default (what we ship and bill for); '480p'/'720p' = cheaper draft passes, '4k' = premium final delivery (more credits). NOT EVERY MODEL OFFERS EVERY TIER — this enum is what the tool accepts, and each model's OWN `resolutions` list in hermoso_capabilities is what it can actually render. Ask for a tier the chosen model does not list and it is rendered at that model's best available tier instead, with nothing in the reply saying so — so check `resolutions` before promising anyone 1080p or 4k."New value: +"'1080p' default; '480p'/'720p' cheaper drafts, '4k' premium (more credits). A model renders only the tiers in its own `resolutions` list in hermoso_capabilities; a tier it lacks renders at its best one without a word, so check before promising 1080p or 4k."
      • changedInput schema / properties / shots / description
        Previous value: -"MULTI-SHOT: one clip cut into these shots, in order. The seconds must add up to a length the model renders; replaces `prompt` (send either). Only models with multiShot true in hermoso_capabilities. Each shot prompt is capped per model (shotPromptMaxChars there; Kling 3.0: 512 characters); a longer one is refused free, never cut."New value: +"MULTI-SHOT: one clip cut into these shots, in order; the seconds add up to a length the model renders. Replaces `prompt`. Only models with multiShot true in hermoso_capabilities; a shot prompt over that model’s shotPromptMaxChars (Kling 3.0: 512) is refused free, never cut."
    • Changedgenerate_voice2 fields changed
      • changedInput schema / properties / engine / description
        Previous value: -"voice-engine id: 'seed-audio' (default), 'eleven-v3', 'minimax-speech', or 'kokoro' — listed in hermoso_capabilities"New value: +"voice-engine id: 'seed-audio' (default), 'eleven-v4' (Eleven v4), 'minimax-speech', or 'kokoro' — listed in hermoso_capabilities"
      • changedInput schema / properties / voice / description
        Previous value: -"a voice preset from the chosen engine (e.g. 'Aria'/'George' on eleven-v3, 'stokie_en' on seed-audio) — omit for the engine default"New value: +"a voice preset from the chosen engine (e.g. 'Aria'/'George' on eleven-v4, 'stokie_en' on seed-audio) — omit for the engine default"
    • Changedlist_drive_files1 field changed
      • addedInput schema / properties / recursive
        Added value: +{
        +  "description": "with folderId: the whole tree",
        +  "type": "boolean"
        +}
    • Changedmake_explainer1 field changed
      • changedInput schema / properties / dryRun / description
        Previous value: -"true = return the exact credits this explainer reserves (its own pricing, stopped at the hold) and render nothing. Quote it before running one; try frameDensity lean or minimal when the balance is short."New value: +"true = return the estimated credits this explainer reserves (its own pricing, stopped at the hold) and render nothing. Quote it before running one; try frameDensity lean or minimal when the balance is short."
    • Changedplan_ad1 field changed
      • addedInput schema / properties / inserts
        Added value: +{
        +  "description": "VIDEO ONLY: the user's own SCREENS (an app, a page, a dashboard, a document, a review, a chart) shown at these seconds as a rounded card over the upper middle of the footage, which keeps playing around it (look 'full' covers the frame, only when the user asked), the voice running underneath. Fewer and shorter: about one per 4 s of film, 1.2-2 s each, on the line that talks about it; never in the first second or the last 1.5 s. The plan frames those shots for the card, keeps them free of screens, and returns the pictures as creative.inserts for render_ad to lay in (about 2 credits). A photo of a person, product or place (kind 'photo') is not laid in: pass a product photo as the brand's product photo and a person as a saved creator. One that does not fit is left out and the reply says so.",
        +  "items": {
        +    "properties": {
        +      "end": {
        +        "description": "second it ends: at most 2 s after start (2.5 s full-screen), never in the last 1.5 s of the film",
        +        "type": "number"
        +      },
        +      "image": {
        +        "description": "an https or upload_file URL, a Library id (L0, L1…) or a local path on the CLI",
        +        "type": "string"
        +      },
        +      "kind": {
        +        "description": "screen (default): an app, a page, a dashboard, a document, a review, a chart. photo: a person, a product or a place, which is NOT laid over the video (a product photo goes in as the brand's product photo, a person to star as a saved creator)",
        +        "enum": [
        +          "screen",
        +          "photo"
        +        ],
        +        "type": "string"
        +      },
        +      "look": {
        +        "description": "card (default): a rounded card over the upper middle of the footage, which keeps playing. full: covers the whole frame, ONLY when the user asked for full-screen",
        +        "enum": [
        +          "card",
        +          "full"
        +        ],
        +        "type": "string"
        +      },
        +      "start": {
        +        "description": "second it appears (never before 1 s: the film opens on moving footage)",
        +        "type": "number"
        +      }
        +    },
        +    "required": [
        +      "image",
        +      "start",
        +      "end"
        +    ],
        +    "type": "object"
        +  },
        +  "maxItems": 8,
        +  "type": "array"
        +}
    • Changedpost_edit1 field changed
      • changedInput schema / properties / dryRun / description
        Previous value: -"true = return the exact credits this edit reserves and run nothing"New value: +"true = return the estimated credits this edit reserves and run nothing"
    • Changedpost_to_meta1 field changed
      • changedInput schema / properties / collaborators / description
        Previous value: -"INSTAGRAM COLLAB — up to 3 Instagram usernames invited to CO-AUTHOR this post. Once one accepts, the post appears on THEIR profile too, with both handles in the header and the likes and comments shared — it is how a brand reaches a creator’s audience without paying for placement, and it is the single most-asked thing a scheduler normally cannot do. Pass handles only (\"hermosoai\"), not profile links; a leading @ is fine. INSTAGRAM ONLY — Facebook and Threads have no collab post at all and are refused BY NAME rather than silently dropping the co-authors — and never on a Story. AN INVITE IS NOT A CO-POST: publishing SENDS a request the other account must accept in their Instagram notifications, and until they do the post is on this brand’s profile ALONE; they may also decline, and Instagram sends no notification either way. So never report the post as live on both accounts — read the invite status back out of the reply, and use instagram_collaborators later to find out whether they accepted."New value: +"INSTAGRAM COLLAB — up to 3 Instagram usernames invited to CO-AUTHOR this post. Once one accepts, the post appears on THEIR profile too, with both handles in the header and the likes and comments shared. Pass handles only (\"hermosoai\"), not profile links; a leading @ is fine. INSTAGRAM ONLY — Facebook and Threads have no collab post at all and are refused BY NAME rather than silently dropping the co-authors — and never on a Story. AN INVITE IS NOT A CO-POST: publishing SENDS a request the other account must accept in their Instagram notifications, and until they do the post is on this brand’s profile ALONE; they may also decline, and Instagram sends no notification either way. So never report the post as live on both accounts — read the invite status back out of the reply, and use instagram_collaborators later to find out whether they accepted."
    • Changedrecast_motion1 field changed
      • changedInput schema / properties / engine / enum
        Previous value: -[
        -  "auto",
        -  "h3",
        -  "seedance",
        -  "wan",
        -  "kling"
        -]New value: +[
        +  "auto",
        +  "h3",
        +  "seedance",
        +  "wan",
        +  "wan-prime",
        +  "kling"
        +]
    • Changedrender_ad6 fields changed
      • changedInput schema / properties / creator / description
        Previous value: -"CAST A SAVED CREATOR in this ad — their id from list_creators, or the name you know them by (“Sarah”), or a PRESET AI creator from list_creators presets by exact name or id (free, no generation). Their saved portrait becomes the on-camera identity for the whole spot, so the same face carries across every act and across every ad you render for this brand — and because we already have their picture, the character portrait this pipeline would otherwise generate is skipped, so casting somebody costs LESS than not casting them. Omit to let the ad cast a fresh person — EXCEPT for a CREATOR account (onboarded from their own @handle): their own saved likeness is cast by default when the plan has a person on camera and the account is on a paid Hermoso plan (a free account gets a fresh AI person), and the read-back says `default:true`; pass \"none\" to render without them. Refused for free, with nothing rendered, if the name matches nobody or more than one creator, if an explicitly named creator is cast on a plan with nobody on camera, or if a REAL person is cast on a free plan. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."New value: +"CAST A SAVED CREATOR in this ad — their id from list_creators, or the name you know them by (“Sarah”), or a PRESET AI creator from list_creators presets by exact name or id (free, no generation). Their saved portrait becomes the on-camera identity for the whole spot, so the same face carries across every act and across every ad you render for this brand (casting skips the portrait we would otherwise generate). Omit to let the ad cast a fresh person — EXCEPT for a CREATOR account (onboarded from their own @handle): their own saved likeness is cast by default when the plan has a person on camera and the account is on a paid Hermoso plan (a free account gets a fresh AI person), and the read-back says `default:true`; pass \"none\" to render without them. Refused for free, with nothing rendered, if the name matches nobody or more than one creator, if an explicitly named creator is cast on a plan with nobody on camera, or if a REAL person is cast on a free plan. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
      • changedInput schema / properties / dryRun / description
        Previous value: -"return the routing decision (single pass vs stitched acts, resolved model + act lengths) and the exact credits the real render reserves, WITHOUT submitting a render — free, nothing charged"New value: +"return the routing decision (single pass vs stitched acts, resolved model + act lengths) and the estimated credits the real render reserves, WITHOUT submitting a render — free, nothing charged"
      • changedInput schema / properties / durationSeconds / description
        Previous value: -"total ad length in seconds (supported range 4–180; outside that it is clamped). Omit to honor the plan’s own duration — that is almost always right. This only RE-TIMES an already-authored board (its scenes are scaled to fit), it does NOT re-write it, so to change the length of the ad the user asked for, re-run plan_ad with durationSeconds instead. A length that fits ONE clip of the render model renders as one continuous pass; longer is stitched from acts filled to that model’s clip maximum with the remainder last — the maximum is 15s on most models and 30s on the longest-clip one, so use dryRun:true to see the exact act split for free before spending."New value: +"total ad length in seconds, 4–180 (clamped). Omit to keep the plan’s own length, almost always right: this only RE-TIMES the authored board; to change the ad’s length, re-run plan_ad with durationSeconds. A length that fits ONE clip renders in one pass; longer is stitched from acts of the model’s clip maximum (15s on most, 30s on the longest-clip one), remainder last. dryRun:true shows the split free."
      • addedInput schema / properties / inserts
        Added value: +{
        +  "description": "the user's own screens laid over the finished video at these seconds, as a card over the playing footage by default (look 'full' covers the frame, only when asked), sound running underneath (about 2 credits). The film always opens and ends on moving footage. Omit to use plan_ad's creative.inserts; [] renders without them. plan_ad with inserts writes the board around them.",
        +  "items": {
        +    "properties": {
        +      "end": {
        +        "description": "second it ends: at most 2 s after start (2.5 s full-screen), never in the last 1.5 s of the film",
        +        "type": "number"
        +      },
        +      "image": {
        +        "description": "an https or upload_file URL, a Library id (L0, L1…) or a local path on the CLI",
        +        "type": "string"
        +      },
        +      "kind": {
        +        "description": "screen (default): an app, a page, a dashboard, a document, a review, a chart. photo: a person, a product or a place, which is NOT laid over the video (a product photo goes in as the brand's product photo, a person to star as a saved creator)",
        +        "enum": [
        +          "screen",
        +          "photo"
        +        ],
        +        "type": "string"
        +      },
        +      "look": {
        +        "description": "card (default): a rounded card over the upper middle of the footage, which keeps playing. full: covers the whole frame, ONLY when the user asked for full-screen",
        +        "enum": [
        +          "card",
        +          "full"
        +        ],
        +        "type": "string"
        +      },
        +      "start": {
        +        "description": "second it appears (never before 1 s: the film opens on moving footage)",
        +        "type": "number"
        +      }
        +    },
        +    "required": [
        +      "image",
        +      "start",
        +      "end"
        +    ],
        +    "type": "object"
        +  },
        +  "maxItems": 8,
        +  "type": "array"
        +}
      • changedInput schema / properties / model / description
        Previous value: -"video model id from hermoso_capabilities (default: the plan’s pick). Naming one is a DELIBERATE pick — the server asks before ever swapping it (no silent fallback)"New value: +"video model id from hermoso_capabilities (default: the engine the router picks for the plan: seedance-2.5 for every ad, a person talking to camera included). Naming one is a DELIBERATE pick — the server asks before ever swapping it (no silent fallback)"
      • changedInput schema / properties / resolution / description
        Previous value: -"'1080p' default (what we ship and bill for); '480p'/'720p' = cheaper draft passes, '4k' = premium final delivery (more credits). NOT EVERY MODEL OFFERS EVERY TIER — this enum is what the tool accepts, and each model's OWN `resolutions` list in hermoso_capabilities is what it can actually render (the longest-clip 30s model, for one, tops out at 720p). Ask for a tier the chosen model does not list and it is rendered at that model's best available tier instead, with nothing in the reply saying so — so check `resolutions` before promising anyone 1080p or 4k."New value: +"'1080p' default; '480p'/'720p' cheaper drafts, '4k' premium (more credits). A model renders only the tiers in its own `resolutions` list in hermoso_capabilities (the 30s model tops out at 720p); a tier it lacks renders at its best one without a word, so check before promising 1080p or 4k."
    • Changedrestyle_video1 field changed
      • changedInput schema / properties / engine / enum
        Previous value: -[
        -  "auto",
        -  "seedance",
        -  "wan",
        -  "kling"
        -]New value: +[
        +  "auto",
        +  "seedance",
        +  "wan",
        +  "wan-prime",
        +  "kling"
        +]
    • Changedsearch_meta_ads2 fields changed
      • changedInput schema / properties / companyName / description
        Previous value: -"one advertiser’s ads by brand name"New value: +"one advertiser’s ads by brand name — resolved to the brand’s own page only on evidence (its Instagram, its domain, or ads that land on its domain); pass domain too for the best match"
      • addedInput schema / properties / domain
        Added value: +{
        +  "description": "the advertiser’s website, e.g. buffer.com — with companyName, used to confirm which Meta page is really theirs",
        +  "type": "string"
        +}
    • Changedupgrade_plan3 fields changed
      • addedInput schema / properties / confirm
        Added value: +{
        +  "description": "true (with quote_token) to make a quoted upgrade",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / period / description
        Previous value: -"billing cadence — monthly (default) or yearly (2 months free)"New value: +"monthly only ('yr' buys monthly)"
      • addedInput schema / properties / quote_token
        Added value: +{
        +  "description": "the quoteToken from the quote step",
        +  "type": "string"
        +}
  10. 4 tool updatesv0.1.410
    • Changedconnect_connector1 field changed
      • changedInput schema / properties / provider / description
        Previous value: -"the connector id: applovin_ads, stripe, openai_ads, apple_ads, bluesky, telegram, bing_webmaster, posthog, mixpanel, amplitude, slack, discord, webhook"New value: +"the connector id: applovin_ads, stripe, vercel, openai_ads, apple_ads, bluesky, telegram, bing_webmaster, posthog, mixpanel, amplitude, slack, discord, webhook"
    • Changedgenerate_video2 fields changed
      • changedInput schema / properties / faceRoute / description
        Previous value: -"ONLY after a render came back saying the video model's safety check flagged a person's face: 'face_lane' is the user's choice \"I own the rights to this face\". Send the SAME request again with it and the same face is rendered on Seedance through the face library, at the normal price. Sending it is the user's confirmation that they have the rights to that face (paid plans, like every real face). Never set it on your own; the other choices that refusal names are a different model (`model`) or another creator (`creator`)."New value: +"'face_lane' records the user's statement \"I own the rights to this face\" for the real person in their reference (paid plans; the likeness terms apply). Set it only when the user picked that option after a render said it could not use that face, and send the same request again; it then renders through Hermoso's face library at the normal price. Never set it on your own. The other options are a different `model` or one of the user's saved `creator`s."
      • changedInput schema / properties / raw / description
        Previous value: -"RAW MODEL ACCESS: dispatch this prompt to the model BYTE-IDENTICAL — no appended packaging/label guidance, no negative prompt, no reference-binding lines, no hex-to-colour-name rewrite. Use it when you want the model itself rather than Hermoso's render craft. Two vendor-required fixes still apply: extra @ImageN tokens are dropped and an over-long prompt is trimmed at a sentence (on Veo it is refused with the length and the cap instead). Billing, durable delivery and per-model validation are unchanged."New value: +"Send the prompt to the model exactly as written, without the wording Hermoso usually adds (packaging and label guidance, reference lines, colour names for hex codes, quality hints). Use it when you want the model's own interpretation rather than Hermoso's render craft. Two vendor-required fixes still apply: extra @ImageN tokens are dropped and an over-long prompt is trimmed at a sentence (on Veo it is refused with the length and the cap instead). Billing, durable delivery and per-model validation are unchanged."
    • Changedplan_ad1 field changed
      • changedInput schema / properties / durationSeconds / description
        Previous value: -"VIDEO ONLY — the total spot length the user explicitly asked for, in seconds, copied verbatim (30 for \"a 30 second ad\"). The planner authors the storyboard TO it: the scenes’ seconds sum to it and the script is word-budgeted for it. Supported range 4–180; anything outside is CLAMPED to it (the reply says so). A length that fits ONE clip of the render model renders as a single continuous pass; anything longer is STITCHED from acts filled to that model’s clip maximum with the remainder last (on a 15s-clip model, 40 -> 15+15+10 and 17 -> 13+4) — never time-compressed. The maximum is the model’s own: 15s on most, 30s on the longest-clip model, so a 30s ad can be one unbroken take rather than two acts. Omit when the user named no length; do NOT pass a guess: an omitted value is the default, a 15s spot in one take, or 30s for a two-person format such as a street interview (2026-10-14), and a length the user names always wins."New value: +"VIDEO ONLY — the total spot length the user explicitly asked for, in seconds, copied verbatim (30 for \"a 30 second ad\"). The planner authors the storyboard TO it: the scenes’ seconds sum to it and the script is word-budgeted for it. Supported range 4–180; anything outside is CLAMPED to it (the reply says so). A length that fits ONE clip of the render model renders as a single continuous pass; anything longer is STITCHED from acts filled to that model’s clip maximum with the remainder last (on a 15s-clip model, 40 -> 15+15+10 and 17 -> 13+4) — never time-compressed. The maximum is the model’s own: 15s on most, 30s on the longest-clip model, so a 30s ad can be one unbroken take rather than two acts. Omit when the user named no length; do NOT pass a guess: when it is omitted the planner picks a length suited to the format, and a length the user names always wins."
    • Changedrender_ad1 field changed
      • changedInput schema / properties / faceRoute / description
        Previous value: -"ONLY after a render came back saying the video model's safety check flagged a person's face: 'face_lane' is the user's choice \"I own the rights to this face\". Send the SAME request again with it and the same face is rendered on Seedance through the face library, at the normal price. Sending it is the user's confirmation that they have the rights to that face (paid plans, like every real face). Never set it on your own; the other choices that refusal names are a different model (`model`) or another creator (`creator`)."New value: +"'face_lane' records the user's statement \"I own the rights to this face\" for the real person in their reference (paid plans; the likeness terms apply). Set it only when the user picked that option after a render said it could not use that face, and send the same request again; it then renders through Hermoso's face library at the normal price. Never set it on your own. The other options are a different `model` or one of the user's saved `creator`s."
  11. 5 tool updatesv0.1.409
    • Changeddub_video2 fields changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "description": "quote only",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / voice / description
        Previous value: -"optional target voice preset, e.g. 'Aria' (warm female) or 'George' (confident male). Defaults to a voice matching the source speaker's register."New value: +"optional, e.g. 'Aria' (woman) or 'George' (man): a native voice of that sex speaks. Default: the source speaker's sex."
    • Changedmake_template_ad1 field changed
      • changedInput schema / properties / config / description
        Previous value: -"MUST include config.template: 'custom' or a preset id, plus its fields. PRESETS: 'slideshow' (IMAGES, TikTok photo mode / Reels 1080x1920, or size:'4:5' feed carousels; no branding): { slides:[{text, sub?, image?, blur?, background?, position?}] (2-35; words never rewritten), style? ('tiktok-classic'|'clean-minimal'|'note-style' or a look in words), textStyle?, video?:true (+ an MP4) }; 2 credits, +1 per slide past 5, +2 for the MP4. 'imessage-chat' (VIDEO ~15s): { thread:{contactName, messages:[{from:'them'|'me', text?, product?:{image,title,domain}}]}, theme?, endCard }. 'chatgpt-chat' (VIDEO): { question, answer (may **bold** the brand), productImage?, endCard }. 'apple-notes' (VIDEO): { title, lines[], theme?, endCard }. 'value-prop' (VIDEO ~17s): { hook ≤40ch, claims[3-5 ≤34ch], productImages[2-3], palette[], endCard }. 'static-mockup' (IMAGE): { style:'imessage'|'notes'|'card', size?:{w,h}, ...fields }. 'airdrop-carousel' (VIDEO): { brandName, products:[{image, title?}] (3-16), endCard }. 'app-ui-tour' (VIDEO): { hook?, appName, iconImage?, beats:[{screenImage, caption}] (2-6), endCard }. 'imessage-cascade' (VIDEO): { notifications:[{sender, text}] (4-8), backgroundImage?, endCard }. 'photo-grid' (VIDEO): { title?, photos:[{image, label?}] (4-9), endCard }. 'vignette' (VIDEO): { hook, lines[2-4 ≤40ch], heroImage, endCard }. 'kinetic-type' (VIDEO, own SFX): { phrases[3-6 ≤34ch], productImages?[≤4], endCard }. 'myth-vs-fact' (VIDEO with a real VOICEOVER, small extra charge): { pairs:[{myth ≤50ch, fact ≤60ch}] (2-4; [brackets] accent), endCard }, real truths only. 'carousel' (IMAGES, 5-10 branded 1080x1080): { cover:{hook?, title}, slides:[{headline, support?, stat?:{value, label}}] (3-8), cta:{headline, cta?, domain?}, productImage?, logo? }. endCard = { headline, cta, domain?, logo?, color? }; palette and fontStack optional."New value: +"MUST include config.template: 'custom' or a preset id, plus its fields. PRESETS: 'slideshow' (IMAGES, TikTok photo mode / Reels 1080x1920, or size:'4:5' feed carousels; no branding): { slides:[{text, sub?, image?, blur?, background?, position?}] (2-35; words never rewritten), style? ('tiktok-classic'|'clean-minimal'|'note-style' or a look in words), textStyle?, video?:true (+ an MP4, which takes config.music like any video) }; 2 credits, +1 per slide past 5, +2 for the MP4. 'imessage-chat' (VIDEO ~15s): { thread:{contactName, messages:[{from:'them'|'me', text?, product?:{image,title,domain}}]}, theme?, endCard }. 'chatgpt-chat' (VIDEO): { question, answer (may **bold** the brand), productImage?, endCard }. 'apple-notes' (VIDEO): { title, lines[], theme?, endCard }. 'value-prop' (VIDEO ~17s): { hook ≤40ch, claims[3-5 ≤34ch], productImages[2-3], palette[], endCard }. 'static-mockup' (IMAGE): { style:'imessage'|'notes'|'card', size?:{w,h}, ...fields }. 'airdrop-carousel' (VIDEO): { brandName, products:[{image, title?}] (3-16), endCard }. 'app-ui-tour' (VIDEO): { hook?, appName, iconImage?, beats:[{screenImage, caption}] (2-6), endCard }. 'imessage-cascade' (VIDEO): { notifications:[{sender, text}] (4-8), backgroundImage?, endCard }. 'photo-grid' (VIDEO): { title?, photos:[{image, label?}] (4-9), endCard }. 'vignette' (VIDEO): { hook, lines[2-4 ≤40ch], heroImage, endCard }. 'kinetic-type' (VIDEO, own SFX): { phrases[3-6 ≤34ch], productImages?[≤4], endCard }. 'myth-vs-fact' (VIDEO with a real VOICEOVER, small extra charge): { pairs:[{myth ≤50ch, fact ≤60ch}] (2-4; [brackets] accent), endCard }, real truths only. 'carousel' (IMAGES, 5-10 branded 1080x1080): { cover:{hook?, title}, slides:[{headline, support?, stat?:{value, label}}] (3-8), cta:{headline, cta?, domain?}, productImage?, logo? }. endCard = { headline, cta, domain?, logo?, color? }; palette and fontStack optional."
    • Changedplan_ad1 field changed
      • changedInput schema / properties / durationSeconds / description
        Previous value: -"VIDEO ONLY — the total spot length the user explicitly asked for, in seconds, copied verbatim (30 for \"a 30 second ad\"). The planner authors the storyboard TO it: the scenes’ seconds sum to it and the script is word-budgeted for it. Supported range 4–180; anything outside is CLAMPED to it (the reply says so). A length that fits ONE clip of the render model renders as a single continuous pass; anything longer is STITCHED from acts filled to that model’s clip maximum with the remainder last (on a 15s-clip model, 40 -> 15+15+10 and 17 -> 13+4) — never time-compressed. The maximum is the model’s own: 15s on most, 30s on the longest-clip model, so a 30s ad can be one unbroken take rather than two acts. Omit when the user named no length; do NOT pass a guess: an omitted value is the default, a 30s spot in one unbroken take (2026-09-29), and a length the user names always wins."New value: +"VIDEO ONLY — the total spot length the user explicitly asked for, in seconds, copied verbatim (30 for \"a 30 second ad\"). The planner authors the storyboard TO it: the scenes’ seconds sum to it and the script is word-budgeted for it. Supported range 4–180; anything outside is CLAMPED to it (the reply says so). A length that fits ONE clip of the render model renders as a single continuous pass; anything longer is STITCHED from acts filled to that model’s clip maximum with the remainder last (on a 15s-clip model, 40 -> 15+15+10 and 17 -> 13+4) — never time-compressed. The maximum is the model’s own: 15s on most, 30s on the longest-clip model, so a 30s ad can be one unbroken take rather than two acts. Omit when the user named no length; do NOT pass a guess: an omitted value is the default, a 15s spot in one take, or 30s for a two-person format such as a street interview (2026-10-14), and a length the user names always wins."
    • Changedrecast_motion3 fields changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "description": "return the credits this exact job would hold and the engine auto picks, without rendering",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / language
        Added value: +{
        +  "description": "the new voice speaks this language (e.g. 'Spanish'), lip-synced to the new words; voice defaults to 'match'",
        +  "type": "string"
        +}
      • addedInput schema / properties / voice
        Added value: +{
        +  "description": "a NEW voice for the new performer: 'match' (a stock voice that fits them), 'female'/'male', or Aria, Sarah, Charlotte, George, Brian, Charlie. Omit = the clip's own sound. Never the original speaker's or a cloned voice",
        +  "type": "string"
        +}
    • Changedupdate_brand1 field changed
      • addedInput schema / properties / profileType
        Added value: +{
        +  "enum": [
        +    "website",
        +    "social",
        +    "describe",
        +    "explore"
        +  ],
        +  "type": "string"
        +}
  12. 8 tool updatesv0.1.398
    • Changedgenerate_image1 field changed
      • addedInput schema / properties / logoPlacement
        Added value: +{
        +  "description": "overlay: real logo file laid flat. in_scene: on something in the scene, from the file, checked. auto (default): from the prompt",
        +  "enum": [
        +    "auto",
        +    "overlay",
        +    "in_scene",
        +    "none"
        +  ],
        +  "type": "string"
        +}
    • Changedgenerate_video4 fields changed
      • changedInput schema / properties / prompt / description
        Previous value: -"the video prompt / shot description (for a refVideo edit, this is the transformation instruction)"New value: +"the video prompt / shot description (for a refVideo edit, this is the transformation instruction); optional with `shots`"
      • changedInput schema / properties / raw / description
        Previous value: -"RAW MODEL ACCESS: dispatch this prompt to the model BYTE-IDENTICAL — no appended packaging/label guidance, no negative prompt, no reference-binding lines, no hex-to-colour-name rewrite. Use it when you want the model itself rather than Hermoso's render craft. Two vendor-required fixes still apply: extra @ImageN tokens are dropped and an over-long prompt is trimmed at a sentence. Billing, durable delivery and per-model validation are unchanged."New value: +"RAW MODEL ACCESS: dispatch this prompt to the model BYTE-IDENTICAL — no appended packaging/label guidance, no negative prompt, no reference-binding lines, no hex-to-colour-name rewrite. Use it when you want the model itself rather than Hermoso's render craft. Two vendor-required fixes still apply: extra @ImageN tokens are dropped and an over-long prompt is trimmed at a sentence (on Veo it is refused with the length and the cap instead). Billing, durable delivery and per-model validation are unchanged."
      • changedInput schema / properties / shots / description
        Previous value: -"MULTI-SHOT: one clip cut into these shots, in order. The seconds must add up to a length the model renders; replaces `prompt` (send either). Only models with multiShot true in hermoso_capabilities."New value: +"MULTI-SHOT: one clip cut into these shots, in order. The seconds must add up to a length the model renders; replaces `prompt` (send either). Only models with multiShot true in hermoso_capabilities. Each shot prompt is capped per model (shotPromptMaxChars there; Kling 3.0: 512 characters); a longer one is refused free, never cut."
      • removedInput schema / required
        Removed value: -[
        -  "prompt"
        -]
    • Changedmake_thumbnail1 field changed
      • addedInput schema / properties / logoPlacement
        Added value: +{
        +  "description": "overlay (exact, flat) | in_scene (from the file, checked); auto: in_scene with logo3d",
        +  "enum": [
        +    "auto",
        +    "overlay",
        +    "in_scene"
        +  ],
        +  "type": "string"
        +}
    • Changedrecast_motion3 fields changed
      • changedInput schema / properties / engine / enum
        Previous value: -[
        -  "auto",
        -  "seedance",
        -  "wan",
        -  "kling"
        -]New value: +[
        +  "auto",
        +  "h3",
        +  "seedance",
        +  "wan",
        +  "kling"
        +]
      • changedInput schema / properties / image / description
        Previous value: -"who performs it: the actor/character image URL (@Image1). Required on kling. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."New value: +"who performs it: the actor/character image URL (@Image1). Required on kling. No one yet: generate_image a portrait of an AI person from words (no reference photo) and pass its url, or a saved / preset creator’s portrait (list_creators). Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
      • changedInput schema / properties / resolution / description
        Previous value: -"seedance and wan; default 1080p"New value: +"seedance and wan default 1080p; h3 takes 720p or 1080p, default the clip’s own"
    • Changedrun_post_refill7 fields changed
      • addedInput schema / properties / approve
        Added value: +{
        +  "description": "REVIEW: draft ids to schedule, or [\"all\"]. A draft whose time has passed moves to the next free posting slot.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / discard
        Added value: +{
        +  "description": "REVIEW: draft ids to delete, or [\"all\"]. Nothing is posted for them.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • changedInput schema / properties / dryRun / description
        Previous value: -"default TRUE (preview only). false actually queues the posts."New value: +"default TRUE (a preview: nothing rendered, nothing queued; the copy it writes bills a few credits). false renders fresh creative and schedules or drafts it per the mode."
      • addedInput schema / properties / edit
        Added value: +{
        +  "description": "REVIEW: change drafts before approving them. Edited copy is screened by the voice rules.",
        +  "items": {
        +    "properties": {
        +      "at": {
        +        "description": "ISO time, at least 5 minutes ahead",
        +        "type": "string"
        +      },
        +      "captions": {
        +        "additionalProperties": {
        +          "type": "string"
        +        },
        +        "propertyNames": {
        +          "type": "string"
        +        },
        +        "type": "object"
        +      },
        +      "channels": {
        +        "description": "narrow to some of the draft’s own channels",
        +        "items": {
        +          "type": "string"
        +        },
        +        "type": "array"
        +      },
        +      "id": {
        +        "type": "string"
        +      },
        +      "message": {
        +        "type": "string"
        +      },
        +      "title": {
        +        "type": "string"
        +      }
        +    },
        +    "required": [
        +      "id"
        +    ],
        +    "type": "object"
        +  },
        +  "type": "array"
        +}
      • changedInput schema / properties / force / description
        Previous value: -"plan even while the refill is switched off — useful for showing someone what it would do before they turn it on. Combined with dryRun:false it still respects a stored dryRun."New value: +"plan a preview even while autoposting is switched off — useful for showing someone what it would do before they turn it on."
      • addedInput schema / properties / learn
        Added value: +{
        +  "description": "notes for future batches, e.g. \"too salesy\". Free.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / redo
        Added value: +{
        +  "properties": {
        +    "change": {
        +      "description": "caption keeps the picture",
        +      "enum": [
        +        "both",
        +        "caption",
        +        "picture"
        +      ],
        +      "type": "string"
        +    },
        +    "confirm": {
        +      "description": "omit for the price only",
        +      "type": "boolean"
        +    },
        +    "feedback": {
        +      "type": "string"
        +    },
        +    "id": {
        +      "type": "string"
        +    },
        +    "remember": {
        +      "description": "save the note too (default true)",
        +      "type": "boolean"
        +    }
        +  },
        +  "required": [
        +    "id"
        +  ],
        +  "type": "object"
        +}
    • Changedset_post_refill18 fields changed
      • removedInput schema / properties / assetCooldownDays
        Removed value: -{
        -  "description": "how long before a Library render may be posted again (default 30). It never repeats one inside this window — it queues fewer posts and says so.",
        -  "type": "number"
        -}
      • addedInput schema / properties / avoid
        Added value: +{
        +  "description": "THE BRIEF: never mention or show these — topics, claims, competitors, faces.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / channelPostsPerDay
        Added value: +{
        +  "additionalProperties": {
        +    "type": "number"
        +  },
        +  "description": "per channel, how many posts a day (the first N of the posting times), e.g. {\"x\":1,\"instagram\":3}. 0 turns a channel off; null resets it to every posting time. Merged into what is saved.",
        +  "propertyNames": {
        +    "type": "string"
        +  },
        +  "type": "object"
        +}
      • removedInput schema / properties / dryRun
        Removed value: -{
        -  "description": "true (the default) = plan and preview only, queue nothing. Set false ONLY after a human has read a preview.",
        -  "type": "boolean"
        -}
      • changedInput schema / properties / enabled / description
        Previous value: -"on/off. false PAUSES it: the recurring job is deleted and nothing new is queued. Already-queued posts are untouched."New value: +"on/off. true needs a connected channel and a goal (refused otherwise). false PAUSES it: the recurring job is deleted and nothing new is made. Already-queued posts are untouched."
      • addedInput schema / properties / forgetCaptionEdits
        Added value: +{
        +  "type": "boolean"
        +}
      • addedInput schema / properties / formats
        Added value: +{
        +  "description": "THE BRIEF: what to make — mix (images with a short video now and then, the default), video (short videos only), carousel (3 new images per post), image.",
        +  "enum": [
        +    "mix",
        +    "video",
        +    "carousel",
        +    "image"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / goal
        Added value: +{
        +  "description": "THE BRIEF: what the posts are for — grow followers, drive sales, promote a launch, or other (say what in goalNote). Required to switch it on.",
        +  "enum": [
        +    "followers",
        +    "sales",
        +    "launch",
        +    "other"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / goalNote
        Added value: +{
        +  "description": "THE BRIEF: a line about the goal (e.g. the launch date and what is launching).",
        +  "type": "string"
        +}
      • addedInput schema / properties / learnings
        Added value: +{
        +  "description": "notes learned from reviews; REPLACES the list",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • removedInput schema / properties / maxCreditsPerDay
        Removed value: -{
        -  "description": "a hard credit ceiling per day, checked BEFORE any render starts. It binds independently of the counts above.",
        -  "type": "number"
        -}
      • removedInput schema / properties / maxImagesPerDay
        Removed value: -{
        -  "description": "how many NEW images a day it may render when the Library runs dry. 0 (default) = none, spend nothing.",
        -  "type": "number"
        -}
      • removedInput schema / properties / maxVideosPerDay
        Removed value: -{
        -  "description": "how many NEW videos a day it may render. 0 (default) = none. Video is the expensive one — hundreds of credits each.",
        -  "type": "number"
        -}
      • addedInput schema / properties / mode
        Added value: +{
        +  "description": "\"review\" (default): each batch of fresh posts waits as drafts for approval. \"auto\": fresh posts are scheduled to publish automatically.",
        +  "enum": [
        +    "review",
        +    "auto"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / pillars
        Added value: +{
        +  "description": "THE BRIEF: content pillars / topics, up to 8 short lines. Every post belongs to one. get_post_refill suggests some from the brand profile.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / postingTimes
        Added value: +{
        +  "description": "the brand's daily posting times, 24-hour \"HH:MM\" (e.g. [\"09:00\",\"13:00\",\"18:00\"]); REPLACES the list. Autopilot makes one post per posting time, and scheduled posts use the same times. get_post_refill shows the current ones.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • changedInput schema / properties / postsPerDay / description
        Previous value: -"cap the posts per day BELOW the number of posting times. 0 (default) = use every posting time, which is where \"3 a day\" comes from. To post MORE per day, add posting times instead."New value: +"posts a day, up to the number of posting times (\"2 times a day\" = 2). 0 (default) = one per posting time. To post MORE per day than there are posting times, also pass postingTimes."
      • addedInput schema / properties / timezone
        Added value: +{
        +  "description": "IANA timezone the posting times are in, e.g. \"America/New_York\" (default: the brand's, else UTC)",
        +  "type": "string"
        +}
    • Changedupdate_brand3 fields changed
      • addedInput schema / properties / logo
        Added value: +{
        +  "type": "string"
        +}
      • addedInput schema / properties / logoDark
        Added value: +{
        +  "description": "for light pictures",
        +  "type": "string"
        +}
      • addedInput schema / properties / logoLight
        Added value: +{
        +  "description": "for dark pictures; \"none\" removes",
        +  "type": "string"
        +}
    • Changedupdate_doc5 fields changed
      • changedInput schema / properties / dropdowns / description
        Previous value: -"dropdown chips to set"New value: +"dropdown chips to set, or dropdowns to change"
      • addedInput schema / properties / dropdowns / items / properties / newTitle
        Added value: +{
        +  "description": "rename the dropdown",
        +  "type": "string"
        +}
      • addedInput schema / properties / dropdowns / items / properties / options
        Added value: +{
        +  "description": "change the dropdown itself: the COMPLETE new option list (2–50) by display text; existing options keep their colour; shared by every chip built from it",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / dropdowns / items / properties / replace
        Added value: +{
        +  "additionalProperties": {
        +    "type": "string"
        +  },
        +  "description": "for an option you remove that a chip shows: {\"Old\": \"New\"}",
        +  "propertyNames": {
        +    "type": "string"
        +  },
        +  "type": "object"
        +}
      • removedInput schema / properties / dropdowns / items / required
        Removed value: -[
        -  "value"
        -]
  13. 53 tool updatesv0.1.374
    • Changedbackfill_posts1 field changed
      • changedInput schema / properties / accountRef / description
        Previous value: -"which Page / account, when the brand has more than one"New value: +"which Page / account, when the profile has more than one"
    • Changedcancel_scheduled1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post is in — id or exact name from list_brands; needed when it is not the brand this connection is pinned to. A name that matches no brand, or two, is REFUSED."New value: +"WHICH PROFILE this post is in — id or exact name from list_brands; needed when it is not the profile this connection is pinned to. A name that matches no profile, or two, is REFUSED."
    • Changedclone_static1 field changed
      • changedInput schema / properties / brandId / description
        Previous value: -"a brand id/name from list_brands to clone for; omit to use the active brand"New value: +"a profile id/name from list_brands to clone for; omit to use the active profile"
    • Changedcollect_post_metrics1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a brand this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no brand, or two, is REFUSED and nothing is done."New value: +"WHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done."
    • Changedconnect_connector1 field changed
      • changedInput schema / properties / provider / description
        Previous value: -"the connector id: stripe, openai_ads, apple_ads, bluesky, telegram, bing_webmaster, posthog, mixpanel, amplitude, slack, discord, webhook"New value: +"the connector id: applovin_ads, stripe, openai_ads, apple_ads, bluesky, telegram, bing_webmaster, posthog, mixpanel, amplitude, slack, discord, webhook"
    • Changedcreate_brand2 fields changed
      • changedInput schema / properties / activate / description
        Previous value: -"switch this connection to the new brand (default true) — everything you do next scopes to it"New value: +"switch this connection to the new profile (default true); everything you do next scopes to it"
      • changedInput schema / properties / name / description
        Previous value: -"the brand / client name for the new workspace"New value: +"the name for the new profile (a brand, client or creator name)"
    • Changeddelete_brand2 fields changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id or exact name from list_brands"New value: +"profile id or exact name from list_brands"
      • changedInput schema / properties / confirm / description
        Previous value: -"REQUIRED true — this destroys the whole workspace and cannot be undone"New value: +"REQUIRED true — this destroys the whole profile and cannot be undone"
    • Changeddelete_creator1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id/name from list_brands, this call only"New value: +"profile id/name from list_brands, this call only"
    • Changeddelete_playbook1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id/name from list_brands, this call only"New value: +"profile id/name from list_brands, this call only"
    • Changeddelete_skill1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id/name from list_brands, this call only"New value: +"profile id/name from list_brands, this call only"
    • Changeddraft_brand1 field changed
      • changedInput schema / properties / save / description
        Previous value: -"save as the workspace’s brand (like Studio onboarding) so plan_ad/create use it automatically. Default: saves only when NO brand is saved yet; pass true to REPLACE the saved brand profile (the drafted fields overwrite the saved ones), false to never save"New value: +"save onto the active profile (like Studio onboarding) so plan_ad/create use it automatically. Default: saves only when NO brand is saved yet; pass true to REPLACE the saved profile (the drafted fields overwrite the saved ones), false to never save"
    • Changedduplicate_scheduled1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post is in — id or exact name from list_brands; needed when it is not the brand this connection is pinned to. A name that matches no brand, or two, is REFUSED."New value: +"WHICH PROFILE this post is in — id or exact name from list_brands; needed when it is not the profile this connection is pinned to. A name that matches no profile, or two, is REFUSED."
    • Changededit_video2 fields changed
      • addedInput schema / properties / engine
        Added value: +{
        +  "description": "default auto: Seedance 2.5 Edit without a real-looking person, else Kling O3 Edit",
        +  "enum": [
        +    "auto",
        +    "seedance",
        +    "kling"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / faceRoute
        Added value: +{
        +  "description": "'face_lane' = the user's \"I own the rights to this face\" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own.",
        +  "enum": [
        +    "face_lane"
        +  ],
        +  "type": "string"
        +}
    • Changedfetch_app_screens1 field changed
      • changedInput schema / properties / brandId / description
        Previous value: -"a brand id/name from list_brands to save the screens onto; omit to use the active brand"New value: +"a profile id/name from list_brands to save the screens onto; omit to use the active profile"
    • Changedforget1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id/name from list_brands, this call only"New value: +"profile id/name from list_brands, this call only"
    • Changedget_brand1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id/name from list_brands, this call only"New value: +"profile id/name from list_brands, this call only"
    • Changedlist_meta_posts2 fields changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a brand this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no brand, or two, is REFUSED and nothing is done."New value: +"WHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done."
      • changedInput schema / properties / pageId / description
        Previous value: -"which connected Page — omit when the brand has only one"New value: +"which connected Page — omit when the profile has only one"
    • Changedlist_product_photos1 field changed
      • changedInput schema / properties / brandId / description
        Previous value: -"a brand id/name from list_brands whose product library to list; omit to use the active brand"New value: +"a profile id/name from list_brands whose product library to list; omit to use the active profile"
    • Changedlist_published_posts1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a brand this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no brand, or two, is REFUSED and nothing is done."New value: +"WHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done."
    • Changedlist_scheduled1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND to list — the id or exact name from list_brands. Needed when the post lives in a brand this connection is not pinned to: a post you can CREATE in a brand must be manageable there too, without switching the whole connection. A name that matches no brand, or two, is REFUSED."New value: +"WHICH PROFILE to list — the id or exact name from list_brands. Needed when the post lives in a profile this connection is not pinned to: a post you can CREATE in a profile must be manageable there too, without switching the whole connection. A name that matches no profile, or two, is REFUSED."
    • Changedmine_angles1 field changed
      • changedInput schema / properties / brandId / description
        Previous value: -"a brand id/name from list_brands to mine for; omit to use the active brand"New value: +"a profile id/name from list_brands to mine for; omit to use the active profile"
    • Changedpost_edit4 fields changed
      • addedInput schema / properties / ops / items / properties / keepSound
        Added value: +{
        +  "type": "boolean"
        +}
      • changedInput schema / properties / ops / items / properties / op / enum
        Previous value: -[
        -  "trim",
        -  "speed",
        -  "mute",
        -  "audio_gain",
        -  "fade_out",
        -  "music",
        -  "append_card",
        -  "watermark",
        -  "grain",
        -  "text",
        -  "join"
        -]New value: +[
        +  "trim",
        +  "speed",
        +  "mute",
        +  "audio_gain",
        +  "fade_out",
        +  "music",
        +  "voiceover",
        +  "append_card",
        +  "watermark",
        +  "grain",
        +  "text",
        +  "join"
        +]
      • changedInput schema / properties / ops / items / properties / text / description
        Previous value: -"text: the words, verbatim"New value: +"text/voiceover: the words, verbatim"
      • addedInput schema / properties / ops / items / properties / voice
        Added value: +{
        +  "type": "string"
        +}
    • Changedpost_performance1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a brand this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no brand, or two, is REFUSED and nothing is done."New value: +"WHICH PROFILE the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a profile this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no profile, or two, is REFUSED and nothing is done."
    • Changedpost_to_bluesky2 fields changed
      • changedInput schema / properties / account / description
        Previous value: -"WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the brand has more than one bluesky account connected (several and none named is refused by name, never guessed); omit when there is one."New value: +"WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the profile has more than one bluesky account connected (several and none named is refused by name, never guessed); omit when there is one."
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."New value: +"WHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted."
    • Changedpost_to_google_business1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."New value: +"WHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted."
    • Changedpost_to_linkedin1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."New value: +"WHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted."
    • Changedpost_to_linkedin_page1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."New value: +"WHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted."
    • Changedpost_to_meta1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."New value: +"WHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted."
    • Changedpost_to_pinterest2 fields changed
      • changedInput schema / properties / account / description
        Previous value: -"WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the brand has more than one pinterest account connected (several and none named is refused by name, never guessed); omit when there is one."New value: +"WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the profile has more than one pinterest account connected (several and none named is refused by name, never guessed); omit when there is one."
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."New value: +"WHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted."
    • Changedpost_to_telegram2 fields changed
      • changedInput schema / properties / account / description
        Previous value: -"WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the brand has more than one telegram account connected (several and none named is refused by name, never guessed); omit when there is one."New value: +"WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the profile has more than one telegram account connected (several and none named is refused by name, never guessed); omit when there is one."
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."New value: +"WHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted."
    • Changedpost_to_tiktok2 fields changed
      • changedInput schema / properties / account / description
        Previous value: -"WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the brand has more than one tiktok account connected (several and none named is refused by name, never guessed); omit when there is one."New value: +"WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the profile has more than one tiktok account connected (several and none named is refused by name, never guessed); omit when there is one."
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."New value: +"WHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted."
    • Changedpost_to_x2 fields changed
      • changedInput schema / properties / account / description
        Previous value: -"WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the brand has more than one x account connected (several and none named is refused by name, never guessed); omit when there is one."New value: +"WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the profile has more than one x account connected (several and none named is refused by name, never guessed); omit when there is one."
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."New value: +"WHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted."
    • Changedpost_to_youtube2 fields changed
      • changedInput schema / properties / account / description
        Previous value: -"WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the brand has more than one youtube account connected (several and none named is refused by name, never guessed); omit when there is one."New value: +"WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the profile has more than one youtube account connected (several and none named is refused by name, never guessed); omit when there is one."
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."New value: +"WHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted."
    • Changedpost_x_article1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."New value: +"WHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted."
    • Changedrecast_motion9 fields changed
      • addedInput schema / properties / engine
        Added value: +{
        +  "description": "default auto",
        +  "enum": [
        +    "auto",
        +    "seedance",
        +    "wan",
        +    "kling"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / faceRoute
        Added value: +{
        +  "description": "'face_lane' = the user's \"I own the rights to this face\" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own.",
        +  "enum": [
        +    "face_lane"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / image / description
        Previous value: -"the actor/character image URL (who should appear). Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."New value: +"who performs it: the actor/character image URL (@Image1). Required on kling. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
      • addedInput schema / properties / images
        Added value: +{
        +  "description": "seedance/wan: more pictures, @Image2…: a character, product, outfit or place",
        +  "items": {
        +    "type": "string"
        +  },
        +  "maxItems": 8,
        +  "type": "array"
        +}
      • changedInput schema / properties / orientation / description
        Previous value: -"which aspect to keep: the video's (default) or the image's"New value: +"kling only: which aspect to keep: the video's (default) or the image's"
      • changedInput schema / properties / prompt / description
        Previous value: -"optional scene/style guidance"New value: +"optional, e.g. 'same moves, new location: Tokyo at night'"
      • addedInput schema / properties / resolution
        Added value: +{
        +  "description": "seedance and wan; default 1080p",
        +  "enum": [
        +    "480p",
        +    "720p",
        +    "1080p"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / tier / description
        Previous value: -"'pro' (default): 1080p and real hand-object interaction. 'standard': about 25% fewer credits and faster, but 720p, and it tends to mime a held object with empty hands. hermoso_capabilities lists the exact credits for both"New value: +"kling only: 'pro' (default): 1080p and real hand-object interaction. 'standard': about 25% fewer credits and faster, but 720p, and it tends to mime a held object with empty hands. hermoso_capabilities lists the exact credits for both"
      • changedInput schema / required
        Previous value: -[
        -  "image",
        -  "video"
        -]New value: +[
        +  "video"
        +]
    • Changedremember1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id/name from list_brands, this call only"New value: +"profile id/name from list_brands, this call only"
    • Changedremix_static1 field changed
      • changedInput schema / properties / brandId / description
        Previous value: -"a brand id/name from list_brands to clone for; omit to use the active brand"New value: +"a profile id/name from list_brands to clone for; omit to use the active profile"
    • Changedreschedule_post1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post is in — id or exact name from list_brands; needed when it is not the brand this connection is pinned to. A name that matches no brand, or two, is REFUSED."New value: +"WHICH PROFILE this post is in — id or exact name from list_brands; needed when it is not the profile this connection is pinned to. A name that matches no profile, or two, is REFUSED."
    • Changedrestyle_video6 fields changed
      • addedInput schema / properties / characters
        Added value: +{
        +  "description": "characters to draw the people as. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "maxItems": 4,
        +  "type": "array"
        +}
      • addedInput schema / properties / engine
        Added value: +{
        +  "description": "default auto",
        +  "enum": [
        +    "auto",
        +    "seedance",
        +    "wan",
        +    "kling"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / faceRoute
        Added value: +{
        +  "description": "'face_lane' = the user's \"I own the rights to this face\" for a real person in the source clip (paid plans). Only on the user's say-so, never on your own.",
        +  "enum": [
        +    "face_lane"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / resolution
        Added value: +{
        +  "description": "Seedance and Wan; default 1080p",
        +  "enum": [
        +    "480p",
        +    "720p",
        +    "1080p"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / style / description
        Previous value: -"a look id from hermoso_capabilities (e.g. claymation, vhs-80s, film-noir)"New value: +"a look id from hermoso_capabilities (e.g. claymation, knitted-yarn, cel-shaded-anime-cg)"
      • addedInput schema / properties / styleImages
        Added value: +{
        +  "description": "pictures that ARE the look (their style, never their subject)",
        +  "items": {
        +    "type": "string"
        +  },
        +  "maxItems": 9,
        +  "type": "array"
        +}
    • Changedretry_scheduled1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post is in — id or exact name from list_brands; needed when it is not the brand this connection is pinned to. A name that matches no brand, or two, is REFUSED."New value: +"WHICH PROFILE this post is in — id or exact name from list_brands; needed when it is not the profile this connection is pinned to. A name that matches no profile, or two, is REFUSED."
    • Changedsave_creator1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id/name from list_brands, this call only"New value: +"profile id/name from list_brands, this call only"
    • Changedsave_playbook1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id/name from list_brands, this call only"New value: +"profile id/name from list_brands, this call only"
    • Changedsave_skill1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id/name from list_brands, this call only"New value: +"profile id/name from list_brands, this call only"
    • Changedsave_to_swipefile1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id/name from list_brands, this call only"New value: +"profile id/name from list_brands, this call only"
    • Changedschedule_post3 fields changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."New value: +"WHICH PROFILE this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no profile, or two, is REFUSED and nothing is posted."
      • changedInput schema / properties / pageId / description
        Previous value: -"FACEBOOK / INSTAGRAM / THREADS — which connected Facebook Page (and its linked Instagram) publishes, from list_meta_pages. Needed when the brand has more than one Page connected; with several and no id the post is refused at fire time rather than sent from the wrong brand."New value: +"FACEBOOK / INSTAGRAM / THREADS — which connected Facebook Page (and its linked Instagram) publishes, from list_meta_pages. Needed when the profile has more than one Page connected; with several and no id the post is refused at fire time rather than sent from the wrong brand."
      • changedInput schema / properties / useQueue / description
        Previous value: -"instead of naming a minute, drop this into the brand’s POSTING QUEUE: Hermoso takes the earliest of its saved posting times that is still free (skipping any slot another queued post already holds). This is the natural answer to “just queue it” / “post it at my next opening”. Mutually exclusive with `at` — passing both is refused rather than one being silently preferred. If the brand has no posting times set, or every slot for the next 90 days is taken, it is refused by name and nothing is scheduled."New value: +"instead of naming a minute, drop this into the brand’s POSTING QUEUE: Hermoso takes the earliest of its saved posting times that is still free (skipping any slot another queued post already holds). This is the natural answer to “just queue it” / “post it at my next opening”. Mutually exclusive with `at` — passing both is refused rather than one being silently preferred. If the profile has no posting times set, or every slot for the next 90 days is taken, it is refused by name and nothing is scheduled."
    • Changedset_connector_accounts1 field changed
      • changedInput schema / properties / accountIds / description
        Previous value: -"the ids (from list_connector_accounts) this brand may use — an empty array shares nothing"New value: +"the ids (from list_connector_accounts) this profile may use — an empty array shares nothing"
    • Changedset_post_refill1 field changed
      • changedInput schema / properties / linkedinOrganizationId / description
        Previous value: -"LINKEDIN — which company Page to post as (list_linkedin_pages). A single shared Page is used automatically; a Page that is not shared with this brand is ignored rather than failing the whole post."New value: +"LINKEDIN — which company Page to post as (list_linkedin_pages). A single shared Page is used automatically; a Page that is not shared with this profile is ignored rather than failing the whole post."
    • Changedset_product_image1 field changed
      • changedInput schema / properties / brandId / description
        Previous value: -"a brand id/name from list_brands to lock the product for; omit to use the active brand"New value: +"a profile id/name from list_brands to lock the product for; omit to use the active profile"
    • Changedtidy_memory1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id/name from list_brands, this call only"New value: +"profile id/name from list_brands, this call only"
    • Changedtiktok_post_status1 field changed
      • changedInput schema / properties / account / description
        Previous value: -"which connected TikTok account the post went out on (@handle or id from list_connector_accounts) when the brand has more than one"New value: +"which connected TikTok account the post went out on (@handle or id from list_connector_accounts) when the profile has more than one"
    • Changedupdate_brand1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id/name from list_brands, this call only"New value: +"profile id/name from list_brands, this call only"
    • Changedupdate_saved_creator1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id/name from list_brands, this call only"New value: +"profile id/name from list_brands, this call only"
    • Changeduse_brand1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"brand id (e.g. default / p_xxx), its exact name from list_brands, or the profile id of a workspace shared with you"New value: +"profile id (e.g. default / p_xxx), its exact name from list_brands, or the profile id of one shared with you"
  14. 6 tool updatesv0.1.372
    • Changedappend_to_doc3 fields changed
      • addedInput schema / properties / dropdown
        Added value: +{
        +  "description": "a dropdown chip to append after the text",
        +  "properties": {
        +    "options": {
        +      "description": "2 to 50 distinct options",
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    },
        +    "selected": {
        +      "description": "the option it starts on (default: the first)",
        +      "type": "string"
        +    },
        +    "title": {
        +      "description": "the dropdown title, e.g. \"Status\"",
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "options"
        +  ],
        +  "type": "object"
        +}
      • changedInput schema / properties / text / description
        Previous value: -"text to append at the end of the doc"New value: +"text to append at the end of the doc (optional when a dropdown is passed)"
      • changedInput schema / required
        Previous value: -[
        -  "documentId",
        -  "text"
        -]New value: +[
        +  "documentId"
        +]
    • Changedpost_to_meta3 fields changed
      • addedInput schema / properties / instagramCommentPrompt
        Added value: +{
        +  "description": "INSTAGRAM — make the caption a COMMENT PROMPT that people answer in the comments. Same limits as instagramPoll, and never together with it.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / instagramPoll
        Added value: +{
        +  "description": "INSTAGRAM — attach a POLL: 2 to 4 answers of 1–25 characters each, and the caption IS the question (so a caption is required). Feed photos, carousels and Reels only (never a story), one caption add-on per post (not with instagramCommentPrompt), and only on an account connected through Meta (a Facebook Page with a linked Instagram). WRITE-ONCE: Instagram cannot add, change or remove it after publishing, so show the user the answers first.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / instagramPollExtended
        Added value: +{
        +  "description": "INSTAGRAM — give that poll Instagram’s extended voting duration instead of its default 3 days (Instagram’s changelog says it then stays open indefinitely). Only with instagramPoll.",
        +  "type": "boolean"
        +}
    • Changedreschedule_post3 fields changed
      • addedInput schema / properties / instagramCommentPrompt
        Added value: +{
        +  "description": "INSTAGRAM — make the caption a COMMENT PROMPT that people answer in the comments. Same limits as instagramPoll, and never together with it.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / instagramPoll
        Added value: +{
        +  "description": "INSTAGRAM — attach a POLL: 2 to 4 answers of 1–25 characters each, and the caption IS the question (so a caption is required). Feed photos, carousels and Reels only (never a story), one caption add-on per post (not with instagramCommentPrompt), and only on an account connected through Meta (a Facebook Page with a linked Instagram). WRITE-ONCE: Instagram cannot add, change or remove it after publishing, so show the user the answers first.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / instagramPollExtended
        Added value: +{
        +  "description": "INSTAGRAM — give that poll Instagram’s extended voting duration instead of its default 3 days (Instagram’s changelog says it then stays open indefinitely). Only with instagramPoll.",
        +  "type": "boolean"
        +}
    • Addedrestyle_video
    • Changedschedule_post3 fields changed
      • addedInput schema / properties / instagramCommentPrompt
        Added value: +{
        +  "description": "INSTAGRAM — make the caption a COMMENT PROMPT that people answer in the comments. Same limits as instagramPoll, and never together with it.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / instagramPoll
        Added value: +{
        +  "description": "INSTAGRAM — attach a POLL: 2 to 4 answers of 1–25 characters each, and the caption IS the question (so a caption is required). Feed photos, carousels and Reels only (never a story), one caption add-on per post (not with instagramCommentPrompt), and only on an account connected through Meta (a Facebook Page with a linked Instagram). WRITE-ONCE: Instagram cannot add, change or remove it after publishing, so show the user the answers first.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / instagramPollExtended
        Added value: +{
        +  "description": "INSTAGRAM — give that poll Instagram’s extended voting duration instead of its default 3 days (Instagram’s changelog says it then stays open indefinitely). Only with instagramPoll.",
        +  "type": "boolean"
        +}
    • Changedupdate_doc1 field changed
      • addedInput schema / properties / dropdowns
        Added value: +{
        +  "description": "dropdown chips to set",
        +  "items": {
        +    "properties": {
        +      "dropdownId": {
        +        "description": "when two dropdowns share a title",
        +        "type": "string"
        +      },
        +      "tabId": {
        +        "type": "string"
        +      },
        +      "title": {
        +        "description": "the dropdown title (from read_doc)",
        +        "type": "string"
        +      },
        +      "value": {
        +        "description": "the option to select, by its display text",
        +        "type": "string"
        +      }
        +    },
        +    "required": [
        +      "value"
        +    ],
        +    "type": "object"
        +  },
        +  "type": "array"
        +}
  15. 17 tool updatesv0.1.371
    • Changeddelete_creator1 field changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "brand id/name from list_brands, this call only",
        +  "type": "string"
        +}
    • Changeddelete_playbook1 field changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "brand id/name from list_brands, this call only",
        +  "type": "string"
        +}
    • Changeddelete_skill1 field changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "brand id/name from list_brands, this call only",
        +  "type": "string"
        +}
    • Changedforget1 field changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "brand id/name from list_brands, this call only",
        +  "type": "string"
        +}
    • Changedget_brand1 field changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "brand id/name from list_brands, this call only",
        +  "type": "string"
        +}
    • Changedpost_to_meta1 field changed
      • addedInput schema / additionalProperties
        Added value: +{}
    • Changedremember1 field changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "brand id/name from list_brands, this call only",
        +  "type": "string"
        +}
    • Changedreschedule_post1 field changed
      • changedInput schema / properties / aiGenerated / description
        Previous value: -"INSTAGRAM / FACEBOOK REEL — Meta’s is_ai_generated self-disclosure. OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, media that came through upload_file or from an external URL (the user’s own photographs or footage) is NOT — a real photo must never carry Instagram’s “AI info” label. Pass true or false only to override: false strips the label from something Hermoso would otherwise declare, true declares a render the user uploaded themselves."New value: +"AI-CONTENT DISCLOSURE (Instagram / Facebook Reel is_ai_generated, TikTok is_aigc, YouTube containsSyntheticMedia). OMIT IT and the value already on the post stays; a post that never had one is decided from provenance at publish: a Hermoso render is declared AI-generated, media that came through upload_file or from an external URL (the user’s own photos or footage) is NOT. Pass true or false only to override."
    • Changedsave_creator1 field changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "brand id/name from list_brands, this call only",
        +  "type": "string"
        +}
    • Changedsave_playbook1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"which brand this is for (defaults to the workspace brand)"New value: +"brand id/name from list_brands, this call only"
    • Changedsave_skill1 field changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "brand id/name from list_brands, this call only",
        +  "type": "string"
        +}
    • Changedsave_to_swipefile1 field changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "brand id/name from list_brands, this call only",
        +  "type": "string"
        +}
    • Changedschedule_post1 field changed
      • changedInput schema / properties / aiGenerated / description
        Previous value: -"INSTAGRAM / FACEBOOK REEL — Meta’s is_ai_generated self-disclosure. OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, media that came through upload_file or from an external URL (the user’s own photographs or footage) is NOT — a real photo must never carry Instagram’s “AI info” label. Pass true or false only to override: false strips the label from something Hermoso would otherwise declare, true declares a render the user uploaded themselves."New value: +"AI-CONTENT DISCLOSURE (Instagram / Facebook Reel is_ai_generated, TikTok is_aigc, YouTube containsSyntheticMedia). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared AI-generated, media that came through upload_file or from an external URL (the user’s own photos or footage) is NOT. Pass true or false only to override."
    • Changedtidy_memory1 field changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "brand id/name from list_brands, this call only",
        +  "type": "string"
        +}
    • Addedtiktok_post_status
    • Changedupdate_brand1 field changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "brand id/name from list_brands, this call only",
        +  "type": "string"
        +}
    • Changedupdate_saved_creator1 field changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "brand id/name from list_brands, this call only",
        +  "type": "string"
        +}
  16. 18 tool updatesv0.1.366
    • Changedclone_static1 field changed
      • changedInput schema / properties / imageUrl / description
        Previous value: -"the URL of the static ad image to clone; a real person in it confirms their likeness consent"New value: +"the URL of the static ad image to clone. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
    • Changeddraft_brand1 field changed
      • changedInput schema / properties / save / description
        Previous value: -"save as the workspace’s brand (like Studio onboarding) so plan_ad/create use it automatically. Default: saves only when NO brand is saved yet; pass true to overwrite, false to never save"New value: +"save as the workspace’s brand (like Studio onboarding) so plan_ad/create use it automatically. Default: saves only when NO brand is saved yet; pass true to REPLACE the saved brand profile (the drafted fields overwrite the saved ones), false to never save"
    • Changededit_image1 field changed
      • addedInput schema / properties / fixLabel
        Added value: +{
        +  "description": "false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product)",
        +  "type": "boolean"
        +}
    • Changedgenerate_avatar8 fields changed
      • addedInput schema / properties / acceptQueue
        Added value: +{
        +  "description": "only for an engine hermoso_capabilities marks oneAtATime: wait in line",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "description": "return the credits this exact job would hold, without rendering",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / engine
        Added value: +{
        +  "description": "leave out for the standard engine; only an engine listed in hermoso_capabilities avatarEngines is accepted",
        +  "type": "string"
        +}
      • changedInput schema / properties / image / description
        Previous value: -"local path or URL of the presenter portrait; a real person’s photo confirms their likeness consent"New value: +"local path or URL of the presenter portrait. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
      • addedInput schema / properties / prompt
        Added value: +{
        +  "description": "motion direction, only for an engine other than standard that hermoso_capabilities lists",
        +  "type": "string"
        +}
      • changedInput schema / properties / resolution / description
        Previous value: -"'1080p' (default) or '480p'/'720p' draft"New value: +"'720p' (default) or '480p'"
      • addedInput schema / properties / seed
        Added value: +{
        +  "description": "fixed seed, only for an engine other than standard",
        +  "maximum": 9007199254740991,
        +  "minimum": -9007199254740991,
        +  "type": "integer"
        +}
      • changedInput schema / properties / voice / description
        Previous value: -"voice name (Rachel/Sarah/George/Adam)"New value: +"voice name (Aria / Sarah / George / Adam). Leave it out and the voice matches the person in `image`; if nobody can be read, the call is refused free asking for one"
    • Changedgenerate_image2 fields changed
      • addedInput schema / properties / fixLabel
        Added value: +{
        +  "description": "default true: when the saved brand's product photo rides in this render, the product's label on the finished image is READ and compared with the photo, and re-printed from the photo at close range ONLY if it came out wrong (a label that is already right costs only the check, a credit or two; a re-print adds about ten). The reply says whether the label was checked, fixed or left as rendered (`labelPass`). Pass false when the user wants the packaging left exactly as generated: nothing is checked or re-printed.",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / refImages / description
        Previous value: -"local file paths or URLs of product/logo references to composite in. ON THE POSE MODELS — any row hermoso_capabilities marks `needsRefs` with a `refsMax`, such as putting your product in someone’s hands or a virtual try-on — THE ORDER IS THE CONTRACT AND IT IS NOT A COMPOSITE: refImages[0] is the PERSON photo, and the rest (up to `refsMax` minus one) are the product or garment photos. Reversed, you get the product wearing the person. A 4th product is dropped and the reply says so. A real person’s photo confirms their likeness consent."New value: +"local file paths or URLs of product/logo references to composite in. ON THE POSE MODELS — any row hermoso_capabilities marks `needsRefs` with a `refsMax`, such as putting your product in someone’s hands or a virtual try-on — THE ORDER IS THE CONTRACT AND IT IS NOT A COMPOSITE: refImages[0] is the PERSON photo, and the rest (up to `refsMax` minus one) are the product or garment photos. Reversed, you get the product wearing the person. A 4th product is dropped and the reply says so. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
    • Changedgenerate_video5 fields changed
      • addedInput schema / properties / angles
        Added value: +{
        +  "description": "OPTIONAL, OFF BY DEFAULT: with `creator`, also send the extra views that creator already has saved (their pose plates, up to 2) beside their portrait. A test showed no visible improvement over the portrait alone, and each extra view adds about 25 s before the render starts, so leave it off unless asked. Views are skipped when the face library is busy, and the render always goes ahead on the portrait.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / creator
        Added value: +{
        +  "description": "STAR A SAVED CREATOR in this clip — their id from list_creators, or the name you know them by, or a PRESET AI creator from list_creators presets (exact name or id; free, no generation). Their saved portrait rides first among the references as the on-camera person, with their saved consent, exactly as render_ad casts them; a real person saved from a photo keeps their real face on camera. An unknown name is refused by name, nothing charged.",
        +  "type": "string"
        +}
      • addedInput schema / properties / faceRoute
        Added value: +{
        +  "description": "ONLY after a render came back saying the video model's safety check flagged a person's face: 'face_lane' is the user's choice \"I own the rights to this face\". Send the SAME request again with it and the same face is rendered on Seedance through the face library, at the normal price. Sending it is the user's confirmation that they have the rights to that face (paid plans, like every real face). Never set it on your own; the other choices that refusal names are a different model (`model`) or another creator (`creator`).",
        +  "enum": [
        +    "face_lane"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / refImage / description
        Previous value: -"local path or URL to anchor the first frame; a real person’s photo here or in refImages confirms their likeness consent"New value: +"local path or URL to anchor the first frame. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
      • addedInput schema / properties / speak
        Added value: +{
        +  "description": "A PERSON SAYING THESE EXACT WORDS TO CAMERA — the default for any \"make my photo talk\" / spokesperson / talk-to-camera ask. Pass with `creator` or a portrait as `refImage`: the video model films them saying it in their own voice, matched to who they are, and the length follows the words (leave durationSeconds out). `prompt` is then the staging (e.g. \"natural\", \"walking in a park\"). Real footage, not an animated photo; generate_avatar is the animated-photo look, only when asked for by name",
        +  "type": "string"
        +}
    • Changedheadline_variants1 field changed
      • addedInput schema / properties / fixLabel
        Added value: +{
        +  "description": "false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product)",
        +  "type": "boolean"
        +}
    • Changedlist_creators1 field changed
      • addedInput schema / properties / gender
        Added value: +{
        +  "description": "filter the PRESET creators by gender (the saved cast is never filtered)",
        +  "enum": [
        +    "female",
        +    "male"
        +  ],
        +  "type": "string"
        +}
    • Changedlocalize_ad1 field changed
      • addedInput schema / properties / fixLabel
        Added value: +{
        +  "description": "false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product)",
        +  "type": "boolean"
        +}
    • Changedmake_explainer14 fields changed
      • changedInput schema / properties / aspectRatio / description
        Previous value: -"'9:16' default"New value: +"'9:16' default (a faceless channel video and a host episode default to 16:9)"
      • addedInput schema / properties / cameras
        Added value: +{
        +  "description": "host_episode: the rotation, ids frontal / three_quarter / close or framings in words; one entry = one fixed camera",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / creator
        Added value: +{
        +  "description": "host_episode: the host, a saved creator or a preset by name or id (list_creators)",
        +  "type": "string"
        +}
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "description": "true = return the exact credits this explainer reserves (its own pricing, stopped at the hold) and render nothing. Quote it before running one; try frameDensity lean or minimal when the balance is short.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / format
        Added value: +{
        +  "description": "blocks lane: 'explainer' (default; Gemini Omni 720p, 16:9 or 9:16, runs exactly the length asked) or 'faceless_channel' (a YouTube/TikTok faceless channel video; MiniMax H3 at 2K, 16:9 by default, five hard cuts per 10s block, a whole number of blocks). Omit and a history / kids / fairytale channel is a faceless channel video. 'host_episode' = the AI host episode (see the top).",
        +  "enum": [
        +    "explainer",
        +    "faceless_channel",
        +    "host_episode"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / frameDensity / description
        Previous value: -"how many pictures per second of narration, and therefore what it costs. 'standard' (default) is a frame about every 1.5s — the density a stills film needs to read as a film rather than a slideshow; 'lean' is one about every 2.5s (the longest hold that still reads as a film, ~40% of the frames and ~40% of the cost); 'minimal' is ONE picture per narration section, which is cheapest and is frankly a slideshow. Only drop below the default if the user asked for something cheaper."New value: +"how many pictures per second of narration, and therefore what it costs. 'standard' (default) is a frame about every 1.5s — the density a stills film needs to read as a film rather than a slideshow; 'lean' is one about every 2.5s (the longest hold that still reads as a film, ~40% of the frames and ~40% of the cost); 'minimal' is one picture about every 3.5s, the cheapest and the longest any still is ever held, and it reads close to a slideshow. Only drop below the default if the user asked for something cheaper."
      • addedInput schema / properties / fromDraft
        Added value: +{
        +  "description": "host_episode: a finished draft's job id, to render it in HD (720p) with the same script, cameras, set and host",
        +  "type": "string"
        +}
      • addedInput schema / properties / hostImage
        Added value: +{
        +  "description": "host_episode: a photo URL of the host instead (upload_file for a local file); a real person's face needs a paid plan",
        +  "type": "string"
        +}
      • addedInput schema / properties / lane
        Added value: +{
        +  "description": "'blocks' (default) = 10-second video blocks, real motion, one narrated line per block; 'stills' = the picture film (a still about every 1.5s, narrated, no video model; cheaper). Quote either with dryRun.",
        +  "enum": [
        +    "blocks",
        +    "stills"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / script
        Added value: +{
        +  "description": "host_episode: the exact words, said verbatim and split at natural breaks into 4-30s pieces",
        +  "type": "string"
        +}
      • addedInput schema / properties / setting
        Added value: +{
        +  "description": "host_episode: the set in words (default: written to fit the topic)",
        +  "type": "string"
        +}
      • addedInput schema / properties / thumbnail
        Added value: +{
        +  "description": "host_episode: one thumbnail of the host (default true)",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / topic / description
        Previous value: -"what the explainer should teach or explain — a topic or a short brief"New value: +"what the explainer should teach or explain — a topic or a short brief (host_episode: or pass `script`)"
      • removedInput schema / required
        Removed value: -[
        -  "topic"
        -]
    • Changedmultiply_ad1 field changed
      • addedInput schema / properties / change
        Added value: +{
        +  "description": "what should change, in the user's own words, e.g. 'older women, winter streets'; every variant applies it (omit to let the variants vary freely)",
        +  "type": "string"
        +}
    • Changedplan_ad2 fields changed
      • changedInput schema / properties / durationSeconds / description
        Previous value: -"VIDEO ONLY — the total spot length the user explicitly asked for, in seconds, copied verbatim (30 for \"a 30 second ad\"). The planner authors the storyboard TO it: the scenes’ seconds sum to it and the script is word-budgeted for it. Supported range 4–180; anything outside is CLAMPED to it (the reply says so). A length that fits ONE clip of the render model renders as a single continuous pass; anything longer is STITCHED from acts filled to that model’s clip maximum with the remainder last (on a 15s-clip model, 40 -> 15+15+10 and 17 -> 13+4) — never time-compressed. The maximum is the model’s own: 15s on most, 30s on the longest-clip model, so a 30s ad can be one unbroken take rather than two acts. Omit when the user named no length; do NOT pass a guess, an omitted value keeps the recipe-aware default."New value: +"VIDEO ONLY — the total spot length the user explicitly asked for, in seconds, copied verbatim (30 for \"a 30 second ad\"). The planner authors the storyboard TO it: the scenes’ seconds sum to it and the script is word-budgeted for it. Supported range 4–180; anything outside is CLAMPED to it (the reply says so). A length that fits ONE clip of the render model renders as a single continuous pass; anything longer is STITCHED from acts filled to that model’s clip maximum with the remainder last (on a 15s-clip model, 40 -> 15+15+10 and 17 -> 13+4) — never time-compressed. The maximum is the model’s own: 15s on most, 30s on the longest-clip model, so a 30s ad can be one unbroken take rather than two acts. Omit when the user named no length; do NOT pass a guess: an omitted value is the default, a 30s spot in one unbroken take (2026-09-29), and a length the user names always wins."
      • addedInput schema / properties / talent
        Added value: +{
        +  "description": "who is on camera; omit to follow the format. Say 'someone new' in product to skip reusing a saved creator.",
        +  "enum": [
        +    "auto",
        +    "creator",
        +    "product_only"
        +  ],
        +  "type": "string"
        +}
    • Changedpost_edit8 fields changed
      • changedInput schema / properties / ops / items / properties / db / description
        Previous value: -"audio_gain -20..+6 dB"New value: +"audio_gain -20..+6 dB / music: trim on its automatic level, -20..+12"
      • addedInput schema / properties / ops / items / properties / duck
        Added value: +{
        +  "anyOf": [
        +    {
        +      "const": "auto",
        +      "type": "string"
        +    },
        +    {
        +      "type": "boolean"
        +    }
        +  ],
        +  "description": "music: 'auto' default = dips while the clip's own sound plays"
        +}
      • addedInput schema / properties / ops / items / properties / fadeOut
        Added value: +{
        +  "description": "music: fade-out seconds 0-5",
        +  "type": "number"
        +}
      • addedInput schema / properties / ops / items / properties / from
        Added value: +{
        +  "description": "music: the second of the track to start from",
        +  "type": "number"
        +}
      • addedInput schema / properties / ops / items / properties / mood
        Added value: +{
        +  "description": "music: mood words for the library pick, or the brief for generated music",
        +  "type": "string"
        +}
      • changedInput schema / properties / ops / items / properties / op / enum
        Previous value: -[
        -  "trim",
        -  "speed",
        -  "mute",
        -  "audio_gain",
        -  "fade_out",
        -  "append_card",
        -  "watermark",
        -  "grain",
        -  "text",
        -  "join"
        -]New value: +[
        +  "trim",
        +  "speed",
        +  "mute",
        +  "audio_gain",
        +  "fade_out",
        +  "music",
        +  "append_card",
        +  "watermark",
        +  "grain",
        +  "text",
        +  "join"
        +]
      • addedInput schema / properties / ops / items / properties / replace
        Added value: +{
        +  "description": "music: true = replaces the clip's own sound",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / ops / items / properties / track
        Added value: +{
        +  "description": "music: omit = a library track by mood; a library track name; an audio link; 'generate' (paid, only on the user's ask)",
        +  "type": "string"
        +}
    • Changedrecast_motion1 field changed
      • changedInput schema / properties / image / description
        Previous value: -"the actor/character image URL (who should appear); a real person’s photo confirms their likeness consent"New value: +"the actor/character image URL (who should appear). Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
    • Changedremix_static1 field changed
      • changedInput schema / properties / imageUrl / description
        Previous value: -"the URL of the static ad image to clone; a real person in it confirms their likeness consent"New value: +"the URL of the static ad image to clone. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
    • Changedrender_ad7 fields changed
      • addedInput schema / properties / board
        Added value: +{
        +  "description": "paint the storyboard board first (default: on for UGC and cinematic spots, off for a product-only commercial); false = render from the text shot list",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / creator / description
        Previous value: -"CAST A SAVED CREATOR in this ad — their id from list_creators, or the name you know them by (“Sarah”). Their saved portrait becomes the on-camera identity for the whole spot, so the same face carries across every act and across every ad you render for this brand — and because we already have their picture, the character portrait this pipeline would otherwise generate is skipped, so casting somebody costs LESS than not casting them. Omit to let the ad cast a fresh person — EXCEPT for a CREATOR account (onboarded from their own @handle): their own saved likeness is cast by default on any plan with a person on camera, and the read-back says `default:true`; pass \"none\" to render without them. Refused for free, with nothing rendered, if the name matches nobody or more than one creator, or if an explicitly named creator is cast on a plan with nobody on camera. Casting a REAL person confirms their likeness consent."New value: +"CAST A SAVED CREATOR in this ad — their id from list_creators, or the name you know them by (“Sarah”), or a PRESET AI creator from list_creators presets by exact name or id (free, no generation). Their saved portrait becomes the on-camera identity for the whole spot, so the same face carries across every act and across every ad you render for this brand — and because we already have their picture, the character portrait this pipeline would otherwise generate is skipped, so casting somebody costs LESS than not casting them. Omit to let the ad cast a fresh person — EXCEPT for a CREATOR account (onboarded from their own @handle): their own saved likeness is cast by default when the plan has a person on camera and the account is on a paid Hermoso plan (a free account gets a fresh AI person), and the read-back says `default:true`; pass \"none\" to render without them. Refused for free, with nothing rendered, if the name matches nobody or more than one creator, if an explicitly named creator is cast on a plan with nobody on camera, or if a REAL person is cast on a free plan. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded."
      • addedInput schema / properties / faceRoute
        Added value: +{
        +  "description": "ONLY after a render came back saying the video model's safety check flagged a person's face: 'face_lane' is the user's choice \"I own the rights to this face\". Send the SAME request again with it and the same face is rendered on Seedance through the face library, at the normal price. Sending it is the user's confirmation that they have the rights to that face (paid plans, like every real face). Never set it on your own; the other choices that refusal names are a different model (`model`) or another creator (`creator`).",
        +  "enum": [
        +    "face_lane"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / foundationImages
        Added value: +{
        +  "description": "images a foundations:'choose' call returned, reused as-is (not repainted)",
        +  "properties": {
        +    "hero": {
        +      "type": "string"
        +    },
        +    "identitySheet": {
        +      "type": "string"
        +    },
        +    "location": {
        +      "type": "string"
        +    },
        +    "moodboard": {
        +      "type": "string"
        +    }
        +  },
        +  "type": "object"
        +}
      • addedInput schema / properties / foundations
        Added value: +{
        +  "description": "product commercial / cinematic: 'choose' paints ONLY the foundations (identity sheet + four-up moodboard, or hero + location) and returns them, no video — then call again with moodboardPick + foundationImages",
        +  "enum": [
        +    "choose"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / moodboardPick
        Added value: +{
        +  "description": "which moodboard panel (1-4, left to right, top to bottom) the storyboard follows; default 1",
        +  "maximum": 4,
        +  "minimum": 1,
        +  "type": "integer"
        +}
      • changedInput schema / properties / music / description
        Previous value: -"music bed: on (default), false, or the bed described in words ('lo-fi jazz, brushed drums')"New value: +"music bed: OFF unless asked. true = the plan's own music line; or the bed in words ('lo-fi jazz, brushed drums'); false = none"
    • Changedresize_ad1 field changed
      • addedInput schema / properties / fixLabel
        Added value: +{
        +  "description": "false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand’s saved product)",
        +  "type": "boolean"
        +}
    • Changedupdate_brand2 fields changed
      • addedInput schema / properties / pronounce
        Added value: +{
        +  "description": "how the brand NAME is said aloud, as a simple respelling with the stressed syllable in capitals (e.g. \"KOH-dee-ak\"). Videos use it as a delivery note beside the spoken line; set it when a render mispronounced the name.",
        +  "type": "string"
        +}
      • addedInput schema / properties / pronunciations
        Added value: +{
        +  "additionalProperties": {
        +    "type": "string"
        +  },
        +  "description": "how PRODUCT names are said aloud, e.g. {\"Power Cakes\": \"POW-er cakes\"}. Merged into the saved ones; an empty string removes one.",
        +  "propertyNames": {
        +    "type": "string"
        +  },
        +  "type": "object"
        +}
  17. 29 tool updatesv0.1.320
    • Changedcancel_scheduled1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post is in — the id or exact name from list_brands. Needed when the post lives in a brand this connection is not pinned to: a post you can CREATE in a brand must be manageable there too, without switching the whole connection. A name that matches no brand, or two, is REFUSED."New value: +"WHICH BRAND this post is in — id or exact name from list_brands; needed when it is not the brand this connection is pinned to. A name that matches no brand, or two, is REFUSED."
    • Changedcollect_post_metrics1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND the post lives in — the id or exact name from list_brands (a workspace shared with you: its profile id). Needed when it was published in a brand this connection is not pinned to: a post you can CREATE in a brand is manageable there too, for THIS CALL ONLY, without switching the connection. A name that matches no brand, or two, is REFUSED and nothing is done."New value: +"WHICH BRAND the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a brand this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no brand, or two, is REFUSED and nothing is done."
    • Changedduplicate_scheduled1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post is in — the id or exact name from list_brands. Needed when the post lives in a brand this connection is not pinned to: a post you can CREATE in a brand must be manageable there too, without switching the whole connection. A name that matches no brand, or two, is REFUSED."New value: +"WHICH BRAND this post is in — id or exact name from list_brands; needed when it is not the brand this connection is pinned to. A name that matches no brand, or two, is REFUSED."
    • Changededit_image1 field changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "description": "true = return the exact credits this edit reserves and render nothing",
        +  "type": "boolean"
        +}
    • Changededit_video11 fields changed
      • changedInput schema / properties / elements / description
        Previous value: -"OPTIONAL identity/product grounding (≤4): a creator portrait or the real product photo, so the edit restores the REAL thing instead of re-inventing it. Describe each one in the instruction. Leave out for a plain restyle"New value: +"OPTIONAL identity/product grounding (≤4): a creator portrait or the real product photo, so the edit restores the REAL thing. Describe each one in the instruction"
      • changedInput schema / properties / instruction / description
        Previous value: -"the exact transformation to apply, in the user’s own words"New value: +"the change, in the user’s own words"
      • changedInput schema / properties / interactionId / description
        Previous value: -"OPTIONAL: the interactionId an earlier Gemini Omni render or edit returned. The edit then continues that clip on the SAME Omni model from its own stored context (identity-true, no re-upload, usually cheaper). If that edit cannot run, the clip is edited by the video editor instead and the reply says so."New value: +"OPTIONAL: the interactionId an earlier Gemini Omni render or edit returned; the edit continues that clip on the same Omni model. If it cannot run, the video editor edits it and the reply says so."
      • changedInput schema / properties / keepAudio / description
        Previous value: -"default true — keep the source clip’s audio track. Set false to return the edit silent"New value: +"default true: keep the source audio; false = silent"
      • addedInput schema / properties / lighting
        Added value: +{
        +  "description": "default auto: keep the source light unless the change needs new light (night, a lamp, fire). 'preserve' or 'relight' forces it",
        +  "enum": [
        +    "auto",
        +    "preserve",
        +    "relight"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / literal
        Added value: +{
        +  "description": "true = send the instruction exactly as written, with no preserve/lock wrapper",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / previewAt
        Added value: +{
        +  "description": "second to preview (default 0); resend with previewStill",
        +  "type": "number"
        +}
      • addedInput schema / properties / previewFirstFrame
        Added value: +{
        +  "description": "true = edit ONE still of the first frame first and quote the clip; the paid clip does not run",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / previewStill
        Added value: +{
        +  "description": "the still a previewFirstFrame call returned; the clip then matches the changed thing to it",
        +  "type": "string"
        +}
      • addedInput schema / properties / reference
        Added value: +{
        +  "description": "OPTIONAL image URL that anchors the MATERIAL of what changes (a fabric, a finish, a colour swatch, the real product). Only its surface is used, never its framing or light",
        +  "type": "string"
        +}
      • changedInput schema / properties / video / description
        Previous value: -"the source video URL (from a previous render, a job result, or list_library)"New value: +"the source video URL (a render, job result or list_library)"
    • Changedfix_beat1 field changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "description": "true = return the exact credits this fix reserves and render nothing",
        +  "type": "boolean"
        +}
    • Changedgenerate_image2 fields changed
      • changedInput schema / properties / prompt / description
        Previous value: -"the full image prompt — subject, composition, lighting, and any on-image ad text. ON A POSE MODEL (product-in-hand / try-on) THIS IS EXTRA DIRECTION AND IT IS OPTIONAL — leave it out and the pose is built for you. If you do write one, DESCRIBE THE POSE (\"she holds the bottle upright in her right hand at chest height, label to camera\"); do NOT phrase it as a swap (\"replace the mug with the bottle\"), which is REFUSED for free, because the product then comes out the size of whatever it replaced — a 30ml bottle rendered mug-sized in testing."New value: +"REQUIRED on every model EXCEPT the pose rows below. the full image prompt — subject, composition, lighting, and any on-image ad text. ON A POSE MODEL (product-in-hand / try-on) THIS IS EXTRA DIRECTION AND IT IS OPTIONAL — leave it out and the pose is built for you. If you do write one, DESCRIBE THE POSE (\"she holds the bottle upright in her right hand at chest height, label to camera\"); do NOT phrase it as a swap (\"replace the mug with the bottle\"), which is REFUSED for free, because the product then comes out the size of whatever it replaced — a 30ml bottle rendered mug-sized in testing."
      • removedInput schema / required
        Removed value: -[
        -  "prompt"
        -]
    • Changedlist_meta_posts1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND the post lives in — the id or exact name from list_brands (a workspace shared with you: its profile id). Needed when it was published in a brand this connection is not pinned to: a post you can CREATE in a brand is manageable there too, for THIS CALL ONLY, without switching the connection. A name that matches no brand, or two, is REFUSED and nothing is done."New value: +"WHICH BRAND the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a brand this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no brand, or two, is REFUSED and nothing is done."
    • Changedlist_published_posts1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND the post lives in — the id or exact name from list_brands (a workspace shared with you: its profile id). Needed when it was published in a brand this connection is not pinned to: a post you can CREATE in a brand is manageable there too, for THIS CALL ONLY, without switching the connection. A name that matches no brand, or two, is REFUSED and nothing is done."New value: +"WHICH BRAND the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a brand this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no brand, or two, is REFUSED and nothing is done."
    • Changedmake_template_ad1 field changed
      • changedInput schema / properties / config / description
        Previous value: -"MUST include config.template: 'custom' or a preset id above, plus its fields"New value: +"MUST include config.template: 'custom' or a preset id, plus its fields. PRESETS: 'slideshow' (IMAGES, TikTok photo mode / Reels 1080x1920, or size:'4:5' feed carousels; no branding): { slides:[{text, sub?, image?, blur?, background?, position?}] (2-35; words never rewritten), style? ('tiktok-classic'|'clean-minimal'|'note-style' or a look in words), textStyle?, video?:true (+ an MP4) }; 2 credits, +1 per slide past 5, +2 for the MP4. 'imessage-chat' (VIDEO ~15s): { thread:{contactName, messages:[{from:'them'|'me', text?, product?:{image,title,domain}}]}, theme?, endCard }. 'chatgpt-chat' (VIDEO): { question, answer (may **bold** the brand), productImage?, endCard }. 'apple-notes' (VIDEO): { title, lines[], theme?, endCard }. 'value-prop' (VIDEO ~17s): { hook ≤40ch, claims[3-5 ≤34ch], productImages[2-3], palette[], endCard }. 'static-mockup' (IMAGE): { style:'imessage'|'notes'|'card', size?:{w,h}, ...fields }. 'airdrop-carousel' (VIDEO): { brandName, products:[{image, title?}] (3-16), endCard }. 'app-ui-tour' (VIDEO): { hook?, appName, iconImage?, beats:[{screenImage, caption}] (2-6), endCard }. 'imessage-cascade' (VIDEO): { notifications:[{sender, text}] (4-8), backgroundImage?, endCard }. 'photo-grid' (VIDEO): { title?, photos:[{image, label?}] (4-9), endCard }. 'vignette' (VIDEO): { hook, lines[2-4 ≤40ch], heroImage, endCard }. 'kinetic-type' (VIDEO, own SFX): { phrases[3-6 ≤34ch], productImages?[≤4], endCard }. 'myth-vs-fact' (VIDEO with a real VOICEOVER, small extra charge): { pairs:[{myth ≤50ch, fact ≤60ch}] (2-4; [brackets] accent), endCard }, real truths only. 'carousel' (IMAGES, 5-10 branded 1080x1080): { cover:{hook?, title}, slides:[{headline, support?, stat?:{value, label}}] (3-8), cta:{headline, cta?, domain?}, productImage?, logo? }. endCard = { headline, cta, domain?, logo?, color? }; palette and fontStack optional."
    • Changedmake_thumbnail2 fields changed
      • changedInput schema / properties / emotion / description
        Previous value: -"the expression on the face (default 'shock') — a preset id or your own phrase"New value: +"the expression on the face (default 'shock') — shock · hype · fear · confusion · determination · smug · charisma · disgust · awe · rage · laugh, or your own phrase"
      • changedInput schema / properties / framework / description
        Previous value: -"concept framework id (default 'posed_portrait'), or your own concept in words"New value: +"concept framework id (default 'posed_portrait') — before_after · social_ui · three_step · screenshot · posed_portrait · posed_action · specific_day · graphical · landscape · map_aerial · product · adding_text · repetition · size_difference · news_clip · amplified_reality — or your own concept in words"
    • Changedpost_edit22 fields changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "description": "true = return the exact credits this edit reserves and run nothing",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / ops / items / properties / background / description
        Previous value: -"append_card: card background — hex or a color name ('red', 'navy'…); the user's stated color always wins over the brand palette"New value: +"append_card: hex or a colour name; the user's colour beats the brand palette"
      • addedInput schema / properties / ops / items / properties / bridge / properties / sound / description
        Added value: +"'auto' default, 'own', 'impact', 'whoosh', 'none', or an audio URL (find_sound)"
      • removedInput schema / properties / ops / items / properties / bridge / properties / sound / enum
        Removed value: -[
        -  "auto",
        -  "own",
        -  "impact",
        -  "whoosh",
        -  "none"
        -]
      • changedInput schema / properties / ops / items / properties / card_html / description
        Previous value: -"append_card: your OWN full-frame card design as inline-styled HTML ({{logo}} inserts the real brand logo) — use when the standard layout cannot honor the request"New value: +"append_card: your OWN full-frame card as inline-styled HTML ({{logo}} = the brand logo)"
      • addedInput schema / properties / ops / items / properties / clips / items / properties / match
        Added value: +{}
      • addedInput schema / properties / ops / items / properties / clips / items / properties / reframe
        Added value: +{}
      • changedInput schema / properties / ops / items / properties / corner / description
        Previous value: -"watermark corner (default br)"New value: +"watermark corner (default br), or x/y"
      • changedInput schema / properties / ops / items / properties / headline / description
        Previous value: -"append_card: big line (defaults to the brand name)"New value: +"append_card: big line (default: brand name)"
      • addedInput schema / properties / ops / items / properties / intensity / anyOf
        Added value: +[
        +  {
        +    "enum": [
        +      "default",
        +      "strong"
        +    ],
        +    "type": "string"
        +  },
        +  {
        +    "type": "number"
        +  }
        +]
      • changedInput schema / properties / ops / items / properties / intensity / description
        Previous value: -"grain look"New value: +"grain: or 0-1 (default 0.25)"
      • removedInput schema / properties / ops / items / properties / intensity / enum
        Removed value: -[
        -  "default",
        -  "strong"
        -]
      • removedInput schema / properties / ops / items / properties / intensity / type
        Removed value: -"string"
      • addedInput schema / properties / ops / items / properties / match
        Added value: +{
        +  "description": "join: seam match, 'auto' default | 'off' | {grade,level,grain,blur,strength}"
        +}
      • changedInput schema / properties / ops / items / properties / position / description
        Previous value: -"text: where"New value: +"text: where, or x/y"
      • addedInput schema / properties / ops / items / properties / reframe
        Added value: +{
        +  "description": "join: shot change at a same-framing stitch, 'auto' | 'off' | step 1.1-1.5"
        +}
      • changedInput schema / properties / ops / items / properties / seconds / description
        Previous value: -"fade_out 0.3-3s / append_card 2-5s / crossfade 0.2-1.5s"New value: +"fade_out 0.3-3s / append_card 2-5s / transition 0.2-1.5s"
      • changedInput schema / properties / ops / items / properties / sub / description
        Previous value: -"append_card: the pill line (defaults to the website) / text: a smaller second line"New value: +"append_card: pill line (default: website) / text: a second, smaller line"
      • changedInput schema / properties / ops / items / properties / transition / description
        Previous value: -"join"New value: +"join: 'cut' default, 'crossfade', or any ffmpeg xfade name (wipeleft…)"
      • removedInput schema / properties / ops / items / properties / transition / enum
        Removed value: -[
        -  "cut",
        -  "crossfade"
        -]
      • addedInput schema / properties / ops / items / properties / x
        Added value: +{
        +  "description": "text/watermark centre: 0-1 of frame, or px",
        +  "type": "number"
        +}
      • addedInput schema / properties / ops / items / properties / y
        Added value: +{
        +  "type": "number"
        +}
    • Changedpost_performance1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND the post lives in — the id or exact name from list_brands (a workspace shared with you: its profile id). Needed when it was published in a brand this connection is not pinned to: a post you can CREATE in a brand is manageable there too, for THIS CALL ONLY, without switching the connection. A name that matches no brand, or two, is REFUSED and nothing is done."New value: +"WHICH BRAND the post lives in — id or exact name from list_brands (a shared workspace: its profile id). Needed when it was published in a brand this connection is not pinned to; applies to THIS CALL ONLY. A name that matches no brand, or two, is REFUSED and nothing is done."
    • Changedpost_to_bluesky1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — the id or exact name from list_brands (a workspace shared with you: its profile id). Use it whenever the account has more than one brand and you are not certain which one this connection is pinned to: it beats the pin for THIS CALL ONLY and changes nothing about the connection. A name that matches no brand, or two brands, is REFUSED and nothing is posted — never resolved to the pin, which is the account you were guarding against."New value: +"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."
    • Changedpost_to_google_business3 fields changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — the id or exact name from list_brands (a workspace shared with you: its profile id). Use it whenever the account has more than one brand and you are not certain which one this connection is pinned to: it beats the pin for THIS CALL ONLY and changes nothing about the connection. A name that matches no brand, or two brands, is REFUSED and nothing is posted — never resolved to the pin, which is the account you were guarding against."New value: +"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."
      • changedInput schema / properties / hook / description
        Previous value: -"WHAT ANGLE THIS POST IS BUILT ON — the single most valuable field here, and the only moment it can ever be recorded. post_performance groups on it to answer \"which hooks work\", and it needs 5 posts sharing ONE hook before it will call anything a winner, so REUSE THE SAME WORDING across a campaign instead of rephrasing it every time. Best of all, pass a hook id from list_hooks (e.g. \"direct_callout\", \"mid_problem\", \"before_after\") — those fold onto a stable key however they are spelled, so a whole brand accumulates evidence on one row. Your own wording is fine too; it just only groups when you repeat it exactly. Omitting it means this post can never vote on which hook works."New value: +"WHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. \"direct_callout\", \"before_after\") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works."
      • changedInput schema / properties / subject / description
        Previous value: -"WHAT THIS POST IS ABOUT — the product, feature, offer or theme (e.g. \"winter coat\", \"free trial\", \"founder story\"). The second grouping axis in post_performance. Same rule as hook: reuse the exact wording so posts about one subject land in one group."New value: +"WHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. \"winter coat\", \"free trial\"). post_performance's second grouping axis: reuse the exact wording, as with hook."
    • Changedpost_to_linkedin4 fields changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — the id or exact name from list_brands (a workspace shared with you: its profile id). Use it whenever the account has more than one brand and you are not certain which one this connection is pinned to: it beats the pin for THIS CALL ONLY and changes nothing about the connection. A name that matches no brand, or two brands, is REFUSED and nothing is posted — never resolved to the pin, which is the account you were guarding against."New value: +"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."
      • changedInput schema / properties / hook / description
        Previous value: -"WHAT ANGLE THIS POST IS BUILT ON — the single most valuable field here, and the only moment it can ever be recorded. post_performance groups on it to answer \"which hooks work\", and it needs 5 posts sharing ONE hook before it will call anything a winner, so REUSE THE SAME WORDING across a campaign instead of rephrasing it every time. Best of all, pass a hook id from list_hooks (e.g. \"direct_callout\", \"mid_problem\", \"before_after\") — those fold onto a stable key however they are spelled, so a whole brand accumulates evidence on one row. Your own wording is fine too; it just only groups when you repeat it exactly. Omitting it means this post can never vote on which hook works."New value: +"WHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. \"direct_callout\", \"before_after\") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works."
      • changedInput schema / properties / idempotencyKey / description
        Previous value: -"SAFE RETRIES. Publishing can take minutes (a video upload, a carousel of ten slides) and a transport can time out while the post SUCCEEDS — retrying blind is how the same thing gets posted twice. Pass any stable string here and a repeat of the SAME publish returns the ORIGINAL post id instead of posting again (24h). You do not have to: an identical publish is auto-recognised for 10 minutes anyway. If a call times out or errors ambiguously, CALL AGAIN WITH THE SAME KEY — that is the safe move, and it will either report the original post or publish it for the first time. It never posts twice."New value: +"SAFE RETRIES — any stable string: a repeat of the SAME publish within 24h returns the ORIGINAL post id instead of posting again (an identical publish is also auto-recognised for 10 minutes). On a timeout or an ambiguous error, CALL AGAIN WITH THE SAME KEY: it reports the original post or publishes it once. It never posts twice."
      • changedInput schema / properties / subject / description
        Previous value: -"WHAT THIS POST IS ABOUT — the product, feature, offer or theme (e.g. \"winter coat\", \"free trial\", \"founder story\"). The second grouping axis in post_performance. Same rule as hook: reuse the exact wording so posts about one subject land in one group."New value: +"WHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. \"winter coat\", \"free trial\"). post_performance's second grouping axis: reuse the exact wording, as with hook."
    • Changedpost_to_linkedin_page5 fields changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — the id or exact name from list_brands (a workspace shared with you: its profile id). Use it whenever the account has more than one brand and you are not certain which one this connection is pinned to: it beats the pin for THIS CALL ONLY and changes nothing about the connection. A name that matches no brand, or two brands, is REFUSED and nothing is posted — never resolved to the pin, which is the account you were guarding against."New value: +"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."
      • addedInput schema / properties / coverAtMs
        Added value: +{
        +  "description": "THE VIDEO COVER of a videoUrl post, as ONE frame: milliseconds from the start (7000 = the frame at 7s). That frame is uploaded as LinkedIn’s video thumbnail (only possible while the video uploads). videoThumbnailUrl wins.",
        +  "type": "number"
        +}
      • changedInput schema / properties / hook / description
        Previous value: -"WHAT ANGLE THIS POST IS BUILT ON — the single most valuable field here, and the only moment it can ever be recorded. post_performance groups on it to answer \"which hooks work\", and it needs 5 posts sharing ONE hook before it will call anything a winner, so REUSE THE SAME WORDING across a campaign instead of rephrasing it every time. Best of all, pass a hook id from list_hooks (e.g. \"direct_callout\", \"mid_problem\", \"before_after\") — those fold onto a stable key however they are spelled, so a whole brand accumulates evidence on one row. Your own wording is fine too; it just only groups when you repeat it exactly. Omitting it means this post can never vote on which hook works."New value: +"WHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. \"direct_callout\", \"before_after\") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works."
      • changedInput schema / properties / idempotencyKey / description
        Previous value: -"SAFE RETRIES. Publishing can take minutes (a video upload, a carousel of ten slides) and a transport can time out while the post SUCCEEDS — retrying blind is how the same thing gets posted twice. Pass any stable string here and a repeat of the SAME publish returns the ORIGINAL post id instead of posting again (24h). You do not have to: an identical publish is auto-recognised for 10 minutes anyway. If a call times out or errors ambiguously, CALL AGAIN WITH THE SAME KEY — that is the safe move, and it will either report the original post or publish it for the first time. It never posts twice."New value: +"SAFE RETRIES — any stable string: a repeat of the SAME publish within 24h returns the ORIGINAL post id instead of posting again (an identical publish is also auto-recognised for 10 minutes). On a timeout or an ambiguous error, CALL AGAIN WITH THE SAME KEY: it reports the original post or publishes it once. It never posts twice."
      • changedInput schema / properties / subject / description
        Previous value: -"WHAT THIS POST IS ABOUT — the product, feature, offer or theme (e.g. \"winter coat\", \"free trial\", \"founder story\"). The second grouping axis in post_performance. Same rule as hook: reuse the exact wording so posts about one subject land in one group."New value: +"WHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. \"winter coat\", \"free trial\"). post_performance's second grouping axis: reuse the exact wording, as with hook."
    • Changedpost_to_meta8 fields changed
      • addedInput schema / properties / audioId
        Added value: +{
        +  "description": "INSTAGRAM REEL — put one of Instagram’s OWN licensed music tracks under the Reel: the `id` search_instagram_audio returns. Instagram mixes it in when the Reel is published; nothing is downloaded or re-rendered. REELS ONLY, and only on an Instagram account connected through Meta (a Facebook Page with a linked Instagram), which is Instagram’s own rule — the Instagram connector cannot take it and is refused by name.",
        +  "type": "string"
        +}
      • addedInput schema / properties / audioVolume
        Added value: +{
        +  "description": "INSTAGRAM REEL — how loud that track plays, 0–100 (Instagram’s default 100). Needs audioId.",
        +  "maximum": 100,
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — the id or exact name from list_brands (a workspace shared with you: its profile id). Use it whenever the account has more than one brand and you are not certain which one this connection is pinned to: it beats the pin for THIS CALL ONLY and changes nothing about the connection. A name that matches no brand, or two brands, is REFUSED and nothing is posted — never resolved to the pin, which is the account you were guarding against."New value: +"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."
      • addedInput schema / properties / coverAtMs
        Added value: +{
        +  "description": "THE VIDEO COVER for every target of this post, as ONE frame: milliseconds from the start (7000 = the frame at 7s). Instagram gets it as thumb_offset, Facebook as its uploaded cover (a Reel’s preferred thumbnail). Instagram’s own thumbOffset, or a Hermoso-hosted coverUrl, also becomes the Facebook cover.",
        +  "type": "number"
        +}
      • changedInput schema / properties / hook / description
        Previous value: -"WHAT ANGLE THIS POST IS BUILT ON — the single most valuable field here, and the only moment it can ever be recorded. post_performance groups on it to answer \"which hooks work\", and it needs 5 posts sharing ONE hook before it will call anything a winner, so REUSE THE SAME WORDING across a campaign instead of rephrasing it every time. Best of all, pass a hook id from list_hooks (e.g. \"direct_callout\", \"mid_problem\", \"before_after\") — those fold onto a stable key however they are spelled, so a whole brand accumulates evidence on one row. Your own wording is fine too; it just only groups when you repeat it exactly. Omitting it means this post can never vote on which hook works."New value: +"WHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. \"direct_callout\", \"before_after\") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works."
      • changedInput schema / properties / idempotencyKey / description
        Previous value: -"SAFE RETRIES. Publishing can take minutes (a video upload, a carousel of ten slides) and a transport can time out while the post SUCCEEDS — retrying blind is how the same thing gets posted twice. Pass any stable string here and a repeat of the SAME publish returns the ORIGINAL post id instead of posting again (24h). You do not have to: an identical publish is auto-recognised for 10 minutes anyway. If a call times out or errors ambiguously, CALL AGAIN WITH THE SAME KEY — that is the safe move, and it will either report the original post or publish it for the first time. It never posts twice."New value: +"SAFE RETRIES — any stable string: a repeat of the SAME publish within 24h returns the ORIGINAL post id instead of posting again (an identical publish is also auto-recognised for 10 minutes). On a timeout or an ambiguous error, CALL AGAIN WITH THE SAME KEY: it reports the original post or publishes it once. It never posts twice."
      • changedInput schema / properties / subject / description
        Previous value: -"WHAT THIS POST IS ABOUT — the product, feature, offer or theme (e.g. \"winter coat\", \"free trial\", \"founder story\"). The second grouping axis in post_performance. Same rule as hook: reuse the exact wording so posts about one subject land in one group."New value: +"WHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. \"winter coat\", \"free trial\"). post_performance's second grouping axis: reuse the exact wording, as with hook."
      • addedInput schema / properties / videoVolume
        Added value: +{
        +  "description": "INSTAGRAM REEL — how loud the video’s own sound plays under the track, 0–100 (Instagram’s default 100; 0 = the track alone). Needs audioId.",
        +  "maximum": 100,
        +  "minimum": 0,
        +  "type": "integer"
        +}
    • Changedpost_to_pinterest5 fields changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — the id or exact name from list_brands (a workspace shared with you: its profile id). Use it whenever the account has more than one brand and you are not certain which one this connection is pinned to: it beats the pin for THIS CALL ONLY and changes nothing about the connection. A name that matches no brand, or two brands, is REFUSED and nothing is posted — never resolved to the pin, which is the account you were guarding against."New value: +"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."
      • addedInput schema / properties / coverAtMs
        Added value: +{
        +  "description": "THE VIDEO COVER of a video Pin, as ONE frame: milliseconds from the start (7000 = the frame at 7s). That frame is cut and sent as the Pin cover image. coverImageUrl wins.",
        +  "type": "number"
        +}
      • changedInput schema / properties / hook / description
        Previous value: -"WHAT ANGLE THIS POST IS BUILT ON — the single most valuable field here, and the only moment it can ever be recorded. post_performance groups on it to answer \"which hooks work\", and it needs 5 posts sharing ONE hook before it will call anything a winner, so REUSE THE SAME WORDING across a campaign instead of rephrasing it every time. Best of all, pass a hook id from list_hooks (e.g. \"direct_callout\", \"mid_problem\", \"before_after\") — those fold onto a stable key however they are spelled, so a whole brand accumulates evidence on one row. Your own wording is fine too; it just only groups when you repeat it exactly. Omitting it means this post can never vote on which hook works."New value: +"WHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. \"direct_callout\", \"before_after\") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works."
      • changedInput schema / properties / idempotencyKey / description
        Previous value: -"SAFE RETRIES. Publishing can take minutes (a video upload, a carousel of ten slides) and a transport can time out while the post SUCCEEDS — retrying blind is how the same thing gets posted twice. Pass any stable string here and a repeat of the SAME publish returns the ORIGINAL post id instead of posting again (24h). You do not have to: an identical publish is auto-recognised for 10 minutes anyway. If a call times out or errors ambiguously, CALL AGAIN WITH THE SAME KEY — that is the safe move, and it will either report the original post or publish it for the first time. It never posts twice."New value: +"SAFE RETRIES — any stable string: a repeat of the SAME publish within 24h returns the ORIGINAL post id instead of posting again (an identical publish is also auto-recognised for 10 minutes). On a timeout or an ambiguous error, CALL AGAIN WITH THE SAME KEY: it reports the original post or publishes it once. It never posts twice."
      • changedInput schema / properties / subject / description
        Previous value: -"WHAT THIS POST IS ABOUT — the product, feature, offer or theme (e.g. \"winter coat\", \"free trial\", \"founder story\"). The second grouping axis in post_performance. Same rule as hook: reuse the exact wording so posts about one subject land in one group."New value: +"WHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. \"winter coat\", \"free trial\"). post_performance's second grouping axis: reuse the exact wording, as with hook."
    • Changedpost_to_telegram2 fields changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — the id or exact name from list_brands (a workspace shared with you: its profile id). Use it whenever the account has more than one brand and you are not certain which one this connection is pinned to: it beats the pin for THIS CALL ONLY and changes nothing about the connection. A name that matches no brand, or two brands, is REFUSED and nothing is posted — never resolved to the pin, which is the account you were guarding against."New value: +"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."
      • addedInput schema / properties / coverAtMs
        Added value: +{
        +  "description": "THE VIDEO COVER in the chat, as ONE frame: milliseconds from the start (7000 = the frame at 7s). Sent as Telegram’s cover image; beats platformCover.",
        +  "type": "number"
        +}
    • Changedpost_to_tiktok4 fields changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — the id or exact name from list_brands (a workspace shared with you: its profile id). Use it whenever the account has more than one brand and you are not certain which one this connection is pinned to: it beats the pin for THIS CALL ONLY and changes nothing about the connection. A name that matches no brand, or two brands, is REFUSED and nothing is posted — never resolved to the pin, which is the account you were guarding against."New value: +"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."
      • changedInput schema / properties / destination / description
        Previous value: -"\"post\" = live on the profile now (needs privacy + an explicit user yes); \"draft\" = to TikTok for the user to review and post themselves. Default \"draft\"."New value: +"\"post\" (default) = live on the profile now (needs the privacy the user chose + their explicit yes); \"draft\" = to their TikTok inbox for them to finish and publish in the app — only when they ask for a draft or want to add a TikTok sound, and only after telling them so."
      • changedInput schema / properties / hook / description
        Previous value: -"WHAT ANGLE THIS POST IS BUILT ON — the single most valuable field here, and the only moment it can ever be recorded. post_performance groups on it to answer \"which hooks work\", and it needs 5 posts sharing ONE hook before it will call anything a winner, so REUSE THE SAME WORDING across a campaign instead of rephrasing it every time. Best of all, pass a hook id from list_hooks (e.g. \"direct_callout\", \"mid_problem\", \"before_after\") — those fold onto a stable key however they are spelled, so a whole brand accumulates evidence on one row. Your own wording is fine too; it just only groups when you repeat it exactly. Omitting it means this post can never vote on which hook works."New value: +"WHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. \"direct_callout\", \"before_after\") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works."
      • changedInput schema / properties / subject / description
        Previous value: -"WHAT THIS POST IS ABOUT — the product, feature, offer or theme (e.g. \"winter coat\", \"free trial\", \"founder story\"). The second grouping axis in post_performance. Same rule as hook: reuse the exact wording so posts about one subject land in one group."New value: +"WHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. \"winter coat\", \"free trial\"). post_performance's second grouping axis: reuse the exact wording, as with hook."
    • Changedpost_to_x3 fields changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — the id or exact name from list_brands (a workspace shared with you: its profile id). Use it whenever the account has more than one brand and you are not certain which one this connection is pinned to: it beats the pin for THIS CALL ONLY and changes nothing about the connection. A name that matches no brand, or two brands, is REFUSED and nothing is posted — never resolved to the pin, which is the account you were guarding against."New value: +"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."
      • changedInput schema / properties / hook / description
        Previous value: -"WHAT ANGLE THIS POST IS BUILT ON — the single most valuable field here, and the only moment it can ever be recorded. post_performance groups on it to answer \"which hooks work\", and it needs 5 posts sharing ONE hook before it will call anything a winner, so REUSE THE SAME WORDING across a campaign instead of rephrasing it every time. Best of all, pass a hook id from list_hooks (e.g. \"direct_callout\", \"mid_problem\", \"before_after\") — those fold onto a stable key however they are spelled, so a whole brand accumulates evidence on one row. Your own wording is fine too; it just only groups when you repeat it exactly. Omitting it means this post can never vote on which hook works."New value: +"WHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. \"direct_callout\", \"before_after\") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works."
      • changedInput schema / properties / subject / description
        Previous value: -"WHAT THIS POST IS ABOUT — the product, feature, offer or theme (e.g. \"winter coat\", \"free trial\", \"founder story\"). The second grouping axis in post_performance. Same rule as hook: reuse the exact wording so posts about one subject land in one group."New value: +"WHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. \"winter coat\", \"free trial\"). post_performance's second grouping axis: reuse the exact wording, as with hook."
    • Changedpost_to_youtube5 fields changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — the id or exact name from list_brands (a workspace shared with you: its profile id). Use it whenever the account has more than one brand and you are not certain which one this connection is pinned to: it beats the pin for THIS CALL ONLY and changes nothing about the connection. A name that matches no brand, or two brands, is REFUSED and nothing is posted — never resolved to the pin, which is the account you were guarding against."New value: +"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."
      • addedInput schema / properties / coverAtMs
        Added value: +{
        +  "description": "THE VIDEO COVER (the custom thumbnail), as ONE frame: milliseconds from the start (7000 = the frame at 7s). That frame is cut from the upload and set with thumbnails.set. thumbnailUrl wins; this beats platformCover.",
        +  "type": "number"
        +}
      • changedInput schema / properties / hook / description
        Previous value: -"WHAT ANGLE THIS POST IS BUILT ON — the single most valuable field here, and the only moment it can ever be recorded. post_performance groups on it to answer \"which hooks work\", and it needs 5 posts sharing ONE hook before it will call anything a winner, so REUSE THE SAME WORDING across a campaign instead of rephrasing it every time. Best of all, pass a hook id from list_hooks (e.g. \"direct_callout\", \"mid_problem\", \"before_after\") — those fold onto a stable key however they are spelled, so a whole brand accumulates evidence on one row. Your own wording is fine too; it just only groups when you repeat it exactly. Omitting it means this post can never vote on which hook works."New value: +"WHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. \"direct_callout\", \"before_after\") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works."
      • changedInput schema / properties / privacy / description
        Previous value: -"default unlisted (anyone-with-link, ad-ready); public = live + searchable on the channel (confirm first); private = eyes-only (cannot run as an ad)"New value: +"default public (live + searchable on the channel); unlisted = link-only (the ad-ready setting, or to add YouTube music in Studio first — only when the user asks); private = eyes-only (cannot run as an ad)"
      • changedInput schema / properties / subject / description
        Previous value: -"WHAT THIS POST IS ABOUT — the product, feature, offer or theme (e.g. \"winter coat\", \"free trial\", \"founder story\"). The second grouping axis in post_performance. Same rule as hook: reuse the exact wording so posts about one subject land in one group."New value: +"WHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. \"winter coat\", \"free trial\"). post_performance's second grouping axis: reuse the exact wording, as with hook."
    • Changedpost_x_article3 fields changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — the id or exact name from list_brands (a workspace shared with you: its profile id). Use it whenever the account has more than one brand and you are not certain which one this connection is pinned to: it beats the pin for THIS CALL ONLY and changes nothing about the connection. A name that matches no brand, or two brands, is REFUSED and nothing is posted — never resolved to the pin, which is the account you were guarding against."New value: +"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."
      • changedInput schema / properties / hook / description
        Previous value: -"WHAT ANGLE THIS POST IS BUILT ON — the single most valuable field here, and the only moment it can ever be recorded. post_performance groups on it to answer \"which hooks work\", and it needs 5 posts sharing ONE hook before it will call anything a winner, so REUSE THE SAME WORDING across a campaign instead of rephrasing it every time. Best of all, pass a hook id from list_hooks (e.g. \"direct_callout\", \"mid_problem\", \"before_after\") — those fold onto a stable key however they are spelled, so a whole brand accumulates evidence on one row. Your own wording is fine too; it just only groups when you repeat it exactly. Omitting it means this post can never vote on which hook works."New value: +"WHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. \"direct_callout\", \"before_after\") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works."
      • changedInput schema / properties / subject / description
        Previous value: -"WHAT THIS POST IS ABOUT — the product, feature, offer or theme (e.g. \"winter coat\", \"free trial\", \"founder story\"). The second grouping axis in post_performance. Same rule as hook: reuse the exact wording so posts about one subject land in one group."New value: +"WHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. \"winter coat\", \"free trial\"). post_performance's second grouping axis: reuse the exact wording, as with hook."
    • Changedpull_competitor_ads7 fields changed
      • addedInput schema / properties / brand
        Added value: +{
        +  "description": "same as companyName",
        +  "type": "string"
        +}
      • addedInput schema / properties / company
        Added value: +{
        +  "description": "same as companyName",
        +  "type": "string"
        +}
      • changedInput schema / properties / companyName / description
        Previous value: -"the advertiser name"New value: +"the advertiser name, e.g. \"Liquid Death\" (company / brand / name are read as this too)"
      • changedInput schema / properties / domain / description
        Previous value: -"the advertiser domain"New value: +"the advertiser domain, e.g. liquiddeath.com — a full website URL works (url / website are read as this too). Pass companyName OR domain"
      • addedInput schema / properties / name
        Added value: +{
        +  "description": "same as companyName",
        +  "type": "string"
        +}
      • addedInput schema / properties / url
        Added value: +{
        +  "description": "same as domain",
        +  "type": "string"
        +}
      • addedInput schema / properties / website
        Added value: +{
        +  "description": "same as domain",
        +  "type": "string"
        +}
    • Changedrender_ad1 field changed
      • changedInput schema / properties / dryRun / description
        Previous value: -"return the routing decision (single pass vs stitched acts, resolved model + act lengths) WITHOUT submitting a render — free, nothing charged"New value: +"return the routing decision (single pass vs stitched acts, resolved model + act lengths) and the exact credits the real render reserves, WITHOUT submitting a render — free, nothing charged"
    • Changedreschedule_post6 fields changed
      • addedInput schema / properties / audioId
        Added value: +{
        +  "description": "INSTAGRAM REEL — put one of Instagram’s OWN licensed music tracks under the Reel: the `id` search_instagram_audio returns. Instagram mixes it in when the Reel is published; nothing is downloaded or re-rendered. REELS ONLY; an empty string removes it, and only on an Instagram account connected through Meta (a Facebook Page with a linked Instagram), which is Instagram’s own rule — the Instagram connector cannot take it and is refused by name.",
        +  "type": "string"
        +}
      • addedInput schema / properties / audioVolume
        Added value: +{
        +  "description": "INSTAGRAM REEL — how loud that track plays, 0–100 (Instagram’s default 100). Needs audioId.",
        +  "maximum": 100,
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post is in — the id or exact name from list_brands. Needed when the post lives in a brand this connection is not pinned to: a post you can CREATE in a brand must be manageable there too, without switching the whole connection. A name that matches no brand, or two, is REFUSED."New value: +"WHICH BRAND this post is in — id or exact name from list_brands; needed when it is not the brand this connection is pinned to. A name that matches no brand, or two, is REFUSED."
      • addedInput schema / properties / coverAtMs
        Added value: +{
        +  "description": "THE VIDEO COVER on every channel, as one frame in milliseconds (see schedule_post).",
        +  "type": "number"
        +}
      • addedInput schema / properties / coverImageUrl
        Added value: +{
        +  "description": "THE VIDEO COVER on every channel, as a Hermoso-hosted picture (see schedule_post).",
        +  "type": "string"
        +}
      • addedInput schema / properties / videoVolume
        Added value: +{
        +  "description": "INSTAGRAM REEL — how loud the video’s own sound plays under the track, 0–100 (Instagram’s default 100; 0 = the track alone). Needs audioId.",
        +  "maximum": 100,
        +  "minimum": 0,
        +  "type": "integer"
        +}
    • Changedretry_scheduled1 field changed
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post is in — the id or exact name from list_brands. Needed when the post lives in a brand this connection is not pinned to: a post you can CREATE in a brand must be manageable there too, without switching the whole connection. A name that matches no brand, or two, is REFUSED."New value: +"WHICH BRAND this post is in — id or exact name from list_brands; needed when it is not the brand this connection is pinned to. A name that matches no brand, or two, is REFUSED."
    • Changedschedule_post8 fields changed
      • addedInput schema / properties / audioId
        Added value: +{
        +  "description": "INSTAGRAM REEL — put one of Instagram’s OWN licensed music tracks under the Reel: the `id` search_instagram_audio returns. Instagram mixes it in when the Reel is published; nothing is downloaded or re-rendered. REELS ONLY, and only on an Instagram account connected through Meta (a Facebook Page with a linked Instagram), which is Instagram’s own rule — the Instagram connector cannot take it and is refused by name.",
        +  "type": "string"
        +}
      • addedInput schema / properties / audioVolume
        Added value: +{
        +  "description": "INSTAGRAM REEL — how loud that track plays, 0–100 (Instagram’s default 100). Needs audioId.",
        +  "maximum": 100,
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • changedInput schema / properties / brand / description
        Previous value: -"WHICH BRAND this post belongs to — the id or exact name from list_brands (a workspace shared with you: its profile id). Use it whenever the account has more than one brand and you are not certain which one this connection is pinned to: it beats the pin for THIS CALL ONLY and changes nothing about the connection. A name that matches no brand, or two brands, is REFUSED and nothing is posted — never resolved to the pin, which is the account you were guarding against."New value: +"WHICH BRAND this post belongs to — id or exact name from list_brands (a shared workspace: its profile id). Beats the connection's pin for THIS CALL ONLY; a name that matches no brand, or two, is REFUSED and nothing is posted."
      • addedInput schema / properties / coverAtMs
        Added value: +{
        +  "description": "THE VIDEO COVER on EVERY channel of this post, as ONE frame: milliseconds from the start (7000 = the frame at 7s). Instagram, TikTok, Facebook, LinkedIn Page, Pinterest, Telegram and YouTube all get that frame; X, Threads and Bluesky have no cover setting. A channel’s own field (thumbOffset, coverTimestampMs, coverUrl) wins there and otherwise counts as this.",
        +  "type": "number"
        +}
      • addedInput schema / properties / coverImageUrl
        Added value: +{
        +  "description": "THE VIDEO COVER as a picture instead of a frame — a Hermoso-hosted image (upload_file). Every channel that takes a cover image gets it (Instagram, Facebook, LinkedIn Page, Pinterest, Telegram, YouTube); TikTok takes only a frame. Never together with coverAtMs.",
        +  "type": "string"
        +}
      • changedInput schema / properties / hook / description
        Previous value: -"WHAT ANGLE THIS POST IS BUILT ON — the single most valuable field here, and the only moment it can ever be recorded. post_performance groups on it to answer \"which hooks work\", and it needs 5 posts sharing ONE hook before it will call anything a winner, so REUSE THE SAME WORDING across a campaign instead of rephrasing it every time. Best of all, pass a hook id from list_hooks (e.g. \"direct_callout\", \"mid_problem\", \"before_after\") — those fold onto a stable key however they are spelled, so a whole brand accumulates evidence on one row. Your own wording is fine too; it just only groups when you repeat it exactly. Omitting it means this post can never vote on which hook works."New value: +"WHAT ANGLE this post is built on, recorded only at publish time. post_performance ranks hooks on it (a winner needs 5 posts sharing ONE hook), so pass a list_hooks id (e.g. \"direct_callout\", \"before_after\") or reuse your own wording EXACTLY across a campaign. Omit it and this post never votes on which hook works."
      • changedInput schema / properties / subject / description
        Previous value: -"WHAT THIS POST IS ABOUT — the product, feature, offer or theme (e.g. \"winter coat\", \"free trial\", \"founder story\"). The second grouping axis in post_performance. Same rule as hook: reuse the exact wording so posts about one subject land in one group."New value: +"WHAT THIS POST IS ABOUT — product, feature, offer or theme (e.g. \"winter coat\", \"free trial\"). post_performance's second grouping axis: reuse the exact wording, as with hook."
      • addedInput schema / properties / videoVolume
        Added value: +{
        +  "description": "INSTAGRAM REEL — how loud the video’s own sound plays under the track, 0–100 (Instagram’s default 100; 0 = the track alone). Needs audioId.",
        +  "maximum": 100,
        +  "minimum": 0,
        +  "type": "integer"
        +}
  18. 20 tool updatesv0.1.285
    • Changedadd_subtitles5 fields changed
      • addedInput schema / properties / auto
        Added value: +{
        +  "enum": [
        +    "sentences",
        +    "words"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / cues
        Added value: +{
        +  "description": "your own lines in seconds, no overlap, max 90 chars, no emoji",
        +  "items": {
        +    "properties": {
        +      "end": {
        +        "type": "number"
        +      },
        +      "start": {
        +        "type": "number"
        +      },
        +      "text": {
        +        "type": "string"
        +      }
        +    },
        +    "required": [
        +      "text",
        +      "start",
        +      "end"
        +    ],
        +    "type": "object"
        +  },
        +  "maxItems": 400,
        +  "type": "array"
        +}
      • changedInput schema / properties / textStyle / anyOf
        Previous value: -[
        -  {
        -    "enum": [
        -      "pill",
        -      "editorial",
        -      "bold",
        -      "minimal",
        -      "handwritten",
        -      "boxed"
        -    ],
        -    "type": "string"
        -  },
        -  {
        -    "properties": {
        -      "background": {
        -        "description": "none | pill | #hex box",
        -        "type": "string"
        -      },
        -      "color": {
        -        "description": "#hex",
        -        "type": "string"
        -      },
        -      "font": {
        -        "enum": [
        -          "sans",
        -          "serif",
        -          "elegant",
        -          "condensed",
        -          "hand"
        -        ],
        -        "type": "string"
        -      },
        -      "italic": {
        -        "type": "boolean"
        -      },
        -      "outline": {
        -        "type": "boolean"
        -      },
        -      "position": {
        -        "enum": [
        -          "top",
        -          "center",
        -          "lower",
        -          "bottom"
        -        ],
        -        "type": "string"
        -      },
        -      "preset": {
        -        "enum": [
        -          "pill",
        -          "editorial",
        -          "bold",
        -          "minimal",
        -          "handwritten",
        -          "boxed"
        -        ],
        -        "type": "string"
        -      },
        -      "shadow": {
        -        "type": "boolean"
        -      },
        -      "size": {
        -        "anyOf": [
        -          {
        -            "enum": [
        -              "s",
        -              "m",
        -              "l",
        -              "xl"
        -            ],
        -            "type": "string"
        -          },
        -          {
        -            "type": "number"
        -          }
        -        ]
        -      },
        -      "textCase": {
        -        "enum": [
        -          "as-is",
        -          "upper",
        -          "lower",
        -          "title"
        -        ],
        -        "type": "string"
        -      },
        -      "tilt": {
        -        "description": "degrees, ±12",
        -        "type": "number"
        -      },
        -      "weight": {
        -        "type": "number"
        -      }
        -    },
        -    "type": "object"
        -  }
        -]New value: +[
        +  {
        +    "type": "string"
        +  },
        +  {
        +    "properties": {
        +      "background": {
        +        "description": "none|pill|a colour",
        +        "type": "string"
        +      },
        +      "cardColor": {
        +        "type": "string"
        +      },
        +      "color": {
        +        "description": "any CSS colour",
        +        "type": "string"
        +      },
        +      "describe": {
        +        "description": "the look in words",
        +        "type": "string"
        +      },
        +      "font": {
        +        "description": "sans|serif|elegant|condensed|hand or any Google Fonts family",
        +        "type": "string"
        +      },
        +      "italic": {
        +        "type": "boolean"
        +      },
        +      "outline": {
        +        "type": "boolean"
        +      },
        +      "outlineColor": {
        +        "type": "string"
        +      },
        +      "position": {
        +        "anyOf": [
        +          {
        +            "type": "string"
        +          },
        +          {
        +            "type": "number"
        +          }
        +        ],
        +        "description": "top|center|lower|bottom or 0.05-0.95 from the top"
        +      },
        +      "preset": {
        +        "type": "string"
        +      },
        +      "shadow": {
        +        "type": "boolean"
        +      },
        +      "size": {
        +        "anyOf": [
        +          {
        +            "type": "string"
        +          },
        +          {
        +            "type": "number"
        +          }
        +        ],
        +        "description": "s|m|l|xl, '120px', or a 0.015-0.15 frame fraction"
        +      },
        +      "subFont": {
        +        "type": "string"
        +      },
        +      "subItalic": {
        +        "type": "boolean"
        +      },
        +      "textCase": {
        +        "enum": [
        +          "as-is",
        +          "upper",
        +          "lower",
        +          "title"
        +        ],
        +        "type": "string"
        +      },
        +      "tilt": {
        +        "description": "degrees, ±45",
        +        "type": "number"
        +      },
        +      "weight": {
        +        "type": "number"
        +      }
        +    },
        +    "type": "object"
        +  }
        +]
      • changedInput schema / properties / textStyle / description
        Previous value: -"the look: a preset or overrides; omit for the default"New value: +"the look: words, a preset or fields; omit for the default"
      • addedInput schema / properties / wordsPerCue
        Added value: +{
        +  "type": "number"
        +}
    • Changedanalyze_video3 fields changed
      • addedInput schema / properties / anchors
        Added value: +{
        +  "description": "{afterWord|beforeWord|atWord, occurrence?, offset?} -> s",
        +  "items": {},
        +  "type": "array"
        +}
      • addedInput schema / properties / frames
        Added value: +{
        +  "description": "also return the frames as images",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / words
        Added value: +{
        +  "description": "true | 'only': each spoken word's start/end"
        +}
    • Changedclip_video2 fields changed
      • changedInput schema / properties / aspectRatio / description
        Previous value: -"clip shape — '9:16' (default) vertical for Reels/Shorts/TikTok; 'keep' leaves the source framing untouched"New value: +"clip shape: '9:16' (default), '1:1', '16:9', any 'W:H', or 'keep' for the source framing"
      • removedInput schema / properties / aspectRatio / enum
        Removed value: -[
        -  "9:16",
        -  "1:1",
        -  "16:9",
        -  "keep"
        -]
    • Changedclone_static1 field changed
      • changedInput schema / properties / imageUrl / description
        Previous value: -"the URL of the static ad image to clone"New value: +"the URL of the static ad image to clone; a real person in it confirms their likeness consent"
    • Addedfind_sound
    • Changedgenerate_avatar1 field changed
      • changedInput schema / properties / image / description
        Previous value: -"local path or URL of the presenter portrait"New value: +"local path or URL of the presenter portrait; a real person’s photo confirms their likeness consent"
    • Changedgenerate_image1 field changed
      • changedInput schema / properties / refImages / description
        Previous value: -"local file paths or URLs of product/logo references to composite in. ON THE POSE MODELS — any row hermoso_capabilities marks `needsRefs` with a `refsMax`, such as putting your product in someone’s hands or a virtual try-on — THE ORDER IS THE CONTRACT AND IT IS NOT A COMPOSITE: refImages[0] is the PERSON photo, and the rest (up to `refsMax` minus one) are the product or garment photos. Reversed, you get the product wearing the person. A 4th product is dropped and the reply says so."New value: +"local file paths or URLs of product/logo references to composite in. ON THE POSE MODELS — any row hermoso_capabilities marks `needsRefs` with a `refsMax`, such as putting your product in someone’s hands or a virtual try-on — THE ORDER IS THE CONTRACT AND IT IS NOT A COMPOSITE: refImages[0] is the PERSON photo, and the rest (up to `refsMax` minus one) are the product or garment photos. Reversed, you get the product wearing the person. A 4th product is dropped and the reply says so. A real person’s photo confirms their likeness consent."
    • Addedgenerate_music
    • Changedgenerate_video1 field changed
      • changedInput schema / properties / refImage / description
        Previous value: -"local path or URL to anchor the first frame"New value: +"local path or URL to anchor the first frame; a real person’s photo here or in refImages confirms their likeness consent"
    • Changedhook_variants1 field changed
      • addedInput schema / properties / hooks
        Added value: +{
        +  "description": "your own openings, one version each",
        +  "items": {
        +    "properties": {
        +      "cutAt": {
        +        "description": "url: override the found payoff cut",
        +        "type": "number"
        +      },
        +      "mechanic": {
        +        "type": "string"
        +      },
        +      "prompt": {
        +        "type": "string"
        +      },
        +      "start": {
        +        "description": "url: skip the video’s own first seconds",
        +        "type": "number"
        +      },
        +      "url": {
        +        "type": "string"
        +      }
        +    },
        +    "type": "object"
        +  },
        +  "type": "array"
        +}
    • Changedmake_explainer3 fields changed
      • changedInput schema / properties / music / description
        Previous value: -"music bed under the narration, measured to sit about 14 dB under the voice and sidechain-ducked beneath it. Omit and the KIDS and FAIRYTALE channels get their recommended bed COMPOSED for this film — those two are the only channels a bed is due on unasked, and it costs a small flat fee; every other channel ships dry. 'off' forces silence. 'library' takes a free curated track only, and ships dry when none is on file. NAME A MOOD — upbeat / calm / warm / epic / tense / playful / elegant / hype / chill / dramatic — to compose one on ANY channel, at the same fee. hermoso_capabilities reports the exact figure as explainerMusicCredits; quote it before you turn a bed on or pick a mood."New value: +"music bed under the narration, measured to sit about 14 dB under the voice and sidechain-ducked beneath it. Omit and the KIDS and FAIRYTALE channels get their recommended bed COMPOSED for this film — those two are the only channels a bed is due on unasked, and it costs a small flat fee; every other channel ships dry. 'off' forces silence. 'library' takes a free curated track only, and ships dry when none is on file. NAME A MOOD (upbeat / calm / warm / epic / tense / playful / elegant / hype / chill / dramatic) or DESCRIBE it in words to compose one on ANY channel, at the same fee. hermoso_capabilities reports the exact figure as explainerMusicCredits; quote it before you turn a bed on or pick a mood."
      • changedInput schema / properties / style / description
        Previous value: -"visual style. 'cinematic' (default) is photoreal; the rest are non-photoreal styled looks — editorial_collage (halftone cutouts + marker accents), flat_vector, stickman, whiteboard, ink_marker, silhouette, storybook (gouache), paper_diorama, isometric, claymation, pixel_art, watercolor, fluffy_toy (felted plush), low_poly, stylized_3d (matte clay render), studio_3d (preschool toy 3D on a white sweep — the Kids default), mannequin (clay-render reenactment figures — a History alternate). Ask the user which they want rather than picking silently; a styled pick costs more (see the cost note)."New value: +"visual style: 'cinematic' (default, photoreal); styled shortcuts editorial_collage, flat_vector, stickman, whiteboard, ink_marker, silhouette, storybook, paper_diorama, isometric, claymation, pixel_art, watercolor, fluffy_toy, low_poly, stylized_3d, studio_3d (the Kids default), mannequin; or ANY look described in words ('80s anime cel animation'), locked across every frame. Ask rather than pick silently; a styled look costs more."
      • removedInput schema / properties / style / enum
        Removed value: -[
        -  "cinematic",
        -  "editorial_collage",
        -  "flat_vector",
        -  "stickman",
        -  "whiteboard",
        -  "ink_marker",
        -  "silhouette",
        -  "storybook",
        -  "paper_diorama",
        -  "isometric",
        -  "claymation",
        -  "pixel_art",
        -  "watercolor",
        -  "fluffy_toy",
        -  "low_poly",
        -  "stylized_3d",
        -  "studio_3d",
        -  "mannequin"
        -]
    • Addedmake_insert
    • Changedmake_template_ad1 field changed
      • changedInput schema / properties / config / description
        Previous value: -"the template config — MUST include config.template (one of the template ids above) plus that template's fields"New value: +"MUST include config.template: 'custom' or a preset id above, plus its fields"
    • Changedmake_thumbnail7 fields changed
      • changedInput schema / properties / font / description
        Previous value: -"headline font (default Anton). Alternatives incl. Bebas Neue, Oswald, Archivo Black, Montserrat, Inter, Playfair Display"New value: +"headline font: Anton (default) or any Google Fonts family"
      • changedInput schema / properties / framework / description
        Previous value: -"concept framework id (default 'posed_portrait'); see the list in this description / hermoso_capabilities"New value: +"concept framework id (default 'posed_portrait'), or your own concept in words"
      • changedInput schema / properties / headlinePlace / description
        Previous value: -"where the headline sits — never over the face (default 'bottom')"New value: +"bottom (default), top, center, or a 0-1 fraction from the top; never over the face"
      • removedInput schema / properties / headlinePlace / enum
        Removed value: -[
        -  "bottom",
        -  "top",
        -  "center"
        -]
      • changedInput schema / properties / overlayStyle / description
        Previous value: -"headline style: 'beast' (default, white + heavy black stroke) / 'fire' / 'neon-lime' / 'clean-glass' / 'marker'"New value: +"headline style: beast (default), fire, neon-lime, clean-glass, marker, or your own CSS declarations"
      • changedInput schema / properties / tweak / description
        Previous value: -"surgical pixel-faithful edit of a FINISHED thumbnail — needs sourceImage"New value: +"surgical pixel-faithful edit of a FINISHED thumbnail (needs sourceImage): kind emotion / background / background_color / rim_light, or any other kind with the edit in words as value"
      • removedInput schema / properties / tweak / properties / kind / enum
        Removed value: -[
        -  "emotion",
        -  "background",
        -  "background_color",
        -  "rim_light"
        -]
    • Changedmultiply_ad2 fields changed
      • changedInput schema / properties / axes / description
        Previous value: -"which axes to vary (default: all four)"New value: +"what to vary: character, outfit, location, objects (default all four), or your own, e.g. \"season\""
      • removedInput schema / properties / axes / items / enum
        Removed value: -[
        -  "character",
        -  "outfit",
        -  "location",
        -  "objects"
        -]
    • Changedpost_edit2 fields changed
      • addedInput schema / properties / ops / items / properties / bridge
        Added value: +{
        +  "description": "join: connects the hook to the first clip",
        +  "properties": {
        +    "cutAt": {
        +      "description": "omit: found from the footage",
        +      "type": "number"
        +    },
        +    "flash": {
        +      "type": "boolean"
        +    },
        +    "kind": {
        +      "enum": [
        +        "impact",
        +        "text"
        +      ],
        +      "type": "string"
        +    },
        +    "matchCut": {
        +      "type": "number"
        +    },
        +    "shake": {
        +      "type": "boolean"
        +    },
        +    "sound": {
        +      "enum": [
        +        "auto",
        +        "own",
        +        "impact",
        +        "whoosh",
        +        "none"
        +      ],
        +      "type": "string"
        +    },
        +    "text": {
        +      "type": "string"
        +    },
        +    "then": {
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "kind"
        +  ],
        +  "type": "object"
        +}
      • addedInput schema / properties / ops / items / properties / textStyle
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "additionalProperties": {},
        +      "properties": {},
        +      "type": "object"
        +    }
        +  ]
        +}
    • Changedrecast_motion1 field changed
      • changedInput schema / properties / image / description
        Previous value: -"the actor/character image URL (who should appear)"New value: +"the actor/character image URL (who should appear); a real person’s photo confirms their likeness consent"
    • Changedremix_static1 field changed
      • changedInput schema / properties / imageUrl / description
        Previous value: -"the URL of the static ad image to clone"New value: +"the URL of the static ad image to clone; a real person in it confirms their likeness consent"
    • Changedrender_ad6 fields changed
      • changedInput schema / properties / creator / description
        Previous value: -"CAST A SAVED CREATOR in this ad — their id from list_creators, or the name you know them by (“Sarah”). Their saved portrait becomes the on-camera identity for the whole spot, so the same face carries across every act and across every ad you render for this brand — and because we already have their picture, the character portrait this pipeline would otherwise generate is skipped, so casting somebody costs LESS than not casting them. Omit to let the ad cast a fresh person — EXCEPT for a CREATOR account (onboarded from their own @handle): their own saved likeness is cast by default on any plan with a person on camera, and the read-back says `default:true`; pass \"none\" to render without them. Refused for free, with nothing rendered, if the name matches nobody or more than one creator, if an explicitly named creator is cast on a plan with nobody on camera, or if they are a REAL person with no likeness consent on file."New value: +"CAST A SAVED CREATOR in this ad — their id from list_creators, or the name you know them by (“Sarah”). Their saved portrait becomes the on-camera identity for the whole spot, so the same face carries across every act and across every ad you render for this brand — and because we already have their picture, the character portrait this pipeline would otherwise generate is skipped, so casting somebody costs LESS than not casting them. Omit to let the ad cast a fresh person — EXCEPT for a CREATOR account (onboarded from their own @handle): their own saved likeness is cast by default on any plan with a person on camera, and the read-back says `default:true`; pass \"none\" to render without them. Refused for free, with nothing rendered, if the name matches nobody or more than one creator, or if an explicitly named creator is cast on a plan with nobody on camera. Casting a REAL person confirms their likeness consent."
      • addedInput schema / properties / music / anyOf
        Added value: +[
        +  {
        +    "type": "boolean"
        +  },
        +  {
        +    "type": "string"
        +  }
        +]
      • changedInput schema / properties / music / description
        Previous value: -"licensed music bed on/off (default on)"New value: +"music bed: on (default), false, or the bed described in words ('lo-fi jazz, brushed drums')"
      • removedInput schema / properties / music / type
        Removed value: -"boolean"
      • changedInput schema / properties / textStyle / anyOf
        Previous value: -[
        -  {
        -    "enum": [
        -      "pill",
        -      "editorial",
        -      "bold",
        -      "minimal",
        -      "handwritten",
        -      "boxed"
        -    ],
        -    "type": "string"
        -  },
        -  {
        -    "properties": {
        -      "background": {
        -        "description": "\"none\", \"pill\", or a #hex box",
        -        "type": "string"
        -      },
        -      "cardColor": {
        -        "description": "#hex end card background",
        -        "type": "string"
        -      },
        -      "color": {
        -        "description": "#hex",
        -        "type": "string"
        -      },
        -      "font": {
        -        "enum": [
        -          "sans",
        -          "serif",
        -          "elegant",
        -          "condensed",
        -          "hand"
        -        ],
        -        "type": "string"
        -      },
        -      "italic": {
        -        "type": "boolean"
        -      },
        -      "outline": {
        -        "type": "boolean"
        -      },
        -      "position": {
        -        "enum": [
        -          "top",
        -          "center",
        -          "lower",
        -          "bottom"
        -        ],
        -        "type": "string"
        -      },
        -      "preset": {
        -        "enum": [
        -          "pill",
        -          "editorial",
        -          "bold",
        -          "minimal",
        -          "handwritten",
        -          "boxed"
        -        ],
        -        "type": "string"
        -      },
        -      "shadow": {
        -        "type": "boolean"
        -      },
        -      "size": {
        -        "anyOf": [
        -          {
        -            "enum": [
        -              "s",
        -              "m",
        -              "l",
        -              "xl"
        -            ],
        -            "type": "string"
        -          },
        -          {
        -            "type": "number"
        -          }
        -        ]
        -      },
        -      "subFont": {
        -        "enum": [
        -          "sans",
        -          "serif",
        -          "elegant",
        -          "condensed",
        -          "hand"
        -        ],
        -        "type": "string"
        -      },
        -      "subItalic": {
        -        "type": "boolean"
        -      },
        -      "textCase": {
        -        "enum": [
        -          "as-is",
        -          "upper",
        -          "lower",
        -          "title"
        -        ],
        -        "type": "string"
        -      },
        -      "tilt": {
        -        "description": "degrees, ±12",
        -        "type": "number"
        -      },
        -      "weight": {
        -        "type": "number"
        -      }
        -    },
        -    "type": "object"
        -  }
        -]New value: +[
        +  {
        +    "type": "string"
        +  },
        +  {
        +    "properties": {
        +      "background": {
        +        "description": "none|pill|a colour",
        +        "type": "string"
        +      },
        +      "cardColor": {
        +        "type": "string"
        +      },
        +      "color": {
        +        "description": "any CSS colour",
        +        "type": "string"
        +      },
        +      "describe": {
        +        "description": "the look in words",
        +        "type": "string"
        +      },
        +      "font": {
        +        "description": "sans|serif|elegant|condensed|hand or any Google Fonts family",
        +        "type": "string"
        +      },
        +      "italic": {
        +        "type": "boolean"
        +      },
        +      "outline": {
        +        "type": "boolean"
        +      },
        +      "outlineColor": {
        +        "type": "string"
        +      },
        +      "position": {
        +        "anyOf": [
        +          {
        +            "type": "string"
        +          },
        +          {
        +            "type": "number"
        +          }
        +        ],
        +        "description": "top|center|lower|bottom or 0.05-0.95 from the top"
        +      },
        +      "preset": {
        +        "type": "string"
        +      },
        +      "shadow": {
        +        "type": "boolean"
        +      },
        +      "size": {
        +        "anyOf": [
        +          {
        +            "type": "string"
        +          },
        +          {
        +            "type": "number"
        +          }
        +        ],
        +        "description": "s|m|l|xl, '120px', or a 0.015-0.15 frame fraction"
        +      },
        +      "subFont": {
        +        "type": "string"
        +      },
        +      "subItalic": {
        +        "type": "boolean"
        +      },
        +      "textCase": {
        +        "enum": [
        +          "as-is",
        +          "upper",
        +          "lower",
        +          "title"
        +        ],
        +        "type": "string"
        +      },
        +      "tilt": {
        +        "description": "degrees, ±45",
        +        "type": "number"
        +      },
        +      "weight": {
        +        "type": "number"
        +      }
        +    },
        +    "type": "object"
        +  }
        +]
      • changedInput schema / properties / textStyle / description
        Previous value: -"THE LOOK of captions and the end card — only meaningful with captions:true or endCard:true, and only when the user described a look. Presets: editorial (a large elegant serif title mid-frame with a small italic line under it, no box), bold (tall condensed caps with a black outline), minimal (small lowercase near the bottom), handwritten (tilted marker), boxed (dark words on a white box), pill (the plain default). Pass a preset name, or an object with a preset plus overrides. A caption written \"TITLE · small line\" puts the part after the middle dot on a second line. An invalid field is refused by name before anything renders."New value: +"THE LOOK of captions and the end card, only with captions or endCard and only when the user described one: the look in WORDS (\"chunky yellow comic letters, purple outline\"), a preset (editorial: big serif title + small italic line; bold: condensed caps, outline; minimal; handwritten; boxed; pill, the default), or fields. \"TITLE · small line\" puts the part after the dot on a second line."
    • Changedsave_creator1 field changed
      • removedInput schema / properties / consented
        Removed value: -{
        -  "description": "REAL people only: the user has confirmed that person consented to their likeness being used in ads",
        -  "type": "boolean"
        -}
  19. 18 tool updatesv0.1.281
    • Changedduplicate_scheduled2 fields changed
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}
    • Changedgenerate_image1 field changed
      • changedInput schema / properties / aspectRatio / description
        Previous value: -"e.g. '1:1', '9:16', '16:9'"New value: +"e.g. '1:1', '9:16', '16:9', '4:5'. Each model draws its own list (hermoso_capabilities prints it per model, e.g. Nano Banana 2 goes to 1:8 and 8:1); a ratio the chosen model cannot draw is refused before anything is charged"
    • Changedlist_published_posts1 field changed
      • addedInput schema / properties / cursor
        Added value: +{
        +  "description": "nextCursor from a previous reply: the next, older page",
        +  "type": "string"
        +}
    • Changedpost_edit11 fields changed
      • addedInput schema / properties / ops / items / properties / clips
        Added value: +{
        +  "description": "join: the clips after this video",
        +  "items": {
        +    "properties": {
        +      "end": {
        +        "type": "number"
        +      },
        +      "start": {
        +        "type": "number"
        +      },
        +      "url": {
        +        "type": "string"
        +      }
        +    },
        +    "required": [
        +      "url"
        +    ],
        +    "type": "object"
        +  },
        +  "type": "array"
        +}
      • changedInput schema / properties / ops / items / properties / end / description
        Previous value: -"trim/mute window end (s)"New value: +"trim/mute/text window end (s)"
      • changedInput schema / properties / ops / items / properties / op / enum
        Previous value: -[
        -  "trim",
        -  "speed",
        -  "mute",
        -  "audio_gain",
        -  "fade_out",
        -  "append_card",
        -  "watermark",
        -  "grain"
        -]New value: +[
        +  "trim",
        +  "speed",
        +  "mute",
        +  "audio_gain",
        +  "fade_out",
        +  "append_card",
        +  "watermark",
        +  "grain",
        +  "text",
        +  "join"
        +]
      • addedInput schema / properties / ops / items / properties / position
        Added value: +{
        +  "description": "text: where",
        +  "enum": [
        +    "top",
        +    "center",
        +    "lower",
        +    "bottom"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / ops / items / properties / seconds / description
        Previous value: -"fade_out 0.3-3s / append_card 2-5s"New value: +"fade_out 0.3-3s / append_card 2-5s / crossfade 0.2-1.5s"
      • changedInput schema / properties / ops / items / properties / start / description
        Previous value: -"trim/mute window start (s)"New value: +"trim/mute/text window start (s)"
      • addedInput schema / properties / ops / items / properties / style
        Added value: +{
        +  "anyOf": [
        +    {
        +      "type": "string"
        +    },
        +    {
        +      "additionalProperties": {},
        +      "properties": {},
        +      "type": "object"
        +    }
        +  ],
        +  "description": "text: a look name or a textStyle"
        +}
      • changedInput schema / properties / ops / items / properties / sub / description
        Previous value: -"append_card: the pill line (defaults to the brand website)"New value: +"append_card: the pill line (defaults to the website) / text: a smaller second line"
      • addedInput schema / properties / ops / items / properties / text
        Added value: +{
        +  "description": "text: the words, verbatim",
        +  "type": "string"
        +}
      • addedInput schema / properties / ops / items / properties / transition
        Added value: +{
        +  "description": "join",
        +  "enum": [
        +    "cut",
        +    "crossfade"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / videoUrl / description
        Previous value: -"the served URL of the video to edit"New value: +"the video to edit: a render / Library URL, a direct file, or a public post link"
    • Changedpost_performance1 field changed
      • addedInput schema / properties / days
        Added value: +{
        +  "description": "look back N days (1-730) over the whole history; omit for the recent posts only",
        +  "type": "number"
        +}
    • Changedpost_to_bluesky6 fields changed
      • addedInput schema / properties / allowDuplicate
        Added value: +{
        +  "description": "post it even though an identical post was just made",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / hook
        Added value: +{
        +  "description": "the post's angle — a list_hooks id or your own wording, reused exactly",
        +  "type": "string"
        +}
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / idempotencyKey
        Added value: +{
        +  "description": "any stable string: a repeat within 24h returns the original post instead of posting again",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}
      • addedInput schema / properties / subject
        Added value: +{
        +  "description": "what the post is about",
        +  "type": "string"
        +}
    • Changedpost_to_google_business2 fields changed
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}
    • Changedpost_to_linkedin2 fields changed
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}
    • Changedpost_to_linkedin_page2 fields changed
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}
    • Changedpost_to_meta2 fields changed
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}
    • Changedpost_to_pinterest2 fields changed
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}
    • Changedpost_to_telegram6 fields changed
      • addedInput schema / properties / allowDuplicate
        Added value: +{
        +  "description": "post it even though an identical post was just made",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / hook
        Added value: +{
        +  "description": "the post's angle — a list_hooks id or your own wording, reused exactly",
        +  "type": "string"
        +}
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / idempotencyKey
        Added value: +{
        +  "description": "any stable string: a repeat within 24h returns the original post instead of posting again",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}
      • addedInput schema / properties / subject
        Added value: +{
        +  "description": "what the post is about",
        +  "type": "string"
        +}
    • Changedpost_to_tiktok2 fields changed
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}
    • Changedpost_to_x2 fields changed
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}
    • Changedpost_to_youtube2 fields changed
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}
    • Changedpost_x_article2 fields changed
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}
    • Changedreschedule_post2 fields changed
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}
    • Changedschedule_post2 fields changed
      • addedInput schema / properties / ideaId
        Added value: +{
        +  "description": "short id of the content-plan idea this post came from",
        +  "type": "string"
        +}
      • addedInput schema / properties / recipe
        Added value: +{
        +  "description": "the post's FORMAT id, e.g. \"slideshow\" or \"imessage_chat\" — post_performance groups by it, so reuse one id per format",
        +  "type": "string"
        +}

TDQS

A3.5/5.0

Scored across 205 tools

Disambiguation2/5

The set contains many tools with genuinely overlapping creative purposes: generate_image, edit_image, clone_static, remix_static, headline_variants, resize_ad, localize_ad, and make_effect all produce or alter static images; generate_video, render_ad, stitch_video, make_motion_film, make_template_ad, product_sizzle, restyle_video, edit_video, fix_beat, and clip_video form a dense video cluster. The descriptions contain extensive routing guidance, but an agent must still parse long in-tool prose to distinguish boundaries, and several analytics/performance tools also overlap.

Naming Consistency3/5

Tool names are consistently snake_case, which is readable, but the verb conventions are not predictable throughout: post_to_x, make_thumbnail, generate_image, render_ad, list_jobs, get_job, create_sheet, hermoso_capabilities, billing_status, and store_get follow different patterns. There is also a legacy alias, remix_static, and several noun-first names mixed with verb-first names.

Tool Count1/5

205 tools is an extreme mismatch for a coherent tool set and far exceeds the usual 3-15 well-scoped range. Even with enable_tools, find_tools, and call_tool as escape hatches, the listed roster itself is massive and imposes a large schema-selection burden.

Completeness3/5

The surface covers an unusually broad domain: publishing, scheduling, media generation, research, Drive/Notion/Sheets/OneDrive, billing, workspaces, connectors, and analytics. However, apparent lifecycle gaps remain in the provided list: several referenced management tools such as manage_meta_post, manage_linkedin_post, delete_x_post, and campaign/admin tools are not present, so agents must rely on call_tool for unreachable-by-name operations.

Maintenance

ActivityActive
ResponsivenessLimited data

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Provides Meta and Google Ads intelligence for AI assistants, enabling users to analyze performance, track competitors, and manage ad campaigns through natural language. It features 17 tools for generating creative concepts, scraping competitor ads, and performing deep account-level analysis.
    17
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Manage ad campaigns across Meta, Google, and TikTok, create campaigns, analyze performance, spy on competitors, and generate AI creatives.
    19
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    50 tools for Meta Ads campaign management, creative analysis, audience building, and conversion tracking, accessible to any MCP-compatible AI agent.
    69
    89 npm
    13
    MIT