Create a complete product ad video through the 5-stage AD Video wizard: Product → Config → Idea → Storyboard → Video. Each stage maps to cawcut ad-video commands against the developer API. Full command flag tables live in references/api.md; stage semantics and the invalidation matrix live in references/stages.md; authoring a script edit lives in references/script-editing.md; error recovery lives in references/troubleshooting.md. Read them at <base_dir>/references/<file>.md using the base directory printed at the top of this skill body — do not find/grep for them.
Reply language. All live replies — prose, status updates, AskUserQuestion / AskQuestion questions and option labels — follow the user's language for that turn. This is judged from the user's actual chat message content for that turn — not from whether the Skill tool call itself carried an args parameter, which is an unrelated, optional pass-through field. This includes implicit triggers: if the skill was invoked because the user's message matched a trigger phrase (e.g. "广告视频", "商品广告", "make an ad for …") rather than the literal /cawcut-ad-video command, that trigger phrase is the user's chat message content for the turn — reply in its language from the very first response, including when asking for the missing product URL. Only default to English when the invocation carries no language signal at all (e.g. literally just /cawcut-ad-video alone with no other text); once the user adds a real request in another language, follow that language for the rest of the session. Keep CLI commands/flags, JSON keys, URLs, and raw error codes in their original form regardless of reply language, and never paste raw English CLI output as the user-facing answer — summarize it in reply_language.
Step 0 — Bootstrap
Run bootstrap as a silent guardrail, not as a user-facing phase.
- At most once per AI session. If any
cawcutcommand has already succeeded in this AI session, skip all bootstrap checks and continue. - Use only one explicit check command:
cawcut upgrade check --json.- If
"update_available": true, runcawcut upgradeyourself in the host shell (do not ask the user to type it). After it succeeds, continue this skill from Step 1. - If
"ahead_of_registry": true, continue without upgrading. - If
cawcutis missing, ask the user to install it:npm install -g @ubnt/cawcut. Then continue bootstrap. - For other check failures, continue and mention the warning only if a later CLI command fails.
- If
- Do not print "bootstrap checks passed" — move directly to Step 1.
- Let the first real command validate auth. If it fails with token/auth errors (including
Token expired), runcawcut auth loginyourself in the host shell; tell the user a browser tab will open for OAuth consent; wait for login, then retry the failed command once. - Structured user-ask tool (check every session). In Claude Code, use
AskUserQuestionfor every enumerable choice; in Cursor, useAskQuestion. If either is available, you must use it for all enumerable decisions in this skill — platform, resolution, duration, video type, storyboard style, language, idea candidate, credit confirmation — including when the candidate list is longer than the popup holds: a long list still opens the tool, with its leading entries as the options and the full numbered list beside them so any later entry can be typed back (see Interactive selection).
Wizard overview
① Product create → analyze run → poll → review/confirm analysis
② Config options → pick settings → config set
③ Idea idea run → poll → pick candidate (or custom) → idea select
④ Storyboard storyboard run → poll → (optional edit / regenerate) → image quote → confirm → image run (×2 if hand-drawn)
⑤ Video video quote → confirm → video run → poll → download
One project = one video_ad_id. Every long stage is triggered without --wait, then polled with the bundled script (see Polling). cawcut ad-video detail <id> --json is the single polling point — only it returns the outputs map.
Bundled-script runner
Use cawcut CLI commands directly for project operations and detail inspection. Choose bundled helpers for the host platform. Windows helpers require PowerShell and the installed cawcut CLI, not Python, jq, bash, or external curl; do not ask Windows users to install those tools for this skill. macOS/Linux .sh helpers require jq; neither platform uses Python.
- macOS/Linux:
bash <base_dir>/scripts/<name>.sh … - Windows PowerShell:
powershell -ExecutionPolicy Bypass -File "<base_dir>\scripts\<name>.ps1" …
The three helpers (poll, script_plan, download_media) have equivalent .sh and .ps1 implementations. Use the matching extension and platform-specific flags throughout one run; for example, --kind video on bash is -Kind video on PowerShell.
Stage ① — Product
- Collect the product URL (Amazon/Shopify/official page all work). If the user didn't give one, ask in plain text (open-ended). Do NOT ask for a project name — the backend auto-names the project from the product info after analysis/export.
cawcut ad-video create --url "<url>" --json→ captureid.--urlis optional at create (an empty draft is allowed), but pass it whenever the user gave one —analyze runrequires it later regardless.--categoryis optional; omit it and the backend auto-detects after analysis. Only pass--nameif the user explicitly asked for a specific title.cawcut ad-video analyze run <id> --url "<the product URL from step 1>" --json(no--wait; the API requiresurlin the body), then poll with the platform runner:poll.sh <id> '.status' stage_completed 600 15on macOS/Linux, orpoll.ps1 <id> '.status' stage_completed 600 15on Windows.- Read
detail <id> --jsonand present the analysis result:- Product info verbatim — every populated
product_infofield:name,description,selling_points[](each in full, numbered),target_audience,brand_style,promotional_info,source_platform,category. Do not summarize, truncate, or translate the product content itself — it is the user's marketing copy and must be shown as returned; only your surrounding labels and prose follow reply_language. - Product images — first download them with the platform-specific
download_mediahelper:<id> --kind product. Then present by category (labels in reply_language):- Logo (
logo_images) and Primary images (primary_images): preview each inline via the host's send-file tool (e.g. Claude'sSendUserFile) with the local path just downloaded (e.g.~/Downloads/ad-video-<name>/product/logo-1.jpg,primary-1.jpg) — a markdown link only renders as a clickable line, not a visible image. - Additional product images (
images[]): if inline previews are supported, list them one by one, each labeledProduct Image #nwith the image sent inline; cap at the first 9 — if there are more, say so (e.g.…and N–9 more) and tell the user the rest are in the<dest>/product/folder. If inline previews are not supported, list them as a table of local paths (#+ path) and point to the same folder. - No
asset_idcolumn in either form — the local path is a preview reference only. These files exist so the user can see the images; never hand them back to the CLI. Then ask the user to confirm or correct via AskUserQuestion:
- Logo (
✅ Looks good→ continue to Stage ②✏️ Edit product info→ collect the changes and pass only those fields tocawcut ad-video product set <id> …: the command reads the project first and carries every field you did not pass forward. (The API replacesproduct_infowholesale, so a bare--product-namewould otherwise erase the description, the images and the selling points.) Re-runanalyze runafterwards if the URL or the product changed materially.- An image list is replaced by passing it, so
--logo/--primary-image/--assettake the complete kept list — that is also how an image is removed. A replaced list drops its own selection, and the backend falls back to its defaults (every candidate for--asset, the first candidate for logo/primary). Never re-send the files you downloaded: the persisted refs already come back asasset_ids. - Pass a local path only for an image the user supplies from outside the project; the CLI uploads it as a brand-new asset (7-day TTL).
--description ""clears the description and--clear-selling-pointsempties the selling points — an empty list is a real state, not "unset".
- An image list is replaced by passing it, so
- Product info verbatim — every populated
- If the user wants to abort a running analysis:
cawcut ad-video analyze cancel <id>.
Material rights (state this once, when the user supplies their own image). Images uploaded through the CLI are not run through the material compliance check and are accepted as-is — there is no rights attestation step to walk through, unlike the web portal. That check exists to catch third-party logos and celebrity likeness; on this path the responsibility for holding the rights to the material sits with the user. Say so plainly in one sentence before uploading, so nobody assumes the tool vetted it. This applies only to new files the user supplies: images already in the project (crawler-extracted product images) were never subject to it.
Stage ② — Config (all options are runtime-fetched, never hardcoded)
-
cawcut ad-video options --video-ad-id <id> --json→ readvideo_settings(publish_to, languages, aspect_ratios, resolutions, duration_ranges, video_types, storyboard_styles, video_models, defaults). -
Ask the axes in this order — video type first, because it decides both whether captions are a question at all and which model/resolution combinations are legal.
- video type — one option per entry in
video_types. - video model — only when
video_modelsis non-empty. Candidates arevideo_models[].value, defaultdefaults.video_model. The table that carries the list has three columns beyond#: the model, itssupported_resolutions, and a default mark ondefaults.video_model— the resolutions belong in view because picking a model is what decides which ones stay legal on the next axis. With exactly one candidate, state it in prose and carry on (one option is not a choice); with none, skip the axis entirely. - resolution — default
defaults.resolution, then clamp to the chosen model'ssupported_resolutions(the same list the server validates against). Never offer a resolution the model does not support:config setrejects it. When no model was chosen, offer the fullresolutionslist. - auto captions — only for
product_with_voiceoverandtalking_video. Captions are burned in from the voiceover/dialogue track, soproduct_showcasehas nothing to caption; do not ask, and pass--auto-captions falseexplicitly (mirrors the web portal, which forces the value off for that type). - platform — show each option's
default_aspect_ratioin its description (e.g.TikTok — 9:16), language, duration, storyboard style (explain: hand-drawn adds a sketch pass before photo-realistic panels). - Aspect ratio is NOT a question. It is bound to the platform (mirrors the web portal, which has no ratio selector): take the picked
publish_tooption'sdefault_aspect_ratioautomatically. Never ask for a ratio separately; only if the user explicitly overrides should you deviate from the platform default.
Batch the axes into one AskUserQuestion call — one question per axis, at most four questions per call. A long axis (platform, language, video model) follows Interactive selection's overflow shape: the full list is a numbered table in the message body —
#plus what that axis actually carries: a platform's label with itsdefault_aspect_ratio, a language's label, a model's label with itssupported_resolutionsand default mark. The question itself is one short sentence, the popup holds the leading entries as options labelled with their number, and the entries past them are answered by number or name in the free-text field. Each axis keeps its own table and its own numbering — the answer goes into that axis's own field, so never ask for two numbers at once (1 + 10). - video type — one option per entry in
-
cawcut ad-video config set <id> --publish-to … --language … --aspect-ratio <the platform's default_aspect_ratio> --resolution … --duration-seconds … --video-type … --video-model … --storyboard-style … --auto-captions … --json
video_model is a real cost lever — say so when offering it (a cheaper model may be available), but never present a model the endpoint did not return.
Stage ③ — Idea
cawcut ad-video idea run <id> --json, poll untiloutputs.generated_marketing_messages.status === "succeeded".- Reveal up to eight, then stop. One run generates up to 16 candidates but
detailhands back one page (4) at a time, so a fresh read shows only the first four. Readoutputs.generated_marketing_messages, and callcawcut ad-video idea load-more <id> --json(re-readingdetaileach time) until eight are visible orhas_more === false— whichever comes first:load-moreonly reveals already-generated candidates — no LLM call, no credit cost.- Eight is the ceiling, not a target. A project holding fewer shows fewer; once eight are visible you stop revealing even if
has_moreis still true. This flow has no "one more batch" step. - State the number you are showing, and never imply the set is everything the project holds when it is not.
- Present them as a table, in full, verbatim — one row per candidate:
#, title, content. No summarizing, no paraphrasing, no truncating, no translating: this is the user's marketing copy and they are choosing between the whole set. Lead with the count (e.g. "Showing 8 candidates:"). The#column is the popup's index — this table is what any number the popup could not hold is typed against. - One decision, one question. The choice is a single AskUserQuestion — never a candidate question followed by a second one asking what to do with that answer. Four options, in this order:
1. <title>,2. <title>,3. <title>— the first three candidates, each labelled with its number✏️ Write my own→ submit the idea text the user or agent produced withidea select <id> --custom "<text>"(60–10000 chars, mirroring the web custom-idea limits). This follows the portal custom path: the CLI sends that text ascustom_contentwithis_custom: true; do not turn it into a generated-candidate selection or add a title wrapper.- a candidate from the fourth to the eighth is selected by typing its number or its title into the free-text field; name that in the question's own text, in
reply_language, so the user answers from a set the question stated rather than guessing
- On a pick:
cawcut ad-video idea select <id> --message-id <mm-N> --title "<title>" --content "<content>" --json. Offer light edits to title/content before submitting. - A fresh batch is user-initiated only.
idea regenerateis not a menu entry: when the user asks for one in their own words ("换一批" / "regenerate"), runcawcut ad-video idea regenerate <id> --json+ poll — it clears the current selection, so warn first — then re-reveal (step 2) and re-ask (step 4).
Stage ④ — Storyboard
-
cawcut ad-video storyboard run <id> --json(optional--style-idfrom the options storyboard section), poll untiloutputs.storyboard_plan.status === "succeeded"— the 3600 deadline from the polling table, which is a give-up line and never a figure to quote. -
Read
detail <id> --json→outputs.storyboard_plan.value. Show the plan as a concise shot table. The table contains the user-reviewable fields only; prompts remain in the stored plan but are not displayed:- plan level, above the table:
title,synopsis - per shot, in this order:
shot_no(Shot #),duration(Duration),shot_type(Shot Type),shot_description(Shot Description),dialogue(Voiceover — that is its business meaning),camera(Camera),light(Light),sfx(SFX) - Omit the Voiceover column for
product_showcase(the web portal hides it too — that type has no spoken track). - Never show
shot_prompt,video_prompt, or their statuses in the table or elsewhere in the reply. They are implementation fields, not review content.
Then ask via AskUserQuestion:
✅ Looks good→ continue✏️ Work on the script→ the script is a file in the project folder; the table is rendered from it. Run the platform-specificscript_planhelper withtake <id>once — it writes the plan to<dest>/storyboard/script.jsonand prints the paths. Edit that file, never a plan retyped from context. After every change, write the fields you touched into a small edits JSON and run the same helper withshow <plan.json> <edits.json>: it applies them, lists what it applied, and prints the whole current review table. Say what changed, then paste that table into your reply text — the script's stdout is a tool result the user does not see; only what you write in your own message counts as shown. A diff alone makes them hold the rest of the script in their head. Nothing reaches the server until they confirm, so a half-formed idea costs no revision, no invalidation and no credit. When you customize any user-visible shot field, you must also update the hiddenshot_promptand/orvideo_promptin the same working copy according to the association table inreferences/script-editing.md; submit them but do not display them. The platform's own Prompt Generator is the alternative when you would rather not write them. The edits-file format, the prompt shapes, the trigger mapping and the server's limits are inreferences/script-editing.md— read it before the first edit.- Editable per-shot fields:
duration,shot_type,shot_description,dialogue(the Voiceover column),camera,light,sfx,shot_prompt,video_prompt. Never add or remove shots — the count comes from the generated script, and the portal's editor has no such action either. Rewriting a shot's content is the way to change it.shot_type/cameraoption values come fromcawcut ad-video options --json→ storyboard section, never hardcoded. Everything else in the file (assets,character_refs,scene_ref, the prompt statuses) is carried through untouched — the script refuses to edit it. - Save once:
cawcut ad-video storyboard set <id> --base-revision <value.revision> --plan-file <dest>/storyboard/script.json --json; on409re-read detail, take a fresh copy and re-apply the same edits, save again — the edits file is your record, so nothing is lost. The CLI checks the whole plan against the server's ownconstraintsbefore sending it: a refusal names the shot and field, nothing was sent, and the shot at fault is often one you did not edit — fix that field and save again, never resend the same plan. If--update-promptswas used, the save only starts asynchronous prompt generation: poll.outputs.storyboard_plan.prompt_statustosucceededbefore treating either prompt as ready;storyboard_plan.status: succeededalone is not sufficient. durationor the shot set changing invalidates the sketch, the panels and the final video: say so before the save, then run the invalidation protocol.
- Editable per-shot fields:
🔁 Regenerate the script→cawcut ad-video storyboard run <id> --force+ poll. Warn before running it: this discards every edit — the script LLM writes a new script from the same idea and config. It is the only way back to a platform-written script; a plain re-run will not do it (unchanged inputs make it a no-op). Re-show the table and ask again.
- plan level, above the table:
-
Quote before every image run:
cawcut ad-video image quote <id> --generation-type <type> --json→ AskUserQuestion to confirm the credit cost → immediatelycawcut ad-video image run <id> --generation-type <type> --confirmation-token <token> --json(token TTL is 5 minutes — do not pause between confirm and run). See Quote timing. -
Branch by storyboard style (from Stage ② config):
hand_drawn_then_photo: pass 1--generation-type hand_drawn→ pollstoryboard_sketch→ preview the sketch (Media preview & downloads), AskUserQuestion✅ Continue to photo-realistic/✏️ Edit script→ pass 2--generation-type photo_realistic→ pollstoryboard_imagephoto_realistic_only: single--generation-type photo_realisticrun → pollstoryboard_image
-
Generation-stage polls use the longest deadline in the polling table (3600). On success, preview the sketch/grid image per Media preview & downloads (platform-specific
download_media <id> --kind storyboard), then continue.
Stage ⑤ — Video
cawcut ad-video video quote <id> --json→ AskUserQuestion confirm credits → immediatelycawcut ad-video video run <id> --confirmation-token <token> --json(add--forceonly when the user explicitly re-renders an unchanged result).- Poll until one of
final_video/voiceover_final_video/talking_videoissucceeded— the 3600 deadline from the polling table. - On success: download the video into the project folder with platform-specific
download_media <id> --kind videoand take the printed path (cawcut ad-video video run <id> --wait --downloadis only for when you haven't triggered the run yet). See Media preview & downloads. - Report
local_pathand the result URL, then hand off unconditionally tocawcut-vnwith<local_path>— no VN menu, nocawcut vncommands of your own (decide-once: the receiving skill owns the VN decision and you never re-check).
Media preview & downloads
All media URLs the API returns (product images, storyboard sketch/grid, final video) are pre-signed — plain curl downloads them directly, no extra signing step. If one ever 403s, re-read detail and retry with the fresh URL (product image URLs are re-signed on every detail call).
Preview priority (product logo/primary images, storyboard sketch/grid):
- If the host has a send-file-to-user tool (e.g. Claude's
SendUserFile), download the file first, then send it so the user previews inline in the chat. - Otherwise
openthe downloaded file (macOS) or just report the local path.
Always use the bundled downloader instead of hand-rolling curl/jq pipelines:
# macOS/Linux
bash <base_dir>/scripts/download_media.sh <id> [--kind product|storyboard|video|all] [dest_dir]
# Windows PowerShell
powershell -ExecutionPolicy Bypass -File "<base_dir>\scripts\download_media.ps1" <id> [-Kind product|storyboard|video|all] [-Destination <dir>]
Default dest: ~/Downloads/ad-video-<product-name> (falls back to ad-video-<id> before the project is named). Layout: product/ (logo + primary + additional images), storyboard/ (sketch + panel grid), final video at the folder root. It prints every saved path — relay those paths to the user in reply_language.
Invalidation semantics (mandatory — mirrors the web portal)
After every mutation (product set, config set, idea select, storyboard set):
- Immediately run
cawcut ad-video detail <id> --json. First distinguish cleared artifacts from retained-but-stale artifacts; never infer either from the project history.- A mutation to any creative config field —
--duration-seconds,--language,--video-type,--storyboard-style, or--publish-to— clears downstream content. Ifoutputs.generated_marketing_messagesno longer has a usable succeededvalue/selection, the previous Idea does not exist in the current project state. Do not ask whether to reuse, keep, or continue using that Idea; do not offer it as an option. Continue at Stage ③ withcawcut ad-video idea run <id>, poll it to success, select a fresh Idea, then generate a new storyboard. - Only an output that remains present with
status: "succeeded"andneed_regenerate === trueis retained-but-stale and eligible for the regeneration/later decision. A cleared output is neither stale nor reusable.
- A mutation to any creative config field —
- Collect every retained-but-stale
outputs.<key>withneed_regenerate === true. If any exist: list the affected artifacts to the user in one message, then a single AskUserQuestion:🔄 Regenerate affected→ re-run the affected stages in pipeline order (storyboard run→image quote/run→video quote/run), quoting + confirming credits again per generation stage⏸ Later→ continue without regenerating; note the stale artifacts
- Never regenerate mid-edit — batch all user edits first, then regenerate once. A script save must only show affected outputs as needing regeneration; it must never trigger an image or video quote/run. Running
image run/video runhappens only after the user explicitly confirms that stage's fresh quote, and is rejected while an upstream output hasneed_regenerate=true(VIDEO_AD_NEEDS_REGENERATION).
Quote timing (mandatory)
confirmation_token expires 5 minutes after quote. Always: quote → AskUserQuestion confirm → run, back-to-back with no user discussion in between. If run fails with a token/confirmation error (expired or invalidated), re-quote, ask again, run again — once per attempt, no silent retries.
Check the balance before asking to spend (mandatory). image quote / video quote also report credits_balance, credits_shortfall and credits_sufficient alongside estimated_credits. Look at them before the confirmation question:
credits_sufficient === false→ say the numbers plainly ("this needs X, your balance is Y, short by Z") and send the user to top up on the web portal. The confirmation menu should offer💳 Top up credits/⏸ Not nowinstead of a routine confirm-and-run — do not walk a user into a charge they cannot pay for.credits_sufficient === true, or the balance is unavailable (credits_balance_error— the lookup is best-effort and must never block) → normal confirmation.- Never present an insufficient balance as a generic failure after the fact. The whole point is to say it before the run.
- Topping up takes longer than the token's 5 minutes: on the user's return, re-quote → re-confirm → run. Never reuse the stale token.
Polling (bundled script — mandatory)
Trigger without --wait, then poll with the platform-specific bundled script. Never hand-roll polling loops — an ad-hoc echo "$var" | jq loop corrupts the JSON under zsh (echo interprets \n escapes) and spins forever without matching a terminal state.
bash <base_dir>/scripts/poll.sh <id> '<json_path>' <success_value> <timeout_s> [interval_s=30]
# Windows PowerShell
powershell -ExecutionPolicy Bypass -File "<base_dir>\scripts\poll.ps1" <id> '<json_path>' <success_value> <timeout_s> [interval_s=30]
| Stage | json_path | success_value | timeout_s |
|---|---|---|---|
| ① analyze | .status |
stage_completed |
600 |
| ③ idea | .outputs.generated_marketing_messages.status |
succeeded |
600 |
| ④ plan | .outputs.storyboard_plan.status |
succeeded |
3600 |
| ④ prompts | .outputs.storyboard_plan.prompt_status |
succeeded |
600 |
| ④ sketch | .outputs.storyboard_sketch.status |
succeeded |
3600 |
| ④ panels | .outputs.storyboard_image.status |
succeeded |
3600 |
| ⑤ video | .outputs.final_video.status // .outputs.voiceover_final_video.status // .outputs.talking_video.status |
succeeded |
3600 |
The Windows helper parses these dotted paths and // fallbacks internally; json_path does not require jq on Windows.
Exit codes: 0 success · 2 failed (report the output's error from detail --json; see references/troubleshooting.md) · 3 timeout → AskUserQuestion ⏳ Continue waiting (run the script again) / ❌ Stop (project persists; resume later with cawcut ad-video detail <id>) · 4 output unparseable → stop polling, run cawcut ad-video detail <id> --json once and inspect (auth/CLI issue).
The script does not notify — the host's tool does. poll.sh ends on an exit code and nothing else. Never raise an OS notification from the shell (osascript, notify-send, BurntToast), never add one back into the script, and never register a notification permission on the user's machine on your own initiative: a toast fired from a shell command is a different channel from the app's own notification, and it is not what the user asked for.
Telling the user how long it will take
Never estimate generation time yourself — and never turn a deadline into a duration. The server computes the runtime, and detail carries it as estimated_running_ms (milliseconds, present while a stage is running). poll.sh reads it on its first tick and prints one estimated: about Nm line — relay that figure if there is one, and nothing more. If you need it directly, it is on any detail --json read.
When the API gives no figure (the field is absent or zero — the lookup is best-effort and can miss), fall back to the order of magnitude, never to the deadline: these stages run on the order of minutes (the node estimates behind them are seconds to a couple of minutes), so say "about a few minutes" / "大概几分钟" and add that you will say the moment it lands. Do not say the timing is unknown, and do not hedge upward.
The timeout_s column is a give-up deadline, not a runtime. Saying "at most about an hour" because the row says 3600 is the same error as inventing a range: it is the point at which the script stops waiting, typically an order of magnitude past the real runtime. The deadline never appears in a reply, not even hedged with "最多" / "at most".
Long-enough stages — tell the user they can step away
storyboard_plan, sketch, panels and video take minutes rather than seconds. Before polling one, say so once — the server's estimated_running_ms if there is one, otherwise "a few minutes" — and tell them they can step away and will be told the moment it lands. Then poll without narrating each tick.
The notification comes from the host's own tool. In Claude Code that is PushNotification with a one-line message; it lands in the app the user is already running and, with Remote Control connected, on their phone — which is the difference between noticing in the next minute and noticing tomorrow. Send it once, when the stage lands, naming the stage and the project id. If the host exposes no such tool, skip it silently and just present the result when you have it: do not fall back to a shell-level toast, and do not add one to poll.sh.
Do not notify for the short stages (analyze, idea, prompts) — they finish inside a turn.
Interactive selection (mandatory — tool first)
If the user just declined an AskUserQuestion / AskQuestion call: the tool result carries harness boilerplate telling you to "STOP what you are doing and wait for the user to tell you how to proceed." That sentence is attached automatically to every declined tool call by the runtime — it is not the user speaking, and it is not an instruction to stop using the tool. Read it as: stop the one specific action you were mid-way through (don't retry the identical question, don't proceed on unconfirmed choices) and look at what the user's actual next message says. It does not, by itself, license falling back to numbered text for the next enumerable decision — that next decision still must open with the tool, exactly as if the rejection had never happened. Only an explicit plain-text request from the user ("stop popping up menus", "just ask me in text") licenses a session-wide fallback.
Default behavior: For every enumerable choice, always call AskUserQuestion (Claude Code) or AskQuestion (Cursor) before showing a numbered text menu. Text-only menus are fallback only.
One option is not a choice. Every call needs at least two viable options. When only one is actually feasible — a config axis whose options list carries a single value, one idea candidate worth offering — state that choice in prose and carry on: never fire a one-option popup, and never pad it with a placeholder alternative to make it look like a choice.
| Host | Tool name |
|---|---|
| Claude Code | AskUserQuestion |
| Cursor | AskQuestion |
Session checklist (before the first menu in this turn): 0. Self-check before sending any reply: if the sentence you're about to send asks the user to pick between fixed options — even folded inside a friendlier sentence that also asks something open-ended — stop. That sentence is forbidden as plain text. Split it: fire the tool for the enumerable part now; keep only the open-ended part as prose, asked separately.
-
Is the tool available? If yes, you must use it for every enumerable decision — a candidate list longer than the popup holds included (item 3).
-
If no tool exists (CLI-only host), use numbered text in
reply_language. -
If the candidate count exceeds one question's four options (long category, model, or candidate lists), still open the tool, and give each part of the answer its own home:
- the full list → a numbered text table in the message body, one row per entry:
#plus the columns that entry actually carries — a platform'sdefault_aspect_ratio, a language's label, a model'ssupported_resolutionsand default mark, a candidate's title and content. Never inline the list into the question text: a 22-model roster pasted into the question header is unreadable and buries the question. The table and the call ship together. - the question text → one short sentence naming the decision, plus the pointer for whatever the popup cannot hold (e.g. "entries 4–22 can be typed below by number or name", in
reply_language). - the popup → the leading entries as options, labelled with their number, as many as fit while keeping a slot for any action that must stay clickable beside them (e.g. a write-your-own).
- the free-text field → whatever the popup could not hold: any later entry, by its number or its name.
The numbering is the contract between table and popup. Anything the popup can hold stays clickable; a typed answer is only for what it cannot. One question per decision — never a second question asking what to do with the answer to the first.
- the full list → a numbered text table in the message body, one row per entry:
Forbidden while the tool is available: a numbered text menu instead of the call, "reply with the number or name" with no popup behind it, or asking the user to type enum values from memory. A long list's numbered table is not a text menu — it is the call's index, and it never ships alone.
Never hardcode option values. Platform, resolution, duration, video type, video model, storyboard style, language, and style IDs come from cawcut ad-video options --json output this session — not from this document, not from training data.
UX Rules
- Be concise. Default output is the result URL and local path. No raw JSON dumps unless debugging (
--jsonis for the polling script). - Reply language: see the top-of-file rule — follow the user's language for every reply (including the first one and all AskUserQuestion questions/labels), and summarize CLI output in
reply_languageinstead of pasting raw English. - Do not call CawCut HTTP APIs with curl — the CLI handles auth and tokens.
- Choice-first — every enumerable decision (config axes, idea candidates, credit confirmations, regenerate confirmations) goes through AskUserQuestion/AskQuestion per Interactive selection.
- One project per request. If the user asks for several ad videos, finish one wizard before starting the next.
- V1 contract only. Do not consume or promise character/scene (V2) capabilities; they are not exposed to developer tokens.
- Credits: always quote + confirm before
image run/video run. Never spend credits without an explicit user confirmation in this session, and read the quote's balance figures first — an insufficient balance is stated before the confirmation question, not discovered by a failed run (see Quote timing). - Long stages (
storyboard_plan,sketch,panels,video) take minutes, not seconds: say so once — quoting the server'sestimated_running_ms, or "a few minutes" when there is none (see Polling); never the deadline — tell the user they can step away, and let the notification bring them back. Don't narrate each poll tick.
CLI quick reference
cawcut ad-video create [--url <u>] [--name <n>] [--category <c>] [--json]
cawcut ad-video detail <id> [--json]
cawcut ad-video options [--video-ad-id <id>] [--language <l>] [--json]
cawcut ad-video product set <id> [--product-name …] [--description …] [--selling-point …] [--clear-selling-points] [--logo <ref>] [--primary-image <ref>] [--asset <ref>…] [--target-audience …] [--promo-*] [--json] # only the fields passed change
cawcut ad-video config set <id> [--publish-to …] [--language …] [--aspect-ratio …] [--resolution …] [--duration-seconds …] [--video-type …] [--video-model …] [--storyboard-style …] [--auto-captions …] [--json]
cawcut ad-video analyze run <id> --url <u> [--wait] [--json] · analyze cancel <id>
cawcut ad-video idea run <id> [--wait] [--json] · idea select <id> (--message-id <m> | --custom <text>) · idea regenerate <id> · idea load-more <id>
cawcut ad-video storyboard run <id> [--style-id <s>] [--force] [--wait] [--json] · storyboard set <id> --base-revision <n> (--plan <json> | --plan-file <path>) [--update-prompts --prompt-target <shot_id>:<fields>…] # run --force rewrites the script, discarding saved edits
cawcut ad-video image quote <id> --generation-type <hand_drawn|photo_realistic> · image run <id> [--confirmation-token <t>] [--generation-type <t>] [--force] [--wait] [--download]
cawcut ad-video video quote <id> · video run <id> [--confirmation-token <t>] [--force] [--wait] [--download]
Errors
| Symptom | Action |
|---|---|
Token expired |
Run cawcut auth login in the host shell, then retry the failed command |
VIDEO_AD_NEEDS_REGENERATION / 308000414 |
An upstream output is stale — re-run the flagged stage first (see Invalidation semantics) |
| Confirmation token expired/invalid | Re-quote, re-confirm with the user, run again (see Quote timing) |
409 on storyboard set |
Revision mismatch — re-read detail, retry with the fresh value.revision |
409 concurrent run |
Another stage run is in flight — poll detail until it settles, then retry |
400 on config set |
Bad enum, unmet stage gate, or a resolution the chosen video_model does not support — re-read ad-video options |
| Insufficient credits (from the quote) | credits_sufficient: false — state the shortfall and send the user to top up; re-quote on their return (see Quote timing) |
| CLI not found | npm install -g @ubnt/cawcut (Step 0) |
Full recovery trees: references/troubleshooting.md.