WhipWhip

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.

JSON schema · llms-full.txt

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.

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:

  1. Put a user_input variable into a prompt as a token. Before the whip runs, WhipWhip shows me a small form for every user_input variable the whip needs. This is the default and usually best.
  2. input_prompt asks 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": true to 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.

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.

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.

submit

Send what's in the message box and wait for the AI's reply. You can turn waiting off.

input_prompt

Pause the run and ask you to type a value.

ask_and_extract

Ask the AI something and save the parts you name into variables.

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.

validate

Check that the reply has the right format and ask again if it doesn't. It checks format only, not content.

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.

var_save

Save text from the message box, the AI's reply or the page into a variable.

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).

upsert_item

Add an item to a list, or replace the item that has the same id.

patch_item

Change only some fields of the list item with a given id. Its other fields stay (e.g. set status to done).

remove_item

Remove the item with a given id from a list.

derive_index

Turn a list into text with one line per item, or just count the items.

go_to_role

Move to the conversation with this tag.

go_to_start

Go back to the tab where the whip started, and close this one if you want. Useful after a side question.

new_chat

Open a new conversation on the current service.

open_service

Switch to a different AI service (ChatGPT, Gemini, Claude…).

open_url

Open any https:// web address. Use it as the last action, because the whip stops there.

tag_current_chat

Tag this chat so other whips can come back to it.

choice_prompt

Show a list of options and save the pick in a variable.

show_result_modal

Show a window with your results, filled in from variables.

import_whipset

Add a whipset the AI wrote, from JSON saved in a variable. You review it in a preview before anything is saved.

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

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

  1. Think about which whips I will press and in what order, then design the smallest set that works.
  2. 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.
  3. 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.
  4. If I paste back a list of problems from WhipWhip, fix every item and send the complete corrected JSON again, not a partial patch.

My request