Build with a chat
Copy everything below, paste it into the chat you already use (ChatGPT, Claude, Gemini…), write what you want at the very end, and send. When the chat answers with whipset JSON, copy it into the import field of the WhipWhip panel. 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
writerorcritic). Whips can put the current conversation into a role and later move back to it. That is how one whip passes work between two separate chats.
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 asks for structured JSON in a separate temporary 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. A role must be given (tag_current_chat) before any whip can go to it (go_to_role),
so a two-role set usually has one short "start" whip per role that primes the chat and tags it:
prompt_write (who you are in this conversation) → submit → tag_current_chat.
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.- 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, one sentence
"inline_guide": "…", optional, how to use the set
"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
Write or modify text in the chat window.
text— text, required, accepts <<var.key>> tokensmode— one ofreplace·append·prependpreflight— true or false. Show an input form for <<var.key>> tokens in this prompt before executinginputStyle— one ofform·inline, used when:preflight=true
submit
Send the current text, and wait for the AI's reply unless you turn waiting off.
skipWait— true or false. Move on right after sending. Use this when the whip never reads the reply — e.g. broadcasting the same prompt to several tabs.
input_prompt
Ask the user to provide necessary information.
title— textselectedVars—[{ "key": …, "question": … }], requiredinputStyle— one ofform·inlinealwaysAsk— true or false. Show this input even when the variable already has a value, so a pre-filled or LLM-drafted value can be reviewed before it is committed
ask_and_extract
Prompt the AI and extract key information as variables automatically.
text— text, required, accepts <<var.key>> tokenspreflight— true or false. Show an input form for <<var.key>> tokens in this prompt before executinginputStyle— one ofform·inline, used when:preflight=truevarsToExtract—[{ "key": …, "description": … }], required
extract_response
Analyze the AI response and convert it into structured data. Opens a temporary tab in the foreground while it works.
key— variable key, required. The variable name to store the captured value in.schema— text, required. Describe the shape of the JSON you expect the AI to return.promptSuffix— text. Optional extra text appended to the extraction prompt.retries— number. How many times to re-ask if the response is not valid JSON.
validate
Check the reply against a FORMAT rule and re-ask if it does not match. Format only, never quality.
source— one oflast_assistant·var. The last reply, or a stored variable. A variable is checked only — re-asking cannot change a past value.key— variable key, used when:source=varrule— one ofjson·regex·non_empty·whipset, required. JSON: parses (optionally with required keys). Regex: matches a pattern. Not empty: any non-blank reply. Whipset: a WhipWhip whipset written by the chat — if it is wrong, the re-ask carries the list of fixes.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. Sent when the format does not match. Leave empty for a default message.variable— variable key
send_to_services
Send the same text to the chosen AIs and gather every answer in one variable. Other AIs open in background tabs.
servicesVar— variable key, required. Variable holding a JSON array of AI service ids. Usually filled by a Choice with the "AI services" source and multiple selection.text— text, required, accepts <<var.key>> tokens. Sent unchanged to every AI. Variable tokens are allowed.tagPrefix— text. When set, each AI's chat is tagged "name_abbr" (gpt, gem, cla, grk, ppx) so the next run can follow up. Up to 6 characters.key— variable key, required. Gets a heading and the answer (or why it failed) for each AI.
var_save
Save text from the composer or AI response 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 name to store the captured value in.valueFilter— one of""·code, used depending onvalueSource
var_flush
Clear all stored variable values.
No settings — use "config": {}.
append_item
Add an item to an array variable (e.g. append a record to an ADR list).
key— variable key, required. An array-typed variable to mutate. Set the variable's type to "array" first.fields—[{ "name": …, "value": … }], required, accepts <<var.key>> tokens. Fields of the record to append (name → value). Values may use <<var.x>> tokens; quotes and newlines are handled for you.autoId— true or false. Give each new item a stable, incrementing id when you don't supply one yourself.idPrefix— text, used when:autoId=true. Prepended to the number, e.g. "adr-" produces adr-1, adr-2, …
upsert_item
Insert or update an item in an array variable, matched by its "id" field.
key— variable key, required. An array-typed variable to mutate. Set the variable's type to "array" first.fields—[{ "name": …, "value": … }], required, accepts <<var.key>> tokens. Fields of the record to insert or update. Include an "id" field — it matches the existing item to replace.
patch_item
Update only the named fields of an item matched by id, keeping its other fields (e.g. flip status).
key— variable key, required. An array-typed variable to mutate. Set the variable's type to "array" first.itemId— text, required, accepts <<var.key>> tokens. The "id" of the item to patch. Supports <<var.x>> tokens.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 an item from an array variable by its "id".
key— variable key, required. An array-typed variable to mutate. Set the variable's type to "array" first.itemId— text, required, accepts <<var.key>> tokens. The "id" of the item to remove. Supports <<var.x>> tokens.
derive_index
Build a summary/index string from an array variable, one line per item.
sourceKey— variable key, required. The array variable to summarize.key— variable key, required. The variable that receives the derived index string.mode— one ofjoin·count. Index text (default), or just how many items are left after filtering.itemTemplate— text, required when used, accepts <<item.field>> tokens, used depending onmode. Rendered once per item and joined by newline. Use <<item.field>> tokens, 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. A missing field counts as an empty value either way.filterValue— text, accepts <<var.key>> tokens, used depending onfilterField. The value the filter field is compared against. Supports <<var.x>> tokens.sortField— text. Order items by this field before rendering. Numeric-aware, so adr-2 comes before adr-10. Leave blank to keep insertion order.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 the run to the conversation a role is bound to.
tagName— tag nametagCandidates— list of tag names. Pick the roles this step may route to. Leave empty for fixed routing. The run is refused unless every candidate is bound.tagVar— variable key. Variable holding the chosen role name (e.g. filled by a Choice step). Only used when candidates are picked.
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·perplexityopenMode— one ofcurrent·new_tab·background
open_url
Navigate to any https:// URL.
target— textopenMode— one ofcurrent·new_tab·background
tag_current_chat
Assign a tag to the current conversation.
tagName— tag name, required
choice_prompt
Show the user a list of options and store their pick in a variable.
label— textoptionsSource— 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. Saved to variable as the chosen line.dynamicOptionsVar— variable key, required when used, used when:optionsSource=variable. Variable containing options (one per line). Parsed at runtime.optionLabel— text, accepts <<item.field>> tokens, used when:optionsSource=variable. Per-item label template for each choice. Use <<item.field>> tokens, e.g. <<item.id>> — <<item.title>>.optionValueField— text, used when:optionsSource=variable. Which item field becomes the picked value (default "id"). Feed it to a later Patch list item or Remove from list step as the item id.multiple— true or false. Show a checklist; on OK the checked values are saved as a JSON array.lockCurrentService— true or false, used when:optionsSource=services. With multiple selection, mark the AI of the tab that ran the whip as "current tab" and keep it checked and locked.warnInConversation— true or false, used when:optionsSource=services. With multiple selection, note above the checklist when the running tab already has a conversation, since only its AI answers with that context.continueGroup— text, used when:optionsSource=services. With multiple selection, when the running tab belongs to the "name_abbr" tag group, add a "Follow up" row on top (on by default) and pre-check those AIs. Match the tag name 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. Variable key where the chosen option will be stored.
show_result_modal
Display a result window using stored variable values.
title— textbody— text, requiredcopyButton— one ofnone·copy_body
import_whipset
Import a chat-written whipset JSON held in a variable. You confirm it in a preview before anything is saved.
key— variable key, required. Usually the variable a preceding Validate card (whipset rule) stored.
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: a writer and a critic in separate chats
{
"format": "whipwhip.authoring/1",
"name": "Writer and Critic",
"inline_guide": "Open a chat and press Start writer, open another chat and press Start critic. Then run Write and review from anywhere.",
"tags": [ { "name": "writer" }, { "name": "critic" } ],
"variables": [
{ "key": "task", "label": "What to write", "source": "user_input", "required": true },
{ "key": "draft", "label": "Draft", "source": "set_by_action", "required": false },
{ "key": "review", "label": "Review", "source": "set_by_action", "required": false },
{ "key": "final", "label": "Final", "source": "set_by_action", "required": false }
],
"whips": [
{
"label": "Start writer",
"actions": [
{ "card": "prompt_write", "config": { "text": "In this conversation you are my writer. You write complete drafts and revise them when I bring criticism. Reply OK.", "mode": "replace" } },
{ "card": "submit", "config": {} },
{ "card": "tag_current_chat", "config": { "tagName": "writer" } }
]
},
{
"label": "Start critic",
"actions": [
{ "card": "prompt_write", "config": { "text": "In this conversation you are a demanding critic. When I show you a draft, list its weakest points as short numbered items. Reply OK.", "mode": "replace" } },
{ "card": "submit", "config": {} },
{ "card": "tag_current_chat", "config": { "tagName": "critic" } }
]
},
{
"label": "Write and review",
"actions": [
{ "card": "go_to_role", "config": { "tagName": "writer" } },
{ "card": "prompt_write", "config": { "text": "<<var.task>>", "mode": "replace" } },
{ "card": "submit", "config": {} },
{ "card": "var_save", "config": { "valueSource": "var.last.assistant", "key": "draft" } },
{ "card": "go_to_role", "config": { "tagName": "critic" } },
{ "card": "prompt_write", "config": { "text": "Task: <<var.task>>\n\nDraft:\n<<var.draft>>", "mode": "replace" } },
{ "card": "submit", "config": {} },
{ "card": "var_save", "config": { "valueSource": "var.last.assistant", "key": "review" } },
{ "card": "go_to_role", "config": { "tagName": "writer" } },
{ "card": "prompt_write", "config": { "text": "Revise your draft using this criticism. Output the full revised version.\n\n<<var.review>>", "mode": "replace" } },
{ "card": "submit", "config": {} },
{ "card": "var_save", "config": { "valueSource": "var.last.assistant", "key": "final" } },
{ "card": "show_result_modal", "config": { "title": "Draft, review, final", "body": "[Draft]\n<<var.draft>>\n\n[Review]\n<<var.review>>\n\n[Final]\n<<var.final>>", "copyButton": "copy_body" } }
]
}
]
}
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" } }
]
}
]
}
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.