---
name: fluent-workflow-builder
description: Create and edit Fluent Commerce workflow JSON definitions. Build rulesets, states, transitions, and triggers. Validate structure before upload. Triggers on "build workflow", "create workflow", "add ruleset", "edit workflow", "workflow json".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: <operation> [workflow-name]
cursor-tier: manual
planning-gate: mandatory
---

# Workflow Builder

Create, edit, and validate Fluent Commerce workflow JSON definitions.

## Pre-flight: Feedback Check

Before starting, check for past learnings from this skill:

1. Check if `accounts/<PROFILE>/feedback/fluent-workflow-builder.jsonl` exists
2. If yes, read last 20 records
3. Filter to: same entity type/subtype if known, FAILURE or USER_CORRECTED outcomes, confidence = CONFIRMED or PROMOTED
4. Extract top 3 relevant learnings — use as internal "WATCH OUT" guidance during execution
5. Do NOT show raw feedback to the user — silently adjust approach based on past issues

## Pre-flight: Project Knowledge Check

Check for project-specific knowledge that should inform workflow construction:

1. Check if `accounts/<PROFILE>/knowledge/` exists
2. If yes, read it and find files where `relevant-skills` includes `fluent-workflow-builder`
   **Account knowledge:** Read from `accounts/<PROFILE>/knowledge/` when `FLUENT_PROFILE` is set. These contain project-specific conventions.

3. Read matching knowledge files — typically:
   - `knowledge/platform/domain-model.md` — use for entity edges, cross-domain relationships, valid GraphQL connections
   - `accounts/<PROFILE>/knowledge/naming-conventions.md` — use for status names, category names, ruleset names
   - `accounts/<PROFILE>/knowledge/coding-standards.md` — use for workflow JSON structure conventions
   - `accounts/<PROFILE>/knowledge/client/business-rules.md` — use for routing logic, constraints, exception handling
   - `accounts/<PROFILE>/knowledge/client/data-model.md` — use for entity types, subtypes, attribute names
4. If `accounts/<PROFILE>/memory/index.md` exists, read the top account-specific workflow and deployment gotchas and apply them silently.
5. Apply knowledge silently — do NOT show raw knowledge to user, just follow the conventions

## Planning Gate

### Pre-flight Chain (Three-Level)

Before creating or modifying any workflow, run the three-level pre-flight check:

1. **Spec check:** If `accounts/<PROFILE>/features/<slug>/status.json` exists for this work:
   - Check `spec: "APPROVED"` — If missing, BLOCK: `→ BLOCKED: PREREQ_MISSING — Run /fluent-use-case-discover first`
   - Check `plan: "APPROVED"` — If missing, BLOCK: `→ BLOCKED: PLAN_REQUIRED — Run /fluent-feature-plan first`
   - If both approved, skip to implementation referencing the plan's §4 (Workflows), §5 (Statuses), and §6 (Rulesets) sections
2. **Multi-artifact work** (workflow + rules + settings): STOP. Invoke `/fluent-feature-plan` first — this skill cannot be used for multi-artifact work without a feature plan
3. **Standalone single-workflow work** (no feature context, no multi-artifact):
   - Check `accounts/<PROFILE>/tasks/` for a task plan with `Status: APPROVED`
   - If approved task plan found, skip to implementation
   - If no plan, continue to the Planning Gate below to write a plan for this skill

**Before creating or modifying any workflow, write a plan using the template from `PLAN_TEMPLATE.md` in the `fluent-feature-plan` skill.** Every table row must carry a Source column (NEW/EXISTING/MODIFIED/REUSED/OOTB).

**If this workflow change is part of a larger feature** (rules + settings + webhooks), use `/fluent-feature-plan` first. Then reference the approved plan during implementation.

**Workflow-builder specific emphasis — ensure these are covered:**

1. **Business Context (§1)** — what business flow this workflow enables or modifies
2. **State machine (§3.1)** — Mermaid `stateDiagram-v2` with ALL statuses; annotate `:::added`, `:::removed`, `:::modified`. Validate syntax per `knowledge/mermaid-syntax-rules.md`
3. **Cross-entity flow (§3.2)** — Mermaid `sequenceDiagram` for cross-entity event chains, webhooks, external systems. Validate syntax per `knowledge/mermaid-syntax-rules.md`
4. **Before/After diff (§3.4)** — if modifying, show changes (or use `workflow.diff`)
5. **Workflows (§4)** — every workflow created or modified with Source column and current version
6. **Statuses (§5)** — new/modified statuses with Source column, entity type, which ruleset adds them
7. **Rulesets (§6)** — rulesets with Source column, trigger event, from/to status, ordered rule list with NEW/OOTB/EXISTING annotations
8. **Rules Inventory (§7)** — all rules with Source classification
9. **Settings (§9)** — settings with Source column, context, value type, JSON example for NEW
10. **Webhooks (§10)** — setting name, method, payload shape, trigger rule
11. **Scheduled Events (§11)** — event name, delay, purpose
12. **GraphQL Operations (§12)** — queries/mutations marked as NEW or REUSED pattern
13. **Deployment Sequence (§17)** — modules before workflows that reference new rules

**Write the plan to:** `accounts/<PROFILE>/tasks/<YYYY-MM-DD>-workflow-builder-<slug>.md`. Set `Status: PENDING`.

Present the full plan content to the user and wait for approval before writing any workflow JSON. On approval, update the file to `Status: APPROVED`. If the user says "just do it", proceed directly (still write the file for audit trail).

## Post-Build: Workflow Validation (MANDATORY)

After building or modifying any workflow JSON, **always run `/fluent-workflow-validate`** before proceeding. This is a hard gate — do not skip.

`/fluent-workflow-validate` runs 50+ labeled checks (S01-S10, G01-G10, T01-T10, R01-R09, U01-U10, X01-X06) covering:
- Schema compliance (required fields, types, formats)
- Status graph integrity (reachability, dead-ends, orphans, category validation)
- Trigger and routing (conflicts, circular event chains, orphaned rulesets)
- Rule references (plugin.list verification, SetState/SendEvent prop checks)
- User action schema (eventName, subtype, modules, context structure)
- Cross-entity consistency (multi-entity type/subtype fields)

If validation returns FAIL (CRITICAL or HIGH issues), fix before deploying. If the user skips this step, `/fluent-workflow-deploy` will block on the same checks.

For deeper structural analysis (Mermaid diagrams, process flow classification, dependency mapping), use `/fluent-workflow-analyzer` after validation passes.

## GraphQL Schema Validation (MANDATORY when plan includes §12)

**Before generating workflow JSON that references rules with GraphQL operations, validate those operations against the live schema.** Full protocol: `knowledge/platform/graphql-preflight.md`.

When the approved plan includes §12 (GraphQL Operations), run this checkpoint before writing any workflow JSON:

### Step 1: Validate planned queries and mutations

For each operation listed in the plan's §12 table:

1. **Full query strings** → `graphql.validate({ query: "<operation>" })` — check `valid` field (not `ok`)
2. **DynamicUtils field paths** → `graphql.introspect({ type: "<EntityType>" })` — verify each path segment
3. **Mutation inputs** → `graphql.introspect({ mutation: "<mutationName>" })` — verify input field names and types

### Step 2: Validate rule props that configure GraphQL behavior

For rules with props that reference entity types, field names, or query parameters:

- `props.entityType` → verify the entity type exists: `graphql.introspect({ type: "<value>" })`
- `props.attributeName` / `props.attributeNames` → verify the attribute name(s) are valid fields on the entity type
- `props.jsonpath` values referencing entity paths → walk the path via introspection

### Step 3: Record validation results

Add a validation summary to the workflow builder output:

```
GraphQL Schema Validation:
  ✓ orderById query — valid (graphql.validate)
  ✓ DynamicUtils paths [ref, status, items.product.name] — valid (graphql.introspect)
  ✗ updateFulfilmentItem mutation — INVALID (field does not exist; use updateFulfilment with nested items)
```

If any validation fails → fix before writing workflow JSON. If MCP is unavailable → proceed with warning.

## Knowledge References

- **Order creation patterns:** `knowledge/platform/create-order-reference.md` — createOrder input schema, fulfilmentChoice wiring, ORDER::MULTI patterns. Essential when building ORDER workflows that process items linked to different fulfilment choices.

## Ownership Boundary

This skill owns workflow structure/design guidance and validation checklists.

Operational workflow commands are owned by `/fluent-workflow`:

- `workflow list`
- `workflow download`
- `workflow merge`
- `workflowlog` operations

## Pre-flight: Custom Rule Availability

Before building or modifying a workflow that references custom rules, verify those rules are deployed:

1. **Identify custom rules** in the plan's §7 (Rules Inventory) — any rule with Source = NEW, MODIFIED, or EXISTING that is NOT `FLUENTRETAIL.*`
2. **For each custom rule**, query: `plugin.list({ name: "<RuleName>" })`
3. **If ALL custom rules found:** Proceed — rules are registered and available.
4. **If ANY custom rule is MISSING:** **STOP.** Inform the user: *"Custom rule `<RuleName>` is not registered. Deploy the module first via `/fluent-build` → `/fluent-module-deploy`, then resume workflow editing."*
5. **Exception:** If the workflow is being built as part of a feature plan where module deployment comes later (§17 Deployment Sequence), note the dependency and proceed — but add a `DEPLOYMENT_ORDER_WARNING` comment in the plan.
6. **Stale file check:** If editing a local workflow file, compare its version against live server (MCP workflow.get) before starting. Warn if local version is behind.

This check prevents deploying workflows that reference unregistered rules, which causes runtime `NO_MATCH` failures.

## Pre-Check: Load Source Analysis

Before adding custom rules to a workflow, check if `/fluent-custom-code` analysis artifacts exist:

```
accounts/<PROFILE>/analysis/code/workflow-rule-map.json
accounts/<PROFILE>/analysis/code/source-map.json
accounts/<PROFILE>/analysis/code/constraints.json
```

If found, use them to:
- `source-map.json` → `modules[].rules[]` — available custom rules, their parameters, and entity types
- `workflow-rule-map.json` → existing rule-to-workflow bindings (avoid duplicates)
- `constraints.json` → extension constraints and risks

If not found, fall back to workflow JSON analysis only. Treat artifact-gate failures the same as "not found" (for example missing required files, missing hashes, or blocking `missingSources` in `constraints.json`). Suggest running `/fluent-custom-code <PROFILE>` first when adding custom rules.

## Handoff Protocol

### Signals emitted by this skill

| Signal | Condition | Example |
|--------|-----------|---------|
| `-> READY: <path>` | Workflow JSON created/modified | `-> READY: accounts/<PROFILE>/workflows/<RETAILER_REF>/ORDER__HD.json` |
| `-> NEXT: /fluent-<skill>` | Always validate after build | `-> NEXT: /fluent-workflow-analyzer` |
| `-> BLOCKED: <reason>` | Prerequisite missing | `-> BLOCKED: PREREQ_MISSING — Custom rule CancelOrderRule not registered. Run /fluent-build + /fluent-module-deploy` |
| `-> SKIP: <reason>` | No workflow changes needed | `-> SKIP: No workflow changes in this feature plan` |

### Error codes

| Code | Condition | Recovery |
|------|-----------|----------|
| `PLAN_REQUIRED` | Multi-artifact work attempted without approved plan | Run `/fluent-feature-plan` first |
| `PREREQ_MISSING` | Custom rules not deployed or source analysis missing | Deploy module first via `/fluent-build` + `/fluent-module-deploy` |
| `VALIDATION_FAILED` | Workflow JSON structure invalid | Fix JSON structure per validation checklist |

## Local Workflow Edit (Cost-Efficient)

For simple edits (field type change, version bump, add/remove a rule, rename a status), avoid loading the full workflow JSON into LLM context. Use the file-based workflow instead:

```
1. Download to file:       workflow.get({ entityType, entitySubtype, outputFile: "./workflows/<TYPE>-<SUBTYPE>.json" })
2. Grep for the target:    Grep for the specific field/value to change
3. Edit with Edit tool:    Surgical string replacement (old_string → new_string)
4. Bump version:           Edit the "version" field (increment minor)
5. Validate (optional):    /fluent-workflow-validate <file>
6. Upload:                 workflow.upload({ workflow: <read file>, dryRun: true })
7. Deploy:                 workflow.upload({ workflow: <read file> })
```

**When to use full LLM analysis:** New workflow design, complex restructuring, adding multi-rule rulesets, or when you need to understand the status graph. **When to use file-based edit:** Property changes, version bumps, rule swaps, field type changes, adding a single rule to an existing ruleset.

**Convention:** Store workflows at `accounts/<PROFILE>/workflows/<RETAILER_REF>/<ENTITY_TYPE>__<SUBTYPE>.json`.

## When to Use

- Creating a new workflow from scratch
- Adding rulesets or states to an existing workflow
- Editing triggers, rules, or gate conditions
- Validating workflow JSON before upload
- Generating workflow fragments for merge operations

## Progress

Emit this block at each phase transition to show progress:

```
▸ /fluent-workflow-builder [1/5]
  ✓ Entity type & subtype    → Design state machine
  ○ Build rulesets           ○ Add gate rulesets
  ○ Validate structure
```

Update the block as each phase completes — mark completed phases with `✓`, the active phase with `→`, and remaining phases with `○`. Replace `[1/5]` with the current phase number.

## Workflow JSON Structure

Every Fluent workflow follows this schema:

```json
{
  "retailerId": "<retailer-id>",
  "version": "<semver>",
  "entityType": "ORDER | FULFILMENT | RETURN_ORDER | ARTICLE | WAVE | ...",
  "entitySubtype": "<subtype matching workflow name after ::>",
  "name": "<ENTITY_TYPE>::<SUBTYPE>",
  "description": "<human-readable description>",
  "versionComment": "<what changed in this version>",
  "createdBy": "<retailer-ref>",
  "rulesets": [ ... ],
  "statuses": [ ... ]
}
```

### Ruleset Structure

```json
{
  "name": "<RulesetName>",
  "description": "[<ENTITY_TYPE>] What this ruleset does",
  "type": "<ENTITY_TYPE>",
  "subtype": "<ENTITY_SUBTYPE>",
  "eventType": "NORMAL",
  "rules": [
    {
      "name": "<ACCOUNT>.<MODULE>.<RuleName>",
      "props": {
        "key": "value"
      }
    }
  ],
  "triggers": [
    { "status": "<STATUS>" }
  ],
  "userActions": []
}
```

### Trigger Types

Rulesets are triggered by one of:

| Trigger | When it fires |
|---------|---------------|
| `{ "status": "CREATED" }` | Entity enters CREATED status (platform CREATE event) |
| `{ "status": "ON_VALIDATION" }` | Entity enters ON_VALIDATION status |
| No status trigger | Ruleset is triggered by a named event matching the ruleset name |

**Convention:** If a ruleset has no `status` trigger, it fires when an event with `name` matching the ruleset name arrives. For example, ruleset `ConfirmValidation` fires on event `ConfirmValidation`.

### User Actions

User actions link UI buttons to workflow rulesets. When a user clicks the button, an event is sent via `/event/sync` to trigger the ruleset.

**Schema:**

```json
{
  "userActions": [
    {
      "eventName": "<EventName>",
      "context": [
        {
          "label": "<Button Label>",
          "type": "PRIMARY",
          "modules": ["adminconsole"],
          "confirm": true
        }
      ],
      "attributes": []
    }
  ]
}
```

**Field reference:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `eventName` | String | **YES** | Event name sent when button is clicked. **MANDATORY per official Fluent docs.** Must match the ruleset `name` unless intentionally routing to a different event. Without `eventName`, the user action is silently invisible — no button, no Transition API result. |
| `context` | Array | Yes | Where and how the button appears. At least one entry required. |
| `context[].label` | String | Yes | Button text shown in the UI |
| `context[].type` | String | Yes | `"PRIMARY"` (prominent) or `"SECONDARY"` (overflow menu / ellipsis) |
| `context[].modules` | Array | Yes | Which UI apps show the button. Values come from the app's `orchestrationAlias` (NOT the manifest setting suffix). Standard values: `"adminconsole"` (OMS App — NOT `"oms"`), `"servicepoint"` (Store App — NOT `"store"`). Custom apps use their `orchestrationAlias` or default to the manifest setting suffix (e.g., `fc.mystique.manifest.custom` → `"custom"`). |
| `context[].confirm` | Boolean | Yes | Whether the UI shows a confirmation dialog before sending the event |
| `attributes` | Array | No | Attributes collected via the confirmation dialog. Empty array if no input needed. |

### Strict User Action Contract (Non-Negotiable)

Use these guardrails to prevent configuration drift and hallucinated schema fields.

1. `userActions` MUST be inside the ruleset object (never at workflow root).
2. **Ruleset MUST include `subtype`**, and it MUST match workflow `entitySubtype` for UI actions to appear. Without `subtype`, the user action is silently invisible — no error, no button, no Transition API result.
3. Set `userActions[].eventName` explicitly and keep it equal to the ruleset `name` unless a deliberate alternate routing is required.
4. Each `context[]` object MUST include: `label`, `type`, `modules`, `confirm`.
5. `context[].modules` values MUST match the target app's `orchestrationAlias` (NOT the manifest setting name suffix): `"adminconsole"` (OMS App — manifest is `fc.mystique.manifest.oms`), `"servicepoint"` (Store App — manifest is `fc.mystique.manifest.store`). Custom apps without `orchestrationAlias` default to their manifest setting suffix. Note: `"oms"` and `"store"` are NOT valid module aliases for OMS App and Store App respectively — both use legacy `orchestrationAlias` overrides.
6. `attributes` is optional; when present, each attribute MUST include `name`, `label`, `type`, and `mandatory`.
7. Do NOT invent undocumented keys (for example: `actionLabel`, `moduleName`, `requiresConfirmation`, `buttonType`).
8. User actions are sync UI transitions (`/event/sync`). Keep payloads minimal and schema-accurate.

**Attribute schema** (when confirmation collects user input):

```json
{
  "attributes": [
    {
      "name": "controlValue",
      "label": "Control Value",
      "type": "INTEGER",
      "source": "",
      "defaultValue": 0,
      "mandatory": true
    }
  ]
}
```

| Field | Type | Description |
|-------|------|-------------|
| `name` | String | Attribute key passed in the event payload |
| `label` | String | Label shown in the confirmation dialog |
| `type` | String | `STRING`, `INTEGER`, `BOOLEAN`, `FLOAT` |
| `source` | String | Setting key to populate dropdown options. **MUST be lowercase `settings.`** (e.g., `"settings.CANCELLATION_REASON"`). `"Settings."` (capital S) silently fails — dropdown shows empty. Use empty string `""` for free-text input. `"OVERRIDE"` works on v4.80+. **The referenced setting value MUST have an `"active"` wrapper key** — see Dropdown Setting Contract below. |
| `defaultValue` | Any | Pre-filled value in the dialog. Use empty string `""` or `0` for defaults. Template expressions like `"{{me.username}}"` work on v4.80+ (does NOT break Transition API — original breakage was missing `subtype`). |
| `mandatory` | Boolean | Whether the field must be filled before confirming |

**Key rules:**

- **The ruleset MUST have a `subtype`** (matching the workflow's `entitySubtype`) for the user action to display in the UI. This is the #1 cause of invisible buttons. Without `subtype`, the platform silently ignores the user action — no error, no button, no transition API result.
- **`eventName` is MANDATORY** — per official Fluent documentation. Without it, the user action is silently invisible. Always set it explicitly, matching the ruleset `name` unless intentionally different. Never omit it.
- A ruleset can be both event-triggered AND have user actions (dual classification). For example, `CancelOrder` can be triggered by a user action button AND by an automated event from another ruleset.
- `modules` values are determined by the app's `orchestrationAlias` property (NOT the manifest setting name suffix). Standard values: `"adminconsole"` (OMS App — manifest is `fc.mystique.manifest.oms`), `"servicepoint"` (Store App — manifest is `fc.mystique.manifest.store`). Custom apps use their `orchestrationAlias` or default to the manifest setting suffix (e.g., `fc.mystique.manifest.custom` → `"custom"`).
- User actions send events via the synchronous endpoint (`/event/sync`), not the async endpoint. This means the UI blocks until the event is processed.

#### OMS App Module Name (Critical Gotcha)

The OMS App internally sends `module: "adminconsole"` in Transition API calls — NOT `"oms"`. This is because the OMS manifest has `"orchestrationAlias": "adminconsole"`, which overrides the module name sent to the Transition API. This means:
- `modules: ["oms"]` → buttons **INVISIBLE** in OMS App
- `modules: ["adminconsole"]` → buttons **VISIBLE** in OMS App

| Target UI App | Use This Module Value | Why |
|---------------|----------------------|-----|
| OMS App | `"adminconsole"` | OMS manifest has `orchestrationAlias: "adminconsole"` (legacy alias) |
| Store App | `"servicepoint"` | Store manifest (`fc.mystique.manifest.store`) has `orchestrationAlias: "servicepoint"` (legacy alias) |

To verify what module your UI sends: open browser DevTools Network tab, navigate to entity detail page, find `POST /api/v4.1/transition` request, check the `module` parameter.

#### User Action Validation Checklist (MANDATORY)

**Before outputting ANY ruleset with `userActions`, run every check. If any fails, fix before presenting.**

| # | Check | Fail = | Common mistake |
|---|-------|--------|----------------|
| UA01 | Ruleset has `subtype` matching workflow entity subtype | Button invisible in UI | Forgetting `"subtype": "HD"` — #1 cause of missing buttons |
| UA02 | `context` is an **array** (not object) | Runtime error | `"context": {...}` instead of `"context": [{...}]` |
| UA03 | Every context entry has ALL 4 fields: `label`, `type`, `modules`, `confirm` | Invisible / broken | Omitting `confirm` or `modules` |
| UA04 | `type` is exactly `"PRIMARY"` or `"SECONDARY"` (uppercase) | Button invisible | `"primary"`, `"default"`, `"action"` — none valid |
| UA05 | `modules` is **non-empty array** of valid app names | Button invisible | Empty `[]` or wrong name |
| UA06 | Module values: `"adminconsole"` (OMS App — NOT `"oms"`), `"servicepoint"` (Store App), or custom app's `orchestrationAlias` | Wrong app / invisible | `"oms"` (invisible in OMS App!), `"store"` (wrong — Store App uses `"servicepoint"`), `"fluent-oms"`, `"console"` — not valid |
| UA07 | `confirm` is **boolean** (`true`/`false`), not string | Dialog broken | `"confirm": "true"` |
| UA08 | If `attributes` has entries, each has `name`, `label`, `type` | Dialog broken | Missing `name` = no key in event payload |
| UA09 | Attribute `type`: `STRING`, `INTEGER`, `BOOLEAN`, `FLOAT` (uppercase) | Wrong input | `"string"`, `"text"`, `"number"` — not valid |
| UA10 | Attribute `source`: empty `""` for free-text, `"settings.<key>"` for dropdown. **MUST be lowercase `settings.`** — `"Settings."` (capital S) silently fails. | Dropdown empty | `"cancellationReasons"` missing prefix, OR `"Settings.X"` capital S. Note: `"OVERRIDE"` does NOT break Transition API (corrected 2026-02-28) |
| UA11 | Attribute `defaultValue`: empty `""` or `0` for defaults | Safe to use template expressions | `"{{me.username}}"` works on v4.80+ — original breakage was caused by missing `subtype`, not defaultValue (corrected 2026-02-28) |
| UA12 | `userActions` is array at **ruleset top level** | Silently ignored | Inside `props` or `rules` — wrong |
| UA13 | `eventName` matches ruleset `name` OR explicitly set to different event | Wrong event | Typo sends event no ruleset catches |
| UA14 | `eventName` is **present** (not omitted) | **Button invisible** | Missing `eventName` = silently invisible user action. MANDATORY per official Fluent docs. |
| UA15 | `modules` includes the TARGET app: `"adminconsole"` for OMS App (NOT `"oms"`), `"servicepoint"` for Store App | Button invisible | `"oms"` makes buttons invisible in OMS App. Use `"adminconsole"`. |
| UA16 | For child entity user actions, verify ruleset is in the PARENT workflow (not standalone child workflow) | Buttons invisible in UI — the UI uses the entity's `workflowRef` (= parent workflow) as `flexType`. Note: the Transition API itself accepts both parent and child workflow names | Move ruleset + userAction to parent workflow for UI visibility |
| UA17 | For OMS App buttons, verify `modules` includes `"adminconsole"` (NOT `"oms"`) | Buttons invisible — OMS App sends `module: "adminconsole"` to Transition API | Change `modules: ["oms"]` to `modules: ["adminconsole"]` |
| UA18 | **Cross-artifact**: Verify all `modules[]` values correspond to a deployed app's `orchestrationAlias` (e.g., `"adminconsole"` → OMS App `fc.mystique.manifest.oms`, `"servicepoint"` → Store App `fc.mystique.manifest.store`) | Button may be invisible if no matching app exists | Verify the target app manifest exists and its `orchestrationAlias` matches the `modules[]` value |
| UA19 | If `attributes[]` is non-empty, every attribute `name` MUST match an `@EventAttribute` declared by some rule in the ruleset | Server rejects with `TRANSITION_ATTRIBUTE_INVALID` | Platform validates attribute names against @EventAttribute annotations from ALL rules in the ruleset. Fix: add @EventAttribute to a rule, or deploy via REST POST/MCP |
| UA20 | If `source` starts with `"settings."`: (1) lowercase prefix, (2) referenced setting exists, (3) setting value has `{ "active": [...] }` wrapper, (4) setting retailer context matches workflow retailer | Dropdown appears empty — no error | `"Settings.X"` capital S, plain array without `"active"` key, or setting on wrong retailer |

**Hard-forbidden patterns (never generate):**

```
WRONG: "modules": ["oms"]            → RIGHT: "modules": ["adminconsole"]  (OMS App uses orchestrationAlias "adminconsole"!)
WRONG: "modules": ["fluent-oms"]     → RIGHT: "modules": ["adminconsole"]
WRONG: "modules": ["fluent-store"]  → RIGHT: "modules": ["servicepoint"]  (Store App uses orchestrationAlias "servicepoint"!)
WRONG: "modules": ["store"]         → RIGHT: "modules": ["servicepoint"]
WRONG: "modules": ["console"]       → RIGHT: "modules": ["adminconsole"]
WRONG: "type": "primary"            → RIGHT: "type": "PRIMARY"
WRONG: "type": "button"             → RIGHT: "type": "PRIMARY" or "SECONDARY"
WRONG: "confirm": "true"            → RIGHT: "confirm": true
WRONG: "context": { ... }           → RIGHT: "context": [{ ... }]
WRONG: attribute "type": "string"   → RIGHT: attribute "type": "STRING"
WRONG: "source": "cancellationReasons"  → RIGHT: "source": "settings.cancellationReasons"
WRONG: "source": "Settings.X"          → RIGHT: "source": "settings.X" (lowercase s)
WRONG: setting value: [{ "name": "...", "value": "..." }]  → RIGHT: { "active": [{ "name": "...", "value": "..." }] }
WRONG: "source": "Settings.CANCELLATION_REASON" → RIGHT: "source": "settings.CANCELLATION_REASON"  (lowercase s!)
WRONG: userActions inside "rules"   → RIGHT: userActions at ruleset top level
WRONG: ruleset without "subtype"    → RIGHT: always include "subtype" matching workflow
WRONG: userAction without "eventName"→ RIGHT: always include "eventName" matching ruleset name
WRONG: attributes without matching @EventAttribute on any rule → RIGHT: add @EventAttribute to a rule in the ruleset, or deploy via REST POST/MCP
```

**Post-output verification:** After generating a user action, verify: (1) Does this ruleset have `subtype`? (2) Does `modules` use the correct app name? (3) Is `context` an array with all 4 fields? If any answer is no, fix before presenting.

**Troubleshooting:** When a user action fires, the browser sends a `POST /api/v4.1/event/sync` call. Use browser DevTools (Network tab) to inspect the request payload, bearer token, and response `eventStatus`. The token is the logged-in user's token -- useful for debugging permission issues.

#### Dropdown Setting Contract

When a user action attribute uses `source: "settings.<KEY>"`, the UI renders a dropdown from a Fluent setting. All three of these must be correct or the dropdown silently appears empty:

1. **Lowercase prefix**: `"settings.CANCELLATION_REASON"` — NOT `"Settings.CANCELLATION_REASON"` (capital S silently fails)
2. **`"active"` wrapper in setting value**: The setting JSON must be `{ "active": [{ "name": "Display Label", "value": "SUBMITTED_VALUE" }] }` — a plain array without the `"active"` key silently fails
3. **Setting retailer context**: The setting's retailer must match the workflow's retailer — a setting scoped to retailer A won't populate dropdowns for entities on retailer B

When generating a workflow with dropdown attributes, also generate the corresponding setting creation instruction:
```
Setting name: CANCELLATION_REASON
Setting value (JSON): { "active": [{ "name": "Default", "value": "DEFAULT" }, { "name": "Wrong Size", "value": "WRONG_SIZE" }] }
Context: ACCOUNT, contextId: 0
```

#### Persisting Event Attributes to Entities

**CRITICAL:** User action attributes are collected via the event — they do NOT automatically persist to the entity. To save a dropdown value (or any event attribute) as an entity attribute, the ruleset MUST include one of these OOTB rules **BEFORE** the state change:

| Rule | Use case | Props |
|------|----------|-------|
| `FLUENTRETAIL.base.AddStringEventAttributeToEntity` | Single string attribute | `{ "attributeName": "cancelReason" }` |
| `FLUENTRETAIL.base.AddEventAttributesToEntity` | Multiple attributes (any type) | `{ "attributeNames": [{ "name": "cancelReason", "type": "STRING" }] }` |
| `FLUENTRETAIL.base.AddEventAttributesToOrderEntity` | ORDER-specific, with defaults | `{ "attributeNames": [...], "attributeTypes": [...], "attributeDefaultValues": [...] }` |

Without one of these rules, the user selects a value from the dropdown, the event fires successfully, but the entity never stores the attribute — the data is lost after the event completes.

### Parent-Child Entity Workflows

When a child entity (e.g., CREDIT_MEMO) is orchestrated through a parent workflow (e.g., BILLING_ACCOUNT::CUSTOMER), the UI uses the **parent's workflow** as `flexType` to resolve user actions for button visibility.

**Key principle:** Child entities inherit `workflowRef` from their parent. The Mystique framework passes this as `flexType` to the Transition API, so user action buttons only appear if they exist in the **parent** workflow. However, the Transition API itself accepts BOTH the parent workflow name (e.g., `"BILLING_ACCOUNT::CUSTOMER"`) and the standalone child workflow name (e.g., `"CREDIT_MEMO::APPEASEMENT"`) as `flexType` — both return the same user actions when paired with the correct `flexVersion`. The "parent only" constraint applies to the UI, not the API.

**Parent-child pairs:**
- BILLING_ACCOUNT (parent) → CREDIT_MEMO (child)
- ORDER (parent) → FULFILMENT, FULFILMENT_CHOICE (children)
- RETURN_ORDER (parent) → RETURN_FULFILMENT (child)

**Rules for child entity user actions:**
1. Add the user action ruleset to the **parent workflow**, NOT a standalone child workflow
2. Set ruleset `type` to the child entity type (e.g., `"CREDIT_MEMO"`)
3. Set ruleset `subtype` to the child entity subtype (e.g., `"APPEASEMENT"`)
4. Child entity statuses in the parent workflow can use empty `category: ""` — this is valid when `core.SetState` transitions child entities
5. The standalone child workflow (e.g., CREDIT_MEMO::APPEASEMENT) still processes events — it's just not consulted for UI button visibility

**Example: CreditMemo approval buttons in BILLING_ACCOUNT::CUSTOMER workflow:**
```json
{
  "name": "ApproveAppeasement",
  "type": "CREDIT_MEMO",
  "subtype": "APPEASEMENT",
  "eventType": "NORMAL",
  "rules": [{ "name": "ACCOUNT.core.SetState", "props": { "status": "APPROVED" } }],
  "triggers": [{ "status": "PENDING_APPROVAL" }],
  "userActions": [{
    "eventName": "ApproveAppeasement",
    "context": [{ "label": "APPROVE", "type": "PRIMARY", "modules": ["adminconsole"], "confirm": true }],
    "attributes": []
  }]
}
```

**When to use user actions:**
- Manual workflow transitions that require human decision (cancel, reject, approve, confirm)
- Collecting information from the user at transition time (cancellation reason, tracking number)
- Actions that should appear as buttons in the OMS App or Store App

**When NOT to use:**
- Automated status transitions -- use `triggers` with status-based firing
- System-to-system integrations -- use `SendEvent` rules or the async event API
- Scheduled/delayed actions -- use `ScheduleEvent` rule
- Transitions that happen without user involvement (e.g., all fulfilments complete → advance order)

**Common user action patterns:**

| Pattern | eventName | modules | confirm | Typical attributes |
|---------|-----------|---------|---------|-------------------|
| Cancel order | `CancelOrder` | `["adminconsole"]` | true | cancellationReason (DROPDOWN from settings) |
| Confirm order | `ConfirmOrder` | `["adminconsole"]` | true | none |
| Reject fulfilment | `RejectFulfilment` | `["store"]` | true | rejectionReason (DROPDOWN) |
| Pick item | `PickItem` | `["store"]` | false | none |
| Pack fulfilment | `PackFulfilment` | `["store"]` | false | none |
| Dispatch fulfilment | `DispatchFulfilment` | `["store"]` | true | trackingNumber (STRING), carrier (DROPDOWN) |
| Approve return | `ApproveReturn` | `["adminconsole"]` | true | none |

**Complete example -- Cancel Order with reason collection:**

```json
{
  "name": "CancelOrder",
  "description": "[ORDER] Cancel order from OMS with reason",
  "type": "ORDER",
  "subtype": "HD",
  "eventType": "NORMAL",
  "rules": [
    {
      "name": "FLUENTRETAIL.base.AddEventAttributesToEntity",
      "props": {
        "attributeNames": [
          { "name": "cancellationReason", "type": "STRING" },
          { "name": "cancellationNote", "type": "STRING" }
        ]
      }
    },
    {
      "name": "<ACCOUNT>.core.SetState",
      "props": { "status": "CANCELLED" }
    }
  ],
  "triggers": [],
  "userActions": [
    {
      "eventName": "CancelOrder",
      "context": [
        {
          "label": "Cancel Order",
          "type": "PRIMARY",
          "modules": ["adminconsole"],
          "confirm": true
        }
      ],
      "attributes": [
        {
          "name": "cancellationReason",
          "label": "Cancellation Reason",
          "type": "STRING",
          "source": "settings.cancellationReasons",
          "defaultValue": "",
          "mandatory": true
        },
        {
          "name": "cancellationNote",
          "label": "Notes",
          "type": "STRING",
          "source": "",
          "defaultValue": "",
          "mandatory": false
        }
      ]
    }
  ]
}
```

**Verifying user actions after deployment (MANDATORY — never skip):**

After deploying any workflow with user actions, perform ALL of these verification steps:

**Step V1 — Transition API check:**
Query the Transition API for each status where a user action should appear. **THREE MANDATORY parameters** — omitting ANY one silently returns empty results:
```
workflow.transitions entityType=ORDER subtype=HD status=BOOKED retailerId=2 module=adminconsole flexType=ORDER::HD flexVersion=<deployed_version>
```
- **`module`** — REQUIRED. Use `"adminconsole"` for OMS App. `"all"` returns NOTHING.
- **`flexType`** — REQUIRED. Workflow identifier (e.g., `"ORDER::HD"`). For child entities, BOTH the parent workflow name and the standalone child workflow name work — use the matching `flexVersion`. The UI uses the entity's `workflowRef` (= parent workflow), so to mirror what the UI does, use the parent workflow.
- **`flexVersion`** — REQUIRED. Exact deployed version number. Without it, cached stale version may be used.

Response `userActions[]` should contain the configured action with `eventName`, `context`, and `attributes`.

**Step V2 — Diagnose empty results:**
If `userActions` is empty, diagnose using this checklist (in order of likelihood):

| # | Check | How to verify | Fix |
|---|-------|--------------|-----|
| V2a | **THREE mandatory params present** | Verify `module`, `flexType`, `flexVersion` ALL provided | Add all three — omitting ANY one silently returns empty |
| V2b | **`module` is specific** (not `"all"`) | Check query uses `"adminconsole"` (OMS App), `"servicepoint"` (Store App), etc. | `"all"` returns NOTHING — use specific module name |
| V2c | `eventName` present | Read workflow JSON — `userActions[].eventName` exists | Add `eventName` matching ruleset name |
| V2d | Ruleset has `subtype` | Read workflow JSON — ruleset has `"subtype": "<value>"` | Add `subtype` matching workflow `entitySubtype` |
| V2e | `modules` includes target app | Read `context[].modules` | Add correct app name (`"adminconsole"` for OMS App, `"servicepoint"` for Store App, etc.) |
| V2f | Entity is at correct status | Query entity status via GraphQL | Entity must be at the trigger status |
| V2g | Attribute poisoning | Check all `attributes[].source` and `defaultValue` | Replace `"OVERRIDE"` with `""`, remove `{{...}}` templates |
| V2h | `context` is array (not object) | Read JSON — `"context": [...]` not `"context": {...}` | Wrap in array |
| V2i | `confirm` is boolean (not string) | Read JSON — `true` not `"true"` | Use boolean |

**Step V3 — Browser UI check (when Chrome DevTools MCP available):**
1. Navigate to the entity detail page using `navigate_page` with `initScript` set to the `__fcTest` helper IIFE (see `fluent-ui-test/BROWSER_OPTIMIZATIONS.md` "FAST PATTERN"). This auto-injects the helper — no separate injection step needed.
2. Single `evaluate_script` with `batch()` to verify page + check button in ONE call:
   ```javascript
   () => JSON.stringify(window.__fcTest.batch([
     { action: 'pageSummary' },
     { action: 'findButton', args: ['BUTTON_LABEL'] },
     { action: 'highlight', args: ['BUTTON_LABEL', 'VERIFIED'] }
   ]))
   ```
3. If button found: `take_screenshot` for evidence (optional — 3rd MCP call max)
4. If button NOT found: check `list_console_messages` for errors, `list_network_requests` for failed `/event/sync` calls

**Step V4 — Ask the user to confirm visibility:**
Always ask: *"Can you see the [button label] button on the [entity type] at status [status] in the [app name] app?"*
Do NOT assume the button is visible just because the workflow JSON looks correct.

**Known Transition API limitations:**

- **THREE MANDATORY parameters**: `module`, `flexType`, and `flexVersion` are ALL required. Omitting any one silently returns empty results (no error). `module: "all"` returns NOTHING — always use a specific module name. All rules treated equally regardless of plugin origin — NO "OOTB vs custom" distinction.
- **Multi-entity workflows**: In workflows like `ORDER::MULTI` that handle multiple entity types, the Transition API query must specify the correct `type` and `subtype` for the entity whose user actions you want. Use `type=ORDER subtype=MULTI` for ORDER actions, `type=FULFILMENT subtype=HD_WH` for fulfilment actions, etc. Each entity type is queried separately.
- **~~Attribute poisoning~~ (CORRECTED)**: Attributes with `source: "OVERRIDE"` or `defaultValue: "{{...}}"` do NOT cause Transition API to return empty results on v4.80+. Original breakage was caused by missing `subtype` on the ruleset. Safe practice: use `source: ""`, `defaultValue: ""` for predictability.

**Strict validation checklist (run before upload):**

1. Confirm each ruleset with `userActions` has `type` + `subtype`, and `subtype` equals workflow `entitySubtype`.
2. Confirm `userActions[].eventName` is present and explicitly set — **MANDATORY, never omit**.
3. Confirm every `context[].modules` value is valid for the target UI app.
4. After upload, run `workflow.transitions` for each entity type + status with user actions:
   - Verify `userActions[]` is non-empty.
   - If empty, run the V2 diagnostic checklist above.
5. If Chrome DevTools MCP available, open the entity in the browser and verify button visibility.
6. Ask the user to confirm they see the button in the UI.

### Statuses Array

```json
{
  "statuses": [
    { "name": "CREATED", "category": "BOOKING", "entityType": "ORDER" },
    { "name": "ON_VALIDATION", "category": "BOOKING", "entityType": "ORDER" },
    { "name": "COMPLETE", "category": "DONE", "entityType": "ORDER" }
  ]
}
```

Categories: `BOOKING`, `FULFILMENT`, `DONE`, `PAYMENT`

## Common Rule Patterns

### Core Rules (provided by Fluent platform)

| Rule | Props | Purpose |
|------|-------|---------|
| `<ACCOUNT>.core.SetState` | `status` | Transition entity to new status |
| `<ACCOUNT>.core.SendEvent` | `eventName` | Fire a named event on current entity |
| `<ACCOUNT>.core.SendEventOnVerifyingEntityStatus` | `eventName`, `statuses` (comma-separated) | Fire event only if entity is in one of the listed statuses |
| `<ACCOUNT>.core.SendEventOnVerifyingAllFcStatus` | `eventName`, `statuses` (comma-separated) | Gate: fire event when ALL fulfilment choices reach one of the listed statuses |
| `<ACCOUNT>.core.ScheduleEvent` | `eventName`, `delay` (seconds) | Schedule a delayed event |
| `<ACCOUNT>.core.IfEventAttributeValueIsEqual` | `eventAttributeName`, `eventAttributeValue`, `eventName` | Conditional: fire event if attribute matches |

### Extension Rules (from custom extension module)

| Rule | Key Props | Purpose |
|------|-----------|---------|
| `<ACCOUNT>.<MODULE>.UpdateStatusHistory` | `toStatus` | Track status transition in attribute |
| `<ACCOUNT>.<MODULE>.SendWebhookWithDynamicAttributes` | `setting` | Send webhook with config from Setting |
| `<ACCOUNT>.<MODULE>.UpdateEntityFromSetting` | `setting` | Batch-update entity from event data via Setting |
| `<ACCOUNT>.<MODULE>.CreateFulfilmentFromSourcingLocation` | *(from event attrs)* | Create fulfilment from sourcing data |
| `<ACCOUNT>.<MODULE>.UpsertAttribute` | `attributeName`, `attributeType`, `attributeValue` | Set entity attribute |
| `<ACCOUNT>.<MODULE>.UpsertAttributeFromPath` | `jsonpath`, `attributeName`, `attributeType` | Extract value via JSON path and set as attribute |

## Plan Before Building

**Before creating or editing any workflow, present an implementation plan to the user.** The plan should include:

1. **State machine design** — All statuses and transitions, shown as a Mermaid `stateDiagram-v2`
2. **Ruleset inventory** — Every ruleset with its trigger condition, rules, and purpose
3. **Settings dependencies** — Any settings referenced by rules (webhook URLs, thresholds, feature flags)
4. **Custom rule requirements** — Any new Java rules needed (entity type, parameters, behavior)
5. **Cross-entity interactions** — How this workflow interacts with other entity workflows (e.g., ORDER → FULFILMENT)
6. **Deployment sequence** — What gets deployed in what order

For workflow edits, additionally show:
- **Before/after diff** — What rulesets are being added, removed, or modified
- **Before/after Mermaid diagrams** — Visual comparison of the state machine changes
- **Risk assessment** — Impact of changes on existing live entities

**Wait for user approval before making any changes to workflow JSON files.**

## Building a New Workflow

### Step 1: Determine entity type and subtype

```
Workflow name = <ENTITY_TYPE>::<SUBTYPE>
Example: ORDER::HD, FULFILMENT::DEFAULT, RETURN_ORDER::DEFAULT
```

### Step 2: Design the state machine

List all statuses and the events/rules that transition between them. Example:

```
CREATED → [CREATE ruleset] → ON_VALIDATION
ON_VALIDATION → [ConfirmValidation event] → IN_PROGRESS
IN_PROGRESS → [ConfirmShipment event] → SHIPPED
SHIPPED → [ConfirmDelivery event] → COMPLETE
```

### Step 3: Build each ruleset

For each transition, create a ruleset with:
1. The rules to execute (in order — rules run sequentially)
2. A trigger (status trigger for the first ruleset, event-name trigger for the rest)

### Step 4: Add gate rulesets (if needed)

Gates check child entity statuses before advancing parent. Example:

```json
{
  "name": "NotifyFCComplete",
  "description": "Gate: advance order when all FCs reach terminal status",
  "type": "ORDER",
  "eventType": "NORMAL",
  "rules": [
    {
      "name": "ACCT.core.SendEventOnVerifyingAllFcStatus",
      "props": {
        "eventName": "SourceOrder",
        "statuses": "ASSIGNED,CANCELLED,ESCALATED"
      }
    }
  ],
  "triggers": [],
  "userActions": []
}
```

### Step 5: Validate structure

Before uploading, verify:
- [ ] Every status referenced in triggers exists in the `statuses` array
- [ ] Every `SetState` target status exists in the `statuses` array
- [ ] Every `SendEvent` target has a matching ruleset name (or is handled elsewhere)
- [ ] Rule names follow `<ACCOUNT>.<MODULE>.<RuleName>` format
- [ ] No duplicate ruleset names
- [ ] Version is bumped from previous

### Step 6: Design for concurrent access

Multiple users on the same Fluent account act on entities simultaneously. The workflow status graph is the concurrency control mechanism — there is no frontend-level session isolation. Design status transitions as concurrency gates.

#### Narrow status windows

Keep entities in "actionable" statuses briefly. Use intermediate statuses so two users cannot both trigger the same action:

```
BAD:  BOOKED → [CancelOrder] → CANCELLED
      (User A and User B both see CANCEL button, both click)

GOOD: BOOKED → [CancelOrder] → CANCELLATION_REQUESTED → [ProcessCancellation] → CANCELLED
      (User A clicks → entity moves to CANCELLATION_REQUESTED → User B's CANCEL button disappears)
```

The intermediate status (`CANCELLATION_REQUESTED`) is the lock. The first event wins; the second user's Transition API response returns no actions for that status.

For high-contention actions (appeasements, cancellations, status changes on popular orders), always introduce an intermediate "processing" status:

| Pattern | Example statuses |
|---------|-----------------|
| Immediate completion | `BOOKED` → `CANCELLED` (race-prone) |
| Intermediate gate | `BOOKED` → `CANCELLATION_REQUESTED` → `CANCELLED` (safe) |
| Approval workflow | `BOOKED` → `APPROVAL_PENDING` → `APPROVED` / `REJECTED` (multi-step safe) |

#### Rules must validate preconditions

The frontend shows a cached snapshot. By the time the user clicks a button, the entity may have moved status or had attributes changed by another user. Rules MUST NOT trust the event payload blindly:

```java
// GOOD: Validate current entity state before mutating
String currentStatus = context.getEntity().getStatus();
if (!"BOOKED".equals(currentStatus)) {
    throw new RubixException(400, "Order is no longer BOOKED (current: " + currentStatus + ")");
}

// BAD: Assume the entity is still in the expected state
// (no check — proceeds to mutate even if another user already cancelled)
```

This is especially important for rules that create child entities or fire financial transactions — a stale UI click should fail safely, not create duplicate records.

#### Attribute-level conflicts (last write wins)

Two users can update different attributes on the same entity at the same status. The workflow engine does not block this — the last `updateOrder` mutation wins silently. Design around this:

| Scenario | Risk | Mitigation |
|----------|------|------------|
| Two users updating the same attribute | Last write wins, first user's change lost | Use dedicated statuses per action, or accept last-write-wins for low-risk attributes |
| Two users triggering different actions at same status | Both succeed if both status triggers match | Acceptable if actions are independent (e.g., "add note" and "update address") |
| Two users triggering the same action | Both fire events, first transitions status, second fails trigger match | Safe — workflow engine rejects the second |

For attributes where overwrites are dangerous (financial amounts, assignment changes), move the entity to an intermediate status as part of the update. This serializes access.

#### UI staleness is expected

Mystique components show a point-in-time snapshot. The Transition API query runs when the page loads — not continuously. Between page load and button click:

- Another user may have changed the entity status (button should have disappeared)
- Another user may have updated attributes the rule depends on
- A background event may have advanced the workflow

**The rule layer is the safety net.** Rules should check `context.getEntity().getStatus()` and validate required preconditions before mutating. A well-designed rule fails safely on stale input; a poorly-designed rule creates duplicate child entities or overwrites concurrent changes.

## Editing an Existing Workflow

### Get current workflow first

Use `/fluent-workflow` for download/list operations, then apply this skill's structure and validation patterns.

### Make changes

Edit the JSON directly. Common operations:

**Add a new ruleset:**
```json
{
  "name": "NewRulesetName",
  "description": "[ORDER] What it does",
  "type": "ORDER",
  "eventType": "NORMAL",
  "rules": [ ... ],
  "triggers": [ { "status": "IN_PROGRESS" } ],
  "userActions": []
}
```

**Add a rule to existing ruleset:**
Find the ruleset in the `rulesets` array, append to its `rules` array.

**Change a gate condition:**
Find the gate ruleset, update the `statuses` prop value.

### Output Validation (Automated)

**The implementation critique protocol lives at: `knowledge/platform/implementation-critique-protocol.md`.** Select the **Workflow JSON Rubric** (WF-1 through WF-10) and follow the protocol exactly.

After generating the workflow JSON, run the adversarial validation loop (max 2 rounds). The critic validates: eventName presence, subtype match, modules correctness, version increment, trigger coverage, status categories, user action attribute ↔ plan contract alignment, confirm boolean type, context type case, and attribute default safety.

**This runs before presenting the workflow to the user or handing off to `/fluent-workflow-deploy`.** Do not skip.

### Upload/merge

Use `/fluent-workflow-deploy` for uploading workflows to environments (MCP tool primary, REST API fallback). Use `/fluent-workflow` for listing, downloading, and merging workflows. Use `/fluent-module-deploy` for deploying full module bundles (JAR + settings + workflows together).

**Important:** If the workflow references new custom rules, deploy the module first via `/fluent-module-deploy` before uploading the workflow. Unregistered rules cause NO_MATCH events at runtime.

## Workflow Fragments

Fragments are partial workflow definitions used with `fluent workflow merge`:

```json
{
  "name": "my-customization",
  "description": "Add custom validation step",
  "rulesets": [
    {
      "name": "CustomValidation",
      "description": "Custom validation ruleset",
      "type": "ORDER",
      "eventType": "NORMAL",
      "rules": [ ... ],
      "triggers": [ ... ],
      "userActions": []
    }
  ],
  "statuses": [
    { "name": "CUSTOM_VALIDATING", "category": "BOOKING", "entityType": "ORDER" }
  ]
}
```

Merge adds the fragment's rulesets and statuses to the base workflow. Use this for modular customizations.

## Session Tracking

When invoked, log the following to the session tracking protocol (consumed by `/fluent-session`):

**On entry:**
```json
{ "skill": "<this-skill-name>", "timestamp": "<ISO-8601>", "arguments": { "<key>": "<value>", "...": "..." } }
```

**On exit:**
```json
{ "skill": "<this-skill-name>", "outcome": "<completed|failed|skipped>", "changesProduced": ["<seq-numbers>"], "toolCallsProduced": ["<seq-numbers>"], "nextRecommended": "<from-handoff-section>" }
```

All MCP tool calls made during execution should include `"skill": "<this-skill-name>"` in their tracking record for attribution. The `arguments` object should capture the key parameters actually passed (profile, retailer, entity type, etc.) — not a fixed schema. The `nextRecommended` value comes from the Handoff section below.

## Handoff

| Output Artifact | Consumed By | Path |
|----------------|-------------|------|
| Workflow JSON file | `/fluent-workflow-analyzer` (validation) | `accounts/<PROFILE>/workflows/<RETAILER_REF>/<ENTITY_TYPE>__<SUBTYPE>.json` |
| Workflow JSON file | `/fluent-workflow-deploy` (deployment) | `accounts/<PROFILE>/workflows/<RETAILER_REF>/<ENTITY_TYPE>__<SUBTYPE>.json` |
| Workflow JSON file | `/fluent-pre-deploy-check` (gate check) | `accounts/<PROFILE>/workflows/<RETAILER_REF>/<ENTITY_TYPE>__<SUBTYPE>.json` |

## Error Reporting

Errors from this skill follow the standard format:

| Field | Description |
|-------|-------------|
| `phase` | Skill phase where error occurred (e.g., `pre-flight`, `design`, `generation`, `validation`) |
| `severity` | `CRITICAL` (blocks downstream), `HIGH` (needs fix), `MEDIUM` (advisory), `LOW` (informational) |
| `message` | Human-readable error description |
| `resolution` | Suggested fix or downstream skill to invoke |

## Validation Checklist

Run through this before uploading any workflow:

1. **JSON syntax** — Valid JSON (no trailing commas, correct brackets)
2. **Required fields** — `retailerId`, `version`, `entityType`, `entitySubtype`, `name`, `rulesets`
3. **Status coverage** — Every SetState target exists in `statuses`
4. **Event coverage** — Every SendEvent target has a matching ruleset (or is expected inbound)
5. **Trigger consistency** — Only one ruleset per status trigger (avoid conflicts)
6. **Rule naming** — `<ACCOUNT>.<MODULE>.<RuleName>` format, all referenced rules exist in platform
7. **Gate logic** — Gate statuses include all terminal states for child entities
8. **Version bump** — Version is higher than currently deployed version
9. **Major vs minor version choice** — Workflow versions use `<major>.<minor>` format. The choice matters:
   - **Minor bump** (e.g., 1.2 → 1.3): All existing entities on that major version **immediately** use the new workflow. Use for backward-compatible changes (new rulesets, rule tweaks, prop changes).
   - **Major bump** (e.g., 1.3 → 2.0): Only **newly created** entities use the new version. Existing entities stay on the old major (latest minor). Use for incompatible structural changes post go-live where in-flight entities must be protected.
   - **During development:** Almost always use **minor** bumps — you want all test entities to pick up changes immediately.
   - **Post go-live:** Use **major** bumps only when the change would break in-flight orders/fulfilments. Minor bumps are still preferred when changes are additive.
   - **The platform always does minor bumps** on update via CLI (`fluent module install`). Major bumps require manual version editing.
10. **Description** — `versionComment` describes what changed

## Post-Execution: Feedback Capture

After completing this skill (whether success or failure), write a feedback record:

1. **Classify outcome:** `SUCCESS` (clean build), `PARTIAL_SUCCESS` (built after fixing issues), `FAILURE` (could not complete), `BLOCKED` (prereq missing), `USER_CORRECTED` (user overrode approach)
2. **Build record:** Create a `feedback-record-v1` JSON object with:
   - `skill`: `"fluent-workflow-builder"`
   - `feature`: feature slug if working within a feature, null otherwise
   - `entityType` / `entitySubtype`: from the workflow being built (e.g., `ORDER`, `HD`)
   - `phases`: trace of each phase (pre-flight, plan, build-rulesets, build-statuses, validate, etc.) with PASS/FAIL/SKIP
   - `learnings`: any gotchas discovered during execution
   - `userCorrections`: any corrections the user provided
3. **Redact secrets:** Strip tokens, passwords, API keys. Keep business identifiers.
4. **Append** one JSON line to `accounts/<PROFILE>/feedback/fluent-workflow-builder.jsonl`
5. **Update** `accounts/<PROFILE>/feedback/index.json` counters (create file and directory with `mkdir -p` if missing)

**Skip capture if:** The execution was trivially simple (e.g., user just asked a question about workflows, no actual build/edit happened).

## Known Pitfalls
<!-- feedback-promoted: managed section, updated by feedback loop -->

- **Child entity user actions must be in parent workflow for UI visibility** — If CREDIT_MEMO buttons don't show in the OMS App, verify the user action ruleset is in the BILLING_ACCOUNT workflow (parent), NOT a standalone CREDIT_MEMO workflow. The UI uses the entity's `workflowRef` as `flexType`, which for child entities = the parent's workflow. Note: the Transition API itself accepts both parent and child workflow names as `flexType` (with matching `flexVersion`), but the UI always uses `workflowRef`. (Confirmed 2026-02-27, corrected 2026-02-28)
- **OMS App uses `module: "adminconsole"` for Transition API** — Despite the app being called "OMS App", it sends `module: "adminconsole"` to the Transition API (legacy alias). Using `modules: ["oms"]` makes buttons invisible. Always use `modules: ["adminconsole"]` for OMS App buttons. (Confirmed 2026-02-27)
- **Empty `category: ""` is valid for child entity statuses** — When adding CREDIT_MEMO statuses to a BILLING_ACCOUNT workflow, empty category is valid. Needed when `core.SetState` transitions child entities within a parent workflow. Don't force BOOKING/FULFILMENT/DONE on child entity statuses. (User-corrected 2026-02-27)
- **POST bypasses UNUSED_CATEGORY_FOUND** — When PUT fails with `UNUSED_CATEGORY_FOUND` on a corrupt workflow, use `POST /api/v4.1/workflow` to create a clean new version. POST works for existing workflows too (overwrites). (Confirmed 2026-02-27)
- **CLI workflowlog merge strips eventName** — The CLI merge process drops eventName from userActions[] in raw workflow JSON. Platform Transition API still works (infers from ruleset name), but downloaded/exported workflow JSONs will show eventName as missing. Only direct MCP/REST deploy preserves eventName in JSON.
- **Stale local workflow files** — fluent workflow download reads from the workflowlog cache, NOT the live server. After MCP/REST deploys, local files are stale. Always verify live version via MCP workflow.get before analyzing or editing local files.
- **Child entity user action buttons need retailerId in manifest query** — Even if user actions are correctly in the parent workflow, fc.page needs `retailerId` from the GraphQL query `variables` to call the Transition API. Child entities (CreditMemo, Fulfilment) have no `retailer` edge at root level, so retailerId can't be passed as a variable → buttons won't appear on child entity detail pages. Workaround: move approval actions to parent entity page or use event-based approval. (Confirmed 2026-02-28)
- **GraphQL strict variable validation breaks manifest queries** — Fluent rejects queries with (1) undeclared variables in variables{} and (2) declared-but-unused variables. Both cause the ENTIRE query to silently fail. When building manifest queries, verify every variable is declared AND used. (Confirmed 2026-02-28)
- **TRANSITION_ATTRIBUTE_INVALID on workflow deploy** — The platform server validates each `userActions[].attributes[].name` against `@EventAttribute` annotations declared by rules in the same ruleset (all rules treated equally regardless of plugin). If no rule in the ruleset declares a matching `@EventAttribute` for an attribute name → server rejects with `TRANSITION_ATTRIBUTE_INVALID`. **Analysis:** Check which attributes fail, then check if any rule in the ruleset has `@EventAttribute(name = "thatAttribute")`. Common cause: rules use `@ParamString` for config props but the user action attributes need `@EventAttribute` on a rule. **Fixes:** (1) Add `@EventAttribute` annotations to an existing rule in the ruleset for the needed attributes (code change + plugin redeploy). (2) Create a lightweight rule that declares the needed `@EventAttribute` names. (3) Deploy via REST POST or MCP (bypasses this validation). (Confirmed 2026-02-28)

<!-- end feedback-promoted -->
