I ran a grep across the repositories I maintain, got zero hits, and felt relieved for about ten seconds.
Then I stopped. The prompts themselves were never the thing living in my code — the ones that matter most for other people are saved on the Console side. A clean code search does not prove you are unaffected.
Three days are left. So before the migration itself, let's settle a more useful question: what do you count, and what do you take back into your own hands?
What actually stops on August 17
Anthropic announced this on July 17, 2026 — 31 days of notice — and two things end on the same day.
| What | Date | Behavior afterward |
|---|---|---|
| Legacy Workbench (platform.claude.com/workbench) | August 17, 2026 | Access ends. Saved prompts, variables, and evals are not supported in the updated Workbench |
/v1/experimental/generate_prompt | August 17, 2026 | Requests return an error |
/v1/experimental/improve_prompt | August 17, 2026 | Same |
/v1/experimental/templatize_prompt | August 17, 2026 | Same |
No successor endpoint has been named. That detail changes the shape of the work: this is not "point the client at a new URL." It is "bring the behavior in-house."
Saved data can be exported from the banner in the Console and from Organizational Settings. Of everything on this list, that is the only step you cannot redo later. Do it first.
A code search only tells you half the story
Start by counting. These endpoints are almost always called by URL string, so one line covers it.
grep -rIn -E "experimental/(generate|improve|templatize)_prompt" \
--include='*.{py,ts,tsx,js,mjs,sh,yml,yaml,json}' .As an indie developer I run four sites from one set of repositories, and running this across all of their source produced zero hits. My prompts live as text files inside each repository and are loaded by scripts, so there was nothing to find.
But that zero was luck, not hygiene. If you drafted prompts in the Workbench and copied them out by hand, your code carries no trace of the dependency at all. The dependency exists in a different form: the wording only exists there.
So pair the code search with two more checks.
- Export the usage CSV from the Usage page in the Console. It breaks traffic down by API key and model. Config files lie when someone forgets to update them; actual traffic does not.
- Move every prompt saved in the Workbench into your repository as a file. Once it is a file, the next tool that shuts down cannot take it with it.
The second one matters more. What ends this month is not really a feature — it is the bill for having let someone else hold the storage.
templatize_prompt fits in about twenty lines
templatize_prompt turned a concrete prompt into a reusable template with {{VARIABLE}} placeholders. That transformation is deterministic, so there is no reason to ask a model for it. You can keep it locally.
#!/usr/bin/env python3
"""Turn a concrete prompt into a reusable template."""
import argparse, json, re, sys
def templatize(prompt: str, values: dict[str, str]) -> tuple[str, list[str]]:
# Replace longer values first, so a short value never eats part of a long one.
ordered = sorted(values.items(), key=lambda kv: len(kv[1]), reverse=True)
template, used = prompt, []
for name, value in ordered:
if not value:
continue
if value not in template:
print(f"warning: value for {name} not found in prompt: {value!r}", file=sys.stderr)
continue
template = template.replace(value, "{{" + name + "}}")
used.append(name)
return template, used
def fill(template: str, values: dict[str, str]) -> str:
return re.sub(r"\{\{(\w+)\}\}", lambda m: values.get(m.group(1), m.group(0)), template)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("prompt_file")
ap.add_argument("values_file", help='JSON: {"VARIABLE_NAME": "literal text in the prompt"}')
args = ap.parse_args()
prompt = open(args.prompt_file, encoding="utf-8").read()
values = json.load(open(args.values_file, encoding="utf-8"))
template, used = templatize(prompt, values)
# If filling the template does not reproduce the original, a replacement went wrong
if fill(template, values) != prompt:
print("round-trip check failed: template does not restore the original", file=sys.stderr)
return 1
print(template)
print(f"\n--- variables: {', '.join(used)} ---", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())Give it this prompt:
You are a support agent for an online store.
A customer asks about "Wireless Earbuds Model A" and reports that delivery is late.
Reply politely in no more than 3 sentences. The order number is ORD-20260814-0031.with these values:
{
"PRODUCT_NAME": "Wireless Earbuds Model A",
"ISSUE": "delivery is late",
"ORDER_ID": "ORD-20260814-0031",
"MAX_SENTENCES": "3"
}and the run produces:
You are a support agent for an online store.
A customer asks about "{{PRODUCT_NAME}}" and reports that {{ISSUE}}.
Reply politely in no more than {{MAX_SENTENCES}} sentences. The order number is {{ORDER_ID}}.
--- variables: PRODUCT_NAME, ORDER_ID, ISSUE, MAX_SENTENCES ---Replacement order changed the result
When I started writing this, I assumed iterating over the dictionary and calling replace would be enough. Then I tried a different prompt:
Summarize the Q3 2026 sales report in 3 sentences.With MAX_SENTENCES set to "3", PERIOD set to "Q3 2026", and no ordering, you get this:
Summarize the Q{{MAX_SENTENCES}} 2026 sales report in {{MAX_SENTENCES}} sentences.The digit inside the quarter label became a variable, and the period variable never got substituted at all. The template still looks plausible at a glance, and it produces broken text the moment you fill it back in. Sorting by length removes the failure entirely — the problem is that nothing prompts you to notice it.
That is why the round-trip check sits at the end. Fill the template back with the same values, compare against the original, and fail loudly when they differ. Five lines catch a silent corruption at the exit. And when a value is only one or two characters long, consider not making it a variable at all.
improve_prompt lasts longer as a meta-prompt you own
improve_prompt was, in essence, a prompt that rewrites prompts. Call the Messages API yourself and you get to decide the editing criteria — which were a black box while the experimental endpoint held them.
#!/usr/bin/env python3
"""Rewrite a prompt with Claude. Replaces improve_prompt."""
import argparse, json, os, urllib.error, urllib.request
ENDPOINT = "https://api.anthropic.com/v1/messages"
META_PROMPT = """You are a prompt editor. Rewrite the prompt you are given,
changing only the following. Do not change its meaning.
1. Reorder instructions as: role, then input, then constraints, then output format
2. Replace vague wording ("appropriately", "nicely") with checkable conditions
3. State the output format explicitly, but only if none was specified
4. Leave placeholders exactly as they are
Output only the rewritten prompt. No preamble, no explanation."""
def build_payload(prompt: str, model: str) -> dict:
# Do not send temperature / top_p / top_k. Non-default values return 400
# on Claude Opus 4.7 and later, and on Claude Sonnet 5.
return {
"model": model,
"max_tokens": 2000,
"system": META_PROMPT,
"messages": [{"role": "user", "content": prompt}],
}
def call(payload: dict, api_key: str) -> str:
req = urllib.request.Request(
ENDPOINT,
data=json.dumps(payload).encode("utf-8"),
headers={
"content-type": "application/json",
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
},
)
try:
with urllib.request.urlopen(req, timeout=120) as res:
body = json.load(res)
except urllib.error.HTTPError as e:
raise SystemExit(f"HTTP {e.code}: {e.read().decode('utf-8', 'replace')}")
except urllib.error.URLError as e:
raise SystemExit(f"network error: {e.reason}")
return "".join(b.get("text", "") for b in body.get("content", []) if b.get("type") == "text")
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("prompt_file")
ap.add_argument("--model", default="claude-sonnet-5")
ap.add_argument("--dry-run", action="store_true", help="print the request body without sending")
args = ap.parse_args()
prompt = open(args.prompt_file, encoding="utf-8").read().strip()
payload = build_payload(prompt, args.model)
if args.dry_run:
print(json.dumps(payload, ensure_ascii=False, indent=2))
return 0
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
raise SystemExit("ANTHROPIC_API_KEY is not set")
print(call(payload, api_key))
return 0
if __name__ == "__main__":
raise SystemExit(main())--dry-run prints the request body without sending it. During a migration it is safer to inspect the shape first and attach a key second.
{
"model": "claude-sonnet-5",
"max_tokens": 2000,
"system": "You are a prompt editor. ...",
"messages": [
{
"role": "user",
"content": "Summarize the reviews nicely."
}
]
}Without a key, the script exits before sending with ANTHROPIC_API_KEY is not set and status 1. If you plan to run this unattended, failing early like that saves cleanup later.
Copying your Workbench settings across returns 400
This is the step I expect to catch the most people. The Workbench UI had a temperature slider, and it is natural to carry that number into the code you are migrating to.
But temperature, top_p, and top_k now return a 400 error when set to a non-default value on Claude Opus 4.7 and later, and on Claude Sonnet 5. The SDK request types still define those fields for compatibility with earlier models, so the code type-checks. It breaks at runtime, most likely after you have already declared the migration finished.
That is why none of them appear in the script above. If you want to steer behavior in the same direction, express it as a condition inside the meta-prompt instead of as a parameter. If you would rather confirm the basic call shape first, the Claude API quickstart is the shortest path.
A checklist for the next three days
| Order | Task | How to confirm |
|---|---|---|
| 1 | Export saved prompts, variables, and evals | Console banner / Organizational Settings. Irreversible, so do it first |
| 2 | Count calls to the three endpoints | The grep above. Keep going even at zero hits |
| 3 | Check real traffic in the usage CSV | Console Usage → Export, broken down by API key and model |
| 4 | Replace templatization with a local script | The round-trip check passes |
| 5 | Replace prompt rewriting with a Messages API call | Inspect with --dry-run, then attach the key |
| 6 | Verify you did not copy temperature and friends across | Send one real request to the target model. Type checking will not catch it |
When a deadline is this close, the only order that works is the one derived from the deadline. Finish the export today. With that done, everything else can still be recovered after August 17.
For the wider habit of preparing for announced removals, the Opus 4.7 fast mode retirement preflight (a premium article) covers how to measure behavior that shifts across a cutoff. Removals that raise an error and removals that quietly change behavior need very different preparation.
This one made me rethink where my prompts live. Thank you for reading.