---
name: fluent-workflow-validate
description: Validate Fluent Commerce workflow JSON structure, status graph integrity, trigger coverage, rule references, user actions, and cross-entity consistency. Triggers on "validate workflow", "check workflow", "lint workflow", "workflow issues", "workflow health".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: <workflow-file-or-directory> [--strict] [--fix-suggestions] [--profile <name>]
cursor-tier: manual
---

# Workflow Structure Validator

Validate Fluent Commerce workflow JSON files for structural correctness, status graph integrity, trigger coverage, rule references, user action schema, and cross-entity consistency.

This is a **backend validation** skill. Workflow JSON defines entity state machines — statuses, rulesets (event handlers), rules (business logic), triggers (status matching), and user actions (UI buttons). Structural errors here cause silent runtime failures: stuck entities, invisible buttons, NO_MATCH events, infinite loops.

## Pre-flight: Feedback Check

Before starting, check for past learnings from this skill:

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

## Pre-flight: Load Domain Model

Before validating, read the entity hierarchy:

```
knowledge/platform/domain-model.md
```

Use the **Workflow Entity Hierarchy** table to:
- Identify which entity types are sub-entities of the root workflow entity (e.g., FULFILMENT is a sub-entity under ORDER, WAVE is a sub-entity under LOCATION)
- Validate that `type` overrides in rulesets reference legitimate sub-entities for this root
- Avoid false positives when a workflow contains rulesets for multiple entity types — this is expected for multi-entity workflows

## Ownership Boundary

This skill owns workflow JSON structural validation, status graph analysis, trigger integrity, rule reference checking, and user action schema verification.

Other skills own:
- Workflow creation and editing: `/fluent-workflow-builder`
- Workflow analysis and visualization: `/fluent-workflow-analyzer`
- Workflow deployment: `/fluent-workflow-deploy`
- Pre-deployment checklist (broader scope): `/fluent-pre-deploy-check`
- Rule reference live verification: MCP `plugin.list`
- Live event simulation: MCP `workflow.simulate`

### Cross-Skill Integration

**Consumed by:**
- `/fluent-workflow-deploy` — run validation as pre-deploy gate; block on CRITICAL/HIGH
- `/fluent-pre-deploy-check` — invoke as Phase 3 backend validation
- `/fluent-workflow-builder` — run as post-edit hook after any workflow modification

**Recommended sequencing:**
1. `/fluent-workflow-builder` creates or edits workflow JSON
2. **`/fluent-workflow-validate`** validates structure (this skill)
3. `/fluent-workflow-deploy` deploys (blocks if validation fails)

## When to Use

- After creating or editing a workflow JSON file
- Before deploying a workflow to any environment
- When inheriting an unfamiliar workflow to assess its structural health
- After downloading a workflow from a live environment
- As a pre-deploy gate in `/fluent-workflow-deploy` and `/fluent-pre-deploy-check`
- When debugging runtime issues (NO_MATCH events, stuck entities, invisible buttons)

## Required Inputs

- **Workflow file or directory**: Path to a `.json` workflow file, or a directory containing workflow files
  - If a directory, validate all `.json` files that contain `"rulesets"` and `"statuses"` arrays
  - Search recursively; skip `node_modules/`, `dist/`, `.git/`, `target/`
- **Flags**:
  - `--strict`: Treat MEDIUM and LOW issues as errors (default: only CRITICAL and HIGH are errors)
  - `--fix-suggestions`: Include suggested JSON patches for each issue
  - `--profile <name>`: Enable rule reference checking against live `plugin.list` via MCP

## Workflow JSON Schema Reference

```
WorkflowDefinition
  retailerId: number             # REQUIRED — retailer ID (integer)
  name: string                   # REQUIRED — "<ENTITY_TYPE>::<SUB_TYPE>"
  version: string                # REQUIRED — "<major>.<minor>" (must always increase)
  entityType: string             # REQUIRED — ORDER, FULFILMENT, ARTICLE, WAVE, etc.
  entitySubtype: string          # REQUIRED — matches subtype in name after ::
  description?: string           # Optional description
  rulesets: Ruleset[]            # REQUIRED — non-empty array of event handlers
  statuses: Status[]             # REQUIRED — non-empty array of status definitions
  settings?: object              # Optional workflow-level settings
```

```
Ruleset
  name: string                   # REQUIRED — unique identifier (also the event name)
  description?: string
  type?: string                  # Entity type override (for multi-entity workflows)
  subtype?: string               # Entity subtype override
  eventType: string              # Usually "NORMAL"
  rules: Rule[]                  # REQUIRED — non-empty array of rule references
  triggers: Trigger[]            # REQUIRED — non-empty array of status triggers
  userActions?: UserAction[]     # Optional — UI buttons that fire this ruleset
```

```
Rule
  name: string                   # REQUIRED — "MODULE.context.RuleName" or "FLUENTRETAIL.context.RuleName"
  props?: object                 # Optional — static properties passed to rule via @ParamString
```

```
Trigger
  status: string                 # REQUIRED — must match a defined status name
  entityType?: string            # For multi-entity workflows
  entitySubtype?: string         # For multi-entity workflows
  eventName?: string             # Optional — filter to specific event name
```

```
Status
  name: string                   # REQUIRED — status name (UPPERCASE_SNAKE_CASE)
  category: string               # REQUIRED — one of the standard categories
  entityType?: string            # For multi-entity workflows
```

```
UserAction
  eventName: string              # REQUIRED — event fired when button is clicked
  context: ContextEntry[]        # REQUIRED — form fields shown to user (array, NOT object)
  attributes?: Attribute[]       # Optional — hidden attributes sent with event
  confirm?: boolean              # Optional — show confirmation dialog
  modules?: string[]             # REQUIRED — which apps show this button
  subtype?: string               # REQUIRED — entity subtype for button visibility
  label?: string | object        # Button label (string or i18n object)
```

**Standard status categories:**

| Category | Meaning | Typical usage |
|----------|---------|---------------|
| `BOOKING` | Initial states | CREATED, PENDING, DRAFT |
| `FULFILMENT` | Active processing | PICKING, PACKING, AWAITING_RETURN |
| `DELIVERY` | In transit | SHIPPED, IN_TRANSIT, DISPATCHED |
| `DONE` | Terminal success | COMPLETE, DELIVERED, REFUNDED |
| `EXCEPTION` | Terminal failure/hold | CANCELLED, REJECTED, ON_HOLD |

---

## Validation Phases

### Phase 1: Schema Compliance

Verify all required fields exist and have correct types.

| ID | Check | Severity | Detail |
|----|-------|----------|--------|
| S01 | `retailerId` required | CRITICAL | Must be a positive integer. Missing = workflow cannot be deployed. |
| S02 | `name` required | CRITICAL | Non-empty string, must match `<ENTITY_TYPE>::<SUB_TYPE>` pattern. |
| S03 | `version` required | CRITICAL | Non-empty string, must match `<major>.<minor>` pattern (e.g., `"1.0"`, `"2.3"`). |
| S04 | `entityType` required | CRITICAL | Non-empty string (ORDER, FULFILMENT, ARTICLE, WAVE, LOCATION, RETURN_ORDER, etc.). |
| S05 | `entitySubtype` required | HIGH | Non-empty string. Must match the subtype in `name` after `::`. |
| S06 | `rulesets` non-empty | CRITICAL | Must be a non-empty array. A workflow with no rulesets does nothing. |
| S07 | `statuses` non-empty | CRITICAL | Must be a non-empty array. A workflow with no statuses has no state machine. |
| S08 | `name` matches entityType/subtype | HIGH | The `name` field's prefix before `::` should match `entityType`, and suffix should match `entitySubtype`. |
| S09 | `version` is valid semver-ish | MEDIUM | Major and minor must be non-negative integers. Warn on `"0.0"`. |
| S10 | `entityType` matches known types | LOW | Warn if entityType is not one of the standard Fluent entity types (ORDER, FULFILMENT, ARTICLE, WAVE, LOCATION, VIRTUAL_POSITION, RETURN_ORDER, RETURN_FULFILMENT, BILLING_ACCOUNT, PAYMENT, CREDIT_MEMO, etc.). Custom types are valid but rare. |

### Phase 2: Status Graph Integrity

Validate the status state machine for reachability, dead-ends, orphans, and category correctness.

| ID | Check | Severity | Detail |
|----|-------|----------|--------|
| G01 | All statuses have `name` | CRITICAL | Every entry in `statuses[]` must have a non-empty `name` string. |
| G02 | All statuses have `category` | HIGH | Every status should have a `category` from the standard set. Missing category = wrong UI badge rendering. |
| G03 | Category values are valid | MEDIUM | Category must be one of: `BOOKING`, `FULFILMENT`, `DELIVERY`, `DONE`, `EXCEPTION`. Other values are silently accepted by the platform but render incorrectly. |
| G04 | No duplicate status names | HIGH | Two statuses with the same `name` (within the same entity type) cause undefined behavior. |
| G05 | Orphaned statuses | HIGH | A status defined in `statuses[]` but never referenced in any ruleset `triggers[].status` or as a `SetState` destination is unreachable dead config. Exception: the first status (typically CREATED) is the entry point and may only appear as a SetState target. |
| G06 | Unreachable statuses | HIGH | Build the status transition graph from SetState rules. If a status cannot be reached from the initial status (CREATED or first defined), it is unreachable. Entities will never enter this state. |
| G07 | Dead-end statuses | MEDIUM | A status that has no outgoing rulesets (no triggers match it) AND is not categorized as `DONE` or `EXCEPTION` may trap entities. Terminal statuses should be explicitly categorized. |
| G08 | At least one DONE/EXCEPTION status | MEDIUM | A workflow should have at least one terminal status (category `DONE` or `EXCEPTION`). Without one, entities have no completion path. |
| G09 | CREATED or entry status exists | HIGH | At least one status should be the natural entry point. Typically the first status or one named `CREATED`. |
| G10 | Status name convention | LOW | Status names should be UPPERCASE_SNAKE_CASE. Warn on lowercase, camelCase, or names with spaces. |

### Phase 3: Trigger and Routing Validation

Verify rulesets have valid triggers and event routing is consistent.

| ID | Check | Severity | Detail |
|----|-------|----------|--------|
| T01 | Ruleset has `name` | CRITICAL | Every ruleset must have a non-empty `name`. This is also the default event name. |
| T02 | Ruleset has `rules` array | HIGH | Every ruleset must have a non-empty `rules[]` array. A ruleset with no rules is a no-op. |
| T03 | Ruleset has `triggers` array | HIGH | Every ruleset must have a non-empty `triggers[]` array. A ruleset with no triggers is unreachable. Exception: the CREATE ruleset may be triggered by entity creation without an explicit trigger. |
| T04 | Trigger `status` references defined status | HIGH | Every `triggers[].status` value must match a status defined in `statuses[]`. Typos here cause the ruleset to never fire. |
| T05 | No duplicate ruleset names | HIGH | Two rulesets with the same `name` cause event routing ambiguity. Only one will execute. |
| T06 | Trigger conflict detection | MEDIUM | Two rulesets triggered by the same status (without distinguishing `eventName`) both fire on any event at that status. This is intentional for some patterns (parallel processing) but often indicates a mistake. Flag as MEDIUM with "verify this is intentional". |
| T07 | SendEvent targets match rulesets | HIGH | For every `SendEvent` rule in any ruleset, the event name it sends must match another ruleset's `name` (or a known cross-workflow event). Unmatched targets cause NO_MATCH events at runtime. Extract event name from `SendEvent` rule `props.eventName`. |
| T08 | Circular event chains | HIGH | Build the SendEvent graph. If ruleset A sends event B and ruleset B sends event A (directly or through a chain), flag as potential infinite loop. May be intentional (retry/poll patterns) but requires verification. |
| T09 | Orphaned rulesets | HIGH | A ruleset whose trigger status is never set by any other ruleset's SetState rule (except the initial status) is orphaned — no event path leads to it being triggered. |
| T10 | `eventType` is valid | LOW | Should be `"NORMAL"`. Other values are rare and may indicate copy-paste errors. |

### Phase 4: Rule Reference Validation

Verify rule names resolve to registered plugins.

| ID | Check | Severity | Detail |
|----|-------|----------|--------|
| R01 | Rule has `name` | CRITICAL | Every rule in `rules[]` must have a non-empty `name` string. |
| R02 | Rule name format | HIGH | Rule name should match `<PREFIX>.<context>.<RuleName>` pattern (e.g., `FLUENTRETAIL.base.SendEvent`, `ACME.common.LoadFulfilmentOption`). Warn on names without dots. |
| R03 | Rule exists in plugin.list | HIGH | If `--profile` is provided, query MCP `plugin.list` and verify each rule name exists. Rules not in plugin.list cause NO_MATCH at runtime. Skip this check if MCP is unavailable. |
| R04 | SetState rule has target status | HIGH | Rules matching `*.SetState` or `*.SendEventOnVerifyingState` should have `props` that reference a defined status. Extract target from `props.status`, `props.toState`, or `props.eventName` depending on the rule. |
| R05 | SendEvent rule has target event | HIGH | Rules matching `*.SendEvent` should have `props.eventName` defined. Missing = event sent with empty name. |
| R06 | SendWebhook rule has URL source | MEDIUM | Rules matching `*.SendWebhook` should have webhook URL configured via `props` or settings reference. |
| R07 | Empty rule props | LOW | Rules with no `props` object may be intentional (rule reads all config from entity/event attributes), but flag for review if the rule is known to require props. |
| R08 | Duplicate rule in same ruleset | MEDIUM | The same rule name appearing twice in one ruleset's `rules[]` is usually a copy-paste error. The rule executes twice. |
| R09 | FLUENTRETAIL vs custom prefix | LOW | Informational: flag rules using `FLUENTRETAIL.*` prefix (platform-provided) vs account-specific prefix (custom rules). Helps understand customization scope. |

### Phase 5: User Action Validation

Verify user action definitions are complete and correctly structured for UI rendering.

| ID | Check | Severity | Detail |
|----|-------|----------|--------|
| U01 | `eventName` required | CRITICAL | Every user action MUST have `eventName`. Without it, the button click fires a null event. This is the #1 cause of invisible/broken user actions. |
| U02 | `subtype` required | CRITICAL | Every user action MUST have `subtype` matching the workflow's entity subtype. Without it, the button is invisible in the UI. |
| U03 | `modules` non-empty | HIGH | Must be a non-empty array. Values: `["adminconsole"]` for OMS, `["servicepoint"]` for Store App, or both. Empty = button invisible everywhere. |
| U04 | `modules` valid values | HIGH | Each entry must be `"adminconsole"` (OMS) or `"servicepoint"` (Store App). Typos (`"oms"`, `"admin"`, `"AdminConsole"`) cause invisible buttons. |
| U05 | `context` is array | HIGH | Must be an array, NOT an object. Common mistake: `"context": {}` instead of `"context": []`. Objects are silently ignored — no form fields rendered. |
| U06 | Context entry has required fields | HIGH | Each entry in `context[]` must have all 4 mandatory fields: `label` (display name), `name` (attribute name), `type` (field type), `source` (data source). |
| U07 | Context `source` format | MEDIUM | Should be one of: `"manual"` (user types), `"settings.<key>"` (dropdown from setting), or other known sources. The `source` value is **case-sensitive**: `"settings.MY_SETTING"` works, `"Settings.MY_SETTING"` does NOT. |
| U08 | `confirm` is boolean | MEDIUM | If present, must be `true` or `false` (boolean type, not string `"true"`). |
| U09 | `attributes` entries have `name` and `value` | MEDIUM | Hidden attributes sent with the event. Each must have `name` and `value` (or `defaultValue`). |
| U10 | `eventName` matches ruleset name | HIGH | The `eventName` in the user action should match the parent ruleset's `name`. If different, verify that the target ruleset exists. Mismatches cause events to route to wrong (or no) handler. |

### Phase 6: Cross-Entity and Advanced Validation

For multi-entity workflows and deployment readiness.

| ID | Check | Severity | Detail |
|----|-------|----------|--------|
| X01 | Multi-entity ruleset `type` field | HIGH | In workflows containing rulesets for multiple entity types, each non-primary ruleset must have a `type` field matching the child entity type. Missing `type` = ruleset matches wrong entity. |
| X02 | Multi-entity status `entityType` | HIGH | In multi-entity workflows, statuses for child entities must have `entityType` set. Otherwise they conflict with parent entity statuses. |
| X03 | Multi-entity trigger `entityType` | HIGH | Triggers in child-entity rulesets must include `entityType` to scope correctly. |
| X04 | Version increment vs deployed | MEDIUM | If `--profile` is provided, query MCP `workflow.list` to get the currently deployed version. The local version must be higher. Flag if same or lower. |
| X05 | Cross-workflow event targets | LOW | SendEvent rules may target rulesets in other workflows (cross-entity events). These cannot be validated locally — flag as informational: "verify target workflow has a matching ruleset". |
| X06 | Settings references exist | MEDIUM | If `--profile` is provided and workflow uses `settings{}` or user action `source: "settings.<key>"`, verify the referenced settings exist via MCP `setting.get`. |

### Phase 7: GraphQL Schema Validation (when --profile provided)

Verify that GraphQL-related rule props reference valid entity types, fields, and operations against the live schema. This phase requires MCP connectivity (`--profile` flag).

**Skip condition:** No `--profile` provided or MCP tools unavailable.

| ID | Check | Severity | Detail |
|----|-------|----------|--------|
| Q01 | Entity types in rule props are valid | HIGH | For rules with `props` referencing entity types (e.g., `entityType`, `type` values), verify via `graphql.introspect({ type: "<value>" })`. Invalid entity types cause NO_MATCH or null responses at runtime. |
| Q02 | DynamicUtils field paths in rule props resolve | HIGH | For rules known to use DynamicUtils (identified by rule name patterns like `*Dynamic*`, `*Attribute*`, `*Path*`), extract field paths from props (`jsonpath`, `attributeName`, `fieldPath`) and verify each path segment via `graphql.introspect`. |
| Q03 | Mutation names in rule props exist | MEDIUM | For rules with props referencing mutation names (`mutationName`, `createMutation`), verify the mutation exists: `graphql.introspect({ mutation: "<value>" })`. |
| Q04 | Setting-referenced query fields valid | LOW | For rules reading settings that contain GraphQL queries (e.g., webhook settings with query templates), extract and validate: `graphql.validate({ query: "<setting-query>" })`. This is LOW because settings are validated separately in Phase 5. |

---

## Execution Flow

```
1. LOCATE workflow file(s)
   a. If file path provided → use directly
   b. If directory → find all .json files with "rulesets" and "statuses"
   c. Also check: accounts/<PROFILE>/workflows/, accounts/<PROFILE>/features/*/
   d. If none found → STOP with guidance

2. PARSE each workflow file
   a. Valid JSON? If not → CRITICAL error, skip file
   b. Has "rulesets" and "statuses" arrays? If not → skip (not a workflow)

3. RUN Phase 1: Schema Compliance (S01-S10)
   a. Check all required fields
   b. Validate field types and formats

4. RUN Phase 2: Status Graph Integrity (G01-G10)
   a. Build status set from statuses[]
   b. Build transition graph from SetState rules
   c. Check reachability from initial status
   d. Detect dead-ends, orphans, category issues

5. RUN Phase 3: Trigger and Routing (T01-T10)
   a. Build ruleset-to-trigger map
   b. Build SendEvent graph
   c. Check trigger references, conflicts, cycles
   d. Detect orphaned rulesets

6. RUN Phase 4: Rule References (R01-R09)
   a. Validate rule name format
   b. If --profile: query plugin.list via MCP
   c. Cross-reference each rule name
   d. Check SetState/SendEvent props

7. RUN Phase 5: User Actions (U01-U10)
   a. Validate every userActions[] entry
   b. Check eventName, subtype, modules, context structure
   c. Verify eventName → ruleset routing

8. RUN Phase 6: Cross-Entity (X01-X06)
   a. Detect multi-entity workflows (multiple entityType values)
   b. Validate type/entityType fields on rulesets, statuses, triggers
   c. If --profile: version and settings checks

9. RUN Phase 7: GraphQL Schema (Q01-Q04) — if --profile provided
   a. For each rule prop referencing entity types → graphql.introspect
   b. For DynamicUtils-pattern rules → validate field paths
   c. For mutation-referencing rules → verify mutation exists
   d. Skip if MCP unavailable (record SKIP with reason)

10. AGGREGATE results
    a. Count by severity: CRITICAL, HIGH, MEDIUM, LOW
    b. Sort: CRITICAL first

11. OUTPUT report
    a. Console summary with labeled check IDs
    b. Exit code: 0 = PASS, 1 = FAIL

12. FEEDBACK capture
    a. Write to accounts/<PROFILE>/feedback/fluent-workflow-validate.jsonl
```

## Building the Status Transition Graph

This is the core algorithm for Phase 2 (G05-G09) and Phase 3 (T07-T09).

```
INPUT: rulesets[], statuses[]

1. Extract all status names from statuses[] → STATUS_SET
2. For each ruleset:
   a. Extract trigger statuses → TRIGGER_MAP[ruleset.name] = [status1, status2, ...]
   b. Scan rules[] for SetState rules:
      - Match rule names containing "SetState", "ChangeState", "SendEventOnVerifying"
      - Extract target status from props (props.status, props.toState, props.eventName)
      - Record: TRANSITIONS[trigger_status] → [target_status, ...]
   c. Scan rules[] for SendEvent rules:
      - Extract target event from props.eventName
      - Record: EVENT_GRAPH[ruleset.name] → [target_event, ...]

3. Build reachability:
   a. START = first status in statuses[] (typically CREATED)
   b. REACHABLE = {START}
   c. Repeat until no changes:
      - For each status in REACHABLE:
        - Find all target statuses via TRANSITIONS[status]
        - Add them to REACHABLE
   d. UNREACHABLE = STATUS_SET - REACHABLE

4. Build dead-ends:
   a. For each status in STATUS_SET:
      - If no ruleset has triggers[].status == this status
      - AND status.category is NOT "DONE" and NOT "EXCEPTION"
      - → Dead-end (entities get stuck here)

5. Detect cycles in EVENT_GRAPH:
   a. Standard DFS cycle detection on EVENT_GRAPH
   b. Report any back-edges as potential infinite loops
```

## Output Format

### Console Output

```
WORKFLOW VALIDATION REPORT
==========================
File: accounts/<PROFILE>/workflows/ORDER__HD_SHIP_FROM_STORE.json
Name: ORDER::HD_SHIP_FROM_STORE
Version: 1.5
Entity: ORDER / HD_SHIP_FROM_STORE
Statuses: 12 | Rulesets: 18 | Rules: 47

RESULTS
-------
CRITICAL [U01] User action missing "eventName" at rulesets[7].userActions[0]
  → Button click fires null event. Add eventName matching the ruleset name.

HIGH [G06] Unreachable status "AWAITING_WAVE" — no transition path from CREATED
  → No SetState rule targets this status. Remove it or add a transition.

HIGH [T07] SendEvent target "FulfilmentComplete" has no matching ruleset
  → Event will produce NO_MATCH. Add a ruleset named "FulfilmentComplete" or fix the event name.

HIGH [R03] Rule "ACME.common.ValidateOrderItems" not found in plugin.list
  → This rule will cause NO_MATCH at runtime. Deploy the module containing it first.

MEDIUM [G07] Dead-end status "ON_HOLD" (category: FULFILMENT) has no outgoing rulesets
  → Entities entering ON_HOLD have no exit path. Add a ruleset triggered by ON_HOLD or change category to EXCEPTION.

MEDIUM [T06] Trigger conflict: rulesets "ProcessOrder" and "ValidateOrder" both trigger on status "CREATED"
  → Both fire on any event at CREATED. Verify this is intentional.

LOW [G10] Status "readyForPicking" uses camelCase — convention is UPPERCASE_SNAKE_CASE
  → Rename to "READY_FOR_PICKING" for consistency.

LOW [R09] 42 FLUENTRETAIL rules, 5 custom rules (ACME prefix)
  → Informational: 10% custom rule density.

────────────────────────────────────────
|  CRITICAL: 1  |  HIGH: 3  |  MEDIUM: 2  |  LOW: 2        |
────────────────────────────────────────

RESULT: FAIL (1 CRITICAL, 3 HIGH issues must be resolved)

Fix the CRITICAL and HIGH issues above, then re-run validation.
MEDIUM and LOW issues are recommendations (enforce with --strict).
```

### Exit Codes

| Code | Meaning |
|------|---------|
| `0` | PASS — no CRITICAL or HIGH issues (or none in `--strict` mode) |
| `1` | FAIL — CRITICAL or HIGH issues found |

### Machine-Readable Output

When invoked by other skills, return structured results:

```json
{
  "file": "path/to/workflow.json",
  "name": "ORDER::HD_SHIP_FROM_STORE",
  "version": "1.5",
  "entityType": "ORDER",
  "stats": {
    "statuses": 12,
    "rulesets": 18,
    "rules": 47,
    "userActions": 5
  },
  "summary": {
    "critical": 1,
    "high": 3,
    "medium": 2,
    "low": 2,
    "status": "FAIL"
  },
  "issues": [
    {
      "id": "U01",
      "severity": "CRITICAL",
      "message": "User action missing \"eventName\"",
      "location": "rulesets[7].userActions[0]",
      "fix": "Add eventName matching the ruleset name"
    }
  ],
  "graph": {
    "reachableStatuses": 10,
    "unreachableStatuses": ["AWAITING_WAVE", "LEGACY_HOLD"],
    "deadEndStatuses": ["ON_HOLD"],
    "terminalStatuses": ["COMPLETE", "CANCELLED"],
    "circularEventChains": []
  }
}
```

## Integration with Other Skills

| Scenario | How this skill integrates |
|----------|--------------------------|
| `/fluent-workflow-builder` completes | Run `/fluent-workflow-validate` as post-edit hook |
| `/fluent-workflow-deploy` pre-flight | Read validation result; block deploy if FAIL |
| `/fluent-pre-deploy-check` Phase 3 | Invoke this skill for deep backend validation |
| `/fluent-workflow-analyzer` | Analyzer maps the graph; validator checks it for errors |
| MCP `workflow.upload --dryRun` | Upload does 3 basic checks; this skill does 50+ |

**Handoff patterns:**

```
After validation FAIL:
  → CRITICAL/HIGH in Phase 1 (Schema): fix JSON structure manually or via /fluent-workflow-builder
  → CRITICAL/HIGH in Phase 2 (Graph): add missing transitions via /fluent-workflow-builder
  → CRITICAL/HIGH in Phase 3 (Triggers): fix trigger statuses or add missing rulesets
  → CRITICAL/HIGH in Phase 4 (Rules): deploy missing module via /fluent-module-deploy
  → CRITICAL/HIGH in Phase 5 (User Actions): fix eventName/subtype/modules/context
  → CRITICAL/HIGH in Phase 6 (Cross-Entity): add type/entityType fields to rulesets/statuses

After validation PASS:
  → Proceed to /fluent-workflow-deploy
```

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

- **SetState target extraction is heuristic.** Different SetState rule variants store the target status in different prop keys (`status`, `toState`, `stateTo`, `props.eventName` for SendEventOnVerifyingState). The validator checks common patterns but may miss custom variants. When a SetState target cannot be extracted, the rule is excluded from graph analysis with a LOW informational note.
- **Cross-workflow SendEvent targets cannot be validated locally.** Events targeting rulesets in other workflows (e.g., ORDER workflow sending events to FULFILMENT workflow) are flagged as informational (X05) because the target workflow may not be in the local file set.
- **Platform auto-transitions are invisible.** Some status transitions happen via platform internals (e.g., entity creation → first status) without an explicit ruleset. The validator accounts for the first status being reachable by default.
- **CLI workflowlog merge strips eventName.** After deploying via CLI, re-downloading the workflow may show missing eventName fields that were present in the original. This is a known CLI bug — validate against the source file, not the downloaded version.

<!-- end feedback-promoted -->

## Post-Execution: Feedback Capture

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

1. **Classify outcome:** `SUCCESS`, `PARTIAL_SUCCESS`, `FAILURE`, `BLOCKED`, `USER_CORRECTED`
2. **Build record:** `feedback-record-v1` with skill, phases, learnings, userCorrections
3. **Redact secrets:** Strip tokens, passwords, API keys
4. **Append** one JSON line to `accounts/<PROFILE>/feedback/fluent-workflow-validate.jsonl`
5. **Update** `accounts/<PROFILE>/feedback/index.json` counters

**Skip capture if:** The execution was trivially simple (e.g., validation passed with 0 issues on first run).
