# How to: create a repeatable sim task

> **Using Claude Code?** Invoke `/repeatable-task` — the skill walks you through authoring, launching, iterating, and debugging. This doc is the narrative reference behind it.

You want to run the same work against N simulators — capture screenshots, sanity-check a flow, backfill some metadata, whatever. This walks through how to do it with `plato pm start from-template`, using the shipping **`extract_screenshots`** template as the worked example.

## Prerequisites

```bash
export PLATO_API_KEY=pk_user_...            # your personal key
export PLATO_DATAGEN_API_KEY=pk_user_...    # datagen account key (injected into templates)
export ANCHOR_API_KEY=sk-...                # required for Anchor browser backend
export BROWSERBASE_API_KEY=bb_live_...      # required if a template opts into Browserbase
```

Sims and their artifacts come from the Plato API — run `plato pm list ready` to see what's launchable.

## 1. The worked example — `extract_screenshots`

Open [`python-sdk/templates/extract-screenshots-launch.json`](../../templates/extract-screenshots-launch.json). This is the whole template (one step, one verify pipeline). Read it end-to-end before writing your own — it's small enough.

The template says: *for each sim, attach a browser MCP, navigate 4 pages, write `screenshots.json`, let the verifier upload them to the sim's catalog.*

Three things make it work:

1. **One step with a tight stop condition** — the instruction repeatedly tells the agent "exactly 4 screenshots, then Write, then stop." Earlier versions without this ran up to 23 screenshots and blew through `max_turns`.
2. **Verifier does the heavy lifting** — the agent just dumps PNGs into `.screenshots/` (the browser MCP's own directory) and writes `screenshots.json`. The 5 verify commands validate, sanitize, rename, and then upload via the Plato API.
3. **Placeholder substitution** — `{sim_name}`, `{plato_api_key}`, `{plato_base_url}` are rendered into the verify shell at launch, so the upload curl knows *which* sim it's talking about.

Run it against three sims:

```bash
plato pm start from-template \
  python-sdk/templates/extract-screenshots-launch.json \
  bookstack mattermost aimeos --data
```

Each sim gets its own Chronos session. About 2 minutes per sim. The CLI prints each session's UUID; check progress with `plato chronos session <id>` or the Chronos UI.

## 2. Writing your own template

Clone `extract-screenshots-launch.json` and change three things:

### (a) The agent's instruction

Describe your task. Good prompts for repeatable tasks:

- **Declare the stop condition up front.** State exactly how many times to call each tool, then say "then stop."
- **Tell the agent NOT to do things that waste turns.** For screenshot extraction, that was: don't `Read` the PNG, don't `cp`/`mv`, don't narrate each step. The verifier handles cleanup.
- **Pick descriptions / metadata from context, not from tool results.** The agent knows what page it just navigated to — it doesn't need to Read an image to describe it.

### (b) The verify shell

Verify steps run as separate subprocesses in the agent's workspace, in order, exiting on the first non-zero. Patterns that work well:

```bash
# Start with a file_exists guard — agent failures show up here first.
builtin_verifier file_exists my-output.json

# Structural validation with jq (fast, clear error messages).
jq -e '.items | length == 4' my-output.json > /dev/null \
  || (echo 'FAIL: expected exactly 4 items' && exit 1)

# Sanitize before parsing if you let the agent write free-form strings.
python3 -c "
import json, re
raw = open('my-output.json').read()
cleaned = re.sub(r'[\x00-\x1f]+', ' ', raw)
open('my-output.json','w').write(json.dumps(json.loads(cleaned)))
"

# Call the Plato API using the injected placeholders.
set -e
BASE_URL="{plato_base_url}"
API_KEY="{plato_api_key}"
SIM_NAME="{sim_name}"
curl -fsS -X POST "$BASE_URL/api/v2/simulators/$SIM_NAME/whatever" \
  -H "X-API-Key: $API_KEY" -H 'Content-Type: application/json' -d "$BODY"
```

Prefer `set -e` + `|| (echo 'FAIL' && exit 1)` patterns so failures have loud, specific error messages in the session log.

### (c) `mcps_required` at the root

Only declare the MCPs your task actually needs. For extract_screenshots: `["browser"]`. If you need shell in the sim VM too: `["browser", "vm"]`. If you need DB access: `["browser", "db"]` (the SDK auto-fetches `db_config` for each sim). Keeping this minimal cuts startup time.

## 3. Iterating

Since the template is a local file, each run uses whatever is on disk:

```bash
# Edit the template, run, watch one session, tune, repeat.
plato pm start from-template ./my-template.json bookstack --data
plato chronos analysis <session-id>   # step-by-step token/tool breakdown
```

This is the biggest reason to use `from-template` over the `experiment push` path — no upload step, zero latency between "edit JSON" and "see what happens."

## 4. Running against many sims

Once your template works on one sim, fan out:

```bash
# Every sim in a given status (here: ready)
SIMS=$(plato pm list ready --json | jq -r '.[].name')
plato pm start from-template ./my-template.json $SIMS --data

# First N by name (cheaper for smoke-testing a batch)
plato pm list ready --json | jq -r '.[].name' | sort | head -20 | xargs \
  plato pm start from-template ./my-template.json --data
```

Auto-confirm the "Proceed?" prompt from a script:

```bash
yes y | plato pm start from-template ./my-template.json sim1 sim2 sim3 --data
```

## 5. Failure modes to expect (learned from extract_screenshots)

- **Agent runs out of `max_turns`.** Check `plato chronos analysis <sid>`'s step log. If the agent is taking ~5 turns per logical action, tighten the prompt: explicitly forbid `Read` on tool outputs, explicitly forbid narration, and declare the exact tool sequence.
- **Template step fails at "file not found"** — means the agent didn't produce what you told it to. The verifier is correct to fail. Don't add "skip if empty" logic; let the failure surface.
- **Some sims hit browser-init timeouts.** `from-template` launches go through Browserbase by default — `_build_mcps_for_sim` in [`pm.py`](../../plato/cli/pm.py) sets `"backend": "browserbase"` when it expands the `"browser"` entry of `mcps_required`. Templates don't declare browser MCP dicts directly (just `mcps_required` names), and there's no CLI flag to switch backends, so changes happen at that call site. See [pm-from-template.md › Browser backend selection](../cli/pm-from-template.md#browser-backend-selection).
- **Partial API uploads.** If your verifier POSTs results to the Plato API one-by-one and fails midway, the already-committed entries persist. `max_attempts: 2` will retry and append more — you'll end up with duplicates. Either wrap the uploads in a transaction pattern (delete existing first, then upload) or accept that re-runs need manual cleanup.

## 6. Debugging a specific session

In Claude Code, invoke `/chronos-session-analysis <session-id>` — it handles step logs, agent drill-downs, trace analysis, and full session reports.

Raw commands if you're not in Claude Code:

```bash
# Session metadata + cost + status_reason
plato chronos session <session-id>

# All OTel spans for the session (errors, tool calls, world-level events)
plato chronos traces <session-id> --errors-only

# Token/tool breakdown per step (where turns are going)
plato chronos analysis <session-id>
```

Session URL format: `https://chronos.plato.so/sessions/<uuid>`.

## See also

- [`pm from-template` reference](../cli/pm-from-template.md) — every flag, every placeholder, template skeleton
- [`extract-screenshots-launch.json`](../../templates/extract-screenshots-launch.json) — the worked example
- [`sim-checker-launch.json`](../../templates/sim-checker-launch.json) — a 5-step multi-verifier template
