---
name: workflow-json-structure
scope: platform
description: "Canonical Fluent Commerce workflow JSON structure — field ordering, ruleset conventions, multi-entity patterns, event execution model"
load: on-demand
priority: high
relevant-skills: [fluent-workflow-builder, fluent-feature-plan, fluent-workflow-deploy, fluent-workflow-analyzer, fluent-pre-deploy-check, fluent-use-case-discover]
version: 2.0
last-updated: 2026-03-06
author: user
evidence: live Fluent workflow evidence, Fluent Commerce official documentation (Workflow Event Execution, 29 Aug 2025)
---

# Workflow JSON Structure

## Event Execution Model (official)

When an orchestration event is received, the Workflow Engine:

1. **Loads** the workflow associated with the entity in the event context
2. **Matches** a ruleset using 4 criteria:
   - **Event Name = Ruleset Name** (primary match)
   - **Entity Type** (e.g., ORDER)
   - **Entity Subtype** (e.g., HD)
   - **Entity Status** (e.g., BOOKED)
3. **Executes** the matched ruleset
4. **Queues inline events** — rules may produce flow control events that trigger additional rulesets in the same execution thread, in sequence

### Inline Events (event chaining)

Rules produce "inline events" that trigger subsequent rulesets within the same action execution thread. These are NOT asynchronous — they execute in sequence after the current ruleset completes.

```
Ruleset "CREATE" executes
  → Rule sends inline event "CheckCoordinatesPresent"
    → Ruleset "CheckCoordinatesPresent" fires (same thread)
      → Rule sends inline event "GetLocations"
        → Ruleset "GetLocations" fires (same thread)
          → ...
```

Each rule has an `"eventName"` prop naming the NEXT ruleset to trigger. This creates a chain without explicit `eventName` in triggers.

## Root Entities, Sub-Entities & CREATE Events

A workflow is named by its **Root Entity** and includes lifecycles for its **Sub-Entities**. The platform automatically generates CREATE events for entities marked ✅.

| Root Entity | Sub-Entities | Event Reference | Platform CREATE? |
|---|---|---|---|
| **Location** | Wave | LOCATION, WAVE | ❌, ✅ |
| **Order** | Fulfilment, Article, Consignment | ORDER, FULFILMENT, ARTICLE, CONSIGNMENT | ✅, ✅, ✅, ✅ |
| **Fulfilment Options** | Fulfilment Plan | FULFILMENT_OPTIONS, FULFILMENT_PLAN | ✅, ✅ |
| **Product Catalogue** | Standard/Variant/Group Product, Category | PRODUCT_CATALOGUE, PRODUCT, CATEGORY | ✅, ✅, ✅ |
| **Inventory Catalogue** | Inventory Position, Inventory Quantity | INVENTORY_CATALOGUE, INVENTORY_POSITION, INVENTORY_QUANTITY | ✅, ✅, ✅ |
| **Virtual Catalogue** | Virtual Position | VIRTUAL_CATALOGUE, VIRTUAL_POSITION | ❌, ❌ |
| **Control Group** | Control | CONTROL_GROUP, CONTROL | ❌, ❌ |
| **Return Order** | Return Fulfilment | RETURN_ORDER, RETURN_FULFILMENT | ✅, ✅ |
| **Billing Account** | Credit Memo, Invoice, Payment, Payment Transaction | BILLING_ACCOUNT, CREDIT_MEMO, INVOICE, PAYMENT, PAYMENT_TRANSACTION | ✅, ✅, ✅, ✅, ✅ |

### Recommended Approaches

- **Creating/updating sub-entities:** Send an event to the **root entity** context on the workflow, let it upsert the sub-entity and handle pre/post-condition logic.
- **Updating root entities:** Send an event rather than use the update mutation directly — this allows orchestrated business logic in the workflow to run.

## Field Order (top-level)

The canonical field ordering in Fluent workflow JSON:

```json
{
  "retailerId": "<retailerId>",
  "version": "<semver>",
  "entityType": "<ENTITY_TYPE>",
  "entitySubtype": "<SUBTYPE>",
  "description": "<what this workflow does>",
  "versionComment": null,
  "name": "<ENTITY_TYPE>::<SUBTYPE>",
  "rulesets": [],
  "statuses": [],
  "settings": {}
}
```

**Key rules:**
- `retailerId` comes FIRST
- `name` uses `::` separator: `ENTITY_TYPE::SUBTYPE`
- `rulesets[]` comes BEFORE `statuses[]`
- `settings: {}` at the end (even if empty)
- `createdBy`, `createdOn`, `id` are server-populated (null/absent in source files)

## Ruleset Structure

Field order inside each ruleset:

```json
{
  "name": "RulesetName",
  "description": "What this ruleset does",
  "type": "ENTITY_TYPE",
  "eventType": "NORMAL",
  "rules": [],
  "triggers": [],
  "userActions": []
}
```

**Key rules:**
- `rules[]` comes BEFORE `triggers[]`
- `userActions: []` ALWAYS present (even if empty)
- NO `subtype` field on rulesets — entity-level `entitySubtype` handles routing
- `eventType` is typically `"NORMAL"`

## Ruleset Matching (4 criteria)

The Workflow Engine matches rulesets using these 4 criteria simultaneously:

1. **Event Name = Ruleset Name** — the event's `name` field must match the ruleset's `name`
2. **Entity Type** — the event's `entityType` matches the ruleset's `type`
3. **Entity Subtype** — the entity's subtype matches the workflow's `entitySubtype`
4. **Entity Status** — the entity's current status must be in the ruleset's `triggers[].status` list

### Examples

| Ruleset Name | Type | Triggers | Matches When |
|---|---|---|---|
| `CREATE` | FULFILMENT_OPTIONS | `[{"status":"CREATED"}]` | FO entity created (platform CREATE event), entity is at CREATED status |
| `PlansCreated` | FULFILMENT_OPTIONS | `[{"status":"SOURCING"}]` | Rule sends event named "PlansCreated", FO entity is at SOURCING status |
| `CREATE` | FULFILMENT_PLAN | `[{"status":"CREATED"}]` | FP entity created (platform CREATE event), FP entity is at CREATED status |

**Two rulesets can have the same name** (e.g., both named `CREATE`) if they have different `type` values — the entity type disambiguates.

Triggers specify **status guards** only. The event name matching is by ruleset name:

```json
"triggers": [
  { "status": "CREATED" },
  { "status": "RECEIVED" }
]
```

**Exception:** You CAN add `"eventName"` to triggers for explicit matching, but the standard convention relies on name matching.

## Multi-Entity Workflows

A workflow named by its Root Entity includes rulesets for both root and sub-entities:

```json
{
  "entityType": "FULFILMENT_OPTIONS",
  "entitySubtype": "DEFAULT",
  "name": "FULFILMENT_OPTIONS::DEFAULT",
  "rulesets": [
    { "name": "CREATE", "type": "FULFILMENT_OPTIONS", ... },
    { "name": "GetLocations", "type": "FULFILMENT_OPTIONS", ... },
    { "name": "CREATE", "type": "FULFILMENT_PLAN", ... }
  ],
  "statuses": [
    { "name": "CREATED", "entityType": "FULFILMENT_OPTIONS", "category": "GENERATION" },
    { "name": "CREATED", "entityType": "FULFILMENT_PLAN", "category": "GENERATION" }
  ]
}
```

**Key rules for multi-entity:**
- Rulesets use `"type"` to specify which entity type they apply to
- Statuses carry `"entityType"` field to distinguish between entity types
- Same ruleset name (e.g., `CREATE`) can appear multiple times with different `type`
- Same status name (e.g., `CREATED`) can appear multiple times with different `entityType`
- The workflow `name` and `entityType` use the ROOT entity type

## Status Format

### Single-entity workflow

```json
{ "name": "BOOKED", "category": "FULFILMENT" }
```

### Multi-entity workflow

```json
{ "name": "BOOKED", "entityType": "FULFILMENT_OPTIONS", "category": "GENERATION" }
```

**`entityType` is required when the workflow contains rulesets for multiple entity types.**

### Status Categories

| Category | Used By |
|---|---|
| `GENERATION` | FULFILMENT_OPTIONS, FULFILMENT_PLAN workflows |
| `BOOKING` | ORDER workflows (pre-fulfilment) |
| `FULFILMENT` | ORDER, FULFILMENT workflows (in-progress) |
| `DELIVERY` | FULFILMENT workflows (shipping) |
| `DONE` | Terminal statuses (COMPLETE, CANCELLED) |
| `EXCEPTION` | Error/exception statuses |

**FO/FP workflows use `GENERATION` for all statuses** — not the order lifecycle categories.

## Rule Structure

```json
{
  "name": "MODULE.RuleName",
  "props": {
    "key": "value"
  }
}
```

- Rule name format: `MODULE.RuleName` (e.g., `ACME.common.LoadFulfilmentOptionFO`)
- `props` can be `null` if no configuration needed
- Props can contain strings, numbers, and arrays (e.g., `"locationStatuses": ["ACTIVE"]`)

## Trigger Structure

Minimal (most common — event name matched by ruleset name):
```json
{ "status": "CREATED" }
```

With explicit event name (optional, overrides name-matching):
```json
{ "status": "SOURCING", "eventName": "PlansCreated" }
```

Multiple status guards (entity must be at ANY of these statuses):
```json
"triggers": [
  { "status": "CREATED" },
  { "status": "RECEIVED" }
]
```

## Event API Format

Events sent to the platform follow this structure:

```json
{
  "name": "CancelOrder",
  "retailerId": "1",
  "entityId": "1234",
  "entityRef": "ON-1234",
  "entityType": "ORDER",
  "rootEntityType": "ORDER",
  "rootEntityId": "1234",
  "rootEntityRef": "ON-1234",
  "attributes": {
    "cancelReason": "Stolen Credit Card"
  }
}
```

- `name` matches the target ruleset name
- `entityType` + `rootEntityType` for sub-entity events (e.g., FULFILMENT event within ORDER workflow)
- `attributes` carry dynamic data read by rules via `@EventAttribute`

## File Naming Convention

Workflow JSON files use `-` as separator (not `::`):

| Workflow Name | File Name |
|---|---|
| `FULFILMENT_OPTIONS::HD` | `FULFILMENT_OPTIONS-HD.json` or `FULFILMENT_OPTIONS__HD.json` |
| `ORDER::MULTI` | `ORDER-MULTI.json` or `ORDER__MULTI.json` |

Both `-` and `__` separators are used in practice. `__` is the download convention; `-` is the deploy convention for `fluent module install`.
