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.
sop_save and sop_rollback refuse anyone
but an admin (or the system), so paste the prompt in a conversation where you
are one.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.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). A
save that removed them is undone with sop_rollback.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.
This fictional example uses synthetic staff and jobs. The prompt turns this:
{ "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:
{ "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.
sop_versions <id> and sop_rollback <id> <version> undo any single change.See sop.md for the full field reference.