Build with a chat
Copy everything below, paste it into the chat you already use (ChatGPT, Claude, Gemini…), and send. Write what you want in the box below and it goes in at the very end (My request). When the chat answers with whipset JSON, paste it into 📥 Import in the Select whipset dropdown of the extension panel. The <<var.key>> tokens below show up as {{var.key}} in the panel builder. If something needs fixing, the panel gives you a list — paste that list back into the chat.
You are going to design a whipset for WhipWhip and write it as one JSON object. Read this whole guide first. At the very end, after "My request", I describe what I want. Ask me at most one short question if something essential is missing; otherwise answer with the JSON right away.
What WhipWhip is
WhipWhip is a browser extension. It puts a panel on top of the web chat I use (ChatGPT, Claude, Gemini, Grok or Perplexity) and runs saved procedures in that chat for me: it types into the chat input box, sends, waits for the answer, reads the answer, stores parts of it, and shows results.
- A whipset is a named collection of whips. It is what you are writing.
- A whip is one button. Pressing it runs its cards from top to bottom.
- A card is one step: write a prompt, send, save the reply into a variable, ask me something,
move to another conversation, show a result, and so on. Every card has a
cardid and aconfig. - A variable is a named value the whips of this set share. Values survive between runs, so one whip can store something that another whip reads later.
- A tag is a role: a name for one specific conversation (for example
authororcritic). Whips can put a conversation into a role and later move back to it. That is how work passes between two separate chats. The panel shows only the whips of the current chat's role — an untagged chat shows the main list.
WhipWhip does not call any API. Everything happens in the real chat page, exactly as if I typed it. So a whip can only do what a person could do in that page, one card at a time.
How the cards fit together
Talking to the chat. prompt_write puts text into the input box (it does not send).
submit sends it and waits until the answer is complete. After submit, the answer is available.
Keeping the answer. var_save with "valueSource": "var.last.assistant" stores the whole
latest answer in a variable. That is the normal way to keep a result. ask_and_extract is for
pulling short named values out of an answer (a title, a score, a tag) — it adds an instruction
to the prompt asking the model to append tagged values, then reads them. Do not use it to capture a
whole long answer. extract_response tidies this chat's latest answer into a JSON shape you set, in a separate tab; use it
rarely. validate checks that the latest answer has a format (JSON, a regex, or simply not empty)
and asks the chat again if it does not.
Asking me. Two ways:
- Put a
user_inputvariable into a prompt as a token. Before the whip runs, WhipWhip shows me a small form for everyuser_inputvariable the whip needs. This is the default and usually best. input_promptasks me in the middle of a run, right where the card is. Use it when the question only makes sense at that moment (for example "name this clipping" after reading an answer), or with"alwaysAsk": trueto confirm a value that is already filled in.
choice_prompt lets me pick from options — a fixed list (one option per line in options) or the
items of a list variable.
Showing results. show_result_modal opens a window with text built from variables. Use
"copyButton": "copy_body" when I will probably copy it.
Moving between conversations. new_chat opens a fresh conversation on the same service.
open_service goes to another service. open_url opens an address. tag_current_chat gives the
conversation the run is standing in a role. go_to_role moves the run to the conversation holding
that role; with "closeCurrent": "true" it closes the tab it leaves. A role must be given
(tag_current_chat) before any whip can go to it (go_to_role).
Whips open the role chats. Don't add a "start" whip that seats a role in each tab. The whip that
needs a role opens the chat on the spot: tag the current chat (tag_current_chat) → new_chat
("openMode": "new_tab") → send the role and the job as one message (prompt_write → submit) →
tag the new chat (tag_current_chat).
Which list a whip stands in. A whip without scopeTags shows in the main list (untagged
chats) — the set's entry points and the whips that open chats go here. A whip with
scopeTags: ["critic"] shows only in that role's chat — whips that read that chat's answer go here.
A set takes one of four shapes.
- One chat — no tags; it runs in the current chat only (polishing, translating).
- Lightweight set — no tags, 3–4 whips around one value I keep (my style, my résumé, an art
style). A "set my …" whip always asks for it (
input_promptwith"alwaysAsk": true); every other whip asks only while it is empty and puts it into its prompt as<<var.key>>. The value is a"scope": "set"variable, so I set it once and every chat uses it. The Writing Helper example below has this shape. - Side chat — the main chat is the one I work in. A whip opens a side chat for objections or
sources; the side chat's whip carries the result back with
go_to_role(closeCurrent) and closes the tab. The example below has this shape. The tag on the main chat (authorin the example) is a role — "the chat where the draft is written" — not a one-off marker. The whip that opens the side chat gives its answer-readingvar_save"refuseIfEmpty": true, so it stops in an empty chat before tagging anything. - Control room — the main chat holds the progress and opens a fresh role chat whenever it needs one (a study coach).
Lists. A variable with "valueType": "array" holds a list of records. append_item adds a
record (fields is a list of name/value pairs, and autoId gives each record an id),
upsert_item adds or replaces by id, patch_item changes some fields of one record,
remove_item deletes one. derive_index turns a list into text (one line per item, from a
template with <<item.field>> tokens) and stores that text in another variable, so a prompt or a
result window can show it. Lists are how a set remembers things across many runs (a notebook, a
library, a log).
Variables.
source: "user_input"— I provide it (form before the run, orinput_prompt).source: "set_by_action"— a card writes it:var_save,input_prompt,choice_prompt,ask_and_extract,extract_response,validate(itsvariable),derive_indexand the list cards.source: "constant"— a fixed text indefaultValue."scope": "set"— the value belongs to the set, not to one workspace: every tab and workspace that uses this set sees the same value. Use it for what I set once and keep (my writing style, my résumé, an art style). Only onuser_inputandset_by_action— aconstantcannot have it.var_flushdoes not clear it. Sharing or posting the set leaves the value out unless I tick the box to include it. Leavescopeout for values that belong to one piece of work.- Text I already have (finished prompts, say) goes in as is through
input_prompt, not through an AI. GiveselectedVarsseveral fields and it asks for each in turn. Turn on"requireAnswer": truewhen the steps after it mean nothing with an empty field. Example: the Prompt Factory's "Send my 4 prompts" takes 4 pasted prompts intop1–p4("scope": "set") as is, then goes to the painter chat and sends them one at a time. - Keys use only letters, digits and
_. Labels are what I see, so write them in my language. - Every variable a card uses must be declared in
variables.
Tokens
Inside card text, a variable is written as <<var.key>> — two angle brackets on each side.
Inside a list template it is <<item.field>>. <<now>> is today's date.
Never write tokens with curly braces. WhipWhip converts <<…>> itself.
The JSON shape
{
"format": "whipwhip.authoring/1",
"name": "…", set name, in my language
"description": "…", optional, what the set does and how to use it
"tags": [ { "name": "writer" } ], roles; 3–10 characters of A–Z a–z 0–9 _ -
"variables": [ { "key": "…", "label": "…", "source": "…", "required": true } ], "whips": [
{
"label": "…", button text, short
"description": "…", optional
"preflightStyle": "form", optional: "form" (default) or "none"
"scopeTags": [ "writer" ], optional: the role conversation this whip belongs to
"actions": [ { "card": "prompt_write", "config": { "text": "…" } } ]
}
]
}
Do not write ids, usedVars, requiresVars, producesVars, askInline, keys, signatures or
versions. WhipWhip fills those in.
Card reference
prompt_write
Type text into the message box, or change what's already there.
text— text, required, accepts <<var.key>> tokensmode— one ofreplace·append·prependpreflight— true or false. Before the run, show a form to fill in this prompt's <<var.key>> variables.
submit
Send what's in the message box and wait for the AI's reply. You can turn waiting off.
skipWait— true or false. Move on right after sending. Use this when the whip doesn't need the reply, e.g. sending the same prompt to several tabs.
input_prompt
Pause the run and ask you to type a value.
title— textselectedVars—[{ "key": …, "question": … }], requiredalwaysAsk— true or false. Ask even if the variable already has a value, so you can check a pre-filled or AI-drafted value before it's used.requireAnswer— true or false. When on, you can't leave the answer empty. Use it when the action needs a value.
ask_and_extract
Ask the AI something and save the parts you name into variables.
text— text, required, accepts <<var.key>> tokenspreflight— true or false. Before the run, show a form to fill in this prompt's <<var.key>> variables.varsToExtract—[{ "key": …, "description": … }], required
extract_response
Turn the AI's last answer here into JSON in a variable. It uses a temporary chat that stays in history outside ChatGPT.
key— variable key, required. The variable to save the value in.schema— text, required. The JSON shape the AI should return. e.g. {"title": "Title", "tags": ["tag"]}promptSuffix— text. Optional extra text appended to the extraction prompt.retries— number. How many times to ask again if the reply isn't valid JSON.
validate
Check that the reply has the right format and ask again if it doesn't. It checks format only, not content.
source— one oflast_assistant·var. The last reply, or a saved variable. A variable can only be checked, because asking again can't change a past value.key— variable key, used when:source=varrule— one ofjson·regex·non_empty·whipset, required. Valid JSON: the reply is JSON (you can require keys). Matches pattern: the reply matches a regular expression. Not empty: any non-blank reply. WhipWhip whipset JSON: a whipset the AI wrote; if it's wrong, the re-ask lists what to fix.pattern— text, used when:rule=regexrequiredKeys— text, used when:rule=json. Comma-separated top-level keys that must exist.retries— number. How many times to send a correction prompt before failing. 0 = check only.correction— text, accepts <<var.key>> tokens. Sent when the format does not match. Leave empty for a default message.variable— variable key
send_to_services
Send the same text to several AIs and collect the answers. You can also just send and tag, or read the tagged chats.
servicesVar— variable key, required. The variable with the list of AIs to send to. Usually filled by Ask to choose with the "AI services" source and Allow multiple on.text— text, required, accepts <<var.key>> tokens. Sent as-is to every AI. You can use variables.tagPrefix— text. If set, each AI's chat is tagged name_gpt, name_cla and so on, so you can follow up later. Up to 6 letters (Korean characters count as 2).key— variable key, required. Each AI's answer (or why it failed), under its own heading. With Send and tag only, it holds just the reasons for AIs that couldn't be reached.mode— one ofcollect·send·read. Wait and collect: wait for every answer and save them in one variable. Send and tag only: send and stop; the answers stay in their tabs. Read the tagged chats: send nothing and read the latest answer in each tagged chat.
var_save
Save text from the message box, the AI's reply or the page into a variable.
valueSource— one of the value sources below, orvar.<variable key>, required. Where to read the value from.key— variable key, required. The variable to save the value in.valueFilter— one of""·code, used depending onvalueSourcerefuseIfEmpty— true or false, used depending onvalueSource. If nothing was read, stop and say why instead of carrying on with an empty value.
var_flush
Clear variable values in this whipset. The variables stay, and lists, JSON objects and fixed values keep their contents.
No settings — use "config": {}.
append_item
Add an item to a list variable (e.g. a new note to your collected notes).
key— variable key, required. The list variable to change. Its type must be List.fields—[{ "name": …, "value": … }], required, accepts <<var.key>> tokens. Fields of the new item (name → value). Values can use <<var.x>>. Quotes and line breaks are handled for you.autoId— true or false. Give each new item a numbered id when you don't set one yourself.idPrefix— text, used when:autoId=true. Text before the number, e.g. "memo-" gives memo-1, memo-2 …
upsert_item
Add an item to a list, or replace the item that has the same id.
key— variable key, required. The list variable to change. Its type must be List.fields—[{ "name": …, "value": … }], required, accepts <<var.key>> tokens. Fields of the item to add or replace. Include an "id" field. An item with the same id gets replaced.
patch_item
Change only some fields of the list item with a given id. Its other fields stay (e.g. set status to done).
key— variable key, required. The list variable to change. Its type must be List.itemId— text, required, accepts <<var.key>> tokens. The id of the item to change. You can use <<var.x>>.fields—[{ "name": …, "value": … }], required, accepts <<var.key>> tokens. Only these fields are updated; the item's other fields are kept. Values may use <<var.x>> tokens.
remove_item
Remove the item with a given id from a list.
key— variable key, required. The list variable to change. Its type must be List.itemId— text, required, accepts <<var.key>> tokens. The "id" of the item to remove. Supports <<var.x>> tokens.
derive_index
Turn a list into text with one line per item, or just count the items.
sourceKey— variable key, required. The list to summarize.key— variable key, required. The variable that gets the summary text.mode— one ofjoin·count. Summary text (default), or just the number of items left after filtering.itemTemplate— text, required when used, accepts <<item.field>> tokens, used depending onmode. Written once for each item, one per line. Use <<item.field>>, e.g. <<item.id>> — <<item.title>>.filterField— text. Keep only items matching the value below. Leave blank to include all.filterOp— one ofeq·ne, used depending onfilterField. If an item doesn't have the field, it counts as empty.filterValue— text, accepts <<var.key>> tokens, used depending onfilterField. The value the filter field is compared against. Supports <<var.x>> tokens.sortField— text. Sort by this field. Numbers sort correctly, so memo-2 comes before memo-10. Leave empty to keep the order items were added.sortDir— one ofasc·desc, used depending onsortFieldlimit— number. Keep at most this many items. Leave blank for all.limitFrom— one ofend·start, used depending onlimit
go_to_role
Move to the conversation with this tag.
tagName— tag nametagCandidates— list of tag names. Pick the tags this action can go to. Leave empty to always go to the tag above. It runs only when every picked tag is on a chat.tagVar— variable key. Variable that holds the tag to go to (e.g. from Ask to choose). Used only when possible tags are picked.closeCurrent— one offalse·true
go_to_start
Go back to the tab where the whip started, and close this one if you want. Useful after a side question.
closeCurrent— one oftrue·false
new_chat
Open a new conversation on the current service.
isTemp— one offalse·trueopenMode— one ofcurrent·new_tab·background
open_service
Switch to a different AI service (ChatGPT, Gemini, Claude…).
service— one ofchatgpt·gemini·claude·grok·perplexity, requiredopenMode— one ofcurrent·new_tab·background
open_url
Open any https:// web address. Use it as the last action, because the whip stops there.
target— text, requiredopenMode— one ofcurrent·new_tab·background
tag_current_chat
Tag this chat so other whips can come back to it.
tagName— tag name, requiredrunOnly— true or false. Remove the tag when the run ends. The chat stays in the workspace list.
choice_prompt
Show a list of options and save the pick in a variable.
label— text, accepts <<var.key>> tokensoptionsSource— one ofstatic·variable·services. Where to get the choice options from. "AI services" fills in ChatGPT, Claude, Gemini, Grok and Perplexity.options— text, required when used, used depending onoptionsSource. Each line becomes a button. The chosen line is saved to the variable.dynamicOptionsVar— variable key, required when used, used when:optionsSource=variable. The variable that holds the options, one per line.optionLabel— text, accepts <<item.field>> tokens, used when:optionsSource=variable. What each option shows. Use <<item.field>>, e.g. <<item.id>> — <<item.title>>.optionValueField— text, used when:optionsSource=variable. Which item field is saved as the pick (default "id"). Use it as the item id in a later Edit list item fields or Remove from list action.filterField— text, used when:optionsSource=variable. Offer only items whose field matches the value below. Leave blank to offer all. Applies to list variables only.filterOp— one ofeq·ne, used depending onfilterField. If an item doesn't have the field, it counts as empty.filterValue— text, accepts <<var.key>> tokens, used depending onfilterField. The value the filter field is compared against. Supports <<var.x>> tokens.multiple— true or false. Show a checklist; on OK the checked values are saved as a list.lockCurrentService— true or false, used when:optionsSource=services,multiple=true. With Allow multiple on, the AI in the tab that ran the whip is marked "current tab" and stays checked.warnInConversation— true or false, used when:optionsSource=services,multiple=true. With Allow multiple on, show a note when this tab already has a conversation, since only this tab's AI sees that context.continueGroup— text, used when:optionsSource=services,multiple=true. With Allow multiple on, if this tab is in that tag group, add a "Follow up" row at the top (on by default) and pre-check those AIs. Use the same group tag name as in Send to several AIs.rememberAs— text, used when:multiple=true. Give a name to remember this pick and pre-check it next run. The same memory is used from any tab or workspace.key— variable key, required. The variable the chosen option is saved in.
show_result_modal
Show a window with your results, filled in from variables.
title— text, accepts <<var.key>> tokensbody— text, required, accepts <<var.key>> tokenscopyButton— one ofnone·copy_body
import_whipset
Add a whipset the AI wrote, from JSON saved in a variable. You review it in a preview before anything is saved.
key— variable key, required. Usually the variable saved by an earlier Validate action (whipset rule).
Value sources (var_save.valueSource)
builtin.date_today · builtin.date_iso · builtin.time_now · builtin.timestamp · builtin.day_of_week · builtin.page_url · builtin.page_title · builtin.page_domain · builtin.language · builtin.random_uuid · builtin.selection · composer · var.last.user · var.last.assistant
Common mistakes
- Writing tokens with curly braces instead of
<<var.x>>. - A
set_by_actionvariable that no card writes, or a variable used in text but not declared. - Reading the answer (
var_savewithvar.last.assistant) without asubmitbefore it. - Forgetting
submitafterprompt_writewhen the prompt should actually be sent. - A list card on a variable without
"valueType": "array". go_to_roleto a role that no whip ever gives withtag_current_chat, or a tag not declared intags.- A tag name longer than 10 characters, with spaces, or named
main. - Setting fields that do not exist on a card, or inventing card ids.
- Using
ask_and_extractto capture a long answer — usevar_savewithvar.last.assistant. - One giant whip that does everything. Prefer a few whips with clear buttons.
Examples
One conversation: polish a draft and show it
{
"format": "whipwhip.authoring/1",
"name": "Draft Polisher",
"description": "Rewrite a rough draft clearly and show the result.",
"tags": [],
"variables": [
{ "key": "draft", "label": "Draft", "source": "user_input", "required": true },
{ "key": "polished", "label": "Polished text", "source": "set_by_action", "required": false }
],
"whips": [
{
"label": "Polish",
"actions": [
{ "card": "prompt_write", "config": { "text": "Rewrite the draft below so it is clear and concise. Keep my meaning and tone. Reply with the rewritten text only.\n\n<<var.draft>>", "mode": "replace" } },
{ "card": "submit", "config": {} },
{ "card": "var_save", "config": { "valueSource": "var.last.assistant", "key": "polished" } },
{ "card": "show_result_modal", "config": { "title": "Polished", "body": "<<var.polished>>", "copyButton": "copy_body" } }
]
}
]
}
Two roles: have my answer reviewed in a side chat
{
"format": "whipwhip.authoring/1",
"name": "Review and revise",
"description": "In the chat you are working in, press 'Get this reviewed' — a reviewer answers in a new tab. In that chat, press 'Apply the review' to come back and revise.",
"tags": [ { "name": "author" }, { "name": "reviewer" } ],
"variables": [
{ "key": "draft", "label": "Draft", "source": "set_by_action", "required": false },
{ "key": "review", "label": "Review", "source": "set_by_action", "required": false }
],
"whips": [
{
"label": "Get this reviewed",
"actions": [
{ "card": "var_save", "config": { "valueSource": "var.last.assistant", "key": "draft", "refuseIfEmpty": true } },
{ "card": "tag_current_chat", "config": { "tagName": "author" } },
{ "card": "new_chat", "config": { "isTemp": "false", "openMode": "new_tab" } },
{ "card": "prompt_write", "config": { "text": "In this chat you are a demanding reviewer. Name the weak points of the draft below, weakest first, one per line. Don't rewrite it.\n\n[Draft]\n<<var.draft>>", "mode": "replace" } },
{ "card": "submit", "config": {} },
{ "card": "tag_current_chat", "config": { "tagName": "reviewer" } }
]
},
{
"label": "Apply the review",
"scopeTags": [ "reviewer" ],
"actions": [
{ "card": "var_save", "config": { "valueSource": "var.last.assistant", "key": "review" } },
{ "card": "input_prompt", "config": { "title": "Review", "selectedVars": [ { "key": "review", "question": "Revise with this review? Delete any line you disagree with first." } ], "alwaysAsk": true } },
{ "card": "go_to_role", "config": { "tagName": "author", "closeCurrent": "true" } },
{ "card": "prompt_write", "config": { "text": "Revise your last answer with this review. Reply with the full revised text only.\n\n<<var.review>>", "mode": "replace" } },
{ "card": "submit", "config": {} }
]
}
]
}
A list that grows: a notebook of answers
{
"format": "whipwhip.authoring/1",
"name": "Answer Notebook",
"tags": [],
"variables": [
{ "key": "clip", "label": "Clipped answer", "source": "set_by_action", "required": false },
{ "key": "clip_title", "label": "Title", "source": "set_by_action", "required": false },
{ "key": "notes", "label": "Notes", "source": "set_by_action", "valueType": "array", "required": false },
{ "key": "notes_index", "label": "Note list", "source": "set_by_action", "required": false },
{ "key": "notes_full", "label": "All notes", "source": "set_by_action", "required": false },
{ "key": "summary", "label": "Summary", "source": "set_by_action", "required": false }
],
"whips": [
{
"label": "Save this answer",
"actions": [
{ "card": "var_save", "config": { "valueSource": "var.last.assistant", "key": "clip" } },
{ "card": "input_prompt", "config": { "title": "Save this answer", "selectedVars": [ { "key": "clip_title", "question": "Give it a short title" } ], "inputStyle": "inline", "alwaysAsk": true } },
{ "card": "append_item", "config": { "key": "notes", "autoId": true, "idPrefix": "note-", "fields": [ { "name": "title", "value": "<<var.clip_title>>" }, { "name": "text", "value": "<<var.clip>>" }, { "name": "date", "value": "<<now>>" } ] } },
{ "card": "derive_index", "config": { "sourceKey": "notes", "key": "notes_index", "itemTemplate": "<<item.id>> — <<item.title>> (<<item.date>>)" } },
{ "card": "show_result_modal", "config": { "title": "Notes", "body": "<<var.notes_index>>" } }
]
},
{
"label": "Summarize my notes",
"actions": [
{ "card": "derive_index", "config": { "sourceKey": "notes", "key": "notes_full", "itemTemplate": "### <<item.title>>\n<<item.text>>" } },
{ "card": "new_chat", "config": { "openMode": "current" } },
{ "card": "prompt_write", "config": { "text": "Summarize what these notes have in common and what they add up to.\n\n<<var.notes_full>>", "mode": "replace" } },
{ "card": "submit", "config": {} },
{ "card": "var_save", "config": { "valueSource": "var.last.assistant", "key": "summary" } },
{ "card": "show_result_modal", "config": { "title": "Summary", "body": "<<var.summary>>", "copyButton": "copy_body" } }
]
}
]
}
Lightweight set: one writing style for every chat
{
"format": "whipwhip.authoring/1",
"name": "Writing Helper",
"description": "Set your writing style once, then polish or resize text in any chat with one click.",
"tags": [],
"variables": [
{ "key": "style", "label": "Style", "source": "user_input", "required": false, "scope": "set" },
{ "key": "length", "label": "Length", "source": "set_by_action", "required": false }
],
"whips": [
{
"label": "Set my style",
"preflightStyle": "none",
"actions": [
{ "card": "input_prompt", "config": { "title": "Style", "selectedVars": [ { "key": "style", "question": "What style should the writing use? (e.g. plain and friendly, short sentences)" } ], "inputStyle": "inline", "alwaysAsk": true } },
{ "card": "prompt_write", "config": { "text": "From now on in this chat, write in this style: <<var.style>>\nIf that is clear, reply only \"OK\".", "mode": "replace" } },
{ "card": "submit", "config": {} }
]
},
{
"label": "Polish",
"preflightStyle": "none",
"actions": [
{ "card": "input_prompt", "config": { "title": "Style", "selectedVars": [ { "key": "style", "question": "What style should the writing use? (e.g. plain and friendly, short sentences)" } ], "inputStyle": "inline" } },
{ "card": "prompt_write", "config": { "text": "Polish the text you just wrote. Keep the meaning and content; fix only awkward sentences, spelling and repetition. Style: <<var.style>>\nShow only the polished text.", "mode": "replace" } },
{ "card": "submit", "config": {} }
]
},
{
"label": "Change length",
"preflightStyle": "none",
"actions": [
{ "card": "input_prompt", "config": { "title": "Style", "selectedVars": [ { "key": "style", "question": "What style should the writing use? (e.g. plain and friendly, short sentences)" } ], "inputStyle": "inline" } },
{ "card": "choice_prompt", "config": { "label": "Which way?", "optionsSource": "static", "options": "shorter\nlonger", "key": "length" } },
{ "card": "prompt_write", "config": { "text": "Rewrite the text you just wrote to make it <<var.length>>. Keep the key points. Style: <<var.style>>\nShow only the rewritten text.", "mode": "replace" } },
{ "card": "submit", "config": {} }
]
}
]
}
Output rules
- Think about which whips I will press and in what order, then design the smallest set that works.
- Write names, labels, questions, prompts and result titles in the language I write my request in. Card ids, field names, variable keys and tag names stay as in this guide.
- Answer with one JSON code block containing the whole whipset. You may add at most two short sentences after the block telling me which button to press first.
- If I paste back a list of problems from WhipWhip, fix every item and send the complete corrected JSON again, not a partial patch.