← OrisuCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Orisu
Snapshot Oct 1, 2026 · 12:02 UTC · version 1.0.0
Collection source: skill API.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"name": "orisu-workflows",
"description": "Use when generating images, video, audio, or text via Orisu — covers building new workflows from scratch, editing existing ones, and reusing published apps with custom inputs. Activates whenever the connected Orisu MCP server is available and the user wants to create, edit, run, or reuse an Orisu workflow.",
"included_files": [],
"skill_md_contents": "---\nname: orisu-workflows\ndescription: Use when generating images, video, audio, or text via Orisu — covers building new workflows from scratch, editing existing ones, and reusing published apps with custom inputs. Activates whenever the connected Orisu MCP server is available and the user wants to create, edit, run, or reuse an Orisu workflow.\n---\n\n# Working with Orisu\n\nOrisu is a node-based workflow builder for AI media generation. You can build, edit, run, and reuse workflows through the Orisu MCP server. This skill teaches you the concepts and gives you the canonical playbook for each common task.\n\n> **Where this skill lives at runtime.** The connected Orisu MCP server exposes the latest version of this content at the resource URI `orisu://skills/workflows`. If you ever need to re-load it (a fresh session, a tool result that hints \"see the workflow skill\", or you suspect this copy is stale), call `resources/read` on that URI — it's always current. No external HTTPS URL is involved, so the path doesn't drift across deploys.\n\n## Concepts\n\nHold this mental model when working with Orisu:\n\n- **Agent** = one workflow: a directed graph of nodes connected by edges, living in an org. An agent carries its **config and input values baked into the graph** (\"burned in\") — it is the canonical, saved workflow. Running it with `trigger_run` executes those saved values. Any `inputs` you pass to `trigger_run` are a **per-run overlay that does NOT persist to the graph** — the canvas stays unchanged. To make a value permanent (so it shows on the node and is reused every run), set the input node's `value` with `update_node_config`, don't pass it as a run input. To seed several inputs at once, `set_input_values({ agent_id, values: { nodeId: value } })` applies them in one call and one version.\n- **Node** = one capability or logic block. There are ~57 node types across categories: **input** (`text_prompt`, `upload_image`, …), **AI image** (`generate_image`, `edit_image`, …), **AI video** (`generate_video`, `edit_video`, …), **AI audio** (`generate_speech`, `generate_music`, …), **AI text** (`generate_text`), **logic** (`if_else`, `switch`, `list_selector`, `human_review`, …), **brand** (`brand_context`, `brand_guidelines`, …), **integrations** (`extract_product`, `google_drive_import`, scrapers), **utility** (`composer`, `sticky_note`).\n- **Port** = an input or output socket on a node. Each port has a type (`image`, `video`, `text`, `audio`, etc.). Edges connect a source port to a target port of compatible type.\n- **Config** = the per-node settings. For an AI node this includes the model and its parameters (aspect ratio, duration, etc.). Media generators (`generate_image`, `generate_video`, `generate_music`, `generate_speech`, `generate_sound_effects`) plus `edit_image`/`edit_video` also take their prompt text inline via `config.prompt` (labeled \"Instructions\" on the edit nodes) — used whenever their prompt/instructions port has no incoming edge. For an input node it's the value the user supplies (the prompt text, the uploaded asset). These inline prompts may embed `{{ref:nodeId#handle|label}}` tokens referencing connected inputs (text → inlined at run time, media → its name); preserve them verbatim when editing.\n- **Run** = one execution of an agent. Runs have status (`queued`, `running`, `completed`, `failed`, `paused`, `cancelled`), per-step outputs, and asset URLs.\n- **App** = a published agent with a defined input schema — the intended surface for running **the same workflow many times with different inputs**. Publish once with `publish_app`, then `trigger_app` per run (inputs validated against the schema). **Rule of thumb: burn fixed choices into the agent; expose only what varies as app inputs.** For a workflow you'll run repeatedly with changing inputs (e.g. the same generator over 15 prompts), publish an app and loop `trigger_app` — do NOT lean on `trigger_run`'s input overlay, which never persists and is meant for one-off/ad-hoc executions or testing.\n- **Version** = an immutable snapshot of an agent's graph. Every save creates or reuses a version. Restore a version to roll back.\n- **Asset** = a generated image / video / audio / file. Returned from runs as signed URLs; download or persist if you need them past the URL TTL.\n\n### Three things that trip people up\n\n1. **Ports drive everything.** Always inspect a node's effective ports (via `get_effective_ports`) before connecting. Some nodes have **dynamic-arity ports** that expand based on config (e.g. `prompt_concatenator` grows inputs as you increase `config.elements`).\n2. **Read-only mode and the run-per-version model.** When you save a new graph, Orisu may fork a new run for it. Don't try to maintain run state yourself — let the server handle it.\n3. **Config keys are camelCase; model parameter listings are not the config vocabulary.** Node config keys come from `orisu://nodes/{type}` — `config.schema_fields` is the authoritative list (each key with its type, enum values, ranges, and description; fall back to the `defaults` object on servers that don't send it). Keys are always camelCase (`aspectRatio`, `negativePrompt`, `numberOfImages`), with plain values (`aspectRatio: \"16:9\"`, not `\"landscape_16_9\"`). The snake_case field names you see under a model in `orisu://models/{variant_id}` (e.g. `aspect_ratio` with options like `square_hd`) describe the **provider's** parameters — Orisu maps camelCase config onto them at run time. Never copy a snake_case key or its option values into node config: the graph will validate, but the value is silently ignored or overridden at run time. When you're unsure a key exists or what values it takes, **omit it** — every field has a sensible default.\n\n## Discovery — always do this before designing\n\nRead the relevant `orisu://` resource before generating any spec. Tool descriptions teach the exact reads — this skill assumes you've done them. As shortcuts: `search({ kind, query })` finds agents/apps/models/nodes/templates by name; `list_runs({ status, agent_id })` finds recent runs without a known id; `suggest_model({ kind, use_case })` recommends a model variant.\n\n**Model on a template before building from scratch.** `search({ kind: 'template', query })` finds published example workflows; `orisu://templates/{id}` returns one with its **actual graph** (nodes, edges, config — sensitive fields scrubbed). Copying real wiring beats reinventing it: keep the structure, swap the inputs/branding/models for the user's needs, then `validate_graph` and `create_agent`.\n\n## Patterns\n\nThese are the most common graph shapes. Use them as starting points.\n\n### Pattern 1 — Text → image\n\n```\ngenerate_image (prompt inline via config.prompt)\n```\n\nOne node. For a fixed prompt, set `config.prompt` on the generator — no separate `text_prompt` node. `generate_image.config.model` picks the image model. Add a `text_prompt` node wired into `generate_image.input_text_prompt` ONLY when the prompt must be dynamic — an end-user app input, produced upstream (`generate_text`), or shared by several generators. A connected prompt port always wins over `config.prompt`.\n\n### Pattern 2 — Image → video\n\n```\nupload_image ──► generate_video (motion description inline via config.prompt)\n```\n\n`generate_video` takes an image (wire `upload_image.output_image` → `input_first_frame`) and the motion description inline at `config.prompt`. Wire a `text_prompt` into `input_text_prompt` only for a dynamic/app-input motion prompt.\n\n### Pattern 3 — Brand-aware multimodal generation\n\n```\nbrand_context ──┐\nupload_image ───┴─► generate_image (or edit_image; prompt inline via config.prompt)\n```\n\nWire a `brand_context` (or `brand_guidelines` / `brand_voice`) node into any generator so its output respects the brand. Use this whenever the user mentions a brand, style, or company.\n\nBrand nodes need a `brand_kit_id` from `orisu://brand-kits`. If no kit matches, **create one from what the user tells you**: `create_brand_kit({ name, colors, fonts, logo_url, voice, guidelines })` (colors are hex, primary first), then wire its id in. Correct or extend an existing kit with `update_brand_kit` — pass only the changed fields, but note array fields like `colors` are replaced wholesale. Automatic extraction from a website stays in the Orisu dashboard (async scraper pipeline). The prompt itself stays inline at `config.prompt` unless it needs to be an app input.\n\n### Pattern 3b — Product-grounded generation from a URL\n\n```\nextract_product ──► generate_image / generate_text / generate_video (prompt inline via config.prompt)\n```\n\nWhen the user has a **product page URL**, add an `extract_product` node (set `config.url`) and wire `output_product` into the generator's `input_product` port. At run time it scrapes the page, structures it with a small LLM (name, description, features, price, audience), and the generator receives the product facts in its prompt — image/video generators also get the product photos and logo as reference images. `config.instructions` steers the extraction (\"focus on the enterprise plan\"). `output_images` / `output_logo` are ordinary image ports for editing or compositing. Prefer this over retyping product facts into prompts; combine with a brand node when both brand voice and product facts matter.\n\n### Pattern 4 — Branching\n\n```\n ┌─► generate_image (vertical)\nupload_image ──► switch┤\n └─► generate_image (horizontal)\n```\n\nUse `if_else` for boolean branches, `switch` for value-keyed branches. Each branch downstream of the switch only runs when its key matches.\n\n### Pattern 5 — Fan-out over a list (loops)\n\n```\ngenerate_text ──► split_text ──► generate_image (edge: { ..., loop: true } → one image per segment)\n```\n\n**Preferred spelling:** set `loop: true` on the EDGE in your GraphSpec — buildGraph compiles it to the target's `<targetPort>_loopMode` config key for you. Setting the config key directly still works and is what update_node_config patches use.\n\nThe canonical \"generate N variations\" shape: an LLM writes N blocks separated by `---`, `split_text` (delimiter `---`) turns them into an array, and the consumer runs **once per item**. Whether a port fans out or broadcasts is per-port:\n\n- **Ports that don't accept arrays** (most single-media ports: `composer` inputs, `edit_image.input_image`, `dub_media.input_video`, `generate_speech.input_text`…) fan out **automatically** — an array of N items = N runs, no config needed.\n- **Ports that accept arrays** (the prompt/media ports on generators: `generate_image.input_text_prompt`, `generate_video.input_text_prompt`, `generate_text.input_text_prompt`/`input_media`, `generate_image.input_reference_images`, `generate_video.input_first_frame`) **broadcast by default** — the whole array arrives as ONE combined payload (e.g. all 20 prompts concatenated into one giant prompt: almost never what you want). To fan out instead, set `\"<portId>_loopMode\": true` in the CONSUMER node's config, e.g. `{ \"model\": \"gpt-image-2\", \"input_text_prompt_loopMode\": true }`. `get_effective_ports` marks loop-capable ports with `loop_mode: true`.\n- Loop outputs collect into an array and **cascade**: a downstream node with its own loop-mode port fans out again over the collected results (LLM-per-concept → image-per-tailored-prompt works end to end).\n- **Multiple loop ports on one node cartesian-multiply** (N×M runs) — there is no zip/pairing. Keep ONE loop dimension per node; broadcast everything else (e.g. loop over prompts, broadcast one product photo into `input_reference_images`).\n- **`list_selector` does NOT iterate.** It picks ONE item (`selectionMode: \"index\"` + `index`, or `manual`/`random`). Use it to ROUTE item *i* to a dedicated branch — e.g. a 5-slide storyboard where each slide node needs different reference images: `split_text → list_selector(index: i) → slide_i`. For plain \"run once per item\", use loop mode, not selectors.\n- **`merge_videos` accepts a single incoming edge** when that edge carries an array (a loop output) — validation shows a warning, not an error, since edge count can't prove item count pre-run. Zero edges is still an error. Explicit per-item branches (`list_selector(index)` → N edges) remain useful when you need per-item control.\n\n### Pattern 6 — Human in the loop\n\n```\ngenerate_image ──► human_review ──► edit_image\n```\n\n`human_review` pauses the run and waits for an `approve` / `reject` decision (with optional note). Use when the user wants quality control before an expensive downstream step.\n\n### Pattern 7 — Composing layers into one deliverable\n\n```\ngenerate_image ──┐\ntext_prompt ─────┼─► composer ──► (output_image | output_video)\ngenerate_music ──┘\n```\n\n`composer` stacks connected images / text / video / audio into a finished canvas (`output_image` when everything is static, `output_video` when any video/audio is present). Set the simple top-level config (`canvasPreset`, `arrangement: \"overlay\" | \"sequence\"`, `duration`, `sequenceCrossfade`) and let defaults handle layer placement. **Always set `canvasPreset` explicitly** — the default is `instagram_post` (1080×1080 square), which silently crops 9:16 or 16:9 content; use `tiktok`/`instagram_reel` for vertical video, `youtube_thumbnail` for 16:9. First-run auto-layers place content at the top-left with small default text styling — tell the user to open the composer's visual editor once to position/style layers; the styling persists (keyed by edge) and applies to every later run, including per-item fan-out runs. The per-layer `layers[]` shape is documented in `orisu://nodes/composer` under `config.schema_fields` (on older servers it's absent — then don't hand-author layer objects at all). Even with the schema, prefer defaults for placement and only set layer fields you're sure about; for pixel-precise layout, build the graph with composer defaults and tell the user to fine-tune layers in the studio.\n\n## Playbook\n\n### Build a new workflow from a description\n\n1. Check `search({ kind: 'template', query })` first — if a published template matches, start from its graph (`orisu://templates/{id}`) instead of a blank spec. Otherwise read `orisu://nodes` and `orisu://models` and identify the smallest set of nodes that satisfies the request.\n2. Sketch a `GraphSpec` (nodes + edges). Don't set positions — dagre auto-layout fills them in.\n3. Call `validate_graph` with the spec. If it returns errors, fix and retry. Don't write to the server until validation passes. **Read the `warnings` too** — an \"unknown config key\" warning means that key will be silently ignored at runtime; fix it now (usually a snake_case key or a typo — see Concepts #3), don't ship it.\n4. Call `create_agent` with name, description, and the spec. If the agent already exists, call `replace_graph` instead. (Tool descriptions have the argument shapes.)\n5. Report back: agent id, what it does, how to run it.\n\n**Organize the canvas with groups.** For any multi-node workflow, add `groups` to the spec so the graph reads as labeled stages instead of a flat pile of nodes. Each group is `{ label, nodeIds, color? }` — `nodeIds` lists the member node ids, `color` is an optional preset (`neutral`, `amber`, `rose`, `green`, `blue`, `violet`, `teal`) that tints the frame so stages are distinguishable at a glance. A node belongs to at most one group. A sensible default taxonomy is **Inputs → Generation → Editing → Review → Output**; adapt to the actual workflow and give every group a real name (never leave one called \"Group\"). Groups are organizational only — they don't execute or affect data flow. Example:\n\n```json\n{\n \"nodes\": [ ... ],\n \"edges\": [ ... ],\n \"groups\": [\n { \"label\": \"Inputs\", \"nodeIds\": [\"prompt\", \"product_photo\"], \"color\": \"blue\" },\n { \"label\": \"Generation\", \"nodeIds\": [\"hero_image\"], \"color\": \"green\" }\n ]\n}\n```\n\n### Edit an existing workflow\n\nTwo mutation verbs, picked by the size of the change:\n\n- **`update_node_config`** — patch ONE node. `config` is **merged** (send only the changed fields), `value` replaces an input node's content, `label` renames. Node positions are preserved; a minor version is created. This is the right tool for the most common edits: swap a model, tweak a prompt or aspect ratio, rename a node.\n- **`replace_graph`** — replace the whole graph. Use only when the **structure** changes (adding/removing nodes or edges, rewiring).\n\nThe shared discipline for both:\n\n1. Call `view_agent({ agent_id })` to get the current graph and the inline canvas. **Keep the `latest_version_id` from the response** — it's your concurrency token.\n2. Single-node change → call `update_node_config({ agent_id, node_id, config: { <changed fields only> }, base_version_id })`. Check `warnings[]` in the result — an ignored/shadowed config key means your patch didn't do what you think.\n3. Structural change → derive the new GraphSpec in your reasoning, `validate_graph` it (follow each error's `next_action`; the errors carry \"Did you mean …?\" hints), then `replace_graph` with the validated spec **and `base_version_id: <latest_version_id>`**.\n4. On `VERSION_CONFLICT` (either tool): someone edited the agent since your read — re-read, recompute, retry with the fresh token.\n5. Tell the user the new version number you created.\n\n### Reuse a workflow with new inputs\n\nThis is the most common case for end-users.\n\n1. Fetch `orisu://apps` and pick the app by name/description. If you don't see a good match, ask the user or fall back to building a new workflow.\n2. Fetch `orisu://apps/{id}` to see the input schema (input fields, types, required flags).\n3. Collect input values from the user. For asset inputs, either pass an existing `asset_id` (if the user already uploaded), or call `upload_asset` to push a local/remote file in.\n4. Call `trigger_app` with the app id and inputs.\n5. Poll `orisu://runs/{run_id}` until `status` is `completed` or `failed`.\n6. Pull asset URLs out of the per-step outputs. Surface them to the user and **persist them if they'll be needed later** — asset URLs have a TTL.\n\n### Trigger a run on an agent (without publishing as an app)\n\nUse this for a **one-off or ad-hoc** execution, or while testing a workflow you're still building. If you'll run the workflow **repeatedly with varying inputs**, publish an app instead (see \"Reuse a workflow with new inputs\" above) — `trigger_run` inputs are a per-run overlay that never persists to the agent, so repeated `trigger_run` calls leave the canvas looking empty and are the wrong tool for a reusable, input-driven pipeline.\n\nSame as above but starting from `orisu://agents/{id}`:\n\n1. Call `get_run_inputs_schema({ agent_id })` to see the exact shape each input node expects. This returns one entry per input-category node with `value_type`, `example`, and the precise field set for file inputs (`{ url, key, contentType, size, name }` — NOT `{ asset_id }` as older docs suggested).\n2. Collect values from the user / call `upload_asset` for file inputs (map the response's `url`/`storage_key`/`size_bytes`/`content_type`/`filename` → `url`/`key`/`size`/`contentType`/`name`).\n3. Call `trigger_run` with the agent id and the assembled `inputs` map.\n4. Call `wait_for_run({ run_id })` to block until terminal — single round-trip instead of polling. For longer workflows pass `timeout_ms` up to 110000 and re-call if `timed_out: true` comes back.\n\n- **Test one branch without a full run:** `trigger_node_run({ agent_id, node_id })` runs only the target node and its upstream dependencies (ancestor-closure), reusing cached unchanged steps — cheap way to validate a mid-graph step. It runs saved input values; set them via `update_node_config` first. Nothing downstream of the target runs. For the whole graph, use `trigger_run`.\n\n### Wait for a run to finish — `wait_for_run` is almost always what you want\n\nAfter `trigger_run` / `trigger_app`, call `wait_for_run({ run_id, timeout_ms? })`. The server polls the DB; you get the full run JSON back in one shot when status reaches `completed` / `failed` / `cancelled` / `paused`. Avoids the token cost of N manual `orisu://runs/{run_id}` reads. Only fall back to reading the resource yourself when you specifically need a snapshot mid-run (e.g. to show progress).\n\n### Iterate on a failed run\n\n1. Read the `error.code` from the failed step (or top-level run.error).\n2. Branch on the code — see \"Error recovery playbook\" below.\n3. Re-trigger with corrected agent/inputs.\n\n## Error recovery playbook\n\nTool errors carry a stable `code` field; an agent that branches on it can recover automatically for the common cases. Every error also carries a `next_action` string and a `details` blob — read both before retrying.\n\n**Where errors actually show up.** Tool calls fail fast with the codes below — including `INSUFFICIENT_CREDITS`, which `trigger_run`/`trigger_app` now raise **before** queueing when the estimated cost exceeds the available balance. Most remaining *generation* problems — a provider timing out, a deprecated model — do **not** fail the tool call: the run is accepted, then the run itself ends `failed`. So after `wait_for_run` returns a `failed` run, read `steps[].error.message` on the failed step and apply the same fixes from the table (cheaper model, different provider). And always pass `base_version_id` on `replace_graph` / `update_node_config` edits — it's what turns a lost-update race into a clean `VERSION_CONFLICT` you can recover from.\n\n| Code | What it means | Fix |\n|---|---|---|\n| `AGENT_NOT_FOUND` | The agent_id doesn't exist in the bound org | `search({ kind: 'agent', query: <user's mention> })` or read `orisu://agents` to find a valid id. `who_am_i` to confirm the bound org. |\n| `RUN_NOT_FOUND` | The run_id doesn't exist (or belongs to another org / was pruned) | `list_runs({ agent_id })` to find the latest valid run. If the workflow needs re-running, call `trigger_run` again. |\n| `APP_NOT_FOUND` | The app_id is invalid | `orisu://apps` to enumerate published apps. |\n| `ASSET_NOT_FOUND` | The asset_id is invalid or expired | `orisu://assets` to find a valid one, or `upload_asset` to add a new one. |\n| `VERSION_NOT_FOUND` | The agent has no graph version yet (just created), OR you tried to restore a version that doesn't belong to this agent | If the agent is brand-new, call `replace_graph` to populate its first version. If you're restoring, read `orisu://agents/{id}/versions` for valid version_ids. |\n| `VALIDATION_FAILED` on `replace_graph` | The GraphSpec failed build- or semantic-time validation | Read `details.errors[]` and `details.summary.byCode` — the throw's `next_action` lists per-code fix hints. Re-run `validate_graph` after fixing; retry `replace_graph` only when `valid: true`. |\n| `VALIDATION_FAILED` on `trigger_run` | An input value didn't match the input node's expected shape | Call `get_run_inputs_schema({ agent_id })` to see the exact shape, fix the value, retry. |\n| `INPUT_TYPE_MISMATCH` | Same as the trigger_run validation case but specifically flagged | Same fix. |\n| `VERSION_CONFLICT` on `replace_graph` | The agent was edited between your `view_agent` read and your write (raised when you pass `base_version_id` and it no longer matches the latest version) | Re-read `view_agent`, recompute the new spec against the LATEST graph, retry with the new `latest_version_id` as `base_version_id`. Do NOT blindly retry the same spec. |\n| `UNAUTHORIZED` | The session lacks a required scope | Tell the user to reconnect Orisu and grant the scope the message names, OR use an Orisu API key whose scope set includes it. |\n| `RATE_LIMITED` | Throttled — `details.retry_after_seconds` says when to retry | Wait `retry_after_seconds` and retry. Don't burst-retry; back off. |\n| `INSUFFICIENT_CREDITS` | The org's credit balance can't cover the estimated/actual cost | Warn the user and either: (a) suggest a top-up, (b) pick a cheaper model via `suggest_model({ kind, use_case })` and swap the node's `config.model`. |\n| `MODEL_UNAVAILABLE` | The selected model is deprecated/retired/not-enabled | `suggest_model({ kind, use_case })` for a replacement; swap the node's `config.model` and retry. |\n| `PROVIDER_ERROR` | Downstream AI provider (fal, openai, …) errored or timed out — often transient | Retry once after a few seconds. If it persists, swap to a different model with the same capability. |\n| `INTERNAL` | Server-side issue we didn't classify | Surface to the user as \"Orisu had an unexpected issue\" and suggest contacting support. Don't retry-loop on this. |\n\n## Cost-aware patterns\n\n- **Before triggering an expensive workflow** (video gen, batch image jobs) call `estimate_run_cost({ agent_id })`. Compare against the org's available credits (`orisu://credits`). If the estimate is large or the balance is tight, warn the user with the estimate before triggering.\n- **Pick the cheapest model that meets the quality bar.** `suggest_model({ kind, use_case })` returns a ranked `suggestions[]`; each entry carries `best_for` / `when_to_use` guidance — use those (not the bare id) to justify the pick, and scan further down the list for cheaper alternatives. Per-variant pricing is in `orisu://models/{variant_id}`.\n- **Reuse generated assets when possible.** If the user wants a variation of an existing output, run from the existing asset via `upload_asset` + reference it; don't regenerate the base asset every time.\n\n### Run-count math — predict fan-out spend BEFORE triggering\n\nA fan-out multiplies real money. Know the count before you run, and **always pass `max_credits` on `trigger_run` / `trigger_app` for fan-out graphs** (your estimate × a safety factor, e.g. 2×) — the run then stops cleanly with `RUN_BUDGET_EXCEEDED` before any step that would push spend past the cap, with everything already generated kept and billed. Without it a run has NO ceiling: it bills iteration by iteration (reserve → settle) and only stops when the org balance can't cover the next one.\n\n- **A node's iteration count = the product of its loop dimensions.** Each port contributes: forced fan-out port → one iteration per item; `_loopMode` port → one per item; broadcast port → ×1 (whole array passed to every iteration). Two loop ports = the CARTESIAN product (10 prompts × 10 images = 100 runs) — keep ONE loop dimension per node unless you truly want the cross product.\n- **Hard cap: 200 iterations per node, enforced at RUN time** — exceeding it fails the node mid-run (after upstream nodes already billed). Design under it with margin.\n- **Chains multiply**: split(20) → per-item LLM (20 LLM calls) → per-item image (20 renders) → per-item 2:3 render (another 20). Walk the graph and write the counts down.\n- **Config multipliers stack on top**: `numberOfImages` (image gen) and `numberOfRuns` (video gen) multiply within each iteration; these ARE included in estimates.\n- **`estimate_run_cost` is a LOWER BOUND for dynamic fan-outs.** It multiplies only arrays already saved as values on the direct upstream node; an LLM-produced split count is unknowable pre-run, so that node is counted once and `total_is_lower_bound: true` is set with `fanout_nodes[{ per_item }]`. YOU know N (you told the LLM \"output exactly 12 blocks\") — quote the user `per_item × N` per fan-out node, not the headline total. Statically-known factors DO compose: chained fan-outs multiply through the graph, two loop ports on one node cartesian-multiply, literal `split_text` segment counts and `numberOfImages`/`numberOfRuns` are folded in, and `exceeds_runtime_cap` flags any node whose count would blow the 200-iteration runtime cap before you spend anything.\n- **Bound N explicitly in the graph**: instruct generator LLMs to output EXACTLY N `---`-separated blocks (never \"as many as you like\"); N is your budget knob — trimming a concept library from 20 to 5 blocks quarters the bill.\n- **Test one branch for pennies before the batch**: `trigger_node_run({ agent_id, node_id })` runs only the target + ancestors, or run once with a trimmed list. Per-iteration caching means a re-run after fixing one segment only pays for that segment — iterate cheaply, then scale N back up.\n- **Failure economics**: one failed iteration releases its reservation and the loop keeps going (partial results, partial spend); `INSUFFICIENT_CREDITS` or `RUN_BUDGET_EXCEEDED` mid-loop aborts the node with whatever completed already settled.\n\n## Handling in-progress / paused runs\n\n- **`paused`** means the run hit a `human_review` node. The workflow is waiting on the user's decision. Surface the prompt to the user (read `orisu://runs/{id}` for the human_review step's input/context), collect their decision, and call `submit_review({ run_id, decision: 'approve' | 'reject', note? })`. The run resumes after.\n- **`queued`** or **`running`** means the executor is working. Don't try to mutate the run mid-flight. If the user asks for progress, summarize the steps that have already completed from the run JSON; don't fabricate progress percentages.\n- **Don't poll faster than every 2 seconds.** The DB-backed status doesn't change faster than that under normal load. Use `wait_for_run` for the longest-acceptable timeout and let the server do the polling.\n\n## Anti-patterns\n\n- **Don't set node positions manually.** The server runs dagre auto-layout on every save. Just supply nodes and edges; ignore positions.\n- **Pick stable node ids and reuse them across versions.** Node ids in a GraphSpec are YOUR strings — `create_agent` / `replace_graph` keep what you supply. Reusing the same id when you mean \"the same node\" preserves UI continuity and run-history attribution; renaming an id is treated as deleting one node and creating another.\n- **Don't skip `validate_graph` before `replace_graph` on a non-trivial spec.** A roundtrip to validate is cheap and saves a write failure.\n- **Don't edit without the concurrency token.** Pass `view_agent`'s `latest_version_id` as `base_version_id` on every `replace_graph` edit. On `VERSION_CONFLICT`, someone else edited the agent between your read and your write — re-read with `view_agent`, recompute the new spec against the latest graph, and retry with the fresh token. Don't blindly retry the same spec.\n- **Don't store run output URLs without downloading.** They expire. If the user needs the asset persistently, push it back via `upload_asset` (which gives you a stable `asset_id`) or download it client-side.\n- **Don't try to enumerate model capabilities yourself.** Always read `orisu://models` — the catalog changes faster than your training data.\n- **Don't invent config keys, and don't import them from model listings.** Only set config keys that appear in the node's `defaults` / `fields` (see Concepts #3). Unknown keys don't error — they surface as validation *warnings* and are dropped at runtime, which reads as \"the model ignored my setting\".\n- **Don't build a workflow when an app already does the job.** Check `orisu://apps` first; reuse beats rebuild.\n- **Don't pass a bare string as an upload node's value.** `update_node_config`'s `value` for `upload_*` nodes must be a file/asset reference or an array of them: `[{ \"kind\": \"image\", \"assetId\": \"<from upload_asset>\" }]` or `[{ \"url\", \"key\", \"contentType\", \"size\", \"name\" }]` (map upload_asset's `storage_key`→`key`, `size_bytes`→`size`, `content_type`→`contentType`, `filename`→`name`). A JSON-encoded string of those shapes is accepted and parsed; any other string is rejected.\n- **Don't hand-assemble what a delimiter can split.** When one LLM call produces N items, have it separate them with a line containing exactly `---` and instruct \"nothing before the first block, nothing after the last\" — then `split_text` (delimiter `---`, trim + remove-empty) feeds the fan-out. Escaped delimiters typed as text (`\\n`, `\\t`) are understood as their control characters.\n\n## When to ask the user vs. decide yourself\n\n- **Ask** when the request is ambiguous about the output (e.g. \"make me something cool\" — clarify medium, style, intent).\n- **Ask** when you would have to pick between meaningfully different models with cost or quality tradeoffs they should know about.\n- **Decide yourself** when the user described the goal clearly and you can map it to a known pattern. Build, run, and report results rather than asking permission for every step.\n- **Always tell** the user the agent id you created (or reused) so they can open it in the studio if they want to inspect.\n"
}SHA-256: 3165b0ab37f3526dbe61b54536ec7bfb79fd62f580f9a7474a9c804220f5efcc