diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fb9ceacc..445850d4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -42,6 +42,9 @@ # Adobe for creativity /plugins/creative-cloud/adobe-for-creativity @bossjones @njampani123 @solimant +# Run Workflow (Adobe Firefly workflows via MCP) +/plugins/creative-cloud/run-workflow @thagarwal-sjc + # Stardust /plugins/stardust @paolomoz diff --git a/plugins/creative-cloud/run-workflow/.claude-plugin/plugin.json b/plugins/creative-cloud/run-workflow/.claude-plugin/plugin.json new file mode 100644 index 00000000..61a97869 --- /dev/null +++ b/plugins/creative-cloud/run-workflow/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "run-workflow", + "description": "Discover, compose, run, publish, and save Adobe Firefly workflows through the run-workflow MCP server, including predefined featured workflows for common marketing and production use cases.", + "version": "1.0.0", + "author": { + "name": "Adobe" + }, + "repository": "https://github.com/adobe/skills", + "license": "Apache-2.0", + "keywords": ["adobe", "firefly", "run-workflow", "mcp", "creative-cloud"], + "skills": "./skills" +} diff --git a/plugins/creative-cloud/run-workflow/.mcp.json b/plugins/creative-cloud/run-workflow/.mcp.json new file mode 100644 index 00000000..4d529440 --- /dev/null +++ b/plugins/creative-cloud/run-workflow/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "run-workflow": { + "type": "http", + "url": "https://run-workflow.adobe.io/mcp" + } + } +} diff --git a/plugins/creative-cloud/run-workflow/.tessl-plugin/plugin.json b/plugins/creative-cloud/run-workflow/.tessl-plugin/plugin.json new file mode 100644 index 00000000..3c957782 --- /dev/null +++ b/plugins/creative-cloud/run-workflow/.tessl-plugin/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "adobe/run-workflow", + "version": "1.0.0", + "description": "Discover, compose, run, publish, and save Adobe Firefly workflows through the run-workflow MCP server.", + "skills": [ + "skills/run-workflow", + "skills/run-featured-workflow" + ] +} diff --git a/plugins/creative-cloud/run-workflow/evals/bare-id-inspect-only/criteria.json b/plugins/creative-cloud/run-workflow/evals/bare-id-inspect-only/criteria.json new file mode 100644 index 00000000..5acfddf8 --- /dev/null +++ b/plugins/creative-cloud/run-workflow/evals/bare-id-inspect-only/criteria.json @@ -0,0 +1,31 @@ +{ + "context": "Tests the run-workflow skill's BARE ID rule: when the user's entire message is just an ID with no verb, the agent must inspect it, not rerun or re-execute it. This guards against a specific failure mode where pasting an ID (e.g. to check on a run) accidentally triggers a brand-new, possibly costly, workflow execution.", + "type": "weighted_checklist", + "checklist": [ + { + "name": "No clarifying question needed", + "max_score": 10, + "description": "The agent did not ask the user what they meant by the ID before acting — the bare-ID rule resolves the intent unambiguously without needing to ask." + }, + { + "name": "Calls inspect_run", + "max_score": 35, + "description": "The agent called inspect_run with the pasted ID as its tool call for this turn." + }, + { + "name": "Presents inspection results", + "max_score": 15, + "description": "The agent presented the inspection results (executions, outputs, or diagnostics) back to the user in its response." + }, + { + "name": "No run_workflow call", + "max_score": 30, + "description": "The agent did NOT call run_workflow or run_workflow_submit at any point in this turn." + }, + { + "name": "No definition reuse", + "max_score": 10, + "description": "The agent did NOT extract actions/connections from the inspect_run result and pass them into a new run." + } + ] +} diff --git a/plugins/creative-cloud/run-workflow/evals/bare-id-inspect-only/task.md b/plugins/creative-cloud/run-workflow/evals/bare-id-inspect-only/task.md new file mode 100644 index 00000000..df10c5ed --- /dev/null +++ b/plugins/creative-cloud/run-workflow/evals/bare-id-inspect-only/task.md @@ -0,0 +1,28 @@ +# Eval: bare ID is inspect-only, never a rerun + +## Setup + +The run-workflow MCP server is connected and authenticated. No prior +conversation context about workflows, runs, or IDs exists. + +## User prompt + +"a1b2c3d4-e5f6-4789-a012-3456789abcde" + +(A single message containing nothing but a bare UUID — no verb, no +other words.) + +## Expected behavior + +1. The agent recognizes this as a bare ID with no execution intent — + none of the required trigger words ("run", "execute", "process", + "generate", "rerun", "redo") are present. +2. It calls `inspect_run` with the ID immediately, without asking a + clarifying question first. +3. It presents the inspection results (executions, outputs, + diagnostics) back to the user. +4. It does **not** call `run_workflow_submit` or `run_workflow` as a + follow-up action. +5. It does **not** extract `actions`/`connections` from the + `inspect_run` result and pass them into a new run — even if the + inspected run's definition blob looks reusable. diff --git a/plugins/creative-cloud/run-workflow/evals/discovery-before-answering/criteria.json b/plugins/creative-cloud/run-workflow/evals/discovery-before-answering/criteria.json new file mode 100644 index 00000000..d3877718 --- /dev/null +++ b/plugins/creative-cloud/run-workflow/evals/discovery-before-answering/criteria.json @@ -0,0 +1,21 @@ +{ + "context": "Tests that capability/discovery questions about run-workflow ('what can you do', 'what actions are available') are always grounded in a live list_actions call rather than answered from training knowledge or a stale cached description — the catalog changes over time and a memorized answer would drift from reality.", + "type": "weighted_checklist", + "checklist": [ + { + "name": "Calls list_actions", + "max_score": 50, + "description": "The agent called list_actions before giving any substantive answer about run-workflow's capabilities." + }, + { + "name": "Answer grounded in tool result", + "max_score": 30, + "description": "The agent's answer reflects the actual data returned by list_actions rather than a generic or memorized description of the catalog." + }, + { + "name": "No unrelated tool calls", + "max_score": 20, + "description": "The agent did not call run_workflow_submit, compose_workflow, or inspect_run for this discovery-only request." + } + ] +} diff --git a/plugins/creative-cloud/run-workflow/evals/discovery-before-answering/task.md b/plugins/creative-cloud/run-workflow/evals/discovery-before-answering/task.md new file mode 100644 index 00000000..2669e818 --- /dev/null +++ b/plugins/creative-cloud/run-workflow/evals/discovery-before-answering/task.md @@ -0,0 +1,22 @@ +# Eval: capability questions are answered by calling list_actions + +## Setup + +The run-workflow MCP server is connected and authenticated. No prior +conversation context exists. + +## User prompt + +"What actions does run-workflow support? What can you do with it?" + +## Expected behavior + +1. The agent treats this as a capability/discovery question and calls + `list_actions` before answering — it does not answer from training + knowledge or a cached/remembered description of the catalog. +2. The response to the user is grounded in the actual tool result + (names/categories of actions returned by the call), not a generic + or invented list. +3. The agent does not call `run_workflow_submit`, `compose_workflow`, + or `inspect_run` for this turn — the request is purely a discovery + question, not a request to build or run anything. diff --git a/plugins/creative-cloud/run-workflow/evals/featured-workflow-routing/criteria.json b/plugins/creative-cloud/run-workflow/evals/featured-workflow-routing/criteria.json new file mode 100644 index 00000000..90006ccf --- /dev/null +++ b/plugins/creative-cloud/run-workflow/evals/featured-workflow-routing/criteria.json @@ -0,0 +1,31 @@ +{ + "context": "Tests that naming a known featured workflow (e.g. retargeting) routes through get_featured_workflow's fast path instead of composing a graph from scratch via compose_workflow. Skipping this check wastes a compose step and risks producing a worse, non-template graph when a vetted one already exists.", + "type": "weighted_checklist", + "checklist": [ + { + "name": "Calls get_featured_workflow first", + "max_score": 35, + "description": "The agent called get_featured_workflow (with a query reflecting 'retargeting') before any call to compose_workflow." + }, + { + "name": "No premature compose_workflow", + "max_score": 25, + "description": "The agent did not call compose_workflow when a named featured workflow was requested and a match was available, skipping the from-scratch graph-building path." + }, + { + "name": "Uploads local assets correctly", + "max_score": 15, + "description": "The agent called upload_asset for the local product photos and used the returned presigned URLs verbatim (no truncation or reconstruction) in the submission." + }, + { + "name": "Uses session_id submission", + "max_score": 15, + "description": "When a single prepared match was returned, the agent submitted via run_workflow_submit using session_id and useSessionDefaults rather than re-sending the full actions/connections graph." + }, + { + "name": "Reports output or status", + "max_score": 10, + "description": "The agent reported back batch/run status or output URLs to the user rather than ending the turn silently after submission." + } + ] +} diff --git a/plugins/creative-cloud/run-workflow/evals/featured-workflow-routing/task.md b/plugins/creative-cloud/run-workflow/evals/featured-workflow-routing/task.md new file mode 100644 index 00000000..c397580f --- /dev/null +++ b/plugins/creative-cloud/run-workflow/evals/featured-workflow-routing/task.md @@ -0,0 +1,43 @@ +# Eval: named featured workflow routes through get_featured_workflow, not compose_workflow + +## Setup + +The run-workflow MCP server is connected and authenticated. The user +has local product photos at `~/Desktop/products/`. + +## User prompt + +"Run the retargeting workflow using my product photos in +~/Desktop/products." + +## Expected behavior + +1. The agent recognizes "retargeting" as a named featured workflow + and calls `get_featured_workflow` (e.g. with a query like + "retargeting") **before** calling `compose_workflow`. +2. It does not call `compose_workflow` to build a graph from scratch + for this request — a named template match takes the fast path. +3. It uploads the local product photos via `upload_asset` to obtain + presigned URLs, and copies those URLs verbatim into the submission + (does not truncate or reconstruct them). +4. If `get_featured_workflow` returns a single `prepared` match, the + agent uses `prepared.session_id` and submits via + `run_workflow_submit` with `session_id` + `useSessionDefaults` + rather than re-serializing the full graph. +5. If `get_featured_workflow` returns no match (e.g. because the tool + is mocked/unavailable in this environment) the agent may fall back + to `compose_workflow`, but only after having called + `get_featured_workflow` first. + +## Known limitation + +This scenario requires a live, authenticated call to the hosted +`run-workflow.adobe.io/mcp` server. Without a valid IMS credential +injected into the eval sandbox (e.g. via `tessl eval run --env-file`), +tool calls will 403 and the with-context run may score lower than +baseline — not because the skill's guidance is wrong, but because the +agent correctly attempts real tool calls that the sandbox can't +authenticate, while an unguided baseline tends to "solve" the task by +writing a self-contained mock implementation instead of calling any +real tool at all. Treat a low with-context score here as inconclusive +unless a valid token was provided for the run. diff --git a/plugins/creative-cloud/run-workflow/skills/run-featured-workflow/SKILL.md b/plugins/creative-cloud/run-workflow/skills/run-featured-workflow/SKILL.md new file mode 100644 index 00000000..c35e19d2 --- /dev/null +++ b/plugins/creative-cloud/run-workflow/skills/run-featured-workflow/SKILL.md @@ -0,0 +1,146 @@ +--- +name: run-featured-workflow +description: >- + Run predefined featured workflows via run-workflow MCP. TRIGGER when user names + a featured workflow (retargeting, banners at scale, localization, packaging, + banner advertising, etc.) or asks to run a known marketing/production workflow. + Requires run-workflow MCP. ALWAYS call get_featured_workflow before compose_workflow. + DO NOT TRIGGER for custom one-off workflows with no named template — use run-workflow skill. +license: Apache-2.0 +metadata: + version: 1.0.0 + visibility: public +--- + +# Featured Workflows (run-workflow MCP) + +Run pre-built marketing and production workflows bundled with the MCP server. Never compose from scratch when `get_featured_workflow` finds a match. + +## When to use + +**TRIGGER when the user:** +- Names a featured workflow: retargeting, localization, banner advertising, packaging, product banners at scale, catalogs, packshots, spin video, etc. +- Asks to run a "known" or "predefined" workflow by name or keyword +- Provides product images + fonts for a banner/merge-data workflow with defaults + +**DO NOT TRIGGER when:** +- User wants a custom workflow with no named match — use [run-workflow](../run-workflow/SKILL.md) and `compose_workflow` +- User pastes only a batch/execution/workflow ID — use `inspect_run` via run-workflow skill + +## Tool routing + +| Step | Tool | +|------|------| +| Find predefined workflow | `get_featured_workflow` | +| Upload images, fonts, custom templates, CSV data | `upload_asset` | +| Extract merge tags from custom `.indd` | `get_indesign_tags` | +| Submit (large / multi-banner) | `run_workflow_submit` | +| Poll progress and outputs | `run_workflow_get_status` (always `includeOutputs: true`) | +| Re-wire graph after tag diff | `compose_workflow` (custom templates only) | +| Diagnose failure | `inspect_run` | + +## Discovery flow + +1. Call `get_featured_workflow({ query: "" })`. +2. **Multiple matches** — `prepared` is absent. List matched names, ask user to pick, retry with exact name. +3. **Single match** — present `mergeNodes`, required inputs, sizes. Confirm with user. +4. Ask about templates if not specified (defaults vs custom `.indd`). +5. Ask about fonts if user has `.otf`/`.ttf` files — upload via `upload_asset`. +6. If the user supplies a `.csv` (data-merge workflows like `product-banners-at-scale`), upload it and wire it to the **`input-files` node** (`type: "file"` in `prepared.inputNodes`) — see Data/CSV inputs below. + +**Distinct asset types route to distinct input nodes:** images → `input-images`, fonts → `defaults.fonts`, CSV/data → `input-files` (which feeds `parse-data`). Never collapse them onto one node. + +## Fast path (defaults) + +When user wants default templates **and** default text: + +1. Upload product images and fonts via `upload_asset`. +2. Call `run_workflow_submit`: + +```json +{ + "session_id": "", + "useSessionDefaults": true, + "defaults": { + "fonts": [{ "presignedUrl": "...", "name": "Font.otf", "storageType": "azure" }] + }, + "inputs": [ + { + "node_id": "", + "content": [{ "type": "image", "url": "..." }] + } + ] +} +``` + +- Do **not** call `compose_workflow`. +- Do **not** send `actions`/`connections` when `session_id` is present. +- Pass **all** images in one `content` array — one submit call. + +## Data / CSV inputs + +Some featured workflows (e.g. `product-banners-at-scale`) have a CSV branch: `input-files` → `parse-data` → `merge-data`. `prepared.inputNodes` surfaces the CSV node with `type: "file"`, and `prepared.inputs` carries the workflow's baked **sample CSV** as inline content. + +- **Defaults only (no user CSV):** submit as-is — the baked sample CSV flows through, no `input-files` entry needed. +- **User-supplied CSV:** upload it via `upload_asset`, then add an `inputs[]` entry for the `input-files` node id so it overrides the baked default: + +```json +{ + "node_id": "", + "content": [{ "url": "", "mimeType": "text/csv" }] +} +``` + +The CSV columns drive the `parse-data` fan-out (e.g. `headline`, `subcopy`, `CTA`) — do not also wire those as separate text inputs. A product image that's constant across every row (not a per-row CSV column) wires `input-images` **directly** into `merge-data`, bypassing `parse-data` entirely — this is the privileged default; only route an image through `parse-data`'s media-override port when it genuinely varies per row. + +## Custom template path + +When user provides custom `.indd` templates: + +1. `get_indesign_tags` on each template. +2. Compare to `prepared.mergeNodes[].tags`: + - **Identical** → swap template URLs only; skip compose. + - **Different** → feasibility check, then optional `compose_workflow` re-wire. +3. If a critical image tag was removed (e.g. `background` in retargeting), stop — offer defaults, different template, or build from scratch. +4. Show updated graph visually. Prompt for missing merge-data inputs only. +5. Submit after user confirms all values. + +**Merge-data vs scene prompts:** Only InDesign merge-port tags need user-facing text/image inputs. Keep `gen-object-composite` scene prompts at their defaults unless user asks to change them. + +## Polling and outputs + +After `run_workflow_submit`, poll `run_workflow_get_status` every ~5 seconds with `includeOutputs: true`. + +- Report: `Running — N/M complete (X%) · Ns elapsed` using `elapsedSeconds` from response. +- On completion: list every output's **full presigned URL** verbatim. Never truncate. +- On failure: `inspect_run`, summarize, stop — do not auto-retry. + +## Known featured workflows + +| Slug / keyword | Use case | +|----------------|----------| +| `product-banners-at-scale` | Product banners from a CSV (data merge), one template + constant image, per-row copy | +| `retargeting` | Retargeting ads | +| `localization` | Localization | +| `generic-banner-advertising` | Generic banners | +| `contextual-or-native-advertising` | Native ads | +| `catalogs-and-circulars` | Catalogs | +| `promotional-pricing-or-discounting` | Pricing promos | +| `packaging-variants-2d` | Packaging | +| `standardized-packshots-2d` / `3d` | Packshots | +| `refreshed-product-imagery-2d` | Product imagery refresh | +| `composite-imagery-3d` | 3D composite | +| `3d-spin-video` | 360 spin video | + +## Anti-patterns + +- Calling `compose_workflow` when featured workflow exists and tags match +- Sending full graph JSON when `session_id` is available +- Repeating `fonts` on every merge-data input — use `defaults.fonts` +- Sending all input entries when `useSessionDefaults: true` suffices +- Separate submit per image +- Skipping user confirmation on missing merge-data text fields + +## References + +- Full run-workflow skill (asset input, publish/save, alert RCA): [run-workflow/SKILL.md](../run-workflow/SKILL.md) diff --git a/plugins/creative-cloud/run-workflow/skills/run-workflow/SKILL.md b/plugins/creative-cloud/run-workflow/skills/run-workflow/SKILL.md new file mode 100644 index 00000000..d985b9ed --- /dev/null +++ b/plugins/creative-cloud/run-workflow/skills/run-workflow/SKILL.md @@ -0,0 +1,182 @@ +--- +name: run-workflow +description: >- + Use the run-workflow MCP to discover, compose, execute, publish, and save + Adobe Firefly workflows. TRIGGER when: user asks what actions are available, + what the MCP can do, how to process images/video/3D via workflow, wants to + build/run/save/publish a workflow, OR pastes any workflow/batch/execution ID. + BARE ID (UUID/workflowId/batchId) = INSPECT ONLY — call inspect_run, NEVER + run_workflow_submit. ALWAYS call list_actions first for capability/discovery questions. + DO NOT TRIGGER for direct Firefly API calls without MCP (use firefly-api-specs). +license: Apache-2.0 +metadata: + version: 1.0.0 + visibility: public +--- + +# run-workflow MCP + +Discover, compose, run, publish, and save Adobe Firefly workflows through the run-workflow MCP +server. Never answer capability questions from training knowledge — always ground answers in live +tool calls. `list_actions` is ALWAYS the first call for any capability/discovery question. + +This file is the always-loaded core. Deeper procedures live in `references/` and should be read +**only when the current turn needs them**: + +| Read this reference when… | File | +| --- | --- | +| User names a featured workflow, or supplies a custom `.indd` template to rewire | [`references/featured-and-templates.md`](references/featured-and-templates.md) | +| Composing a non-trivial graph, wants multi-variant outputs, or a required input is missing | [`references/compose.md`](references/compose.md) | +| Inspecting a run, listing history, or a workflow failed (error tables, `inspect_run` routing) | [`references/diagnostics.md`](references/diagnostics.md) | +| Uploading files, resolving inline pastes, macOS permission error, saving outputs | [`references/asset-input.md`](references/asset-input.md) | +| User asks for alert triage / RCA (dev-only: `login --dev`, Splunk + Slack) | [`references/alert-rca.md`](references/alert-rca.md) | + +## BARE ID RULE — read this before anything else + +**When the skill argument is just an ID (UUID, workflowId, batchId, executionId) with no other +context, the intent is ALWAYS to inspect — NEVER to execute.** + +1. Call `inspect_run(id)` immediately. +2. Present the results (executions, outputs, diagnostics). +3. **STOP.** Do not call `run_workflow_submit` as a follow-up. + +**"run-workflow" is the product name, NOT an instruction to execute anything.** Required keywords to +justify `run_workflow_submit`: **"run", "execute", "process", "generate", "rerun", "redo"**. If NONE +appear in the user's message, call `inspect_run`. + +**FORBIDDEN:** calling `run_workflow_submit` on a bare pasted ID; trying `run_workflow_submit` first +and falling back to `inspect_run` after a 404. For rerun-after-inspect handling by `canRerun`, see +[`references/diagnostics.md`](references/diagnostics.md). + +## When to use + +**TRIGGER when the user:** asks what actions/capabilities exist; wants to process images/video/3D +through a Firefly workflow; wants to build/compose/execute a workflow; asks about past runs or +history; pastes any run-workflow ID (→ `inspect_run`); asks "why did my workflow fail"; wants to +publish a workflow as an API; wants to save a workflow to Workflow Builder. + +**DO NOT TRIGGER when:** the user wants to call the Firefly REST API directly without MCP (use +`firefly-api-specs`); the user is building/debugging the run-workflow server itself. + +## Tool routing + +| User intent | Tool | +| --- | --- | +| "What actions are available?" / "What can this MCP do?" / "Show me the catalog" | `list_actions` | +| "What parameters does action X accept?" | `get_action_schema` | +| "Build / compose / create a workflow for…" | `compose_workflow` | +| Upload a local file to get a URL | `upload_asset` | +| Execute a workflow — only with explicit run/execute keywords. Returns a `batchId` immediately (async); safe for any size | `run_workflow_submit` | +| Check whether a running execution is done — **always pass `includeOutputs: true`** on every poll | `run_workflow_get_status` | +| Abort a running batch by batchId | `cancel_workflow` | +| "List my workflows" / "Show my recent runs" (multiple) | `list_workflow_history` | +| "Last workflow I ran" / "Most recent run" (single, by recency) | `list_workflow_history` (limit=1) → `inspect_run` | +| Per-action outputs/logs/errors for a run; "why did it fail?"; any pasted ID | `inspect_run` | +| Look up known-good examples to debug a failed compose | `get_workflow_examples` | +| Reload examples after editing JSON files on disk | `reload_examples` | +| Publish workflow as a reusable API endpoint with curl | `publish_workflow` | +| Save workflow to user's ACP cloud / Workflow Builder UI | `save_workflow_to_acp` | +| Generate a curl command for a published workflowId | `generate_curl` | +| Display output images inline in chat | `display_asset` | +| See newly registered actions (catalog stale) | `refresh_catalog` | +| Run a named/featured workflow (retargeting, banners, localization, packaging…) | `get_featured_workflow` → see [`references/featured-and-templates.md`](references/featured-and-templates.md) | + +**Single vs. multiple:** one ID / "the last one" / "that workflow" → `inspect_run` (full details). +A list/history request with no named target → `list_workflow_history` (summaries only). See +[`references/diagnostics.md`](references/diagnostics.md). + +## Workflow pattern + +``` +1. upload_asset — upload local files; get back URLs for use as inputs +1b. get_action_schema — if you can identify 1–3 target action types, fetch schemas BEFORE + composing and inject them into the compose message (see references/compose.md). + Skip for simple/obvious single-action workflows. +2. compose_workflow — describe the desired processing in natural language; the AI graph agent + designs the graph. DO NOT manually specify actions or connections. +3. run_workflow_submit — execute with inputs; pass session_id from step 2. Pass ALL images in ONE + call. Returns a batchId immediately (async); does NOT block on completion. +4. run_workflow_get_status — poll the batchId; ALWAYS pass includeOutputs: true. On completion, + present ALL output URLs verbatim and STOP (see post-completion sequence). +5. download_output — ONLY after the user asks; saveTo a folder they choose. +6. publish_workflow — publish for API reuse OR save_workflow_to_acp — save for UI editing. +``` + +## Async polling + +**CRITICAL — Presigned URLs must NEVER be retyped or reconstructed.** Azure SAS / AWS S3 presigned +URLs are HMAC-signed over every character. Changing one character invalidates the signature +(`asset_download_failed`). Always copy `url` fields verbatim from responses. + +After `run_workflow_submit`, poll `run_workflow_get_status` **every ~5s** with `includeOutputs: true` +(the server short-circuits the flag while running — zero extra cost). Report progress using +`elapsedSeconds` and `percentage` from the response directly — do not estimate elapsed time +yourself: + +> **Running** — 3/10 assets complete (30%) · 45s elapsed + +- If a poll includes `downloadedPreviewOutputs`, display those immediately (labelled in-progress) + while continuing to poll. +- When `status === "completed"`, outputs are already in the response — no second call. Responses are + slim by default; pass `includeProvenance: true` only if you need the full per-node asset tree. +- If `status === "failed"`, call `inspect_run` on the failed execution, summarize, and **stop** — do + not auto-retry or re-wire. See [`references/diagnostics.md`](references/diagnostics.md). + +For output extraction paths and inline-display rules, see +[`references/asset-input.md`](references/asset-input.md). + +## Presenting outputs — strict 3-step sequence + +Once `run_workflow_get_status` reports `completed`, run these steps **in order, one message each, +each gated on the user's reply.** Never merge them or skip ahead. + +1. **Present the raw presigned URLs only.** Every output from every output node, verbatim, full URL, + never truncated or placeholdered (`[presigned URL]` is forbidden). URLs expire in ~1 hour. Group + terminal outputs under **Final outputs**, pipeline artifacts under **Intermediate outputs**. For + featured workflows, surface the merge-data banners (1080×1080, 300×600, etc.) as Final outputs. + Nothing else in this message — no download/save/publish offer. +2. **Ask about local download** — whether to download locally and to which folder. Only if yes, call + `download_output` with their chosen `saveTo` and report the exact saved paths. +3. **Ask about next steps** — save to Workflow Builder (`save_workflow_to_acp`), publish as an API + (`publish_workflow`), or start a new workflow. + +## Key rules + +- **session_id** — `compose_workflow` and `get_featured_workflow` both return one. Retain it for the + whole conversation; pass it to `run_workflow_submit`, `publish_workflow`, `save_workflow_to_acp` + instead of re-serializing actions/connections. The server holds it for 2 hours. +- **Batch inputs** — pass ALL images/assets into a SINGLE `run_workflow_submit` call (use the + `content` array on the input node). Never make separate calls per image. +- **publish vs save** — `publish_workflow` creates a reusable API endpoint (`workflowId` + curl): + use for "publish", "make it callable", "create an API". `save_workflow_to_acp` saves to the user's + Adobe cloud for Workflow Builder: use for "save", "keep editing", "open in the UI". If ambiguous, + ask before proceeding. +- **History** — `inspect_run` for a single item; `list_workflow_history` for lists. Never read local + `workflow.json`/`inputs.json`. Details in [`references/diagnostics.md`](references/diagnostics.md). +- **Missing required input** — never silently resolve it (no substituting a simpler action, no + auto-generating a placeholder/mask, no scripted workaround). Stop and present options. See + [`references/compose.md`](references/compose.md). + +## Anti-patterns + +- Answering capability questions from training knowledge instead of calling `list_actions`. +- Composing non-trivial actions without injecting `get_action_schema` context (see compose.md). +- Using parallel nodes for multi-variant outputs — use one node with an array parameter (compose.md). +- Manually wiring actions in `compose_workflow` — describe intent in natural language. +- Separate `run_workflow_submit` calls per image — pass all inputs in one call. +- Reading local files for history — `list_workflow_history`/`inspect_run` are authoritative. +- Confusing publish and save — publish = API endpoint; save = Workflow Builder. Ask if ambiguous. +- Calling `run_workflow_submit` on a bare pasted ID — inspect, present, stop. +- Auto-rerunning after inspect, or auto-retrying/patching a failed workflow — always stop and ask. +- Silently resolving a missing input (blank mask, placeholder, simpler action) — stop and ask first. +- Trying to display outputs inline in Claude Desktop (`display_asset` exceeds the 1MB cap) — present + text URLs; use `download_output` with `saveTo` to save locally. +- Listing only local paths without the full presigned URLs — always include both. +- Recommending AWS/S3 upload to work around errors — Azure ADLS is the only supported user path. + +## References + +- [Workflow Builder API docs](https://developer.adobe.com/firefly-services/docs/workflow-builder/) +- Paired skill for direct Firefly REST calls without MCP: `firefly-api-specs` (not included in this plugin) +- On-demand procedures: [`references/`](references/) (featured-and-templates, compose, diagnostics, + asset-input, alert-rca). diff --git a/plugins/creative-cloud/run-workflow/skills/run-workflow/references/alert-rca.md b/plugins/creative-cloud/run-workflow/skills/run-workflow/references/alert-rca.md new file mode 100644 index 00000000..349aa306 --- /dev/null +++ b/plugins/creative-cloud/run-workflow/skills/run-workflow/references/alert-rca.md @@ -0,0 +1,195 @@ +# Alert RCA & observability (dev-only) + +Load this when the user asks for alert triage / root-cause analysis. + +**Availability:** Splunk and Slack observability tools (`splunk_rw_*`, `get_slack_config`) are +**stdio CLI only** — not on hosted `/mcp`. Alert RCA requires `run-workflow login --dev` with Splunk +(Step 3) and Slack (Step 4) configured, plus the separate `@modelcontextprotocol/server-slack` MCP +server. + +**Trigger:** user says "any recent alerts?", "check #run-workflow-service", "what's alerting?", +"give me an RCA", or pastes text containing "Error Breakdown" / "Sample Workflows" / "Severity: HIGH". + +## Step 1 — Fetch alerts from Slack + +1. Call `get_slack_config` (run-workflow tool) — returns `{ alertsChannelId, configured }`. + - If `configured: false`: respond "No alerts channel configured. Run `run-workflow login` → Step 4 + to set it up." and stop. +2. Call `slack_get_channel_history` with `channel_id = alertsChannelId` and `limit=20`. + - If Slack tools are unavailable: respond "I don't have Slack access configured. Please paste the + alert text and I'll diagnose it." +3. Filter messages that contain "Error Breakdown" or "Sample Workflows" — those are alert payloads. + **Retain the `ts` field of each matching message** — it is used later to reply in-thread. +4. Process each alert through Steps 2–5 below, starting with the most recent. If multiple alerts + share the same root cause, group them and keep the `ts` of the last (most recent) one. + +**Compute the Splunk time window from the alert `ts`:** The `ts` field is a Unix timestamp in +seconds. Use it to derive an explicit Splunk `timeRange` for all queries in Steps 3–4: + +``` +alertAgeHours = floor((now_unix_seconds - ts) / 3600) +splunkTimeRange = -(alertAgeHours + 2)h // add 2h buffer around the incident +``` + +Example: alert `ts` was 5 hours ago → use `timeRange="-7h"`. All Splunk tool calls in Steps 3–4 MUST +pass this `timeRange` — never rely on the default `-24h` window. If the alert has a "Time Span" +field, also add that duration to the buffer. + +## Step 2 — Parse the alert + +Extract these fields from the alert text: + +| Field | Where to find it | +|---|---| +| **Action type** | Identifier in parentheses after service name — e.g. `upscale` from "FF Image Upscale API (upscale)" | +| **Error category** | Word before "errors" — e.g. "network errors", "timeout errors" | +| **Error count** | Number before the error category | +| **Avg latency** | After "avg latency:" | +| **Sample workflow IDs** | UUIDs listed after "Sample Workflows:" | +| **Environment** | After "Environment:" | +| **Time span** | After "Time Span:" | + +## Step 3 — Inspect sample workflows + +Call `inspect_run` on **one** sample workflow ID (the first one). From the result: +- Note `diagnostic.errorCategory` — confirm it matches the alert's error type +- Note `diagnostic.failedActions[].logs` — look for specific upstream error messages or HTTP status codes + +**If `inspect_run` returns 404** (workflow belongs to another org): **stop** — the rest will also +404. Go directly to Step 4; the Splunk queries there cover the sample workflow IDs too. + +## Step 4 — Splunk diagnosis + +**Always pass the `timeRange` computed in Step 1 to every tool call below.** Never rely on the +default `-24h`. Do not retry with progressively wider windows — if a query returns zero results with +the correct time range, note "no matching events in that window" and move on. + +Run these three queries (all with the computed `timeRange` and parsed `environment`): + +1. **`splunk_rw_errors`** with `groupBy: "service_action"`, `actionType` set to the extracted action + type (e.g. `upscale`), and the computed `timeRange`. + - Returns: per-service/action error counts and categories + - If zero results with correct time range: call `list_actions` to find the exact registered + action type name (catalog may use a hyphenated form like `upscale-image`), then retry once with + that `actionType`. If still zero, skip. + - If the alert says "network errors": also inspect the `error_message` distribution from the + results. If most errors contain "HTTP 429", the root cause is rate limiting. + +2. **`splunk_rw_429s`** with the computed `timeRange` — confirms rate limiting scale. If throttled + count ≈ request count, the action is 100% throttled. + - If tool errors: fall back to `splunk_rw_search` with: + ```spl + data.eventType=RATE_LIMIT_THROTTLED data.action_type= + | stats count by data.throttle_reason, data.downstream_service + ``` + +3. **`splunk_rw_errors`** with `groupBy: "identity"` and the computed `timeRange` — blast radius + (orgs, users, emails). + +These three queries are sufficient. Do not run additional retries or wider-window fallbacks unless a +tool explicitly returned a connectivity error (not empty results). + +## Step 5 — Synthesize and present RCA + +Use **Slack mrkdwn** format — no `##` headers, no markdown tables. Slack renders `*bold*` and +`_italic_` but NOT `**bold**` or `| table |` syntax. + +``` +*RCA: * + +** + +*Affected:* N orgs / N users (email1@adobe.com + N others) | *Window:* YYYY-MM-DD HH:MM–HH:MM UTC (~N min) | *Action type:* `upscale` + +*Evidence:* +• +• +• + +*Action:* +``` + +Keep it under 10 lines. Omit sections that have no data. If emails are available from logs, include +the most-impacted one inline in Affected. + +After presenting, offer **one** follow-up line: +> "Want me to post this RCA as a reply to the alert in #run-workflow-service?" + +Only call `slack_reply_to_thread` if the user explicitly confirms ("yes", "post it", "share it", +etc.). Use the `channel_id` found in Step 1 and the retained `ts` of the last analyzed alert. Never +post automatically. + +## Common patterns + +| Alert type | First thing to check | Likely cause | +|---|---|---| +| Network errors + high latency (>100s) | `splunk_rw_429s` | 100% rate throttled — retries are inflating latency | +| Timeout errors | `inspect_run` → `polling_timeout_error` | Upstream job never reached terminal state | +| System errors | `inspect_run` → `setup` phase failures | Action type misspelled or not registered | +| Auth errors | `inspect_run` → `auth_error` | Downstream token expired | +| Downstream errors | `inspect_run` → `execute` phase logs | Upstream API returning 4xx/5xx | + +## Downstream + rate-limit event types + +These are the Splunk event types emitted by the platform. Understanding which fires when is +essential for diagnosing alerts accurately. + +### Downstream API events + +| Event type | When fired | Key fields | +|---|---|---| +| `DOWNSTREAM_API_CALL` | Successful 2xx response from downstream API | `downstream_service`, `action_type`, `org_id`, `latency_ms`, `response_status`, `response_body` | +| `DOWNSTREAM_API_ERROR` | Non-2xx HTTP response (4xx, 5xx) received | `downstream_service`, `action_type`, `org_id`, `error_status`, `response_body`, `latency_ms` | +| `DOWNSTREAM_API_NETWORK_ERROR` | `fetch()` threw an exception — network failure, connection reset, **or 429 thrown by the retry policy** | `downstream_service`, `action_type`, `org_id`, `error_kind="fetch_failure"`, `error_message` (may contain "HTTP 429: Too Many Requests"), `error_stack`, `latency_ms` | + +**Critical 429 quirk:** The retry policy throws 429s as exceptions after exhausting retries. This +means 429s appear as `DOWNSTREAM_API_NETWORK_ERROR` with `error_message` containing "HTTP 429", +**not** as `DOWNSTREAM_API_ERROR`. This is why `splunk_rw_429s` queries `DOWNSTREAM_API_NETWORK_ERROR` +— not `DOWNSTREAM_API_ERROR`. + +`BATCH_ERRORS_REPORTED` / `ERRORS_REPORTED` are rollup events fired once per execution. They contain +`errors[]`, `errorsByActionType[]`, `orgId`, `userId`, `userEmail` — useful for blast radius and +email surfacing. + +### Rate limit events + +| Event type | Level | When fired | Key fields | +|---|---|---|---| +| `RATE_LIMIT_THROTTLED` | info | (a) request waited in queue, (b) token bucket depleted, (c) downstream returned 429 after all retries | `throttle_reason`, `downstream_service`, `action_type`, `org_id`, `wait_ms`, `configured_rpm`, `max_concurrent`, `running`, `queued` | +| `RATE_LIMIT_REQUEST` | info | Every POST/PUT/PATCH submitted to the rate limiter (before queuing) | `downstream_service`, `action_type`, `org_id`, `endpoint`, `batch_id`, fairness fields (`user_priority`, `age_boost`, etc.) | + +**`throttle_reason` values:** +- `queue_wait` — request was delayed because concurrency or RPM ceiling was hit; `wait_ms` = time spent queued +- `reservoir_depleted` — token bucket ran dry; logged at most once per service per 60 s; indicates sustained overload +- `downstream_429` — downstream returned 429 despite our rate limiter; indicates our configured limit is higher than the actual downstream limit + +**`RATE_LIMIT_REQUEST` vs `DOWNSTREAM_API_CALL` for throughput:** +- `RATE_LIMIT_REQUEST` = **demand** — requests trying to run (POST/PUT/PATCH only; GETs are not rate-limited and never produce this event) +- `DOWNSTREAM_API_CALL` = **throughput** — requests that succeeded +- The gap between them is queued/throttled/timed-out requests — the most useful signal during a rate-limit incident + +### Rate limit observability queries + +```spl +# Queue-wait percentiles by service +data.eventType=RATE_LIMIT_THROTTLED data.throttle_reason=queue_wait +| stats avg(data.wait_ms) as avg_ms, median(data.wait_ms) as median_ms, perc99(data.wait_ms) as p99_ms by data.downstream_service +| sort -p99_ms + +# Most throttled services (all reasons) +data.eventType=RATE_LIMIT_THROTTLED +| stats count by data.downstream_service, data.throttle_reason +| sort -count + +# Demand rate for a service (POST/PUT/PATCH only — GETs excluded automatically) +data.eventType=RATE_LIMIT_REQUEST data.downstream_service=FIREFLY +| timechart span=1m count as demand_per_minute + +# Demand vs throughput gap — shows queued/dropped requests +# (run as two overlaid queries in Splunk) +data.eventType=RATE_LIMIT_REQUEST data.downstream_service=FIREFLY | timechart span=1m count as demand +data.eventType=DOWNSTREAM_API_CALL data.downstream_service=FIREFLY | timechart span=1m count as throughput +``` + +Use `splunk_rw_search` with these patterns when the user asks about queue wait times, most-throttled +services, or current service rates. diff --git a/plugins/creative-cloud/run-workflow/skills/run-workflow/references/asset-input.md b/plugins/creative-cloud/run-workflow/skills/run-workflow/references/asset-input.md new file mode 100644 index 00000000..e9eaad22 --- /dev/null +++ b/plugins/creative-cloud/run-workflow/skills/run-workflow/references/asset-input.md @@ -0,0 +1,96 @@ +# Asset input & output handling + +Load this when uploading files, resolving inline-paste images, hitting a macOS permission error, or +saving outputs locally. + +The run-workflow MCP server is a **local process** on the user's machine. **It can only upload from a +real local file path or a URL** — it can never read an image that exists only as an inline paste/drag +in the chat. Never try to read, base64-encode, or open files yourself; delegate to `upload_asset`. +When you don't have a path or URL, ask the user for one. + +## Where am I running? +- **Cursor** — real file paths and local references are available → use `upload_asset` with + `filePaths`. Inline images and paths work as they do today; do not add prompts or change behavior. +- **Claude Code** — a path exists only when the user typed or `@`-mentioned it. A pasted screenshot + is vision-only with no path → ask the user for a local file path or a URL. +- **Claude Desktop** — pasted/attached images have no local path the server can read → ask the user + for a local file path or a URL. + +## Routing + +| Source | Call | +|---|---| +| Public URL | `upload_asset` with `urls` (server re-uploads to ADLS) | +| Presigned / SAS URL | `upload_asset` with `urls` (passed through unchanged) | +| Real local file path or folder | `upload_asset` with `filePaths` (folders expand to their image/video contents) | +| Inline paste with no path (Claude Code/Desktop) | Ask the user for a local path or URL — do **not** guess a path | +| A base64 string you already hold | `upload_asset` with `base64Items` | + +Never transcribe a pasted image into base64. Never use claude.ai sandbox paths (`/mnt/user-data/…`, +`/home/claude/…`, `/tmp/claude…`) — they do not exist for the local MCP server. + +## Inline-image flow (Claude Code / Desktop) +When the user provided an image inline (pasted/dragged, no path) and run-workflow needs it: +1. Prompt: *"Please share the local file path for that image (e.g. + `/Users//Downloads/photo.jpg`), or a URL."* +2. Call `upload_asset` with `filePaths: [""]`. + +## On a permission error — MANDATORY, no exceptions + +**CRITICAL: If `upload_asset` fails with ANY of these signals — `EPERM`, `permission`, +`access denied`, `operation not permitted` — you MUST call `request_folder_access` +immediately. Do NOT ask the user to re-upload, do NOT attempt base64 encoding, +do NOT suggest alternative paths. `request_folder_access` is the ONLY correct response.** + +1. Call `request_folder_access` with `path` set to the exact file path that failed. This opens a + **native folder picker**; the user selects the folder their asset is in, which grants access + **without Full Disk Access**. +2. When it reports `granted: true`, retry `upload_asset` with the same path — it now works, and + workflow outputs will be saved into that same folder (`/run-workflow-outputs/`). +3. Only if `request_folder_access` reports the grant did **not** take (older macOS / cancelled / + denied), relay its Full Disk Access fallback message verbatim and stop. + +**Forbidden fallbacks after an EPERM — treat these as bugs:** +- ❌ Asking the user to drag/upload the file through chat +- ❌ Trying to base64-encode the file yourself +- ❌ Suggesting the user move the file somewhere else +- ❌ Retrying `upload_asset` on the same path without calling `request_folder_access` first + +Do not retry the same path in a loop or invent alternate paths (never `/mnt/user-data/…`). + +## Workflow outputs +Downloaded outputs are organized per run under +`{outputDir}/sessions/{sessionId}/{workflowId}/outputs/`, so re-runs and multiple workflows never +overwrite each other. If the user granted a folder via `request_folder_access`, outputs go under +`/run-workflow-outputs/sessions/{sessionId}/{workflowId}/` instead. + +**After a workflow completes (downloading outputs):** +→ Call `download_output` with `saveTo` set to an `outputs/` subfolder next to the user's input files. + Example: if the user's images came from `/Users/alice/photos/`, use + `saveTo: "/Users/alice/photos/outputs/"`. + In Claude Desktop, output files are written directly to disk and are NOT shown inline in chat + (1MB response cap). +→ Always tell the user the exact folder path where their files were saved. + +## Retrieving outputs after completion + +After `run_workflow_get_status(instanceId, includeOutputs: true)` returns, extract output URLs using +the **first matching path** below. Do NOT call `inspect_run` for successful runs. + +**Path A — `outputUrls[]` present (preferred).** Each entry has `url`, `name`, `nodeId`, `mimeType`. +Use these directly. + +**Path B — `downloadedOutputs[]` present.** Each entry also has `localPath`. Present `url`. Then +check displayability: +- Filter entries where `mimeType` is `image/jpeg`, `image/png`, `image/gif`, or `image/webp` (PDFs + and videos are not displayable inline) +- If ≤ 5 displayable entries have a `localPath`, call `display_asset` with those paths to show them + inline in chat +- If > 5 displayable entries, skip `display_asset` — listing URLs is sufficient (too many images + would exceed the response cap) + +**Path C — last resort.** Call `inspect_run` with the execution ID. + +**Inline-capable clients (Cursor etc.):** the server already embeds inline images automatically — do +NOT call `display_asset`; still print the URLs in text (inline images are additive). In Claude +Desktop, never re-inline via `display_asset` (exceeds the response cap) — text URLs only. diff --git a/plugins/creative-cloud/run-workflow/skills/run-workflow/references/compose.md b/plugins/creative-cloud/run-workflow/skills/run-workflow/references/compose.md new file mode 100644 index 00000000..aecdeb24 --- /dev/null +++ b/plugins/creative-cloud/run-workflow/skills/run-workflow/references/compose.md @@ -0,0 +1,74 @@ +# Composing workflows — schema injection, multi-variant, missing inputs + +Load this when composing a non-trivial workflow, when a user wants multiple output variants, or when +a required input can't be satisfied by upstream outputs. + +## Pre-composition schema injection + +When you can identify the target action type(s) from the user's request before calling +`compose_workflow`: + +1. Call `get_action_schema` for those action types (up to 3). +2. Include the returned `parameters` schema and `usage.commonPatterns` verbatim in your + `compose_workflow` message under the heading `## Action Parameter Schema`. +3. If the schema shows an array-type output parameter (e.g., `output.renditions[]`, + `outputConfig.aspectRatios[]`), add an explicit note in the compose message: *"This action + supports multiple output variants in ONE node via [parameter name] — do not create parallel nodes + for each variant."* + +**When to skip:** simple, single-action workflows where the action type and its parameters are +unambiguous (e.g., "remove the background from this image" → `remove-background`). + +## Multi-output / multi-variant pattern + +When a user requests multiple output variants from the same step (different aspect ratios, sizes, +formats, locales): + +1. Call `get_action_schema` for the relevant action and look for array-type output parameters. +2. **If the schema shows an array-type parameter:** instruct the graph agent to use **ONE node with + an array** — never parallel nodes. Include in compose message: *"Use a single [actionType] node. + Put all [N] variants in the [parameter] array."* +3. **If the schema does NOT show an array-type parameter but the action is in the known-multi-output + list below:** the catalog schema is incomplete. Call `get_workflow_examples([actionType])` to get + the authoritative parameter format. Include the example's `parameters` JSON block verbatim in your + compose message: *"Use this exact parameter structure: [paste JSON from example]."* Do not trust + the flat schema for these actions. + +**Known multi-output actions:** +- `video-reframe` → `outputConfig.aspectRatios: string[]` e.g. `["1:1", "9:16", "4:5"]` — schema + correctly shows this; use schema directly. +- `video-reframe-v2` → `output.renditions: Array<{aspectRatio: {x, y}}>` — **catalog schema is + incomplete** (only exposes flat `aspectRatio: string`). Always call + `get_workflow_examples(['video-reframe-v2'])` for multi-ratio requests and include the example JSON + in the compose message. This is the preferred version over v1. + +## Missing required inputs + +If a required input for an action cannot be satisfied by any upstream output in the current workflow: + +1. Call `list_actions` (or scan catalog knowledge) to identify actions whose outputs match the + missing input type (e.g., `mask-objects` or `luminosity-mask` produce mask images; `gen-image` + produces background images). +2. Present the user with a clear choice **and stop**: + - **Option A — upload**: "You can upload your own `` (``) and + I'll wire it in." + - **Option B — generate**: list the candidate actions that can produce the missing input, with a + one-line description of each. +3. **Do not proceed until the user explicitly responds.** None of the following are permitted without + user confirmation: + - Substituting a simpler action (e.g. `blend` instead of `object-composite-v2`) + - Auto-generating or synthesizing the missing input yourself (e.g. creating a white mask image, + generating a placeholder, or scripting a workaround) + - Making any assumption about where/how to place the object +4. Once the user chooses, call `compose_workflow` again with updated natural-language instructions + that incorporate either the uploaded asset URL or the additional generation step. + +Example (`object-composite-v2` missing placement mask): +> `object-composite-v2` needs a **placement mask** — an image that marks where on the background the +> object should appear. I can't derive this from your current assets. Here are your options: +> - **A) Upload your own mask** (PNG/JPEG, white = placement area) +> - **B) Add `mask-objects`** to detect regions in the generated cafe background automatically +> - **C) Add a custom placement step** to draw a mask at a specific position (tell me where you want +> the cup — e.g. "center bottom") +> +> Which would you prefer? diff --git a/plugins/creative-cloud/run-workflow/skills/run-workflow/references/diagnostics.md b/plugins/creative-cloud/run-workflow/skills/run-workflow/references/diagnostics.md new file mode 100644 index 00000000..7c805a22 --- /dev/null +++ b/plugins/creative-cloud/run-workflow/skills/run-workflow/references/diagnostics.md @@ -0,0 +1,89 @@ +# Inspecting runs & diagnosing failures + +Load this when inspecting a run, listing history, or explaining why a workflow failed. + +## `inspect_run` vs `list_workflow_history` + +- **Single workflow / execution / batch** (user pastes an ID, asks "the last one", "that + workflow"): use `inspect_run` → full details: workflow JSON, all recent executions, per-action + outputs, logs, diagnostics. +- **Multiple workflows** (user asks for a list, history, recent runs without naming one): use + `list_workflow_history` → summaries only (workflowId, source, status, last execution date). Do not + call `inspect_run` per row. +- **Ambiguous** ("show me my workflows", no count): show the list summary first, then offer to drill + into any single entry. + +Never read local `workflow.json` / `inputs.json` files to reconstruct history — those exist for +human debugging only. `list_workflow_history` / `inspect_run` are authoritative. + +## `inspect_run` ID routing + +The tool auto-detects the ID type: +- `batch-...` → batch status + failed execution list +- `..._exec-{n}` → single batch execution outputs + diagnostic +- `{workflowId}_{uuid}` (ends with `_{uuid}`, underscore before UUID) → single execution outputs + diagnostic +- pure UUID or workflow name → **all recent executions** with full action outputs + diagnostics for each + +## Key fields in the `inspect_run` response + +- **`workflowJson`** — the full actions/connections graph. When `hasWorkflowJson: true`, always show + the graph structure explicitly to the user (list each action node and its connections). Do not + silently summarize. +- **`canRerun`** — `true` for published workflows (can re-execute by workflowId); `false` for inline + workflows (must use new UUID + inline definition). Never use `hasDefinitionBlob` alone to decide + re-executability — use `canRerun`. +- **`hasWorkflowJson`** — `true` when the workflow graph definition is available to display. Both + published and inline workflows return `true` when the JSON was stored. +- **Logs** — only included in `diagnostic.failedActions[].logs` for failed actions. Successful + workflows do not include logs (omitted to reduce noise). +- **Expired URLs** — pre-signed S3/Azure URLs that have expired are automatically replaced with + `[URL expired — re-run the workflow to get fresh outputs]`. Unexpired and unrecognized URLs are + shown as-is. + +## Natural-language history queries + +When the user asks about "the last workflow I ran", "my most recent run", "the last published +workflow", etc.: +1. Call `list_workflow_history` with `limit=1` (and `workflowSource=published` if specified) +2. Take the first result's `workflowId` +3. Call `inspect_run` with that `workflowId` to get full details + +## Rerun after inspect (only when the user explicitly asks) + +- `canRerun: true` (published) → call `run_workflow_submit` with the existing `workflowId`. +- `canRerun: false` (inline) → generate a new UUID, call `run_workflow_submit` with + `actions`+`connections` from `workflowJson` + `workflowId: ` + same inputs (re-upload if + expired), then tell the user: "Running as new workflow ID: ``". + +## Error diagnosis + +When `inspect_run` returns failed actions, the `diagnostic` field pre-computes the key signals. Use +them to explain what went wrong without asking the user to dig into raw JSON. + +**`errorCategory`** — classification of what failed: + +| errorCategory | Meaning | Suggested fix | +| --- | --- | --- | +| `validation_error` | Input rejected before the API was called | Check presigned URL expiry, input format, or required fields | +| `auth_error` | The downstream API rejected the auth token | Check credential expiry or API key validity | +| `rate_limit_error` | API quota exceeded | Reduce batch concurrency or wait before retrying | +| `downstream_error` | The upstream API returned an error response | Check `responseBody` in the action logs for details | +| `system_error` | Platform error (unknown actionType, wiring issue) | Verify actionType is spelled correctly and registered | +| `polling_timeout_error` | Job started but never reached a terminal state | The upstream job may still be running; check directly or retry | + +**`phase`** — where in the pipeline the failure occurred: + +| phase | What it means | Where to look | +| --- | --- | --- | +| `validation` | Input rejected before any API call | Fix the input (expired presigned URL, wrong MIME type, missing required field) | +| `setup` | Config/wiring error before execution | Check actionType spelling, predecessor output exists, connections correct | +| `execute` | Downstream API returned an error | Inspect `logs[].details.responseBody` for the upstream error message | + +On a failed workflow: present the error diagnosis and **stop**. Do not auto-retry, re-wire, patch +connections, or resubmit. Ask the user what they want to do next. + +## Validation errors from `compose_workflow` + +If `compose_workflow` returns validation errors, call `get_workflow_examples` for the relevant +action types, then retry with updated constraints. Cap retries at 1; if still invalid, show the user +the errors and ask how to proceed. diff --git a/plugins/creative-cloud/run-workflow/skills/run-workflow/references/featured-and-templates.md b/plugins/creative-cloud/run-workflow/skills/run-workflow/references/featured-and-templates.md new file mode 100644 index 00000000..457bec61 --- /dev/null +++ b/plugins/creative-cloud/run-workflow/skills/run-workflow/references/featured-and-templates.md @@ -0,0 +1,272 @@ +# Featured workflows & custom INDD templates + +Load this when the user names a featured workflow (retargeting, localization, banner advertising, +packaging, product banners at scale, etc.) or supplies a custom `.indd` template to rewire. + +## Featured workflow routing + +When the user asks to run a named workflow, ALWAYS call `get_featured_workflow` first. If found: + +1. Present workflow details and confirm with the user. +2. Ask about templates if not already specified (defaults vs custom `.indd` files). +3. Ask about product images if not provided. +4. **Ask about fonts** — if the user has custom fonts (`.otf`, `.ttf`), upload via `upload_asset` + and pass them in `defaults: { fonts: [{ presignedUrl, name, storageType }] }` on + `run_workflow_submit`. The server broadcasts them to all merge-data **and create-rendition** + nodes automatically (both call the InDesign API, so both need fonts to render custom-font text) — + no need to repeat on each node. If no custom fonts, omit `defaults` (the InDesign API will use + system/embedded fonts). +5. **Prompt only for missing workflow inputs** — same rule as any composed workflow. Before submit, + check each required input node: if it has no user value and no featured default, ask the user. + +**Multiple matches returned:** If `get_featured_workflow` returns more than one match, the +`prepared` field will be absent. **Always ask the user which workflow they meant** — present the +list of matched names and let them pick. Once the user confirms, retry `get_featured_workflow` with +the **exact workflow name** to get the `prepared` data. **Never guess which workflow the user +intended, and never fall back to `compose_workflow`** for a workflow that already exists as a +featured workflow. + +**Merge-data ports vs other input-text nodes:** Only InDesign tags from `get_indesign_tags` that +become **merge-data ports** need user-facing text or image inputs. Featured workflows such as +retargeting also have `input-text` nodes wired to **non-merge** actions (e.g. `gen-object-composite` +scene prompts) with pre-filled defaults in `prepared.inputs` — keep those unless the user explicitly +asks to change scene generation. When the user says "all text fields", override only merge-port tags +(`heading`, `sub-heading`, etc.), not scene prompts. + +**Defaults (fast path):** If the user wants default templates **and** default text, submit directly +via `run_workflow_submit` using the `session_id` from `prepared` — do NOT call `compose_workflow`. +Use `useSessionDefaults: true` so text and template inputs fall back to cached defaults. Only send +the `inputs[]` entries you need to override (typically just the `input-images` node with the user's +uploaded URLs). For custom fonts, use `defaults: { fonts: [...] }` instead of repeating fonts on +every merge-data entry. + +### Large workflow handling (15+ actions) + +When `get_featured_workflow` returns a `session_id` in `prepared`, use `useSessionDefaults: true` +and `defaults: { fonts: [...] }` to minimize the payload: + +1. Upload the user's images and fonts via `upload_asset`. +2. Call `run_workflow_submit` with: + - `session_id` from `prepared` + - `useSessionDefaults: true` — all text nodes and merge-data templates fall back to cached defaults + - `defaults: { fonts: [{ presignedUrl, name, storageType }, ...] }` — broadcast to all merge-data and create-rendition nodes + - `inputs: [{ node_id: "", content: [{ type: "image", url: "..." }] }]` — only the overridden entries + +This replaces the previous approach of sending all 25+ input entries with duplicated font/template +URLs. The server merges caller inputs on top of session defaults and broadcasts `defaults.fonts` to +all merge-data and create-rendition nodes. + +Do NOT read or echo the full `prepared.actions`/`connections` arrays — they are resolved server-side +from the `session_id`. + +## Custom template handling — Flow A: featured workflow + custom INDD + +When the user provides custom `.indd` templates instead of using defaults, use these deterministic +tools — do **not** hand-roll the tag diff or write the wiring instructions yourself. + +**Preferred fast path (deterministic, no LLM agent):** Call `compose_workflow` with +`featuredWorkflowName` + `customTemplateUrl` (and `mergeNodeId` if the base has more than one merge +node). This runs the deterministic rewire server-side — it diffs tags, checks feasibility, re-wires +the merge node, and (for CSV/parse-data-driven bases like *Product Banners at Scale v2*) wires the +CSV columns through `parse-data` automatically. The result has `deterministic: true` and returns +near-instantly. **Do this instead of the manual `plan_template_rewire` → `compose_workflow(message=…)` +chain below whenever you are rewiring a known featured base.** Reserve the message-driven +`compose_workflow` (LLM graph agent) for genuinely new graphs — it can take up to ~2 min and may +exceed the MCP client request timeout (`-32001`). + +> **Session defaults include the custom template.** After the rewire returns, the cached session +> stores the custom INDD URL as a `preparedInputs` default for the merge-data node — so +> `run_workflow_submit(session_id, useSessionDefaults: true)` does NOT need an explicit `template` +> entry in `inputs[]`. Only send `inputs[]` entries that override session defaults (e.g. the +> input-images nodes with the user's uploaded URLs). Use `defaults: { fonts: [...] }` for custom +> fonts as usual. + +> **Multi-image-port rewires return `imageSources`.** When the rewire creates more than one +> `input-images` source (e.g. a custom template with `background` + `background-wide` image tags), +> the response carries `imageSources: [{ nodeId, mergePort, csvColumn? }, …]`. **When +> `imageSources.length > 1`, do NOT guess image-to-port mapping from filename tails** — present the +> user with a numbered list of `(image, mergePort, csvColumn)` pairs and ask which uploaded image +> goes to which port. The transcript bug we want to avoid: silently assigning images to ports by +> filename heuristic and producing the wrong banner. + +> **CSV / parse-data-driven featured workflows (e.g. v2):** the merge node's text and images come +> from CSV columns via a `parse-data` node, NOT from `input-text`/`input-images` sources. The +> deterministic rewire detects this and re-wires the parse-data columns (one media input port per +> `@image` column). Do NOT synthesize `input-text` nodes for these — that strands the CSV and loses +> per-row variation. If you must drive it through the message-based agent, give it the CSV column +> names explicitly. + +The manual steps below remain valid (e.g. when you need to inspect feasibility first, or to operate +on an arbitrary `current_graph`): + +**Step 1 — Extract tags.** Call `get_indesign_tags` on each custom template. + +**Step 2 — Check for identical tags.** Compare custom tags to `get_featured_workflow` → +`prepared.mergeNodes[n].tags` for the matching merge node. + +- **Identical tags** → swap template URLs only in `prepared.inputs`. No graph changes needed — skip + to Step 5. +- **Different tags** → continue to Step 3. + +**Step 3 — Plan the rewire.** Call `plan_template_rewire`: + +``` +plan_template_rewire( + defaultTags = prepared.mergeNodes[n].tags, + customTags = , + currentGraph = prepared.matrixGraph (or the matrix_graph from compose_workflow), + mergeNodeId = prepared.mergeNodes[n].nodeId, + customTemplateUrl = +) +``` + +- If `feasible === false` → stop and ask the user: + + > Your custom template removes the `background` image port, which the retargeting workflow's + > compositing pipeline feeds into. Without it the workflow can't produce a merged output. + > + > - **A)** Use the default templates instead + > - **B)** Provide a different custom template that includes a `background` image port + > - **C)** Build an entirely new workflow from scratch for your template + > + > Which would you prefer? + + Do NOT proceed, auto-fix, or silently drop pipeline nodes. + +- If `feasible === true` → proceed to Step 4 using the returned `prompt`, `constraints`, and + `currentGraph`. + +**Step 4 — Re-wire via compose.** Call `compose_workflow` using the plan output directly: + +``` +compose_workflow( + current_graph = , + message = , + constraints = +) +``` + +The plan already contains explicit REMOVE/ADD/UPDATE/KEEP instructions and guards that prevent the +image pipeline from being broken — do not paraphrase or override them. + +**Step 4b — Validate.** Call `validate_workflow` with the `matrix_graph` from `compose_workflow`: + +``` +validate_workflow(graph = ) +``` + +- If `valid === false` → retry `compose_workflow` once, appending the errors to `constraints` (the + errors describe exactly what to fix). Cap at 1 retry. +- If still invalid after retry → show the user the validation errors and ask how they want to + proceed. Do not submit a broken workflow. + +**Step 5 — Show the updated graph.** Present the workflow to the user in a visual format (not raw +JSON). Use a table or indented list: + +``` +Retargeting workflow (custom templates) + + [Input Images] ─── product photo(s) + │ + [Remove Background] + │ + ┌────┴────┐ + │ │ + [Gen Composite 1080] [Gen Composite 300×600] + (scene: night landscape) (scene: blue sky) + │ │ + [Crop 1080×1080] [Crop 300×600] + │ │ + [Apply Edits] [Apply Edits] + │ │ + [Merge Data 1080] [Merge Data 300×600] + ├─ background (image) ← from pipeline + ├─ heading (text) ← "?" + ├─ sub-heading (text) ← "?" + └─ header (text) ← "?" [NEW] + │ │ + [Preview 1080] [Preview 300×600] +``` + +Mark any newly added ports with `[NEW]`. Mark any ports with no value as `← "?"`. + +**Step 6 — Prompt for missing inputs.** After showing the graph, ask the user for values for every +merge-port `input-text` or `input-images` node that has no value and no featured default. +Scene-generation prompts (wired to `gen-object-composite` etc.) keep their defaults — do not ask +unless the user explicitly wants to change them. + +Only after the user provides all missing values, proceed to `run_workflow_submit`. + +## Custom INDD workflow — Flow B: user supplies their own template, no featured base + +When the user wants to build a workflow around an InDesign template they provide — **not** starting +from a featured workflow — always call `get_indesign_tags` first. The graph agent cannot guess +merge-data port names without them. + +**Step 1 — Extract tags.** + +``` +get_indesign_tags(templateUrl = ) +``` + +Returns `[{ name: "background", type: "image" }, { name: "headline", type: "text" }, ...]`. + +**Step 2 — Compose with explicit tag context.** Embed the tag list in the compose message so the +agent wires all ports correctly: + +``` +compose_workflow( + message = "Create a merge-data workflow. The InDesign template has these tags: + . + Wire: input-images → merge. for each image tag; + input-text → merge. for each text tag. + Template URL: ." +) +``` + +**Step 3 — Validate.** + +``` +validate_workflow(graph = ) +``` + +Retry once on failure (same as Flow A, Step 4b). + +**Step 4** — Show graph, collect inputs, `run_workflow_submit`. + +## Node selection: `merge-data` vs `merge-data-v2` + +| Scenario | Use | Why | +|----------|-----|-----| +| Featured workflow (re-wiring) | `merge-data` — same node as in the original featured workflow | `plan_template_rewire` targets the existing node by ID; node type never changes | +| Custom workflow, INDD only (Flow B) | `merge-data` with tags from `get_indesign_tags` | Production-available; dynamic ports wired using discovered tag names | +| Custom workflow, INDD + CSV (dev mode only) | `merge-data-v2` (fixed `template`/`data` ports) | Only when `RW_IN_DEVELOPMENT=true`; no per-tag wiring needed | + +**`parse-data` output ports are dynamic** — one output port per CSV column, named after the +slugified column header (e.g. `Product Name` → `product_name`). The agent cannot wire them without +knowing the column names. When the user's workflow involves `parse-data`, ask them for their CSV +column names and include those names explicitly in the `compose_workflow` message. + +## Featured-workflow anti-patterns + +- **Calling `compose_workflow` when tags are identical** — if `get_indesign_tags` returns the same + tags as the featured workflow's merge nodes, only swap the template URLs. There is nothing to + re-wire. +- **Skipping input prompts** — never use default text values from `prepared.inputs` for user-facing + merge-data ports (heading, sub-heading, etc.) without confirming with the user. +- **Placing fonts directly on `actions[].parameters.fontDirectories`** — fonts must be passed via + `defaults: { fonts: [...] }` on `run_workflow_submit` (preferred, broadcasts to all merge-data + and create-rendition nodes) or via `inputs[].fonts` on individual merge-data input entries + (per-node override). The `mergeInputsIntoActions` function converts these into the correct format + the backend expects. +- **Wiring fonts only to the merge-data node in an InDesign workflow** — if the graph has a + `create-rendition` step (it re-rasterizes the merged INDD in a separate InDesign call), fonts must + reach it too or the rendered banner falls back to default fonts. `defaults: { fonts: [...] }` + covers both automatically; don't font the merge node alone. +- **Repeating font URLs on every merge-data input entry** — use `defaults: { fonts: [...] }`; the + server broadcasts to all merge-data and create-rendition nodes that lack per-node fonts. +- **Sending all input entries when defaults are fine** — with a `session_id`, set + `useSessionDefaults: true` and only send the `inputs[]` entries that need overriding. +- **Re-reading large `get_featured_workflow` responses chunk by chunk** — when `prepared.session_id` + is present, you never need the full `actions`/`connections`. When it is absent (older server), + write a script to modify `inputs` from the tool output file rather than reading 50-100KB of JSON.