# Upgrading existing SOPs — agent prompt

The step data-handoff model (`uses_outputs`, `outputs`, `for_each`) is opt-in.
Every SOP written before it still runs exactly as it did, which also means every
SOP written before it still has the old failure: a step can only see the result
of the step immediately before it, and a step that handles a whole collection
handles it in one call.

This page holds a prompt you give an agent so it audits and upgrades the SOPs it
owns. Paste it into a chat with the agent.

## Before you start

- **Give it as an administrator.** `sop_save` and `sop_rollback` refuse anyone
  but an admin (or the system), so paste the prompt in a conversation where you
  are one.
- **Every `sop_save` archives the previous version.** `sop_versions <id>` lists
  them and `sop_rollback <id> <version>` restores one, so a bad rewrite is one
  call away from being undone. Say so to the operator if they hesitate.
- **Keep the agent away from SOPs with a virtual level or step time limits.**
  `sop_save` replaces the whole SOP and has no way to keep a `virtual_level`,
  and `sop_get` does not show a step's `timeout`, so the agent's save removes
  both. Name those SOPs in the prompt as ones to skip, and change them by hand
  through `POST /api/sop` (see [Editing an SOP](sop.md#3-editing-an-sop)). A
  save that removed them is undone with `sop_rollback`.
- **Run it against one agent at a time**, and read the report before letting it
  loose on the next.
- **Do not let the agent run the modified SOPs.** Most pilot SOPs write
  somewhere — assign work, send messages, update tickets. Re-running one to
  "check the change" performs those side effects again. The prompt below tells
  the agent to stop and ask.

## The prompt

````text
You are upgrading the Standard Operating Procedures you own so they use the
platform's step data-handoff model. Work through them ONE AT A TIME and finish
with a report.

# Background: what the platform now supports

1. `uses_outputs` — a step declares which earlier results it consumes:
   ["s1"] for a whole step's answer, ["s1.staff_list"] for one published field.
   Without it a step sees ONLY the step immediately before it. Declaring
   uses_outputs replaces that implicit previous-step block.

2. `outputs` — a step declares the values it publishes:
   "outputs": ["staff_list","total_count"]. The step must then answer with a
   JSON object whose top-level keys are exactly those names, and later steps
   read them as {{steps.s1.outputs.staff_list}} or via uses_outputs.

3. `for_each` — a step runs once per item of a collection instead of once for
   all of them:
     "for_each": "{{steps.s2.outputs.categories}}", "item_var": "category"
   and the instruction refers to the current item as {{category}}.
   Each iteration's prompt carries one item, so it stays the same size however
   long the list grows. Declared outputs are gathered across iterations:
   arrays concatenate into one list, scalars become a list.

4. Every step's output — and every named output — is written to
   sop_runs/<sop_id>/<exec_id>/<step_id>[.<name>].md, and each step is handed
   the exact paths. Values over 12000 characters are truncated in the prompt
   with a pointer to that file.

# Procedure

1. Call `sop_list`, then `sop_get` each SOP, and take only those whose Owner is
   your agent ID. Leave out the SOPs the operator told you to skip.
2. For each SOP, in order:
   a. `sop_get` the full definition.
   b. `sop_history` for its recent runs — a step that keeps failing, or an
      execution that "completed" while producing nothing, is the strongest
      evidence of the problem you are fixing.
   c. Diagnose it against the checklist below.
   d. If nothing on the checklist applies, say so and move on. DO NOT rewrite
      a SOP that is already correct — churn on a working SOP is a regression,
      not an improvement.
   e. Otherwise apply the smallest set of changes that fixes what you found,
      and `sop_save` that one SOP.
3. Write the report described at the end.

# Diagnosis checklist

Check every step of every SOP against ALL of these — a step can match several,
and C in particular hides behind whatever else the step does. Each line is:
what you see → what you change.

A. FAN-IN — the step's instruction refers to a result from further back than
   the step immediately before it. Look for phrases like "the list from step 1",
   "the employees found earlier", "as identified above", "өмнөх алхамд гарсан"
   ("from the previous step"), "дээрх жагсаалт" ("the list above"), or a
   {prev_output} that is meant to reach back further than one step.
   → Add `uses_outputs` naming those steps. Keep the instruction's wording; it
     is now true instead of aspirational.

B. PARTIAL CONSUMPTION — a later step needs only part of what an earlier step
   produced (its list, its count, its date), but receives the whole answer.
   → Add `outputs` to the PRODUCING step naming the parts, set its
     `output_format` to a JSON object with exactly those keys, and have the
     consuming step take `uses_outputs: ["s1.that_field"]`.
   → This is the main token saving. Prefer it over passing whole answers.

C. COLLECTION IN ONE CALL — a step's instruction says it handles every member
   of a group. Read the instruction for the quantifier, in whatever language it
   is written:
     English  — "for each X", "for every X", "per X", "each X in the list",
                "all X", "one by one"
     Mongolian — "X бүрийн хувьд", "X тус бүрийн хувьд", "X тус бүрээс",
                "X бүрийг", "бүх X-ээр", "X бүр дээр", "нэг бүрчлэн"
                (all: "for each X", "per X", "every X", "one by one")
   This is the finding most often missed. If the instruction quantifies over a
   collection ANYWHERE in its text — even in a subordinate clause after some
   other work — it is a candidate. Check every step for it explicitly before
   moving on.
   → Turn it into a `for_each` step over the collection, with `item_var` named
     after the domain, and rewrite the instruction to handle ONE item.
   → The step that produces the collection must publish it as a named output
     containing a JSON array.
   → SPLIT WHEN THE LIST IS FETCHED INSIDE THE STEP. If one step both obtains
     the collection and then processes every member of it ("read the source
     list from the wiki, then for each site fetch its content"), `for_each`
     cannot reference a list that does not exist yet. Split it in two:
       s1a — obtain the collection, publish it: outputs: ["sites"]
       s1b — for_each: "{{steps.s1a.outputs.sites}}", handling ONE site
     Renumbering steps is allowed here. Update every reference to the old step
     id, and say in the report that you split it.
   → Do NOT also list the iterated collection in `uses_outputs`. `for_each`
     already hands each iteration its own item; adding the collection puts the
     whole list into every iteration's prompt, which is exactly the growth the
     map step exists to remove. The validator warns about this.
   → Set `on_item_failure: "continue"` when one bad group should not stop the
     rest; leave it unset when it should.
   → `for_each` cannot be combined with `requires_approval`. If the step needs
     approval, split it: a map step that prepares, then a separate approval
     step that commits.

D. ROWS THROUGH THE PROMPT — a step returns rows whose count grows with the
   data ("return all pending jobs", "list every unpaid invoice"), and a later
   step consumes them.
   → Change the producing step to publish a REFERENCE instead: the SQL that
     selects the rows, a count, and any small grouping list. The consuming step
     re-runs the query with db_query.
   → Aggregates and identifiers travel through the prompt; rows do not. A step
     whose output grows with the dataset will eventually truncate silently.

E. MISSING TOOLS — a step consumes an earlier step's output but does not
   declare `file_read`.
   → Add `file_read`, and `read_file_lines` when the value may be long.
   → Only ever ADD these two tools. Do not add any other tool, and never remove
     one.

F. ORDERING — a step references a result produced by a step that comes LATER in
   the array.
   → This is rejected at save time. Reorder the steps, or drop the reference —
     whichever matches what the SOP actually needs. Say which you chose.

G. PROBLEMS YOU ARE NOT ALLOWED TO FIX — while reading a step you will see
   faults outside your remit. Do not fix them; do not stay silent about them
   either. Collect them under "found but not fixed" in the report, one line
   each, naming the SOP, the step and the exact symptom.
   The one to look for in every step: the instruction tells the agent to call a
   tool that the step's `tools` array does not declare. At runtime that fails
   with 'tool "<name>" exists but was not offered for this request', so the
   step has never worked
   as written. Compare every tool name mentioned in the instruction against the
   declared list, and report each mismatch. You may still only ADD file_read
   and read_file_lines yourself — adding anything else is an authority change
   and is the operator's decision, not yours.

# Rules you must not break

- Keep every instruction, intent, constraint and context in ITS ORIGINAL
  LANGUAGE. Do not translate anything. If the SOP is written in Mongolian, your
  edits are in Mongolian.
- Do not change what a SOP does. You are changing how data moves between its
  steps, not its purpose, its schedule, or its business rules.
- Do not touch: id, name, purpose, owner, cron, enabled, prerequisites,
  virtual_level, rollback_command, requires_approval, on_success_goto,
  on_failure_goto, end_run_if, skip_if, success_check, max_retries,
  failure_mode.
- You may change: instruction (wording needed to reference data correctly),
  output_format, outputs, uses_outputs, for_each, item_var, max_items,
  on_item_failure, and tools (to ADD file_read / read_file_lines only).
- `sop_save` replaces the whole SOP with what you send. Send `owner`, the
  SOP-level `tools`, `context` and `prerequisites` exactly as `sop_get` showed
  them, and every field of every step, including the ones you did not change.
  Anything you leave out is removed; an owner left out becomes `default`.
- One `sop_save` per SOP. Never batch several SOPs into one call.
- If `sop_save` rejects your change, read the error — it names the exact
  problem — fix it and retry. After three failed attempts on the same SOP,
  leave that SOP UNCHANGED and record it in the report as needing a human.
- Do NOT call `sop_run`. These SOPs assign work, send messages and update
  records; re-running one repeats those effects. If a change should be
  verified by running it, say so in the report and let the operator decide.
- If a SOP is unclear enough that you cannot tell what a step consumes, do not
  guess. Leave it alone and list it under "needs a human".
- Report only what the definition, `sop_history` and the change log actually
  show. Do not infer why a SOP is disabled, what it was superseded by, or what
  anyone intended — if the reason is not written down, say the state you
  observed and that the reason is not recorded. An invented history is worse
  than no history: it gets acted on.

# Report

Finish with a table, one row per SOP:

| SOP | Finding (A–G, or "none") | What you changed | Status |

Then, separately:
- SOPs left unchanged because they were already correct.
- **Found but not fixed** — every finding G, one line each: SOP, step, symptom.
- SOPs that need a human, and the specific question you would ask about each.
- Any SOP where you recommend a test run, and why.
````

## A worked example

This fictional example uses synthetic staff and jobs. The prompt turns this:

```json
{ "id": "s3", "name": "Assign jobs",
  "instruction": "Assign the jobs from step 2 to the staff from step 1, by category." }
```

— where `s3` in fact only ever saw `s2` — into this:

```json
{ "id": "s3", "name": "Assign jobs",
  "uses_outputs": ["s1.staff_list", "s2.jobs_by_category"],
  "instruction": "Assign the jobs from step 2 to the staff from step 1, by category.",
  "outputs": ["assignments", "unassigned_count"],
  "output_format": "JSON object {assignments: [{staff_id, job_id, category}], unassigned_count: N}",
  "tools": ["db_query", "file_read"] }
```

The instruction is untouched — it was always describing the right thing. What
changed is that it is now true.

## After the run

- Read the report before enabling anything on a schedule.
- Spot-check one upgraded SOP with the operator present.
- `sop_versions <id>` and `sop_rollback <id> <version>` undo any single change.

See [sop.md](sop.md) for the full field reference.
