--- name: create-workflow description: Author, smoke-check, and save a new saved workflow (invoke via /create-workflow). user-invocable: true model-invocable: true --- # create-workflow Author a saved workflow: a deterministic JavaScript orchestration script that fans out subagents. The script — not the model turn — holds the loop, the fan-out, the branching, and the intermediate results; child agents do the judgment, the script shards work and verifies. Do not write Rhai. **This turn fails unless `.dsh/workflows/.workflow.json` exists.** Chat, repo walks, interviews, and live child launches are not a saved workflow. ## Fast path (default) If `/create-workflow` already states a usable objective (what to do, and optionally how many agents / what fans out), skip the interview. Do not call `ask_user_question`. Do not explore the workspace first — child agents inspect the code at run time. In this same turn after the skill is loaded: 1. Infer a kebab `meta.name`, project save scope (`.dsh/workflows/`) unless the user asks for a global/user workflow, and a simple fan-out. If they named an agent count, stay at or under it (default 8). Add adversarial verification only when it still fits that budget; otherwise skip it and say so. 2. Author the `{ meta, script }` envelope as **plain JavaScript** (commas in object/array literals, never semicolons — `{ a: 1, b: 2 }` not `{ a: 1; b: 2 }`). `Unexpected token ';'` means you put `;` inside `{ ... }`. Prefer `complete(value)`. When output cardinality matters, declare it with the supported inclusive `minItems`/`maxItems` array bounds instead of relying only on prompt wording or clipping. 3. Call the workflow tool with inline `script` + `meta`. Inline script **defaults to `validate_only`** (canned stubs, no live children) and **SAVES** after a passing smoke. Omit `save_scope` for project scope, or pass `save_scope: "user"` for `/workflows/`; user scope works even when the Session has no cwd. Do not pass `validate_only: false`. Do not launch `/workflow ` yet. 4. If smoke fails with a parse error, fix commas and retry once. A successful inline authoring result always includes `saved_path`. If validation or saving fails (including a missing project cwd), report the error and fix or retry it; do not claim success or write a second, unvalidated copy manually. Use `save_scope: "user"` when a cwd-independent save is intended. 5. Report the path, smoke limits, and `/workflow `. Offer a live launch; do not start children until the user agrees. Copy this call shape (plain JS, commas, no `validate_only: false`): ```js workflow({ meta: { name: "review-changes", description: "Review a diff and verify findings" }, script: "phase(\"Review\");\ncomplete({ ok: true });", save_scope: "project", }) ``` Ask in ordinary chat (not a picker) only when the objective is empty or contradictory. One short question max. ## Required seven-stage procedure Use this only when the fast path cannot run because the objective is missing. Do not interview a user who already gave a brief. 1. **Gather intent.** If the user already stated the objective, fan-out, and agent budget, skip this stage. Otherwise ask in chat for the missing piece only. 2. **Design fan-out.** Identify independent agents, concurrency, labels, phase grouping, and maximum fan-out. 3. **Design verification.** Add an adversarial or independent verification stage when the budget allows; missing, failed, or unusable verification is not a confirming vote. Require concrete evidence. 4. **Choose the artifact and tolerance.** Decide whether results live inline or in scratch files and how `null` child failures affect the result. Optional advice may fail open; a proof gate fails closed. 5. **Choose identity and scope.** Pick a lowercase kebab name (at most 64 UTF-16 code units, `^[a-z](?:[a-z0-9]*)(?:-[a-z0-9]+)*$`) and project (`.dsh/workflows/`, default, shareable) or user (`/workflows/`) save scope. Do not use `pause`, `resume`, `save`, `stop`, `workflow`, `workflows`, `create-workflow`, or a Windows device basename. When a name collides with another slash command, the existing command keeps `/` and the saved workflow is advertised as `/workflow-`; the host repeats the `workflow-` prefix if that name is also occupied. Canonical `/workflow ` always works. 6. **Author and validate.** Write the strict envelope, then run inline `validate_only` with representative args and the chosen `save_scope` (`project` or `user`). Validation parses the entire script, then executes one args-selected canned path; it does not exercise all branches, live tools, or every possible agent output. A gate ends the smoke as `would pause: `. 7. **Publish and report.** Save only after validation succeeds. Report the file path, smoke result and its limits, launch syntax (`/` or `/workflow ...`), and maximum fan-out. Offer, but do not force, a real background launch watched in `/workflows`. If they decline, say only the path-specific smoke check ran. ## File format A flat `.workflow.json` file contains exactly `meta` and `script`. Metadata contains only `name`, `description`, optional `whenToUse`, and optional `phases`; a phase contains only `title`, optional `detail`, `provider`, and `model`. Metadata is JSON data beside the script and is never evaluated. Filename must equal `.workflow.json`. Unknown envelope, meta, or phase fields fail the whole observation. ```json { "meta": { "name": "review-changes", "description": "Review a diff across dimensions, adversarially verify each finding", "whenToUse": "After a large diff, before merge", "phases": [ { "title": "Review", "detail": "one reviewer per dimension" }, { "title": "Verify", "detail": "one skeptic per finding" } ] }, "script": "// plain JS body, top-level await, complete(value) or return value" } ``` ## JavaScript hooks - `agent(prompt, { label?, phase?, schema?, provider?, model? })` returns final text or a schema-validated JSON object; an ordinary child failure returns `null`. - `parallel(thunksOrJobs)` is a barrier and preserves slot order. Items are zero-arg functions or job maps `{ prompt, label?, phase?, schema?, provider?, model? }`. Declarative job panels preflight the unreplayed panel atomically; arbitrary thunks use per-call admission. Failed slots resolve `null`. - `pipeline(items, ...stages)` advances items independently and preserves input order. - `phase(title)` and `log(message)` publish bounded progress. - `complete(jsonValue)` settles the first valid JSON result and stops later hooks. Prefer `complete()` over falling through. Stock workers have no native `complete`; this package injects one. `return jsonValue` also settles the run. - `await await_user(kind, message)` commits an acknowledged gate; `await pause(kind, message)` repeats after resume while its condition is unchanged. Both hooks are asynchronous and must be awaited. - `budget()` returns `{ total, spent, reserved: 0, remaining }`. - `write_scratch_file(name, content)` and `read_scratch_file(name)` use one safe filename. Supported schemas use `type`, `properties`, `required`, `additionalProperties`, `items`, `minItems`, `maxItems`, `enum`, `const`, and `oneOf`. `minItems` and `maxItems` are inclusive array-length bounds. Each must be a non-negative safe integer (not `-0`), may appear only on a `type: "array"` node, must satisfy `minItems <= maxItems`, and is forbidden beside `oneOf`. The package validates the authored schema before any child starts, removes only these two keywords from the provider-facing copy for stock RC2, and post-validates the returned structured value against the authored bounds. Replay uses immutable script, args, and a committed checkpoint. Replayed and schema-correction calls spend zero, but an external effect whose result was not committed can repeat. Keep prompts and external operations idempotent. Replay-capable scripts cannot use `Date`, `Math.random`, `Atomics`, `SharedArrayBuffer`, `WeakRef`, or `FinalizationRegistry`. There is no nested workflow hook. Default scratch quotas are 4,096 operations, 64 pending operations, 64 files, 1 MiB per file, and 8 MiB total. Default agent budget is 128 and the hard maximum is 1,024. ## Good patterns - Build the fan-out work-list the simplest deterministic way (a fixed list, `args`, a file walk). Spend agents on judgment, not on deciding scope. - If an agent discovers the work-list, treat it as untrusted: re-filter it in plain JavaScript against the invariant (for example, keep only paths under `args.root`) before sharding. - Plan → parallel fan-out → synthesize. - Adversarial verification: independent skeptics prompted to refute each finding. Missing, failed, or unusable verification is not a confirming vote; require concrete evidence. - Loop until dry: spawn finders until two consecutive rounds surface nothing new; fingerprint each round to detect stalls. - Vote panels: N skeptics per item in one flat `parallel()`, regroup by index arithmetic. - Failure policy by purpose: optional advice may fail open; a proof gate fails closed. ## Pitfalls that actually happen - Terse prompts return empty structured objects without using tools. Command tool use; say what a valid empty answer requires. - Guard every agent output against the schema value itself: for example, `r != null && Array.isArray(r.findings)`. A schema-backed `agent()` returns that structured object directly; failed `parallel()` slots are `null`. - Meta is pure data — no computed meta. - Keep `meta.phases` titles in sync with `phase()` calls. - `pause()` in a result-derived branch re-fires forever; use `await_user` for resumable human gates. - Silent truncation is not coverage; `log()` whatever a `MAX_*` cap dropped. - Agents do not enforce invariants — the script does. Filter and assert in JavaScript. - A complete `/create-workflow` brief is enough — do not stall on `ask_user_question` or a repo walk. - Do not pass `validate_only: false` while authoring. That launches live children and the parent tool call can sit for tens of minutes. Inline script already defaults to smoke + save. - When an array count is an invariant, use valid `minItems`/`maxItems`; prompt wording may guide generation but is not enforcement. - Do not put `meta` in JavaScript, use TypeScript/export syntax, add unsupported agent options such as `fork_context`, mix thunk and declarative parallel forms in one call, assume `null` is success, omit verification, hide truncation, use nondeterministic globals, use nested workflows, or claim validate-only exhaustively proves the workflow. ## Example (review-changes) Meta is JSON data beside this body in the `{ "meta", "script" }` envelope: ```json { "name": "review-changes", "description": "Review a diff across dimensions, adversarially verify each finding", "whenToUse": "After a large diff, before merge", "phases": [ { "title": "Review", "detail": "one reviewer per dimension" }, { "title": "Verify", "detail": "one skeptic per finding" } ] } ``` ```js const findingsSchema = { type: "object", required: ["findings"], properties: { findings: { type: "array", maxItems: 8, items: { type: "object", required: ["file", "issue"], properties: { file: { type: "string" }, issue: { type: "string" } } } } } }; const verdictSchema = { type: "object", required: ["real", "reason", "evidence"], properties: { real: { type: "boolean" }, reason: { type: "string" }, evidence: { type: "string" } } }; const target = args && args.target; if (target == null) await pause("verification", "Pass args.target — the diff, branch, or path to review."); phase("Review"); const dimensions = ["correctness bugs", "error handling gaps", "performance problems"]; const results = await parallel(dimensions.map((d) => async () => await agent( "Review " + target + " for " + d + ". Use read-only tools to inspect the actual code — " + "do not answer from memory. Report at most 8 concrete findings as {file, issue}; " + "an empty list is valid only after you have read the code.", { label: "review:" + d, schema: findingsSchema }))); const findings = []; for (const r of results) { if (r != null && Array.isArray(r.findings)) for (const f of r.findings) findings.push(f); } if (findings.length === 0) complete({ summary: "No findings.", confirmed: [] }); phase("Verify"); const verdicts = await parallel(findings.map((f) => async () => await agent( "Adversarially verify this review finding by reading the shipped code: \"" + f.issue + "\" in " + f.file + ". Set real=true only with concrete evidence you " + "independently inspected. Otherwise default real=false.", { label: "verify:" + f.file, schema: verdictSchema }))); const confirmed = []; for (let i = 0; i < verdicts.length; i++) { const v = verdicts[i]; if (v != null && v.real === true && v.evidence) confirmed.push(findings[i]); } log(String(confirmed.length) + "/" + String(findings.length) + " findings survived verification"); complete({ summary: String(confirmed.length) + " confirmed findings", confirmed }); ```