---
name: fluent-mystique-builder
description: Build and edit Mystique manifest JSON definitions. Create pages, routes, component trees, data queries, and fragment compositions using the v2.0 schema. Triggers on "build manifest", "create mystique page", "add route", "mystique component", "edit manifest", "build ui", "add page to manifest".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: <operation> [manifest-file] [--page <path>] [--section <name>] [--fragment]
cursor-tier: manual
planning-gate: mandatory
---

# Mystique Manifest Builder

Create, edit, and validate Fluent Commerce Mystique manifest JSON definitions. Mystique is the declarative, manifest-driven UI framework that powers the OMX frontend. Manifests are JSON documents that define the entire application structure: routes, pages, component trees, GraphQL data queries, role-based visibility, template strings, and plugin references. This skill understands the v2.0 manifest schema end-to-end and generates structurally correct JSON that passes `/fluent-mystique-validate`.

## Pre-flight: Feedback Check

Before starting, check for past learnings from this skill:

1. Check if `accounts/<PROFILE>/feedback/fluent-mystique-builder.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

## Platform Fundamentals

**OMX vs Mystique naming:** "OMX" (Order Management Experience) is the umbrella framework covering UX + Workflows + Connect. "Mystique" is the internal/historical code name for the UX Framework layer specifically. All import paths and code references use `mystique/` (e.g., `mystique/hooks/useAuth`, `mystique/registry`), while documentation uses "OMX" or "UX Framework" externally. Both refer to the same thing for frontend purposes.

**Three registries:**
- **ComponentRegistry** — standard display components (`fc.list`, `fc.card.attribute`, custom components)
- **FieldRegistry** — form input components for user action attributes (auto-selected by type name)
- **TemplateRegistry** — Handlebars helper functions for template string expressions

### Frontend Category and Registry Quick Matrix

Use this quick matrix when planning custom aliases or deciding whether work belongs in manifest config vs SDK code:

| Component `category` | Use when | `data` scope | `<Children />` impact | Typical API |
|---|---|---|---|---|
| `page` | Replacing the route-level page shell | Unscoped full page query | Usually required | `useData`, `useUserActions`, `<Children />` |
| `layout` | Arranging descendant components/regions | Scoped by parent `dataSource` | Core pattern | `<Children />`, `dataOverride`, `descendantsOverride` |
| `content` | Rendering widget/card/action blocks (default) | Scoped by parent `dataSource` | Optional | `<DynamicValue />`, `TemplateRegistry.render`, `useData` |
| `filter` | Updating query variables (search/filter/sort) | Scoped + mutable query variables | Rarely used | `useData().variables.setVariables()` |

Registry choice:
- `ComponentRegistry` for manifest `"component"` renderers (`page`/`layout`/`content`/`filter`).
- `FieldRegistry` for workflow user action form fields (`eventAttributes[].type` mapping).
- `TemplateRegistry` for `{{helper ...}}` expression helpers.

**Configuration over code:** Most UI should be built via manifest JSON, not custom components. Only build custom components when: novel visualizations, drag-and-drop, external service integrations, real-time features, or interaction patterns not covered by user action forms. The framework is **versionless with guaranteed backward compatibility** — old configurations never break.

**Multi-tenancy context:** Account → Retailer → Location. Manifests can be scoped at ACCOUNT level. User context (`activeRetailer`, `activeLocation`) is available in template strings for query variables and conditional rendering.

**Workflow-UI integration:** User action buttons and forms appear automatically based on the entity's current workflow state. No manifest changes needed when workflows evolve — the UI adapts. Custom form field components are selected by matching the workflow `eventAttributes[].type` to FieldRegistry entries.

## Pre-flight: Project Knowledge Check

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

1. Check if `accounts/<PROFILE>/knowledge/` exists
2. If yes, read it and find files where `relevant-skills` includes `fluent-mystique-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 types, GraphQL edges for data queries, valid connections
   - `accounts/<PROFILE>/knowledge/naming-conventions.md` — use for component naming, route naming, i18n key patterns
   - `accounts/<PROFILE>/knowledge/client/data-model.md` — use for entity types, attributes to display in UI
4. If `accounts/<PROFILE>/memory/index.md` exists, read the top account-specific UI, manifest, 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 manifest JSON, run the three-level pre-flight check matching the backend rigor:

1. **Feature check:** If `accounts/<PROFILE>/features/<slug>/status.json` exists for this work:
   - Check `spec: "APPROVED"` — If missing, BLOCK — redirect to `/fluent-use-case-discover`
   - Check `plan: "APPROVED"` with Section 10 populated — If missing, BLOCK — redirect to `/fluent-feature-plan`
   - If both approved, proceed with implementation

2. **Standalone manifest work** (no feature context):
   - Write plan to `accounts/<PROFILE>/features/<slug>/plan.md` (NOT `tasks/`) — create feature directory with status.json
   - status.json: `{ "status": "PLANNING", "spec": null, "plan": "PENDING" }` (spec skipped for manifest-only work with explicit user acknowledgment)
   - Plan must cover: route table, component tree, data requirements (GraphQL queries/fragments), user action schemas, settings dependencies
   - Wait for user approval — set `plan: "APPROVED"` before building

3. **Multi-artifact work** (manifest + rules/workflows):
   - STOP. Invoke `/fluent-feature-plan` first — this is the unified entry point
   - Feature plan Section 10 (UI / Manifest Artifacts) must be populated and approved

### Plan Verification

After the pre-flight chain passes, verify the plan content covers manifest-specific concerns. Every table row must carry a Source column (NEW/EXISTING/MODIFIED/REUSED/OOTB).

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

1. **Wireframe (ASCII/Mermaid)** — layout sketch showing page structure and component hierarchy
2. **Route table** — all routes with path, component, section, nav label, roles
3. **Component tree** — hierarchical component layout per page (nested list or tree diagram)
4. **Data requirements** — GraphQL queries needed per page with entity types and key fields
5. **Settings dependencies** — fragment references (`settingName`), feature flags, plugin settings
6. **i18n keys** — translatable strings requiring `i18n:` prefixed keys
7. **Plugin references** — custom SDK components needed and their bundle URLs

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

## Post-Build: Manifest Validation

After building or modifying any manifest JSON, **always recommend running `/fluent-mystique-validate`** to check for:
- Schema compliance (manifestVersion, required fields, route types)
- Component alias resolution (unknown `fc.*` aliases or custom components)
- Data query syntax and dataSource path validity
- Template string syntax (balanced `{{`/`}}`, valid helper names)
- Cross-reference issues (homePath resolves, fragment settingName conventions)
- Component prop completeness (fc.list needs attributes, fc.conditional needs value/matches)

If the user skips this step, `/fluent-pre-deploy-check` will enforce it before deployment. But catching issues early avoids broken UI in production.

### Output Validation (Automated)

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

After writing manifest JSON (any operation), run the adversarial validation loop (max 2 rounds). The critic validates: plugins in root only (never fragments), setting parameters (JSON/ACCOUNT/0/lobValue), query variable correctness, component alias registration, fragment reference resolution, and plan UI design alignment.

**This runs after `/fluent-mystique-validate` structural checks and before presenting to user for deployment approval.** Do not skip.

## Post-Build: Visual Preview (Automatic)

After building or editing any manifest JSON, **automatically generate a visual preview** so the user can see the page layout before deployment. This is a quality gate — present the preview alongside the manifest JSON for approval.

### Step 1: Generate ASCII Wireframe

Walk the manifest's component tree recursively and render an ASCII box diagram showing the page layout:

**Component-to-visual mapping:**

| Component | Visual representation |
|---|---|
| `fc.page` | Full-width page container with title bar |
| `fc.page.section` | Transparent pass-through (no visible border) |
| `fc.page.section.column` | Side-by-side column (width from props: `"half"` = 50%, `"third"` = 33%, `"quarter"` = 25%) |
| `fc.card.attribute` | Bordered card box listing attribute labels |
| `fc.card.attributes.grid` | Multi-column attribute grid |
| `fc.list` | Table outline with column headers from `attributes[].label` |
| `fc.tabs` / `fc.tabs.card` | Tab bar with `[Tab1] [Tab2] [Tab3]` labels |
| `fc.tab.content` | Tab panel content area |
| `fc.conditional` | Dashed border with condition annotation |
| `fc.repeater` | Repeating block with `N x` annotation |
| `fc.filterPanel` | Filter form area |
| `fc.accordion` | Collapsible section with toggle indicator |
| `fc.chart.*` | Chart placeholder with type label |
| `fc.tile.metric` | Small metric box with label |
| `fc.modal.button` / `fc.drawer.button` | Button with `[label]` |
| Custom (`!fc.*`) | Highlighted box with `[CUSTOM: alias]` label |

**Wireframe generation procedure:**

```
function renderWireframe(page):
  output = []
  output.append("┌─── fc.page: {page.props.title} ──────────────────────┐")
  if page.props.actions:
    output.append("│ Actions: [user action buttons auto from workflow]      │")
  if page.props.backButtons:
    output.append("│ ← {backButtons[0].label}                              │")
  output.append("├──────────────────────────────────────────────────────┤")
  renderDescendants(page.descendants, output, indent=1)
  output.append("└──────────────────────────────────────────────────────┘")

function renderDescendants(instances, output, indent):
  // Group consecutive columns into rows
  columnGroup = []
  for each instance:
    if instance is column:
      columnGroup.append(instance)
    else:
      if columnGroup: flushColumnRow(columnGroup, output, indent)
      renderComponent(instance, output, indent)
  if columnGroup: flushColumnRow(columnGroup, output, indent)

function flushColumnRow(columns, output, indent):
  // Render columns side-by-side using width props
  widthMap = { "half": 50%, "third": 33%, "quarter": 25%, "full": 100% }
  for each column: render box at calculated width

function renderComponent(instance, output, indent):
  alias = instance.component
  match alias:
    "fc.card.attribute" →
      render bordered box with title + attribute labels
    "fc.list" →
      render table outline with column headers from attributes[].label
    "fc.tabs" | "fc.tabs.card" →
      render tab bar: [tab1] [tab2] [tab3]
      render descendants per tab
    "fc.conditional" →
      render dashed box: "if {value} matches {matches}"
      renderDescendants(instance.descendants)
    "fc.repeater" →
      render "N × " prefix
      renderDescendants(instance.descendants)
    custom (not fc.*) →
      render highlighted box: "[CUSTOM: {alias}]"
    default →
      render labeled box: "{alias}"
  if instance.descendants:
    renderDescendants(instance.descendants, output, indent+1)
```

**Example output:**

```
┌─── fc.page: {{capitalise status}} - Appeasement {{ref}} ─────────┐
│ ← Appeasements                              Actions: [workflow]   │
├───────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────┐ ┌─────────────────────────┐          │
│ │ Summary          (half) │ │ Approval Details (half) │          │
│ │ ○ Reference             │ │ ○ Outcome               │          │
│ │ ○ Status                │ │ ○ Approved By            │          │
│ │ ○ Amount                │ │ ○ Approved On            │          │
│ │ ○ Currency              │ │ ○ Notes                  │          │
│ └─────────────────────────┘ └─────────────────────────┘          │
│                                                                   │
│ ┌─────────────────────────────────────────────────────────────┐  │
│ │ [Items] [Activity] [Attributes]                 fc.tabs.card│  │
│ ├─────────────────────────────────────────────────────────────┤  │
│ │ Tab 1: fc.list                                              │  │
│ │ ┌──────┬────────┬─────────┬────────┬────────┐              │  │
│ │ │ Ref  │ Product│ Qty     │ Amount │ Status │              │  │
│ │ └──────┴────────┴─────────┴────────┴────────┘              │  │
│ └─────────────────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────────────────┘
```

### Step 2: Generate Mermaid Component Tree

Generate a Mermaid flowchart showing the component hierarchy and data flow:

```mermaid
graph TD
    P["fc.page<br/>Order Detail"] --> C1["fc.card.attribute<br/>Summary (half)"]
    P --> C2["fc.card.attribute<br/>Approval Details (half)"]
    P --> T["fc.tabs.card<br/>3 tabs"]
    T --> T1["fc.list<br/>Items (5 cols)"]
    T --> T2["fc.activity.entity<br/>Activity"]
    T --> T3["fc.attribute.jsoneditor<br/>Attributes"]

    style P fill:#1976d2,color:#fff
    style C1 fill:#43a047,color:#fff
    style C2 fill:#43a047,color:#fff
    style T fill:#f57c00,color:#fff
```

### Step 3: Generate Data Flow Summary

List the data binding chain showing how page query data flows to each component:

```
Page Query: creditMemo { ref, status, totalAmount { amount, currency }, items { edges { node { ... } } } }
├─ fc.card.attribute "Summary" → uses: creditMemo.ref, creditMemo.status, creditMemo.totalAmount
├─ fc.card.attribute "Approval" → uses: creditMemo.attributes (approvalOutcome, approvedBy, ...)
└─ fc.tabs.card
   ├─ fc.list (dataSource: "creditMemo.items") → uses: node.ref, node.product, node.quantity
   ├─ fc.activity.entity → uses: creditMemo.ref (entityRef)
   └─ fc.attribute.jsoneditor → uses: creditMemo.attributes
```

### Presenting the Preview

After generating all three artifacts (wireframe, component tree, data flow), present them to the user in a single review block:

```
## Manifest Preview: {page-path}

### Page Layout
{ASCII wireframe}

### Component Hierarchy
{Mermaid diagram}

### Data Flow
{Data binding summary}

### Manifest JSON
{The actual manifest JSON}
```

**Wait for user approval** before proceeding to deployment. The user may request adjustments — iterate on the wireframe + manifest until approved. This is the visual quality gate.

### Integration with `/fluent-mystique-preview`

If status.json has `preview: "APPROVED"`, read the `previewReport` path and use the approved preview as the visual target for manifest construction. The preview report's Change Manifest table maps directly to manifest operations (ADD → new descendants, MODIFY → altered descendants, REMOVE → descendants to delete).

For new previews (no approved preview exists), recommend `/fluent-mystique-preview` after the wireframe review. The wireframe is the minimum gate; live-app previews are optional enhancements.

## Ownership Boundary

This skill owns manifest JSON creation/editing and component tree composition.

| This skill OWNS | Other skills OWN |
|-----------------|------------------|
| Manifest JSON creation/editing | Manifest validation -> `/fluent-mystique-validate` |
| Page and route structure | SDK component code -> `/fluent-mystique-scaffold` |
| Component tree composition | Deployed manifest analysis -> `/fluent-mystique-analyze` |
| GraphQL query scaffolding in pages | Live GraphQL execution -> MCP tools |
| i18n key generation in manifest | Settings management -> `/fluent-settings` |
| Fragment composition | Workflow actions -> `/fluent-workflow-builder` |

## Pre-flight: Plugin Availability

Before building a manifest that references custom SDK components (non-`fc.*` aliases), verify the plugin bundle is accessible:

1. **Identify custom components** in the plan — any component alias that does NOT start with `fc.`
2. **For each custom component**, check if an SDK project exists in `accounts/<PROFILE>/SOURCE/frontend/*/src/index.tsx` with matching `ComponentRegistry.register` calls
3. **If ALL custom components found locally:** Ensure the `plugins[]` array references the correct plugin folder URL (localhost for dev, CDN for production, trailing `/` preferred for `version.json` auto-discovery)
4. **If ANY custom component is MISSING:** **STOP.** Inform the user: *"Custom component `<alias>` is not registered in any local SDK project. Scaffold it first via `/fluent-mystique-scaffold`, then resume manifest building."*
5. **Exception:** If the manifest is being built as part of a feature plan where scaffolding comes later, note the dependency and proceed — but add a `DEPLOYMENT_ORDER_WARNING` comment

## Frontend Development and Deployment Model

Understanding how manifests and custom components reach users is critical for correct builder output.

### Manifests are JSON Configuration

Manifests are pure JSON — they are NOT compiled or bundled. They are deployed to Fluent UX Apps through one of:
- **OMS App** — upload via the UX App configuration UI
- **Fluent API** — create/update as a Fluent Setting (key convention: `fc.mystique.manifest.<app-name>`)
- **Fragment references** — deploy manifest fragments as separate Settings, referenced via `type: "reference"` routes

### Custom Components are Externally Hosted Bundles

Custom SDK components (anything not `fc.*`) are React code compiled to a bundle. Fluent does NOT host these bundles. One SDK project can register one or many components, and all registrations ship in the same bundle. The deployment model is:

1. **Development:** `yarn start` serves from `http://localhost:3001/` (host reads `/version.json` then loads hashed bundle)
2. **Production:** `yarn build` produces `dist/bundle.[hash].js` + `dist/version.json` — deploy the full `dist/` folder to CDN (Vercel, S3, CloudFront, etc.)
3. **Manifest references the plugin folder URL** via `plugins[]` (trailing `/` recommended)

### Plugin Configuration

**CRITICAL: `plugins[]` belongs ONLY in the ROOT manifest (`fc.mystique.manifest.<app>`), NEVER in fragment settings.** Fragments contain ONLY `routes[]`. To load a custom JS bundle, add it to the root manifest's `plugins[]` — all fragments inherit the loaded bundles automatically.

The `plugins[]` array in the root manifest tells Mystique where to load external component bundles:

```json
{
  "plugins": [
    {
      "type": "url",
      "src": "http://localhost:3001/"
    }
  ]
}
```

For production deployment, replace with the CDN URL:

```json
{
  "plugins": [
    {
      "type": "url",
      "src": "https://my-components.vercel.app/"
    }
  ]
}
```

Plugins can also be loaded from a Fluent Setting:

```json
{
  "plugins": [
    {
      "type": "setting",
      "setting": "fc.mystique.plugin.custom-components"
    }
  ]
}
```

### Fragment Deployment

Manifest fragments are deployed as Fluent Settings with the naming convention:

```
fc.mystique.manifest.<app>.fragment.<name>
```

For example: `fc.mystique.manifest.oms.fragment.inventory`, `fc.mystique.manifest.store.fragment.picking`.

**Fragments contain ONLY `routes[]` — NEVER `plugins[]`.** Custom JS bundles are loaded via the root manifest's `plugins[]`. Fragments inherit all loaded bundles. If you need a custom component (FieldRegistry, ComponentRegistry) to work on a fragment's pages, add the bundle URL to the **root manifest** (`fc.mystique.manifest.<app>`), not the fragment.

The main manifest references them via:

```json
{
  "type": "reference",
  "settingName": "fc.mystique.manifest.oms.fragment.inventory"
}
```

This allows sections to be independently deployed and versioned without editing the main manifest.

### Baseline Architecture (Override-Only Model)

The OMS App ships with **baseline manifests hosted on the platform**. You do NOT need to create any manifest settings to use the default UI — just navigate to:

```
https://<ACCOUNT_ID>.<ENVIRONMENT>.apps.fluentcommerce.com/oms/
```

The UX Framework checks for ACCOUNT-level settings first. If no `fc.mystique.manifest.*` settings exist, the app loads platform-hosted defaults automatically.

**Platform manifest CDN URL pattern:**

```
https://fluent.apps.fluentcommerce.com/_plugins/{pluginName}/manifests/{settingName}.json
```

Examples:
- Root: `https://fluent.apps.fluentcommerce.com/_plugins/oms/manifests/fc.mystique.manifest.oms.json`
- Fragment: `https://fluent.apps.fluentcommerce.com/_plugins/oms/manifests/fc.mystique.manifest.oms.fragment.ordermanagement.json`

Use these URLs to download the current platform baseline before customising — compare your override against the baseline to understand what you're changing. This is the canonical source of truth for default manifests.

**ONLY OVERRIDE WHAT YOU NEED.** This is the single most important principle:
- Override only the fragment(s) you need to customize
- Unmodified fragments continue to inherit platform bug fixes and feature enhancements automatically
- Once you override a fragment, that fragment no longer receives platform updates — you must merge changes manually
- To track what's changed, check released capabilities to see if baseline updates enhance your experience beyond your custom component

### Standard OMS Fragment Catalogue

| Fragment | Setting Name | Content |
|----------|-------------|---------|
| Root manifest | `fc.mystique.manifest.oms` | App shell, fragment references, plugins (oms, returns) |
| Dashboard | `fc.mystique.manifest.oms.fragment.dashboard` | Home page, KPI widgets |
| Order Management | `fc.mystique.manifest.oms.fragment.ordermanagement` | Order list, detail, fulfilment views |
| Availability | `fc.mystique.manifest.oms.fragment.availability` | Product availability management |
| Products | `fc.mystique.manifest.oms.fragment.products` | Product catalogue browsing |
| Unified Inventory | `fc.mystique.manifest.oms.fragment.inventory.unifiedview` | Cross-location inventory view |
| Global Inventory | `fc.mystique.manifest.oms.fragment.globalinventory` | Inventory positions and management |
| Stores | `fc.mystique.manifest.oms.fragment.stores` | Location/store management |
| Insights | `fc.mystique.manifest.oms.fragment.insights` | Analytics and reporting |
| Admin | `fc.mystique.manifest.oms.fragment.admin` | Users, roles, retailers management |

To download any baseline fragment: `https://fluent.apps.fluentcommerce.com/_plugins/oms/manifests/{settingName}.json`

### Fragment Splitting Strategy (8+ Fragments)

When customisation grows beyond 2-3 fragments, adopt domain-based fragment organisation:

**When to split into a new fragment:**
- New domain/feature area not covered by existing fragments (e.g., custom returns, appeasements)
- Existing fragment exceeds ~500 lines of JSON — split by sub-domain
- Multiple teams editing the same fragment — split to reduce merge conflicts
- Feature can be independently enabled/disabled per environment

**Naming convention:** `fc.mystique.manifest.<app>.fragment.<domain>` where `<domain>` is a single lowercase word describing the business area. Examples: `ordermanagement`, `dashboard`, `stores`, `returns`, `appeasements`, `sourcing`.

**Fragment dependency rules:**
- Fragments are loaded in the order listed in the root manifest's `fragments` array
- Later fragments can override routes/pages from earlier fragments (last-write-wins for same path)
- Queries and plugins declared in a fragment are scoped to that fragment's pages
- Shared plugins (SDK bundles used across fragments) should be declared in the root manifest

**Scaling checklist (8+ fragments):**
1. One fragment per business domain — don't cross domain boundaries
2. Each fragment owns its routes, pages, and queries — no cross-fragment page references
3. Shared components go in the root manifest's `plugins` section
4. Test fragment load order — reorder in root manifest to verify no accidental overrides
5. Document which team owns each fragment in a comment or external registry
6. Version control fragment JSONs independently — deploy only changed fragments

**Reference:** Store app uses 8+ fragments (fulfilment, picking, packing, dispatch, returns, waves, inventory, admin) each mapping to a distinct store workflow domain.

### Admin Console Migration

If migrating from the legacy Admin Console (`ADMIN_CONSOLE_MANIFEST`):
1. Identify custom elements in your `ADMIN_CONSOLE_MANIFEST` that are missing in the default OMS App
2. Migrate those custom elements to the corresponding OMS fragment settings
3. Configure any mutation actions (user actions) not present in the OMS default — see workflow `userActions` configuration
4. Test workflow user actions with the new manifest
5. Both the OMS App and Admin Console can run in parallel in separate browser tabs using the same credentials
6. The Workflow Builder remains in the Admin Console only

### Manifest Deployment via Settings API

Manifests are stored as **Fluent Settings**. To deploy a manifest or fragment, you create or update its corresponding setting.

**Storage model — setting names are app-scoped:**

| App | Root Manifest Setting | Example Fragment Setting |
|-----|----------------------|------------------------|
| OMS | `fc.mystique.manifest.oms` | `fc.mystique.manifest.oms.fragment.dashboard` |
| Store App | `fc.mystique.manifest.store` | `fc.mystique.manifest.store.fragment.picking` |
| Custom App | `fc.mystique.manifest.<app>` | `fc.mystique.manifest.<app>.fragment.<name>` |

**Manifest setting values (non-negotiable):** `valueType: "JSON"`, `context: "ACCOUNT"`, `contextId: 0`, body in `lobValue` (raw JSON object, NEVER `value` field). See **Manifest Setting Rules** below.

Each app serves at its own URL path: `/_plugins/<app>/manifests/...`

**CRITICAL: Always use `lobValue`, not `value`.** Manifests exceed the regular `value` field's size limit. Using `value` will **silently truncate** the JSON — this causes data loss with no error.

**Fetch an existing manifest or fragment:**

**Preferred: Use `setting.get` with `outputFile`** to save the manifest to a local file without loading large JSON into LLM context (manifests are 10-120KB). This avoids MCP response budget truncation:

```
setting.get({
  name: "fc.mystique.manifest.oms",
  outputFile: "accounts/<PROFILE>/manifests/fc.mystique.manifest.oms.json"
})
```

Then read the local file when you need to inspect or modify it. For fragments, change the `name` parameter:

```
setting.get({
  name: "fc.mystique.manifest.oms.fragment.dashboard",
  outputFile: "accounts/<PROFILE>/manifests/fc.mystique.manifest.oms.fragment.dashboard.json"
})
```

**Fallback: GraphQL query** (when `setting.get` is unavailable or you need the setting ID for update mutations):

```graphql
query Manifest {
  settings(name: "fc.mystique.manifest.oms") {
    edges { node { id name lobValue } }
  }
}
```

Note: GraphQL via MCP `graphql.query` is subject to `FLUENT_RESPONSE_BUDGET_CHARS` (default 50K) and will silently truncate large manifests. Prefer `setting.get` with `outputFile` or use the official CLI MCP `graphql.execute` (no budget limit) for large payloads.

If a fragment has no ACCOUNT-level override, the platform loads the **global default** via HTTP:
```
https://<host>/_plugins/<app>/manifests/fc.mystique.manifest.<app>.fragment.<name>.json?v=<cachebuster>
```

**Create a new fragment override** (first-time deployment):

```graphql
mutation CreateFragment {
  createSetting(input: {
    name: "fc.mystique.manifest.oms.fragment.dashboard"
    context: "ACCOUNT"
    contextId: 0
    valueType: "JSON"
    lobValue: { "manifestVersion": "2.0", "routes": [...] }
  }) { id }
}
```

**Update an existing manifest** (requires setting ID — fetch first if unknown):

```graphql
mutation UpdateManifest {
  updateSetting(input: {
    id: 242
    lobValue: { "manifestVersion": "2.0", "routes": [...] }
  }) { id }
}
```

`lobValue` must be **raw JSON** (an object), not a stringified JSON string.

**CRITICAL — lobValue double-encoding trap:** When constructing GraphQL mutation variables or script payloads, `lobValue` MUST be the parsed JSON object reference, NEVER `JSON.stringify(fragment)`. If you stringify lobValue before the outer serialization, it gets double-encoded as a string value instead of a JSON object. Mystique will reject the fragment with "Manifest contains an invalid route" and the sidebar section will disappear silently. Correct: `lobValue: fragmentObject`. Wrong: `lobValue: JSON.stringify(fragmentObject)`.

### Manifest Setting Rules (ENFORCED — no exceptions)

| Field | Value | Notes |
|-------|-------|-------|
| `valueType` | `"JSON"` | ALWAYS JSON. Never LOB, never STRING. |
| `context` | `"ACCOUNT"` | ALWAYS ACCOUNT. Never RETAILER. |
| `contextId` | `0` | ALWAYS 0. Never a retailer ID. |
| `lobValue` | `{...}` raw JSON object | The manifest body. Raw object, NOT `JSON.stringify()`'d. Double-encoding causes silent UI failures. |
| `value` | **NEVER populate** | Silently truncates large JSON (255 char limit) — data loss with no error. |

These apply to ALL manifest settings — root manifests AND fragments — across ALL deployment methods.

**Manifest settings require `valueType: "JSON"`** — if using MCP `setting.upsert`, pass `valueType: "JSON"` explicitly. Without it, defaults to LOB which Mystique cannot parse. Preferred paths: CLI `fluent module install --include settings` or GraphQL `createSetting`/`updateSetting` mutations with explicit `valueType: "JSON"`.

If you encounter existing settings with `valueType: "LOB"` or `context: "RETAILER"`, those are legacy anomalies. New and updated manifests MUST use `JSON`/`ACCOUNT`/`0`. Do NOT replicate legacy values.

### Safety Guardrails (ALL steps are HARD GATES -- skipping ANY is a BLOCKING violation)

**NEVER create or update a manifest setting without completing ALL of these steps in order:**

**GATE 1 — Backup existing manifest BEFORE any modification:**

Use `setting.get` with `outputFile` to save the manifest to disk without loading it into LLM context (manifests are 10-120KB):

```
setting.get({
  name: "fc.mystique.manifest.oms.fragment.<name>",
  outputFile: "accounts/<PROFILE>/manifests/backups/<setting-name>_<YYYY-MM-DD_HHmmss>.json"
})
```

This returns only metadata (name, size, valueType) — the full content is written to disk. Then read the local file when you need to inspect or modify it.

- **If new fragment (does not exist yet):** Note "NEW SETTING — no backup needed" in the approval request so user knows there is no rollback path.
- **If backup query fails for a setting that should exist:** STOP. Investigate before making changes.

**GATE 2 — Validate with `/fluent-mystique-validate`:**

```
/fluent-mystique-validate → PASS (0 CRITICAL, 0 HIGH) → proceed to Step 3
                          → FAIL → fix issues → re-validate → DO NOT proceed until PASS
```

The `/fluent-mystique-validate` skill checks 49 rules across 6 phases. A manifest with CRITICAL or HIGH issues MUST NOT be deployed.

**GATE 2B — GraphQL query gate (MANDATORY when route query changes):**

Before approval/deploy, validate the exact query used by changed route(s):

1. **Schema check (MANDATORY GATE)** — validate fields/arguments against schema via `graphql.validate`. Check the `valid` field in the response (not `ok`). If `valid: false` → **STOP. Do NOT embed the query in the manifest.** Fix the query using the `errors[]` array, then re-validate. Full protocol: `knowledge/platform/graphql-preflight.md`.
2. **Runtime check** — execute the exact route `data.query` with representative real variables (entity IDs/refs from target environment).
3. **Hard fail on errors** — any GraphQL `errors[]` (especially non-null field errors) blocks deployment.
4. **Default-value dependency check** — if overrides use `{{me.username}}` (or similar), the route query MUST include `me { username }`.

**GATE 2C — Large payload local-first transport (MANDATORY for big manifests):**

For large fragment payloads (practically anything non-trivial), do NOT patch by inlining huge JSON bodies in ad-hoc tool calls.

1. Export the current setting JSON locally first (via GraphQL settings query or CLI settings export workflow).
2. Edit/patch local JSON file only.
3. Deploy from file using `fluent module install --include settings --force` (preferred) or GraphQL with file-backed body.
4. Keep backup + patched artifacts on disk for audit and rollback.

**Step 3 — Ask user for explicit approval BEFORE deploying:**

Present to the user clearly:
- **Setting name** being created or updated
- **Action**: CREATE (new) or UPDATE (existing, show setting ID)
- **What changed**: Summary of modifications (routes added/removed, components changed, etc.)
- **Backup location**: Path to the backup file from Step 1 (or "NEW — no prior version")
- **Rollback**: "To restore the previous version, re-deploy the backup file at `<path>`"

**DO NOT deploy until user explicitly says "yes", "approve", "go ahead", or equivalent.** Questions about the change ("what does that route do?") do NOT count as approval — answer and keep waiting. Silence does NOT count. "Looks good" or "ok" DO count.

**Step 4 — Deploy (choose method based on payload size):**

**Size-based routing (MANDATORY):**

| Setting size | Method | Why |
|---|---|---|
| **>4KB** (manifest fragments, large configs) | **Method A — CLI** | Reads from local file — zero LLM context cost. Manifest fragments are 10-120KB. |
| **<4KB** (root manifests, feature flags) | **Method B — MCP/GraphQL** | Inline is fine for small payloads. |
| **Multiple settings** | **Method A — CLI** | One deploy, all settings in one `manifests.json` array. |

**NEVER use MCP `setting.upsert` for payloads >4KB** — stuffing 10-120KB into a tool parameter wastes LLM context tokens. Always use CLI `fluent module install` for large settings.

**Method A — CLI Module Install (RECOMMENDED for large settings and production):**

Package manifest JSON into a module's `assets/settings/` directory and deploy via CLI. This gives version control, config token substitution, and repeatable CI/CD deployments.

**Settings file format** (`assets/settings/manifests.json`):

```json
[
    {
        "name": "fc.mystique.manifest.oms.fragment.ordermanagement",
        "valueType": "JSON",
        "lobValue": { "manifestVersion": "2.0", "routes": [] },
        "context": "ACCOUNT",
        "contextId": 0
    }
]
```

ALL manifest settings use `valueType: "JSON"`, `context: "ACCOUNT"`, `contextId: 0` — root manifests and fragments alike.

**Module directory structure:**

```
my-module/
├── module.json              # { "_schema": "1.0.0", "name": "...", "version": "1.0.0" }
├── module.config.json       # {} or token values
└── assets/
    └── settings/
        └── manifests.json   # Array of manifest settings
```

Deploy:
```bash
fluent module install <module-dir> -p <PROFILE> -r <RETAILER> --include settings --force
```

Key points:
- `valueType` MUST be `"JSON"` for ALL manifest settings — no exceptions
- ALL manifest settings: `context: "ACCOUNT"`, `contextId: 0`
- `lobValue` is a raw JSON object — not a stringified string
- Config tokens (`[[variable]]`) in the JSON are resolved during `module install -c <config>`
- Multiple manifest fragments can be in the same settings file
- Use `--include settings` to deploy only settings (skip workflows, rules, etc.)
- Use `--force` to bypass version check when iterating

**Method B — GraphQL Direct (interactive edits, single fragments):**

1. Backup existing (Step 1 above — MANDATORY)
2. Make changes to the local copy (user reviews diff)
3. Validate with `/fluent-mystique-validate` (Step 2 above — MANDATORY)
4. Get user approval (Step 3 above — MANDATORY)
5. Upload via GraphQL `createSetting`/`updateSetting` mutation with `valueType: "JSON"`, `context: "ACCOUNT"`, `contextId: 0`, `lobValue` as raw JSON object (NEVER stringified). If using MCP `setting.upsert`, pass `valueType: "JSON"` explicitly.
6. Verify in browser — Settings changes are immediate, no cache delay

**Step 5 — Verify in browser:**

After deployment, verify the manifest renders correctly via `/fluent-ui-test` or by asking the user to check in the browser.

### Rollback Procedure

If the deployed manifest causes issues:

1. Read the backup file from `accounts/<PROFILE>/manifests/backups/<setting-name>_<timestamp>.json`
2. Extract the `lobValue` from the backup
3. Deploy the backup content using the same deployment method (Method A or B)
4. Verify in browser that the previous version is restored

**Backups are never auto-deleted.** They accumulate in the backups directory for audit trail purposes.

### Plugin File Precedence (CRITICAL)

The OMS app loads its main manifest from the plugin static file at `/_plugins/oms/manifests/fc.mystique.manifest.oms.json`, NOT from the `fc.mystique.manifest.oms` setting. The setting is IGNORED when the plugin file exists. The plugin file defines which fragment references to load. Each fragment resolves from either plugin static files (`/_plugins/{plugin}/manifests/{settingName}.json` for OOTB: dashboard, availability, insights) or Settings API via GraphQL (for custom/modified: ordermanagement, products, stores, admin). To add custom routes, add them to an existing fragment loaded from settings. You CANNOT add new fragment references to the main manifest setting.

### Manifest Fragment Deployment Protocol

**Pre-Deployment:** Present user with which setting will be modified, what sections added, and existing content affected. Show diff of existing vs proposed routes. NEVER modify without explicit approval.

**Backup:** Before ANY modification to existing fragment, download current value via MCP `setting.get` (with `outputFile`) or GraphQL settings query, save to `accounts/<PROFILE>/manifests/backups/<setting-name>_<YYYY-MM-DD_HHmmss>.json` with full metadata (id, name, context, contextId, valueType).

**Setting rules:** ALL manifest settings use `valueType: "JSON"`, `context: "ACCOUNT"`, `contextId: 0`, body in `lobValue` (NEVER `value`). Deploy via `fluent module install --include settings --force` (recommended), GraphQL `createSetting`/`updateSetting` mutations, or MCP `setting.upsert` with `valueType: "JSON"`. `lobValue` MUST be a raw JSON object, not `JSON.stringify()`'d.

**Steps:** (1) identify target fragment loaded from settings not plugin static files, (2) backup current value, (3) download full content via `setting.get` with `outputFile` to save to `accounts/<PROFILE>/manifests/<setting-name>.json`, (4) read the local file and add sections to routes array, (5) get user approval, (6) create module directory with `assets/settings/`, (7) deploy, (8) verify in browser, (9) rollback from backup if needed.

## When to Use

- Creating a new Mystique app manifest from scratch
- Adding pages or sections to an existing manifest
- Composing component trees for entity detail or list pages
- Building manifest fragments for modular deployment
- Generating page-level GraphQL data queries for Fluent entities
- Editing existing manifest JSON (add/remove/update routes, components, props)
- Wiring custom SDK components into manifests via `plugins[]`

## Progress

Emit this block at each phase transition to show progress:

```
> /fluent-mystique-builder [1/6]
  Done Plan verification       -> Collect manifest requirements
  Todo Build route structure    Todo Compose component trees
  Todo Generate data queries    Todo Validate output
```

Update the block as each phase completes — mark completed phases with `Done`, the active phase with `->`, and remaining phases with `Todo`. Replace `[1/6]` with the current phase number.

## Manifest v2.0 Schema

Every Mystique manifest follows this top-level structure:

```json
{
  "manifestVersion": "2.0",
  "name": "admin",
  "title": "i18n:fc.admin.title",
  "icon": "settings",
  "context": {
    "level": "retailer",
    "role": "ADMIN"
  },
  "plugins": [
    { "type": "url", "src": "https://cdn.example.com/my-plugin/" }
  ],
  "homePath": "dashboard",
  "routes": []
}
```

### Required Fields

| Field | Type | Description |
|-------|------|-------------|
| `manifestVersion` | `"2.0"` | Always `"2.0"` — the only supported schema version |
| `name` | `string` | App identifier used in logs and user action filtering. No spaces. |
| `title` | `string` | User-facing app name. Supports `i18n:` prefix for translation |
| `homePath` | `string` | Landing route path. Must match a defined route's `path` |
| `routes` | `MystiqueManifestRoute[]` | Array of sections, pages, and fragment references |

### Optional Fields

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `icon` | `string` | none | Material icon name for the app |
| `context` | `{ level, role? }` | `{ level: "retailer" }` | App context: `account`, `retailer`, or `location`. Optional `role` restricts access |
| `plugins` | `MystiquePlugin[]` | `[]` | External component bundle URLs or setting references |
| `orchestrationAlias` | `string` | none | Overrides the `module` value the app sends to the Transition API. OMS App uses `"adminconsole"` so `modules: ["adminconsole"]` in workflow user actions. Store App uses `"servicepoint"` as its orchestration alias (legacy alias — distinct from the manifest setting name `fc.mystique.manifest.store`). |

### Route Types

Routes come in three types:

**Section** — groups pages under a navigation header:

```json
{
  "type": "section",
  "path": "orders",
  "nav": { "label": "i18n:fc.orders.nav", "icon": "shopping_cart" },
  "roles": ["ADMIN", "OPERATOR"],
  "pages": []
}
```

**Page** — a routable screen with data and components:

```json
{
  "type": "page",
  "path": "orders",
  "component": "fc.page",
  "nav": { "label": "i18n:fc.orders.list.nav", "icon": "list" },
  "data": {
    "query": "query { orders(first: 25) { edges { cursor node { id ref status type createdOn } } pageInfo { hasNextPage } } }"
  },
  "props": { "title": "i18n:fc.orders.list.title" },
  "descendants": [],
  "roles": ["ADMIN"],
  "fullScreen": false
}
```

**Reference** — includes a manifest fragment stored as a Fluent Setting:

```json
{
  "type": "reference",
  "settingName": "fc.mystique.manifest.custom-inventory"
}
```

### Component Instance

The recursive building block used throughout `descendants[]`:

```json
{
  "component": "fc.card.attribute",
  "dataSource": "orderById",
  "props": {
    "title": "Order Details",
    "width": "half",
    "attributes": [
      { "label": "Reference", "value": "{{ref}}" },
      { "label": "Status", "value": "{{status}}" },
      { "label": "Type", "value": "{{type}}" },
      { "label": "Created", "value": "{{dateRelative createdOn}}" }
    ]
  },
  "descendants": [],
  "roles": ["ADMIN"]
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `component` | `string` | yes | Registry alias — `fc.*` for core, `custom.*` for SDK components |
| `dataSource` | `string` | no | JSONPath scoping the data context for this component and its descendants |
| `props` | `Record<string, any>` | no | Component-specific configuration (varies by component) |
| `descendants` | `MystiqueComponentInstance[]` | no | Nested child components |
| `roles` | `string[]` | no | Role-based visibility — component hidden if user lacks listed roles |

### Page Data Query

The `data` block on a page defines the GraphQL query executed before rendering:

```json
{
  "data": {
    "query": "query($id: ID!) { orderById(id: $id) { id ref status type createdOn totalPrice customer { firstName lastName } items { edges { node { id ref quantity productRef } } } } }",
    "variables": {
      "id": "{{id}}"
    }
  }
}
```

Variables support template strings (`{{id}}` resolves from URL params) and `ConvertableVariable` for non-string types:

```json
{
  "variables": {
    "id": { "value": "{{id}}", "parse": true }
  }
}
```

### Multiple Data Sources

For pages needing multiple independent queries, use the plural `dataSources` key instead of `data`:

```json
{
  "path": "/dashboard",
  "component": "fc.page",
  "dataSources": {
    "recentOrders": {
      "query": "query($retailerId:[Int!]) { orders(retailerId:$retailerId, first:10) { edges { node { id ref status } } } }",
      "variables": { "retailerId": "{{activeContext.id}}" }
    },
    "pendingFulfilments": {
      "query": "query($retailerId:[Int!]) { fulfilments(retailerId:$retailerId, status:[\"CREATED\"]) { edges { node { id ref status } } } }",
      "variables": { "retailerId": "{{activeContext.id}}" }
    }
  },
  "descendants": [
    {
      "component": "fc.list",
      "dataSource": "recentOrders",
      "props": { "attributes": [{ "label": "Ref", "value": "{{node.ref}}" }] }
    },
    {
      "component": "fc.list",
      "dataSource": "pendingFulfilments",
      "props": { "attributes": [{ "label": "Ref", "value": "{{node.ref}}" }] }
    }
  ]
}
```

Each key in `dataSources` becomes a named data source that descendants reference via their `dataSource` prop. Queries execute independently and in parallel.

### Manifest Fragment

Fragments are cut-down manifests for modular deployment:

```json
{
  "manifestVersion": "2.0",
  "routes": [
    {
      "type": "section",
      "nav": { "label": "Custom Section", "icon": "extension" },
      "pages": [
        {
          "type": "page",
          "path": "custom-page",
          "component": "fc.page",
          "nav": { "label": "Custom Page", "icon": "build" },
          "props": { "title": "Custom Page" },
          "descendants": []
        }
      ]
    }
  ]
}
```

## Operations

### Operation 1: Create Manifest

Interactive wizard collecting:

```
1. App name (slug: "admin", "servicepoint", "store-ops")
2. Title (user-facing, supports i18n: prefix)
3. Context level (account / retailer / location)
4. Role restriction (optional)
5. Home page path
6. Initial sections and pages (from wireframe/plan)
7. Plugin references (if custom components needed)
```

Generates a complete `manifest.json` with:
- `manifestVersion: "2.0"` and all required top-level fields
- Route structure with sections and pages
- Page-level GraphQL data queries for each entity page
- Component trees with props wired to data sources
- Plugin references for custom SDK components

### Operation 2: Add Page

```
Input: manifest file + page definition
1. Determine target section (existing or new)
2. Define page path (validate uniqueness against existing routes)
3. Select page component (fc.page recommended as top-level)
4. Define data query (entity type -> auto-generate GraphQL using entity map)
5. Build component tree:
   - Start with fc.page as the route component
   - Add layout components (fc.column, fc.tabs.card)
   - Add content components (fc.list, fc.card.attribute, fc.provider.graphql)
   - Wire dataSource paths to connect components to query results
   - Set component props per component documentation
6. Add nav config if page should appear in sidebar
7. Add role restrictions (optional)
```

### Operation 3: Add Component to Page

```
Input: manifest file + page path + component spec
1. Load manifest and locate the target page by path
2. Find insertion point in descendants tree (by index or parent component)
3. Generate component instance:
   - Select component alias from known registry
   - Set props from component schema (see Component Props Reference below)
   - Wire dataSource if the component consumes query data
   - Add descendants recursively for container components
4. Validate resulting tree structure
```

### Operation 4: Build Fragment

```
Input: fragment definition (sections and pages)
1. Generate MystiqueManifestFragment JSON structure
2. Define fragment routes (sections + pages with full component trees)
3. Output as standalone JSON file
4. Generate Fluent setting key: fc.mystique.manifest.<fragment-name>
5. Deploy via `fluent module install --include settings --force`
```

### Operation 5: Generate Data Query

```
Input: entity type + required fields + query type (list or detail)
1. Map entity type to GraphQL root query using Entity Type Query Map
2. Build query with requested fields plus standard fields
3. Handle connections/edges/node patterns for nested entities
4. Add variables for dynamic filtering (pagination, URL params)
5. Output as a page.data block ready to embed in the manifest
```

### Operation 6: Edit Existing Manifest

```
Input: manifest file + edit instructions
1. Load and parse the manifest JSON
2. Locate target element (route by path, component by tree position)
3. Apply modifications (add/remove/update routes, components, props)
4. Preserve existing structure — minimal diff approach
5. Validate modified manifest against v2.0 schema
6. Write back to the same file path
```

## Component Composition Patterns

### Pattern 1: Entity Detail Page

The standard layout for viewing a single entity with attributes and related data:

```json
{
  "type": "page",
  "path": "orders/:id",
  "component": "fc.page",
  "data": {
    "query": "query($id: ID!) { orderById(id: $id) { id ref status type totalPrice totalTaxPrice createdOn updatedOn customer { id firstName lastName primaryEmail } items { edges { node { id ref productRef quantity paidPrice currency } } } fulfilments { edges { node { id ref status type fromAddress { name } } } } attributes { name value type } } }",
    "variables": { "id": { "value": "{{id}}", "parse": true } }
  },
  "props": {
    "title": "{{capitalise status}} - Order {{ref}}",
    "backButtons": [{ "label": "Orders", "path": "/orders" }],
    "actions": true
  },
  "descendants": [
    {
      "component": "fc.card.attribute",
      "props": {
        "title": "Order Summary",
        "width": "half",
        "attributes": [
          { "label": "Reference", "value": "{{ref}}" },
          { "label": "Status", "value": "{{status}}", "options": { "styles": [
            { "matches": "COMPLETE", "icon": { "name": "MdCheckCircle", "colour": "success" } },
            { "matches": "CANCELLED", "icon": { "name": "MdCancel", "colour": "error" } }
          ]}},
          { "label": "Type", "value": "{{type}}" },
          { "label": "Created", "value": "{{dateRelative createdOn}}" },
          { "label": "Total", "value": "${{totalPrice}}" }
        ]
      }
    },
    {
      "component": "fc.card.attribute",
      "props": {
        "title": "Customer",
        "width": "half",
        "attributes": [
          { "label": "Name", "value": "{{customer.firstName}} {{customer.lastName}}" },
          { "label": "Email", "value": "{{customer.primaryEmail}}" }
        ]
      }
    },
    {
      "component": "fc.tabs.card",
      "props": {
        "tabs": [
          { "label": "Items" },
          { "label": "Fulfilments" },
          { "label": "Attributes" }
        ]
      },
      "descendants": [
        {
          "component": "fc.list",
          "dataSource": "items",
          "props": {
            "defaultPageSize": 25,
            "attributes": [
              { "label": "Product", "value": "{{node.productRef}}" },
              { "label": "Quantity", "value": "{{node.quantity}}" },
              { "label": "Price", "value": "${{node.paidPrice}}", "align": "right" }
            ]
          }
        },
        {
          "component": "fc.list",
          "dataSource": "fulfilments",
          "props": {
            "defaultPageSize": 10,
            "rowLink": "/fulfilments/{{node.id}}",
            "attributes": [
              { "label": "Reference", "value": "{{node.ref}}" },
              { "label": "Status", "value": "{{node.status}}" },
              { "label": "Type", "value": "{{node.type}}" }
            ]
          }
        },
        {
          "component": "fc.list",
          "dataSource": "attributes",
          "props": {
            "defaultPageSize": 25,
            "attributes": [
              { "label": "Name", "value": "{{node.name}}" },
              { "label": "Value", "value": "{{node.value}}" },
              { "label": "Type", "value": "{{node.type}}" }
            ]
          }
        }
      ]
    }
  ]
}
```

### Pattern 2: Entity List Page

The standard paginated list page with filtering:

```json
{
  "type": "page",
  "path": "orders",
  "component": "fc.page",
  "nav": { "label": "i18n:fc.orders.list.nav", "icon": "shopping_cart" },
  "data": {
    "query": "query($retailerId: [Int!], $orders_first: Int, $status: [String], $type: [String], $createdOn: DateRange) { orders(retailerId: $retailerId, first: $orders_first, status: $status, type: $type, createdOn: $createdOn) { edges { node { id ref status type totalPrice createdOn customer { firstName lastName } } } pageInfo { hasNextPage } } }",
    "variables": {
      "orders_first": 25,
      "retailerId": "{{activeRetailer.id}}"
    }
  },
  "props": { "title": "i18n:fc.orders.list.title" },
  "descendants": [
    {
      "component": "fc.list",
      "dataSource": "orders",
      "props": {
        "defaultPageSize": 25,
        "rowLink": "/orders/{{node.id}}",
        "filter": { "enabled": true },
        "attributes": [
          { "label": "Reference", "value": "{{node.ref}}" },
          { "label": "Customer", "value": "{{node.customer.firstName}} {{node.customer.lastName}}" },
          { "label": "Status", "value": "{{node.status}}" },
          { "label": "Type", "value": "{{node.type}}" },
          { "label": "Total", "value": "${{node.totalPrice}}", "align": "right" },
          { "label": "Created", "value": "{{dateRelative node.createdOn}}", "hideBelow": "sm" }
        ]
      }
    }
  ]
}
```

### Pattern 3: Dashboard with Metrics

```json
{
  "type": "page",
  "path": "dashboard",
  "component": "fc.page",
  "data": {
    "query": "query($todayStart: DateTime!, $weekStart: DateTime!) { ordersToday: orders(createdOn: {from: $todayStart}) { edges { node { id } } } ordersWeek: orders(createdOn: {from: $weekStart}) { edges { node { id } } } escalations: fulfilments(status: \"ESCALATED\") { edges { node { id } } } }",
    "variables": {
      "todayStart": "{{dateStringFormatter (dateAdd day=-1) 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'}}",
      "weekStart": "{{dateStringFormatter (dateAdd day=-7) 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'}}"
    }
  },
  "props": { "title": "Overview Dashboard" },
  "descendants": [
    {
      "component": "fc.dashboard.threshold",
      "props": {
        "label": "Orders Today",
        "value": "{{ordersToday.edges.length}}",
        "thresholdLow": 100,
        "thresholdHigh": 500,
        "link": "#/orders"
      }
    },
    {
      "component": "fc.dashboard.threshold",
      "props": {
        "label": "Orders This Week",
        "value": "{{ordersWeek.edges.length}}",
        "thresholdLow": 500,
        "thresholdHigh": 2000,
        "link": "#/orders"
      }
    },
    {
      "component": "fc.dashboard.threshold",
      "props": {
        "label": "Escalations",
        "value": "{{escalations.edges.length}}",
        "thresholdLow": 5,
        "thresholdHigh": 20,
        "link": "#/fulfilments"
      }
    }
  ]
}
```

### Pattern 4: Conditional Content

Show different components based on entity data using `fc.conditional`:

```json
{
  "component": "fc.conditional",
  "props": {
    "value": "{{type}}",
    "matches": "HD"
  },
  "descendants": [
    {
      "component": "fc.card.attribute",
      "props": {
        "title": "Home Delivery Details",
        "attributes": [
          { "label": "Delivery Address", "value": "{{fulfilmentChoice.deliveryAddress.street}}" },
          { "label": "Postcode", "value": "{{fulfilmentChoice.deliveryAddress.postcode}}" }
        ]
      }
    }
  ]
}
```

Use multiple `fc.conditional` siblings for if/else-if patterns:

```json
[
  {
    "component": "fc.conditional",
    "props": { "value": "{{type}}", "matches": "HD" },
    "descendants": [{ "component": "fc.card.attribute", "props": { "title": "Home Delivery" } }]
  },
  {
    "component": "fc.conditional",
    "props": { "value": "{{type}}", "matches": "CC" },
    "descendants": [{ "component": "fc.card.attribute", "props": { "title": "Click & Collect" } }]
  }
]
```

### Pattern 5: Data Provider Nesting

Use `fc.provider.graphql` to fetch additional data within a page:

```json
{
  "component": "fc.provider.graphql",
  "props": {
    "query": "query($ref: [String]) { locations(ref: $ref) { edges { node { id ref name type status primaryAddress { street city state postcode country } } } } }",
    "variables": { "ref": "{{fromLocation.ref}}" },
    "path": "locations"
  },
  "descendants": [
    {
      "component": "fc.card.attribute",
      "dataSource": "locations",
      "props": {
        "title": "Fulfilment Location",
        "attributes": [
          { "label": "Name", "value": "{{node.name}}" },
          { "label": "Address", "value": "{{node.primaryAddress.street}}, {{node.primaryAddress.city}}" },
          { "label": "Status", "value": "{{node.status}}" }
        ]
      }
    }
  ]
}
```

The `path` prop enables auto-pagination for connections. The `loadingBehavior` prop controls rendering during fetch: `"wait"`, `"wait-with-animation"`, or `"pass-immediately"` (default).

### Pattern 6: Tab Card with Multiple Views

Use `fc.tabs.card` to organize related content into tabs. Descendants are matched to tabs by index:

```json
{
  "component": "fc.tabs.card",
  "props": {
    "tabs": [
      { "label": "i18n:fc.order.items.tab" },
      { "label": "i18n:fc.order.fulfilments.tab" },
      { "label": "i18n:fc.order.activity.tab" }
    ],
    "urlKey": "orderTab"
  },
  "descendants": [
    {
      "component": "fc.list",
      "dataSource": "items",
      "props": {
        "defaultPageSize": 25,
        "attributes": [
          { "label": "Product", "value": "{{node.productRef}}" },
          { "label": "Qty", "value": "{{node.quantity}}" }
        ]
      }
    },
    {
      "component": "fc.list",
      "dataSource": "fulfilments",
      "props": {
        "defaultPageSize": 10,
        "rowLink": "/fulfilments/{{node.id}}",
        "attributes": [
          { "label": "Ref", "value": "{{node.ref}}" },
          { "label": "Status", "value": "{{node.status}}" }
        ]
      }
    },
    {
      "component": "fc.list",
      "dataSource": "events",
      "props": {
        "defaultPageSize": 50,
        "attributes": [
          { "label": "Event", "value": "{{node.name}}" },
          { "label": "Time", "value": "{{dateRelative node.createdOn}}" }
        ]
      }
    }
  ]
}
```

### Pattern 7: Page Actions (User Actions, Mutations, Navigation)

#### How `"actions": true` Works (Transition API Chain)

When you set `"actions": true` on `fc.page`, the Mystique framework performs these steps at runtime:

1. **Extracts workflow info** — Reads `workflowRef` and `workflowVersion` from the page's GraphQL query result
2. **Builds Transition API request** — Calls `POST /api/v4.1/transition` with: `type` (entity type), `subtype`, `status`, `retailerId`, `flexType` (from workflowRef), `flexVersion`, and `module: "adminconsole"` (the OMS App's module alias, legacy alias)
3. **Renders buttons** — Iterates returned `userActions[]`, renders PRIMARY as main buttons, SECONDARY under "More Actions" dropdown

**Requirements for buttons to appear:**
- GraphQL query **MUST** include entity fields that expose workflow info (the framework extracts `workflowRef` automatically from the entity response)
- Workflow user action `context[].modules` must include `"adminconsole"` (the OMS App sends this value via its `orchestrationAlias`, NOT `"oms"`)
- Ruleset must have `subtype` matching the entity subtype
- For child entities (CREDIT_MEMO under BILLING_ACCOUNT), the entity's `workflowRef` points to the PARENT workflow, so the UI queries the parent workflow for buttons. However, the Transition API accepts BOTH the parent workflow name AND the standalone child workflow name as `flexType` (with matching `flexVersion`) — user actions should still be placed in the parent workflow for the UI to find them

**Console warning:** If you see "Error building user action lookups" in browser console, the Transition API call failed. Check: (1) entity has a valid workflowRef, (2) workflow exists with matching user actions, (3) `modules` includes `"adminconsole"` (the OMS App's module alias).

**CRITICAL — retailerId resolution for fc.page actions:**
- fc.page reads `retailerId` from the query `variables` object to call the Transition API — NOT from URL route params directly
- For Order pages: `$retailerId` is declared in the query, used in a subquery (e.g., `returnOrders(order: {retailer: {id: $retailerId}})`), and mapped from `{{params.retailerId}}`
- For child entities (CreditMemo, Fulfilment): entity has no `retailer` edge at root level, so `$retailerId` cannot be declared/used in the query → fc.page cannot resolve it → user action buttons will NOT appear
- **Workaround**: Move approval actions to the parent entity page, build a custom Mystique SDK component, or use event-based approval (MCP/API)

#### Explicit Action Configuration

Configure page-level action buttons via the `actions` prop on `fc.page`:

```json
{
  "props": {
    "title": "Order {{ref}}",
    "actions": {
      "primary": [
        {
          "type": "userAction",
          "name": "ConfirmShipment",
          "condition": "{{eq status 'PICK_PACK'}}"
        },
        {
          "type": "navigate",
          "label": "View Fulfilments",
          "link": "/orders/{{id}}/fulfilments"
        }
      ],
      "secondary": [
        {
          "type": "mutation",
          "name": "updateOrder",
          "label": "Edit Order",
          "args": { "id": "{{id}}" },
          "filter": { "type": "include", "names": ["status", "attributes"] },
          "extensions": {
            "postSubmit": { "type": "navigate", "link": "/orders/{{id}}" }
          }
        },
        {
          "type": "component",
          "component": "custom.order-export-button",
          "props": { "orderId": "{{id}}" }
        }
      ],
      "userActionFilter": { "type": "exclude", "names": ["CancelOrder"] }
    }
  }
}
```

Action types:
- `userAction` — triggers a workflow user action. Auto-appended from workflow but can be reordered or conditionally shown.
- `mutation` — executes a GraphQL mutation with auto-generated form. Use `overrides` to customize field labels, sources, and components.
- `navigate` — navigates to another page. `link` supports template strings.
- `component` — renders a custom SDK component in the action bar.

### Custom Component Override for User Action Fields

The event payload (`POST /event/sync`) is the contract boundary between UI and backend. Two patterns:

**Pattern A — Field-level override (recommended):** Change workflow attribute `type` to a custom FieldRegistry key. Standard form still renders all other attributes. Zero backend impact.

```json
{ "name": "amount", "type": "company.appeasement.amountCalculator", "label": "Amount", "mandatory": true }
```

**Pattern B — Full-form override:** Add `component` to the action config to replace the entire form. Your component must dispatch ALL `@EventAttribute` names rules expect.

```json
{ "type": "userAction", "name": "RequestAppeasement", "component": "my-custom.AppeasementForm" }
```

**Backend impact: zero for both** — rules see only `event.getAttributes()`, agnostic to UI. See `knowledge/platform/user-action-contract.md` §9.

### User Action Form Customization Props (from OOTB)

OOTB reference plugins use several action configuration props not yet documented:

| Prop | Purpose | Example |
|------|---------|---------|
| `overrides` | Customize individual form fields (label, source, component, defaultValue) | `"overrides": { "reason": { "source": "settings.RETURN_REASON" } }` |
| `noCard` | Render the action form without the wrapping Card container | `"noCard": true` |
| `noSuccessMessage` | Suppress the default success toast after submission | `"noSuccessMessage": true` |
| `postSubmit` | Action after successful form submission | `"postSubmit": { "type": "navigate", "link": "/orders" }` |
| `navigateWhenUnchanged` | Allow navigation even if no form fields were changed | `"navigateWhenUnchanged": true` |

**`overrides` example (pre-fill fields from settings):**
```json
{
  "type": "userAction",
  "name": "CancelOrder",
  "overrides": {
    "cancellationReason": {
      "source": "settings.CANCELLATION_REASON",
      "label": "i18n:fc.oms.order.cancel.reason"
    },
    "comments": {
      "label": "i18n:fc.oms.order.cancel.comments",
      "component": "textarea"
    }
  },
  "postSubmit": { "type": "navigate", "link": "/orders" }
}
```

### Pattern 8: Wizard (Multi-Step Form)

The `fc.page.wizard` component creates multi-step form workflows. Heavily used in the Store app for pick, pack, and dispatch flows.

```json
{
  "type": "page",
  "path": "orders/:orderId/dispatch",
  "component": "fc.page.wizard",
  "data": {
    "query": "query($id: ID!) { fulfilmentById(id: $id) { id ref status articles { edges { node { id ref quantity } } } } }",
    "variables": { "id": { "value": "{{id}}", "parse": true } }
  },
  "props": {
    "title": "Dispatch {{ref}}",
    "backButtons": [{ "label": "Back", "path": "/fulfilments" }],
    "steps": [
      { "label": "i18n:fc.store.dispatch.step.verify", "condition": "{{gt articles.edges.length 0}}" },
      { "label": "i18n:fc.store.dispatch.step.confirm" }
    ],
    "actions": {
      "primary": [
        {
          "type": "userAction",
          "name": "ConfirmDispatch",
          "label": "i18n:fc.store.dispatch.confirm"
        }
      ]
    }
  },
  "descendants": [
    {
      "component": "fc.list",
      "dataSource": "articles",
      "props": {
        "title": "Articles to Dispatch",
        "attributes": [
          { "label": "Ref", "value": "{{node.ref}}" },
          { "label": "Quantity", "value": "{{node.quantity}}" }
        ]
      }
    },
    {
      "component": "fc.page.wizard.summary",
      "props": {
        "title": "Confirm Dispatch",
        "attributes": [
          { "label": "Fulfilment", "value": "{{ref}}" },
          { "label": "Total Articles", "value": "{{articles.edges.length}}" }
        ]
      }
    }
  ]
}
```

**Wizard key props:**
- `steps[]` — Array of step definitions with `label` and optional `condition` (skip step if false)
- `actions.primary[]` — Action buttons shown on the final step
- Descendants map 1:1 to steps (first descendant = first step content, etc.)
- `fc.page.wizard.summary` — Special component for the final confirmation step
- `navigateWhenUnchanged` — If set, navigates even when the user hasn't changed form values

## Entity Type to GraphQL Query Map

**Note:** For pages with `"actions": true`, the entity's workflow information must be accessible in the GraphQL response. The framework uses this to determine which workflow to query for available user actions via the Transition API. For child entities (e.g., CREDIT_MEMO), the UI reads the entity's `workflowRef` (which points to the parent workflow, e.g., `"BILLING_ACCOUNT::CUSTOMER"`) and uses that as `flexType`. Note that the Transition API itself accepts BOTH the parent workflow name and the standalone child workflow name — but the UI always uses `workflowRef`, so user actions must be in the parent workflow for buttons to appear.

Use this mapping to auto-generate page data queries:

| Entity Type | Root Query (list) | Root Query (detail) | Key Fields |
|-------------|-------------------|---------------------|------------|
| ORDER | `orders(first, after, status, type, retailerId, createdOn)` | `orderById(id)` | id, ref, status, type, createdOn, totalPrice, customer, items, fulfilments, fulfilmentChoice, attributes |
| FULFILMENT | `fulfilments(first, after, status, type, retailerId)` | `fulfilmentById(id)` | id, ref, status, type, createdOn, order, items, fromAddress, toAddress, attributes |
| ARTICLE | `articles(first, after, status, retailerId)` | `articleById(id)` | id, ref, status, type, items, consignmentArticles, attributes |
| WAVE | `waves(first, after, status, retailerId)` | `waveById(id)` | id, ref, status, type, fulfilments, retailer, attributes |
| LOCATION | `locations(first, after, status, type, retailerId)` | `locationById(id)` | id, ref, name, type, status, primaryAddress, networks, openingSchedule, attributes |
| INVENTORY_POSITION | `inventoryPositions(first, after, productRef, locationRef)` | `inventoryPositionById(id)` | id, ref, productRef, locationRef, onHand, status, type, attributes |
| PRODUCT_CATALOGUE | `productCatalogues(first, after, retailerId)` | `productCatalogueById(id)` | id, ref, name, type, retailer, attributes |
| VIRTUAL_CATALOGUE | `virtualCatalogues(first, after, retailerId)` | `virtualCatalogueById(id)` | id, ref, name, type, retailer, attributes |
| CARRIER | `carriers(first, after, retailerId)` | `carrierById(id)` | id, ref, name, type, status, retailer, attributes |
| RETURN_ORDER | `returnOrders(first, after, status, retailerId)` | `returnOrderById(id)` | id, ref, status, type, order, returnAuthorisation, items, attributes |
| NETWORK | `networks(first, after, retailerId)` | `networkById(id)` | id, ref, name, type, status, locations, retailers, attributes |
| SETTING | `settings(first, after, name, context)` | n/a | id, name, value, lobValue, valueType, context, contextId |

### Connection Pattern for Nested Entities

All list queries return connections with edges/node pattern. Always include `pageInfo` for pagination:

```graphql
query($first: Int, $after: String) {
  orders(first: $first, after: $after) {
    edges {
      node {
        id
        ref
        status
      }
    }
    pageInfo {
      hasNextPage
    }
  }
}
```

Nested connections follow the same pattern:

```graphql
{
  orderById(id: $id) {
    items {
      edges {
        node { id ref quantity productRef }
      }
    }
  }
}
```

## Component Knowledge

Each OOTB component is documented in its own `.md` file under `components/` (same directory as this skill). `COMPONENT_TEMPLATE.md` defines the standard format — used for both OOTB and client custom component documentation.

Use this lookup order for fastest and most reliable component selection:
1. Start with `components/index.md` for alias/category/module lookup (AI-friendly table).
2. Open matching `components/<alias>.md` files for detailed props, data binding, and composition guidance.
3. Use each file's frontmatter (`alias`, `aliases`, `category`, `module`) and `## AI Parse Notes` section for deterministic parsing.

**Before building ANY manifest:**
1. Grep `components/*.md` for the user's UI need (e.g., "list", "filter", "tabs")
2. Read matching component files for props, data binding, and composition patterns
3. Propose a composition plan → get user approval → build manifest JSON
4. Only if no OOTB combination works → escalate to `/fluent-mystique-scaffold`

### Build vs Configure Decision (use EVERY time)

```
Step 1: Check Need → Grep components/*.md for matching capability
├── Found a match? → Configure via manifest using the documented props
├── Close match? → Step 2: Creative composition of OOTB components
│   ├── Combine fc.conditional + fc.repeater + fc.provider.graphql?
│   ├── Nest tabs inside providers? Use multiple data providers?
│   └── Found a working composition? → Build manifest JSON
├── Need a custom form field or template? → Step 3: Lightweight SDK extension
│   └── FieldRegistry.register() or TemplateRegistry.register()
└── Genuinely no OOTB equivalent? → Step 4: Full custom component
    └── Scaffold via /fluent-mystique-scaffold → ComponentRegistry.register()
```

**Most requests resolve at Step 1 or 2.** Only proceed to Step 4 for: novel visualizations, drag-and-drop, external service integrations, real-time features, or interaction patterns not covered by user action forms.

### Quick Alias Reference (86 components in `components/`)

**Page:** `fc.page`, `fc.page.section`, `fc.page.section.column`, `fc.page.section.header`, `fc.page.wizard`, `fc.page.wizard.summary`, `fc.page.refresh`, `fc.page.filter.select`

**Layout:** `fc.conditional`, `fc.repeater`, `fc.provider.graphql`, `fc.workflow.provider`, `fc.tabs`, `fc.tabs.card`, `fc.tab.content`, `fc.accordion`, `fc.filterPanel`

**Content:** `fc.list`, `fc.list.customAction`, `fc.card.attribute`, `fc.card.image`, `fc.card.product`, `fc.card.multi`, `fc.card.attributes.grid`, `fc.card.map.point`, `fc.tile.metric`, `fc.dashboard.threshold`, `fc.stepper`, `fc.progress.circular`, `fc.attribute.json`, `fc.attribute.column`, `fc.mystique.collapsible.text`, `fc.mystique.collapsible.attributes`, `fc.mystique.link`, `fc.json.viewer`, `fc.json.editor`, `fc.quantity.list`, `fc.settingForm`

**Charts:** `fc.chart.line`, `fc.chart.bar`, `fc.chart.area`, `fc.chart.gauge` (+ wrapper variants)

**Actions:** `fc.action.inline`, `fc.mutation.inline`, `fc.button.print` (+ `.pick`, `.inline`, `.download`), `fc.button.bar`, `fc.modal.button`, `fc.modal.button.addItem`, `fc.drawer.button`, `fc.sourcing.strategy.modal.button`, `fc.sourcing.profile.drawer.button`, `fc.sourcing.profile.modal.button`

**Forms/Fields:** `select`, `fc.field.daterange`, `fc.field.intrange`, `fc.field.multistring`, `fc.field.filterComplex`, `fc.scanner.barcode`, `fc.scanner.camera`, `fc.scanner.barcodeFilter`

**OMS:** `fc.events.search`, `fc.activity.entity`, `fc.event.detail`, `fc.order.itemDetails`, `fc.order.shipmentDetails`, `fc.return.rowExpansion`

**Store:** `fc.buttons.add.reject`, `fc.action.field.wavepick`, `fc.action.field.fulfilmentpack`, `fc.action.field.multiparcel`, `fc.action.field.returnitems`

**Analytics:** `fc.analytics.viz` (Looker embed)

**Legacy aliases** (v1 → v2): `shared.components.material.DynamicPage` → `fc.page`, `shared.components.material.DynamicList` → `fc.list`, `shared.components.material.DynamicCard` → `fc.card.attribute`, `shared.components.material.MaterialTabsSet` → `fc.tabs`

#### Advanced List Column Types

`fc.list` attributes support special `type` values beyond the default text rendering:

| Type | Purpose | Example |
|------|---------|---------|
| `"component"` | Embed a custom component inside a list cell | `{ "label": "Actions", "type": "component", "component": "ns.content.inline-action", "props": { "id": "{{node.id}}" } }` |
| `"image"` | Render an image in the cell | `{ "label": "Photo", "type": "image", "value": "{{node.imageUrl}}", "props": { "width": 48, "height": 48 } }` |
| (default) | Text with optional link, icon, condition | `{ "label": "Ref", "value": "{{node.ref}}", "link": "#/order/{{node.id}}" }` |

**`type: "component"` pattern (from OOTB Store plugin):**
```json
{
  "component": "fc.list",
  "dataSource": "articles",
  "props": {
    "attributes": [
      { "label": "Ref", "value": "{{node.ref}}" },
      { "label": "Status", "value": "{{node.status}}" },
      {
        "label": "Actions",
        "type": "component",
        "component": "fc.action.inline",
        "props": { "entityId": "{{node.id}}", "entityType": "ARTICLE" }
      }
    ]
  }
}
```

This allows embedding interactive components (action buttons, status badges, custom renderers) directly inside list rows.

#### When to Create Custom Template Helpers

**Check built-in helpers first** — Mystique provides 50+ helpers (dates, currency, strings, logic, arrays). See the "Template String Reference" section below. Only create a custom helper when:

| Scenario | Example | Custom helper needed? |
|----------|---------|----------------------|
| Format currency | `{{currency node.totalPrice node.currency}}` | NO — use built-in `currency` |
| Format date | `{{dateFormat node.createdOn "DD/MM/YYYY"}}` | NO — use built-in `dateFormat` |
| User's timezone | `{{currentTimezone}}` | YES — runtime browser context |
| Custom business logic | `{{acme.helpers.delivery-eta node.distance}}` | YES — app-specific calculation |
| Conditional HTML badge | `{{acme.helpers.status-badge node.status}}` | YES — needs `noEscape()` for HTML |

**To create a custom helper:** Run `/fluent-mystique-component --type template` to scaffold the helper file, test file, and registration. See `/fluent-mystique-sdk-reference` for the TemplateRegistry API and helper patterns.

**Wiring custom helper output in manifests:** Once registered, use your custom helper in any manifest template string just like built-in helpers — e.g., `"label": "{{acme.helpers.delivery-eta node.distance}}"`. The helper runs at render time in the browser. If your helper returns HTML, it must use `Handlebars.SafeString` (via `noEscape()`) and the manifest prop must support HTML rendering.

## Template String Reference

Mystique uses Handlebars-based template strings throughout manifests. **Only use documented helpers — guessing undocumented helpers produces broken manifests.**

> **Custom helpers require SDK registration.** All helpers listed below are built-in and available without any SDK work. If you need a helper not listed here (e.g., custom formatting, computed display values), it must be registered via `TemplateRegistry.register('namespace.helpers.kebab-name', fn)` in a Mystique SDK component project. Use `/fluent-mystique-component --type template` to scaffold a new helper. The SDK bundle containing the helper must be loaded as a plugin in the manifest root. See `/fluent-mystique-sdk-reference` for the full `TemplateRegistry` API.

### Field Access

| Expression | Purpose | Example |
|-----------|---------|---------|
| `{{fieldName}}` | Direct field access | `{{ref}}`, `{{status}}` |
| `{{nested.path}}` | Nested field access | `{{customer.firstName}}` |
| `{{attributes.byName.X}}` | Attribute by name shorthand | `{{order.attributes.byName.vipStatus}}` |
| `{{node.field}}` | List row data access | `{{node.ref}}` in fc.list attributes |
| `{{edges.length}}` | Connection count | `{{orders.edges.length}}` |

### Context Variables

| Expression | Purpose |
|-----------|---------|
| `{{activeUser.username}}` | Current user's username |
| `{{activeUser.firstName}}` | Current user's first name |
| `{{activeUser.timezone}}` | Current user's timezone |
| `{{activeContext.id}}` | Current context ID (retailer or location) |
| `{{activeContext.ref}}` | Current context ref |
| `{{activeRetailer.id}}` | Current retailer ID |
| `{{activeRetailer.ref}}` | Current retailer ref |
| `{{activeLocation.id}}` | Current location ID |
| `{{activeLocation.ref}}` | Current location ref |
| `{{params.id}}` | Route parameter access |
| `{{params.filter_status}}` | Query parameter access |

### String Helpers

| Expression | Purpose | Example |
|-----------|---------|---------|
| `{{capitalise value}}` | Capitalize string | `{{capitalise status}}` |
| `{{humanify value}}` | Human-readable (underscore/camel to spaced) | `{{humanify entityType}}` |
| `{{toLowerCase value}}` | Lowercase | `{{toLowerCase order.type}}` |
| `{{toUpperCase value}}` | Uppercase | `{{toUpperCase lastName}}` |
| `{{concat a b separator='X'}}` | Concatenate with separator | `{{concat firstName lastName separator=' '}}` |
| `{{stringify value}}` | JSON stringify | `{{stringify order.attributes.byName.payload}}` |
| `{{preserveWhitespace text}}` | Preserve whitespace in rendering | `{{preserveWhitespace notes}}` |
| `{{partialStringMatch a b}}` | Case-insensitive partial match | `{{partialStringMatch 'item1' 'eM1'}}` returns true |
| `{{decodeUrlParams value}}` | URL decode | `{{decodeUrlParams 'TEST%3A%3AL%2FXL'}}` returns `TEST::L/XL` |
| `{{{currentUrlParams}}}` | Inject current URL params (triple-brace, unescaped) | `"#/search?{{{currentUrlParams}}}"` |
| `{{placeholderText text color='X' defaultValue='X'}}` | Styled fallback text | `{{placeholderText value defaultValue='-'}}` |

### Date Helpers

**CRITICAL: Always use `_locale='en'` for dates in query variables.** Without it, user locale may produce non-ASCII digits (e.g., Arabic numerals) that break backend queries.

| Expression | Purpose | Example |
|-----------|---------|---------|
| `{{dateFormat date fmt}}` | Formatted date (display) | `{{dateFormat createdOn 'MMM DD, YYYY'}}` |
| `{{dateFormat date fmt true _locale='en'}}` | **Backend-safe** formatted date | `{{dateFormat (dateAdd day=-1) 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]' true _locale='en'}}` |
| `{{dateRelative date}}` | Relative time ("10 minutes ago") | `{{dateRelative order.expiryTime}}` |
| `{{dateFormatByLocale date}}` | Locale-aware format (dd/mm or mm/dd) | `{{dateFormatByLocale order.createdOn}}` |
| `{{dateAdd date day=N}}` | Date arithmetic (add) | `{{dateAdd order.createdOn day=3}}` |
| `{{dateSubtract date day=N}}` | Date arithmetic (subtract) | `{{dateSubtract order.createdOn day=5}}` |
| `{{dateStringFormatter (dateAdd day=N) fmt}}` | Date arithmetic + format | `{{dateStringFormatter (dateAdd day=-7) 'YYYY-MM-DD'}}` |
| `{{placeholderDate date color='X' defaultValue='X'}}` | Date with styled fallback | `{{placeholderDate date defaultValue='[...]'}}` |

### Number and Currency Helpers

| Expression | Purpose | Example |
|-----------|---------|---------|
| `{{currency value code}}` | Format as currency | `{{currency totalPrice 'AUD'}}` |
| `{{currency value code _locale='X' _decimalPlaces=N}}` | Currency with locale control | `{{currency 123.87 'AUD' _locale='en-AU' _decimalPlaces=3}}` |
| `{{add a b}}` | Addition | `{{add 1 2}}` returns 3 |
| `{{subtract a b _decimalPlaces=N}}` | Subtraction | `{{subtract 50.32 20.11 _decimalPlaces=2}}` returns 30.21 |
| `{{multiply a b}}` | Multiplication | `{{multiply 3 4}}` returns 12 |
| `{{divide a b _decimalPlaces=N}}` | Division | `{{divide 12 4 _decimalPlaces=3}}` returns 3.000 |
| `{{pow base exp}}` | Exponent | `{{pow 3 2}}` returns 9 |

### Array Helpers

| Expression | Purpose | Example |
|-----------|---------|---------|
| `{{len array}}` | Array length | `{{len items.edges}}` |
| `{{arraySum array 'path'}}` | Sum numeric field across array | `{{arraySum order.items 'node.totalPrice'}}` |
| `{{placeholderArray arr separator='X' charCutoff=N}}` | Array display with truncation | `{{placeholderArray tags separator=', ' charCutoff=50}}` |

### Logic Helpers

| Expression | Purpose | Example |
|-----------|---------|---------|
| `{{eq a b}}` | Equality | `{{eq status 'ACTIVE'}}` |
| `{{ne a b}}` | Not equal | `{{ne status 'CANCELLED'}}` |
| `{{gt a b}}` | Greater than | `{{gt totalPrice 0}}` |
| `{{lt a b}}` | Less than | `{{lt items.count 3}}` |
| `{{gte a b}}` | Greater or equal | `{{gte items.count 5}}` |
| `{{lte a b}}` | Less or equal | `{{lte items.count 10}}` |
| `{{and a b}}` | Logical AND | `{{and (eq status 'BOOKED') (gt items.count 0)}}` |
| `{{or a b}}` | Logical OR | `{{or order.status order.items.count}}` |
| `{{in value ...options}}` | Inclusion check | `{{in type 'HD' 'CC' 'MULTI'}}` |
| `{{switch val k1 v1 k2 v2 default}}` | Switch expression | `{{switch status 'COMPLETE' 'green' 'red'}}` |
| `{{hasRole 'ROLE'}}` | Role check | `{{hasRole 'admin'}}` |
| `{{firstDefinedValue a b c}}` | First non-null value | `{{firstDefinedValue null undefined 10}}` returns 10 |
| `{{anyMatch 'path' 'val' 'op'}}` | Array element match | `{{anyMatch 'node.entity.field' 'delta' 'eq'}}` |

### i18n Helpers

| Expression | Purpose | Example |
|-----------|---------|---------|
| `{{i18n 'key'}}` | Translate key | `{{i18n 'fc.om.orders.title'}}` |
| `{{i18n 'prefix' value _fallback=val}}` | Dynamic key with fallback | `{{i18n 'fc.om.order.type.' node.type _fallback=node.type}}` |
| `{{i18n 'a' '.' 'b'}}` | Concatenated key | `{{i18n 'fc.om.order' '.' 'status'}}` |

### Special Helpers

| Expression | Purpose | Example |
|-----------|---------|---------|
| `{{barcode value format}}` | Generate barcode SVG | `{{barcode '1234 abc' 'CODE128B'}}` |
| `{{barcode value}}` | Barcode with auto-detected format | `{{barcode fulfilmentById.ref}}` |
| `{{barcode value format false}}` | Barcode without text below | `{{barcode '1234' 'EAN-13' false}}` |

Supported barcode formats: CODE128, CODE128A, CODE128B, CODE128C, EAN-13, EAN-8, EAN-5, EAN-2, UPC-A, UPC-E, CODE39, ITF, ITF-14, MSI, MSI10, MSI11, MSI1010, MSI1110, Pharmacode, Codabar.

## Dynamic Attribute Types

Dynamic attributes are configurable value displays used in `fc.list` columns, `fc.card.attribute` rows, and similar components. Three types: **standard** (text), **image**, and **component**.

### Standard (Text) with Conditional Styling

```json
{
  "label": "i18n:fc.om.orders.column.status",
  "value": "{{status}}",
  "link": "#/order/{{id}}",
  "condition": "{{and id}}",
  "enableCopyIcon": "displayOnHover",   // or "displayAlways"
  "options": {
    "styles": [
      {
        "value": "{{status}}",
        "matches": ["BOOKED", "CONFIRMED"],
        "icon": { "name": "MdCheckCircle", "colour": "primary" },
        "text": { "colour": "green" }
      },
      {
        "matches": ["CANCELLED"],
        "icon": { "name": "MdCancel", "colour": "error" },
        "text": { "colour": "red" }
      },
      { "icon": { "name": "MdInfo" } }
    ]
  }
}
```

Style matching: entries with `matches` checked in order — first match wins. Entry without `matches` is the default. Icon `name` must include library prefix: `Md*` (Material Design) or `Fa*` (Font Awesome).

### Image Attribute

```json
{ "type": "image", "label": "Product", "value": "{{node.attributes.byName.imageUrl}}", "options": { "width": 50, "height": 50 } }
```

### Component Attribute

Embed another component inside an attribute slot:

```json
{ "type": "component", "label": "Actions", "options": { "component": "fc.button.print.download", "props": { "label": "Print", "href": "api/v4/consignment/{{id}}/labelStream", "filename": "label.pdf" } } }
```

## CardActions Pattern

CardActions configure action buttons on cards and list rows. Primary actions render as buttons; secondary actions group under "More".

```json
{
  "cardActions": {
    "primary": [{ "type": "userAction", "name": "ConfirmShipment", "label": "i18n:fc.action.confirm" }],
    "secondary": [{ "type": "userAction", "name": "CancelOrder", "condition": "{{ne status 'SHIPPED'}}" }],
    "showAtLeast": 1
  }
}
```

- `type: "userAction"` triggers a workflow user action. `name` matches the ruleset name.
- Workflow-defined user actions are **automatically appended** — only add to manifest when reordering or adding conditions.
- `showAtLeast` — primary actions to keep visible on mobile (rest collapse into "More").
- `condition` — template string; action hidden when evaluates to `"false"`.

## Settings vs Manifest Decision Matrix

| Use **Settings** when | Use **Manifest** when |
|---|---|
| Config varies by retailer/location | Structure is stable across tenants |
| Values change frequently | Routes, pages, component layout |
| Non-developers need to update | Developers manage changes |
| API keys, external URLs, thresholds | Static labels, i18n keys |

## Troubleshooting

Common manifest issues and fixes:

1. **Template not resolving** — Missing `{{ }}` syntax. `"value": "order.ref"` shows literal text. Fix: `"value": "{{order.ref}}"`
2. **Date query returns localized digits** — User locale produces Arabic/Thai numerals that break backend. Fix: Add `_locale='en'`: `{{dateFormat (dateSubtract day=1) 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]' true _locale='en'}}`
3. **Condition not working** — String `"false"` is truthy. Fix: Use a template expression: `"condition": "{{and field1 field2}}"`
4. **Link not rendering** — The data value is null/undefined. Fix: Add `"condition": "{{and customer.id}}"` alongside the link
5. **Icon not showing** — Missing library prefix. `"CheckCircle"` is wrong. Fix: `"MdCheckCircle"` (Material Design) or `"FaCheck"` (Font Awesome)
6. **Debugging tips** — Check browser DevTools Network tab for actual GraphQL response shape. Use React DevTools to inspect component props. Validate JSON syntax with `jq .`
7. **User action buttons not appearing** — Several possible causes:
   - Workflow user action `context[].modules` doesn't include `"adminconsole"`. The OMS App sends `module: "adminconsole"` (the OMS App's module alias, legacy alias) to Transition API, NOT `"oms"`. Fix: change `modules: ["oms"]` to `modules: ["adminconsole"]`
   - Child entity (e.g., CreditMemo under BillingAccount) — the UI reads `workflowRef` which points to parent workflow. The Transition API accepts both parent and child workflow as `flexType`, but the UI always uses `workflowRef`. Fix: add user action rulesets to the parent workflow (BILLING_ACCOUNT::CUSTOMER) so the UI can find them
   - Workflow ruleset missing `subtype`. Fix: add `subtype` matching entity subtype (e.g., `"APPEASEMENT"`)
   - Workflow user action missing `eventName`. Fix: add `eventName` to every user action
   - Console shows "Error building user action lookups" — Transition API call failed. Check browser Network tab for the `/api/v4.1/transition` request and response
   - **Child entity detail page (CreditMemo, Fulfilment)** — fc.page reads retailerId from query `variables`, NOT URL params. Since child entities have no `retailer` edge at root level, retailerId can't be passed as a GraphQL variable → Transition API never called → buttons always missing. This is a platform limitation, not a config bug.
   - Cross-reference with `/fluent-transition-api` for detailed Transition API diagnostics
8. **GraphQL variable validation failures** — Fluent GraphQL strictly validates variables:
   - **Undeclared variables**: If `variables{}` has keys not declared in the query signature (`query ($ref: String!)`) → entire query fails silently
   - **Unused declared variables**: If query declares `$retailerId: ID` but no field uses it → entire query fails
   - **Type mismatch**: URL params are strings — `$retailerId: Int` rejects "2". Use `$retailerId: ID` instead
   - Every variable in `variables{}` MUST be declared in the query AND used in the query body. No exceptions.

## Output Locations

| Artifact | Path |
|----------|------|
| New manifest | `accounts/<PROFILE>/SOURCE/frontend/<sdk-project>/manifests/<name>.json` |
| Fragment | `accounts/<PROFILE>/SOURCE/frontend/<sdk-project>/manifests/fragments/<name>.json` |
| Standalone manifest (no SDK project) | `accounts/<PROFILE>/manifests/<name>.json` |
| Build plan | `accounts/<PROFILE>/features/<slug>/plan.md` |

## Handoff Protocol

### Signals emitted by this skill

| Signal | Condition | Example |
|--------|-----------|---------|
| `-> READY: <path>` | Manifest JSON created/modified | `-> READY: accounts/MY_PROFILE/SOURCE/frontend/mystique-plugin/manifests/admin.json` |
| `-> NEXT: /fluent-<skill>` | Always validate after build | `-> NEXT: /fluent-mystique-validate` |
| `-> BLOCKED: <reason>` | Prerequisite missing | `-> BLOCKED: PREREQ_MISSING -- Custom component custom.order-tracker not registered. Run /fluent-mystique-scaffold` |
| `-> SKIP: <reason>` | No manifest changes needed | `-> SKIP: No UI 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 SDK components not scaffolded or plugins missing | Scaffold component via `/fluent-mystique-scaffold` |
| `VALIDATION_FAILED` | Manifest JSON structure invalid after editing | Fix JSON structure per validation checklist |

## Session Tracking

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

**On entry:**
```json
{ "skill": "fluent-mystique-builder", "timestamp": "<ISO-8601>", "arguments": { "operation": "<op>", "manifest": "<file>", "page": "<path>" } }
```

**On exit:**
```json
{ "skill": "fluent-mystique-builder", "outcome": "<completed|failed|skipped>", "changesProduced": ["<seq-numbers>"], "toolCallsProduced": ["<seq-numbers>"], "nextRecommended": "/fluent-mystique-validate" }
```

## Error Reporting

Errors from this skill follow the standard format:

| Field | Description |
|-------|-------------|
| `phase` | Skill phase where error occurred (e.g., `pre-flight`, `route-design`, `component-tree`, `query-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 considering any manifest complete:

1. **JSON syntax** — Valid JSON (no trailing commas, correct brackets)
2. **manifestVersion** — Must be `"2.0"`
3. **Required fields** — `name`, `title`, `homePath`, `routes` all present
4. **homePath resolves** — Must match a defined route's `path`
5. **Route types valid** — Every route is `section`, `page`, or `reference`
6. **Page paths unique** — No duplicate `path` values across all pages
7. **Component aliases** — All `fc.*` aliases exist in the known registry; custom aliases have corresponding plugin
8. **dataSource paths** — JSONPath notation valid; references query result fields
9. **Template strings** — Balanced `{{`/`}}`, valid helper names
10. **Plugin URLs** — `src` is a valid URL (http/https for dev, https for production)
11. **Fragment settingName** — Follows `fc.mystique.manifest.*` convention
12. **Roles** — Role strings are valid and consistent across the manifest
13. **i18n keys** — Translatable strings use `i18n:` prefix consistently

## Integration with Other Skills

| Task | Skill |
|------|-------|
| Validate manifest structure before deploy | `/fluent-mystique-validate` |
| Scaffold custom SDK component project | `/fluent-mystique-scaffold` |
| Test custom components locally (hot-reload) | `/fluent-frontend-build dev` |
| Run production quality gates on SDK bundle | `/fluent-frontend-build full` |
| Review SDK code quality and patterns | `/fluent-frontend-review` |
| Analyze existing manifests for patterns | `/fluent-mystique-analyze` |
| Deploy manifest via CLI module install (production) | `/fluent-module-deploy` with `--include settings` |
| Deploy manifest via MCP/GraphQL (interactive) | `/fluent-settings` |
| Build workflows that power user actions | `/fluent-workflow-builder` |
| Create feature plan covering UI work | `/fluent-feature-plan` |
| Run pre-deploy checks including manifest validation | `/fluent-pre-deploy-check` |
| Verify manifest renders correctly in browser | `/fluent-ui-test` |

## GraphQL Query Construction for Manifests

Building correct GraphQL queries for manifest pages is the single most impactful factor in whether a page works or fails silently. Follow this strict workflow.

### Query Construction Workflow

1. **Discover available root queries** — Use MCP `graphql.listRoots` to find available root query fields for the entity type you need
2. **Generate a full query** — Use MCP `graphql.generateFull` to produce a complete query with ALL valid fields for that root
3. **Remove fields you do not need** — Strip unnecessary fields from the generated query to keep it lean
4. **Validate the query** — Use MCP `graphql.validate` or `graphql.execute` (with a test variable) to confirm the query is structurally valid

**CRITICAL RULE: NEVER add fields to a generated query. Only remove fields.** Hallucinated field names cause silent page failures — the page loads but shows no data, with no visible error. The GraphQL schema is the single source of truth. If you need a field and it is not in the generated output, it does not exist.

### Adding the `me` Query for User Action Pages

When a page has `"actions": true` in its props (meaning it shows workflow user action buttons), you should include the `me` query to make the current user's identity available for user action event attributes:

```graphql
me {
  username
  primaryRetailer { id }
  roles { role { name } }
}
```

**When to include `me`:**
- Any page where `"actions": true` is set on `fc.page` props
- Any page that uses `{{activeUser.username}}` or similar context variables in mutation payloads
- Any page where user action forms pass `actionedBy` or similar identity attributes

**How it works:** The `me` query result is available in the page data context. Manifest templates can then reference `{{me.username}}`, `{{me.primaryRetailer.id}}`, and `{{me.roles}}`. User Action forms can pass these as event attributes using `{{me.username}}` in their override configuration.

### Query Validation Before Deploy

Always validate queries before deploying a manifest:

1. **Schema validation** — `graphql.validate` checks field names, types, and argument shapes against the live schema
2. **Introspection check** — `graphql.introspect` on a specific type reveals all available fields and edges
3. **Test execution** — `graphql.execute` with safe read-only variables confirms the query returns data in the expected shape

If MCP tools are unavailable, use the Entity Type to GraphQL Query Map section above as a fallback reference, but note that it only covers common fields — edge cases require schema introspection.

## Component Configuration Quick Reference

Practical configuration examples for the most commonly used Mystique components. These are condensed from the full component documentation in `components/*.md`.

### fc.card.attribute — Summary and Detail Cards

Displays key-value pairs in a two-column card layout. The workhorse component for entity detail pages.

```json
{
  "component": "fc.card.attribute",
  "dataSource": "orderById",
  "props": {
    "title": "i18n:fc.order.summary.title",
    "width": "half",
    "highlight": "primary",
    "attributes": [
      { "label": "Type", "value": "{{type}}" },
      { "label": "Status", "value": "{{status}}" },
      {
        "label": "Total",
        "value": "{{currency totalAmount.amount currency.alphabeticCode}}"
      },
      {
        "label": "Order",
        "value": "{{order.ref}}",
        "link": "#/orders?orders_ref[]={{order.ref}}",
        "condition": "{{and order.ref}}"
      },
      {
        "label": "Created",
        "value": "{{dateFormat createdOn}}",
        "enableCopyIcon": "displayOnHover"
      }
    ]
  }
}
```

**Key props:**

| Prop | Type | Description |
|------|------|-------------|
| `title` | `string` | Card heading text (supports i18n) |
| `width` | `CardWidth` | `quarter`, `third`, `half`, `two-thirds`, `full`, or number 1-12 |
| `highlight` | `string` | Colored bar above card — theme name (`primary`, `success`, `error`) or hex value |
| `attributes` | `DynamicAttribute[]` | Key-value pairs to display |
| `link` | `string` | Makes entire attribute value clickable |

### fc.list — Data Tables

Renders paginated, filterable data tables. Used for entity list pages and nested entity lists (items, fulfilments, etc.).

```json
{
  "component": "fc.list",
  "dataSource": "orders",
  "props": {
    "title": "i18n:fc.orders.list.title",
    "defaultPageSize": 25,
    "rowLink": "/orders/{{node.id}}",
    "density": { "toggle": true, "initial": "standard" },
    "filter": { "enabled": true, "exclude": ["workflowRef"] },
    "attributes": [
      {
        "label": "Ref",
        "value": "{{node.ref}}",
        "link": "#/orders/{{node.id}}"
      },
      { "label": "Status", "value": "{{node.status}}" },
      {
        "label": "Amount",
        "value": "{{currency node.totalPrice node.currency}}"
      },
      {
        "label": "Created",
        "value": "{{dateRelative node.createdOn}}",
        "hideBelow": "sm"
      }
    ],
    "row": {
      "actions": {
        "primary": [
          { "type": "userAction", "name": "Review" }
        ]
      },
      "expansion": {
        "toggle": true,
        "component": "fc.card.attribute",
        "props": { "title": "Details" }
      }
    }
  }
}
```

**Key props:**

| Prop | Type | Description |
|------|------|-------------|
| `title` | `string` | Table heading |
| `attributes` | `DynamicAttribute[]` | Column definitions — `label` is header, `value` re-evaluated per row |
| `defaultPageSize` | `number` | Rows per page (default varies by context) |
| `rowLink` | `string` | Template string — entire row becomes clickable link |
| `filter` | `object` | `{ enabled: true, exclude: ["field1"] }` — auto-generates filters from query variables |
| `density` | `DensityConfig` | `{ toggle: true, initial: "compressed" }` — row height controls |
| `row` | `RowConfig` | Per-row actions and expansion component |

### fc.tabs.card / fc.tabs — Tabbed Layouts

Organize content into tabs. Descendants are matched to tabs by array index — first descendant renders in first tab, etc.

**fc.tabs.card** (card with tabs inside):

```json
{
  "component": "fc.tabs.card",
  "props": {
    "tabs": [
      { "label": "Details" },
      { "label": "Items" },
      { "label": "Attributes" },
      { "label": "Activity" }
    ],
    "urlKey": "activeTab"
  },
  "descendants": [
    { "component": "fc.card.attribute", "props": { "attributes": [...] } },
    { "component": "fc.list", "dataSource": "items", "props": { "attributes": [...] } },
    { "component": "fc.list", "dataSource": "attributes", "props": { "attributes": [...] } },
    { "component": "fc.activity.entity", "props": {} }
  ]
}
```

**fc.tabs** (standalone tabs, typically wrapping `fc.page.section` descendants):

```json
{
  "component": "fc.tabs",
  "props": {
    "layouts": [
      { "label": "Details" },
      { "label": "Items" },
      { "label": "Activity" }
    ]
  },
  "descendants": [
    { "component": "fc.page.section", "descendants": [...] },
    { "component": "fc.page.section", "descendants": [...] },
    { "component": "fc.page.section", "descendants": [...] }
  ]
}
```

**Key difference:** `fc.tabs.card` wraps content in a card container with `tabs` prop. `fc.tabs` is a standalone layout component with `layouts` prop. Both match descendants by index.

### fc.tile.metric — Dashboard Tiles

Metric display tiles for dashboards with conditional styling:

```json
{
  "component": "fc.tile.metric",
  "props": {
    "label": "Escalated Orders",
    "value": "{{escalatedOrders.edges.length}}",
    "variant": "small",
    "width": "third",
    "link": "#/orders?status=ESCALATED",
    "options": {
      "styles": [
        {
          "expression": "{{gt escalatedOrders.edges.length 10}}",
          "text": { "color": "error" }
        }
      ]
    }
  }
}
```

| Prop | Type | Description |
|------|------|-------------|
| `label` | `string` | Metric name |
| `value` | `string` | Metric value (template string) |
| `variant` | `'large'` / `'small'` | Font size — large: 54px centered, small: 36px left-aligned |
| `width` | `CardWidth` | Grid width |
| `link` | `string` | Click navigation URL |

### fc.provider.graphql — Nested Data Providers

Fetch additional data within a page without adding to the page-level query:

```json
{
  "component": "fc.provider.graphql",
  "props": {
    "query": "query($ref: [String]) { locations(ref: $ref) { edges { node { id ref name } } } }",
    "variables": { "ref": "{{fromLocation.ref}}" },
    "path": "locations",
    "loadingBehavior": "wait-with-animation"
  },
  "descendants": [
    {
      "component": "fc.card.attribute",
      "dataSource": "locations",
      "props": { "attributes": [...] }
    }
  ]
}
```

| Prop | Type | Description |
|------|------|-------------|
| `query` | `string` | GraphQL query to execute |
| `variables` | `object` | Query variables (supports templates from parent data context) |
| `path` | `string` | Enables auto-pagination for connection results |
| `loadingBehavior` | `string` | `"wait"`, `"wait-with-animation"`, or `"pass-immediately"` (default) |

### fc.conditional — Conditional Rendering

Show or hide components based on data values:

```json
{
  "component": "fc.conditional",
  "props": {
    "value": "{{type}}",
    "matches": "HD"
  },
  "descendants": [
    { "component": "fc.card.attribute", "props": { "title": "HD Details", "attributes": [...] } }
  ]
}
```

Use multiple `fc.conditional` siblings for if/else-if patterns. Each checks its own `value`/`matches` independently.

## Attribute Styling Patterns

Conditional styles control icons, text colors, and visual formatting based on data values. Used in `fc.card.attribute`, `fc.list`, and other components that accept `DynamicAttribute[]`.

### Basic Conditional Styling with `styles[]`

```json
{
  "label": "Status",
  "value": "{{status}}",
  "options": {
    "styles": [
      {
        "value": "{{status}}",
        "matches": ["BOOKED", "CONFIRMED"],
        "icon": { "name": "MdCheckCircle", "colour": "success" },
        "text": { "colour": "green" }
      },
      {
        "matches": ["CANCELLED", "EXPIRED"],
        "icon": { "name": "MdCancel", "colour": "error" },
        "text": { "colour": "red" }
      },
      {
        "matches": ["CREATED"],
        "icon": { "name": "loading" },
        "text": { "colour": "primary" }
      },
      {
        "icon": { "name": "MdInfo" }
      }
    ]
  }
}
```

**How matching works:**
- Entries with `matches` array are checked in order — first match wins
- Entry **without** `matches` is the catch-all default (place last)
- `value` on a style entry overrides which field to compare (default: the attribute's `value`)

### Icon Naming Rules

Icons MUST use the library prefix — bare icon names will not render:

| Prefix | Library | Examples |
|--------|---------|----------|
| `Md` | Material Design | `MdCheckCircle`, `MdCancel`, `MdInfo`, `MdShoppingCart`, `MdLocalShipping`, `MdWarning`, `MdDashboard`, `MdAdd` |
| `Fa` | Font Awesome | `FaCheck`, `FaTruck`, `FaBoxOpen`, `FaUser`, `FaClipboard` |
| `loading` | Built-in spinner | `loading` (no prefix needed — special case) |

**Common mistake:** `"name": "CheckCircle"` will NOT render. Always use `"name": "MdCheckCircle"`.

### Text Color Options

| Value | Meaning |
|-------|---------|
| `"primary"` | Theme primary color |
| `"success"` | Green |
| `"error"` | Red |
| `"warning"` | Orange/amber |
| `"#RRGGBB"` | Any hex color value |

### Column-Level Status Styling (fc.attribute.column)

For list columns with complex styling requirements, use the `fc.attribute.column` component type:

```json
{
  "label": "Status",
  "type": "component",
  "options": {
    "component": "fc.attribute.column",
    "props": {
      "contentColumnSetting": "fc.order.list.status.column",
      "value": "{{status}}"
    },
    "dataSource": "node"
  }
}
```

This delegates styling to a separate settings key, enabling column styling to be managed independently of the manifest.

## Data Channel Decision Tree

When building a page or component, choose the data source using this priority order:

```
1. Can the page query provide this data?
   YES → Use page-level data.query + dataSource scoping (preferred)
   NO  ↓

2. Does the page need multiple independent queries?
   YES → Use dataSources (plural) on the page object
         NOTE: data and dataSources are mutually exclusive
   NO  ↓

3. Does a section of the page need extra data from a subquery?
   YES → Use fc.provider.graphql component (secondary query for descendants)
   NO  ↓

4. Is this a custom SDK component needing ancillary data?
   YES → Use useQuery() inside the component (last resort)
   NO  ↓

5. Is this a FieldRegistry field inside a user action dialog?
   YES → MUST use useQuery() — useData() and data prop are EMPTY in dialogs
         Extract entity ID from entityContext[0].entity.id (preferred)
         or window.location.hash (fallback)
```

**Data scoping chain:** When a manifest has `dataSource: "order"` on a component, the framework calls `getIndex(parentData, "order")` — simple dot-splitting with NO auto-unwrap of GraphQL connections. However, `fc.list` handles `edges[].node` unwrapping internally, so use `dataSource: "orderById.items"` (not `orderById.items.edges`).

**useSettings() status check pattern** (from OMS/Store plugin source):
```typescript
const res = useSettings({ key: 'fc.mystique.fields.myfield' });
// ALWAYS check status before accessing .result.value
if (res.key.status === 'ok') {
  const config = res.key.result.value;
} else if (res.key.status === 'loading') {
  // show spinner
} else {
  // fallback to defaults
}
```

## Data Source and Variable Patterns

Understanding how data flows from URL parameters through GraphQL queries into component templates is essential for correct manifest construction.

### URL Path Parameters

Route params defined with `:paramName` are accessed via `{{params.paramName}}`:

```json
{
  "path": "orders/:id",
  "data": {
    "query": "query($id: ID!) { orderById(id: $id) { ... } }",
    "variables": { "id": { "value": "{{id}}", "parse": true } }
  }
}
```

For detail pages, use `"parse": true` to convert string params to the correct type (e.g., ID).

For multi-param routes:
```json
{
  "path": "products/:productRef/:catalogueRef",
  "data": {
    "variables": {
      "productRef": "{{params.productRef}}",
      "catalogueRef": "{{params.catalogueRef}}"
    }
  }
}
```

### Context Variables (No Query Needed)

These values are available in every page template without being queried:

| Variable | Description | Common Use |
|----------|-------------|------------|
| `{{activeUser.username}}` | Logged-in user's username | Display, audit trails |
| `{{activeUser.firstName}}` | User's first name | Greeting text |
| `{{activeUser.timezone}}` | User's timezone | Date display formatting |
| `{{activeRetailer.id}}` | Current retailer ID | Query variables for retailer-scoped data |
| `{{activeRetailer.ref}}` | Current retailer ref | Display, links |
| `{{activeContext.id}}` | Current context ID (retailer or location) | Query variables |
| `{{activeContext.ref}}` | Current context ref | Display |
| `{{activeLocation.id}}` | Current location ID (Store app) | Location-scoped queries |
| `{{activeLocation.ref}}` | Current location ref | Display |

### Named Attribute Access

Fluent entities store custom data as attribute arrays. In manifests, access named attributes using the `byName` shorthand:

```
{{entity.attributes.byName.fieldName}}
```

**NEVER use array index access** for attributes — attribute ordering is not guaranteed:
- WRONG: `{{entity.attributes.0.value}}`
- CORRECT: `{{entity.attributes.byName.vipStatus}}`

### Edge Traversal for Related Entities

For accessing specific items in a connection (e.g., first order item):

```
{{entity.items.edges.0.node.ref}}
```

For nested connections from a list context (inside `fc.list` attributes):
```
{{node.order.ref}}
{{node.items.edges.length}}
```

### Query Params in Links and Filters

Pass URL query params for pre-filtered list pages:

```json
{
  "link": "#/orders?orders_ref[]={{ref}}&status[]={{status}}"
}
```

Access query params in data source variables:
```json
{
  "variables": {
    "status": "{{params.filter_status}}",
    "fromDate": "{{params.filter_from}}"
  }
}
```

Preserve current URL params when navigating:
```json
{
  "link": "#/search?{{{currentUrlParams}}}"
}
```

Note the **triple braces** `{{{ }}}` for `currentUrlParams` — this prevents HTML escaping of the `&` characters in the URL.

## Pre-Build Validation Checklist

Run through this checklist before considering any manifest page complete. This supplements the existing Validation Checklist section with query and data-specific checks.

- [ ] **GraphQL query validated (MANDATORY GATE)** — `graphql.validate` returned `valid: true`. No hallucinated fields. Query was generated from schema (via `graphql.generateFull`) and only had fields removed, never added. If `valid: false`, this is a BLOCKING failure — do not output the manifest.
- [ ] **`me` query included** if page has `"actions": true` — Required for user action forms that pass user identity.
- [ ] **Variables use `{{params.*}}`** for URL path and query parameters — Not hardcoded values.
- [ ] **dataSource references match query root names** — If query returns `orderById`, components reference `orderById` (or sub-paths like `orderById.items`).
- [ ] **Links use `#/` prefix** for hash routing — Not `/` alone (which causes full page reload).
- [ ] **Conditions use `{{and field}}`** — Not string literals `"true"` or `"false"` (which are always truthy as strings).
- [ ] **Currency uses `{{currency amount code}}`** — `{{currency totalAmount.amount currency.alphabeticCode}}`, not raw number display.
- [ ] **Dates use formatters for display** — `{{dateFormat createdOn}}` or `{{dateRelative createdOn}}`, not raw ISO strings. Backend query dates use `_locale='en'`.
- [ ] **Icons use library prefix** — `MdCheckCircle`, `FaTruck`, never bare `CheckCircle` or `Truck`.
- [ ] **Width values are valid** — `quarter`, `third`, `half`, `two-thirds`, `full`, or number 1-12.
- [ ] **i18n keys prefixed** — Translatable strings use `i18n:fc.app.section.label` convention.
- [ ] **No duplicate route paths** — Every page path is unique across the entire manifest.
- [ ] **Fragment settingName follows convention** — `fc.mystique.manifest.<app>.fragment.<name>`.
- [ ] **lobValue is raw JSON object** — Not `JSON.stringify()`'d. Double-encoding causes silent failures.

## Knowledge References

For comprehensive reference material on Mystique frontend development, consult these in-repo references:

| Knowledge File | Contents | When to Use |
|---------------|----------|-------------|
| `components/index.md` | Full OOTB component alias catalog (`alias`, `category`, `module`) | Selecting candidate components quickly |
| `components/<alias>.md` | Prop-level docs, data binding patterns, composition guidance, and gotchas | Configuring chosen components correctly |
| `SKILL.md` (`Template String Reference`) | Documented template helper usage (string/date/logic/number/i18n) | Writing template expressions in props, conditions, and query variables |
| `SKILL.md` (`Component Composition Patterns`, `Troubleshooting`) | Reusable page/layout/action patterns and failure diagnosis checklists | Debugging page failures, optimizing manifest structure |
| `../fluent-mystique-scaffold/SKILL.md` | SDK component development — scaffolding, registry, hooks, deployment | Building custom components when OOTB components are insufficient |

These references are loaded on-demand by skills that need frontend context. They complement the inline guidance in this skill file with deeper detail and edge-case coverage.

## Handoff

| Output Artifact | Consumed By | Path |
|----------------|-------------|------|
| Manifest JSON file | `/fluent-mystique-validate` (validation) | `accounts/<PROFILE>/SOURCE/frontend/<project>/manifests/<name>.json` |
| Manifest JSON file | `/fluent-module-deploy` (CLI deploy via `assets/settings/`) | Package into `assets/settings/manifests.json` |
| Manifest JSON file | `/fluent-settings` (MCP/GraphQL deploy) | `accounts/<PROFILE>/SOURCE/frontend/<project>/manifests/<name>.json` |
| Fragment JSON file | `/fluent-module-deploy` or `/fluent-settings` | Same as above — fragments are just smaller manifests |
| Deployed manifest | `/fluent-ui-test` (browser verification) | Verify rendered page after deployment |
| Build plan | User review | `accounts/<PROFILE>/features/<slug>/plan.md` |

## Post-Execution: Feedback Capture

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

1. **Classify outcome:** `SUCCESS` (completed cleanly), `PARTIAL_SUCCESS` (completed 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-mystique-builder"`
   - `feature`: feature slug if applicable, null otherwise
   - `phases`: trace of each phase 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-mystique-builder.jsonl`
5. **Update** `accounts/<PROFILE>/feedback/index.json` counters (create with `mkdir -p` if missing)

**Skip capture if:** The execution was trivially simple (e.g., user asked a question, no actual write operation happened).

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

### `dataSource` must be OUTSIDE `props` (HIGH — unreliable when inside)

**Wrong** (may work via fallback but unreliable):
```json
{ "component": "fc.list", "props": { "dataSource": "items", "attributes": [...] } }
```

**Correct** (`dataSource` is sibling of `component` and `props`):
```json
{ "component": "fc.list", "dataSource": "items", "props": { "attributes": [...] } }
```

**Source code nuance:** `Children.tsx` has a fallback: `comp.dataSource || comp.props?.['dataSource']` — so placing it in props MAY work through the standard `<Children />` rendering path. However, components that render descendants directly (bypassing `<Children />`) will NOT pick it up. The canonical TypeScript interface `MystiqueComponentInstance` defines `dataSource` as a peer of `component` and `props`. Always use the canonical placement. `/fluent-mystique-validate` checks this as rule S16.

**Where data config goes:**
- `dataSource` → sibling of `component`, `props`, `descendants` (on the component instance object)
- `data.query` / `dataSources` → on the page object (data fetching)
- Everything else component-specific → inside `props`

### Fragment routes MUST be wrapped in a section

Fragments deployed as settings (e.g., `fc.mystique.manifest.store`) must have their pages inside a `type: "section"` wrapper with `nav: { label, icon }`. **Pages placed directly in `routes[]` without a section wrapper will NOT appear in the sidebar.** Mystique builds the sidebar menu exclusively from section `nav` objects.

**Wrong — pages disappear from sidebar:**
```json
{
  "manifestVersion": "2.0",
  "routes": [
    { "type": "page", "path": "stores", "component": "fc.page", "props": {...} },
    { "type": "page", "path": "waves", "component": "fc.page", "props": {...} }
  ]
}
```

**Correct — section wrapper with nav:**
```json
{
  "manifestVersion": "2.0",
  "routes": [
    {
      "type": "section",
      "nav": { "label": "i18n:fc.store.nav", "icon": "domain" },
      "pages": [
        { "type": "page", "path": "stores", "nav": { "label": "Stores", "icon": "store" }, ... },
        { "type": "page", "path": "waves", "nav": { "label": "Waves", "icon": "waves" }, ... }
      ]
    }
  ]
}
```

Compare against the CDN baseline fragment (`/fluent-mystique-diff`) to verify section structure matches. `/fluent-mystique-validate` checks this as rule S17.

### Connection paths require manual navigation (no auto-unwrap)

The framework's `getIndex()` uses simple dot-splitting: `index.split('.').reduce((o, i) => o[i] ?? {}, obj)`. It does **NOT** auto-unwrap GraphQL `edges[].node` connections. You must specify the correct path manually.

**For `fc.list` bound to a connection:**
```json
{ "component": "fc.list", "dataSource": "orderById.items", "props": { "attributes": [...] } }
```
The `fc.list` component internally handles the `edges[].node` unwrapping — do NOT include `.edges` in your dataSource path. But for template expressions within list rows, you reference fields directly: `"{{node.ref}}"` (each row's context is a single node).

**For nested connections or direct access:**
```json
{ "component": "fc.card.attribute", "dataSource": "orderById", "props": { "attributes": [
  { "label": "Item count", "value": "{{items.edges.length}}" }
] } }
```
Here you must navigate the full path including `edges` since you're accessing the raw connection structure outside a list component.

<!-- end feedback-promoted -->
