---
name: fluent-settings
description: Manage Fluent Commerce settings — discover, audit, create, update, and migrate retailer-scoped settings. Cross-reference workflow rules with settings to find gaps. Triggers on "manage settings", "create setting", "audit settings", "settings migration", "missing settings", "webhook settings".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: [--audit <workflow-file>] [--context RETAILER|ACCOUNT|LOCATION] [--migrate --from <profile> --to <profile>]
cursor-tier: manual
planning-gate: mandatory
---

# Settings Lifecycle Management

Manage Fluent Commerce settings that configure how workflow rules behave at runtime. Settings are the critical glue between deployed workflows and working business logic — when a setting is missing, rules fail silently, orders get stuck, and webhooks never fire.

## Pre-flight: Feedback Check

Before starting, check for past learnings from this skill:

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

## Pre-flight: Project Knowledge Check

Check for project-specific knowledge that should inform settings management:

1. Check if `accounts/<PROFILE>/knowledge/` exists
2. If yes, read it and find files where `relevant-skills` includes `fluent-settings`
   **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, orchestrable classification, settings context scoping
   - `accounts/<PROFILE>/knowledge/naming-conventions.md` — use for settings key naming conventions
   - `accounts/<PROFILE>/knowledge/client/data-model.md` — use for entity types, retailer context
   - `accounts/<PROFILE>/knowledge/client/integrations.md` — use for webhook URLs, external system configs
4. If `accounts/<PROFILE>/memory/index.md` exists, read the top account-specific settings and integration 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 updating any settings, run the three-level pre-flight check:

1. **Spec check:** If `accounts/<PROFILE>/features/<slug>/status.json` exists for this work:
   - Check `spec: "APPROVED"` — If missing, BLOCK: `→ BLOCKED: PREREQ_MISSING — Run /fluent-use-case-discover first`
   - Check `plan: "APPROVED"` — If missing, BLOCK: `→ BLOCKED: PLAN_REQUIRED — Run /fluent-feature-plan first`
   - If both approved, skip to implementation referencing the plan's §9 (Settings) section
2. **Multi-artifact work** (settings + rules + workflows): STOP. Invoke `/fluent-feature-plan` first — this skill cannot be used for multi-artifact work without a feature plan
3. **Standalone settings-only work** (no feature context, no multi-artifact):
   - Check `accounts/<PROFILE>/tasks/` for a task plan with `Status: APPROVED`
   - If approved task plan found, skip to implementation
   - If no plan, continue to the Planning Gate below to write a plan for this skill
4. **Read-only operations** (audit, discover, list) are always exempt from planning

**Before creating or updating any settings, write a plan using the template from `PLAN_TEMPLATE.md` in the `fluent-feature-plan` skill.** Every setting row must carry a Source column (NEW/EXISTING/MODIFIED). Read-only operations (audit, discover, list) are exempt.

**Settings-specific emphasis — ensure these are covered:**

1. **Business Context (Section 1)** — why these settings are needed; what workflow behavior they enable
2. **Settings impact (Section 4.4)** — full table: setting key, context (RETAILER/ACCOUNT/LOCATION), contextId, action (create/update), value/shape, which ruleset reads it. Show **current → new** value for updates
3. **Webhooks (Section 4.5)** — if creating webhook URL settings: endpoint, HTTP method, payload shape, which ruleset fires it
4. **Impacted rulesets (Section 4.3)** — which workflow rules depend on each setting; what behavior changes
5. **Impacted retailers (Section 4.8)** — settings take effect immediately; confirm target environment
6. **Risks (Section 7)** — wrong values can break live flows; note immediate-effect risk

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

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

## Handoff Protocol

### Signals emitted by this skill

| Signal | Condition | Example |
|--------|-----------|---------|
| `-> READY: <path>` | Settings applied | `-> READY: 3 settings upserted for retailer MY_RETAILER` |
| `-> NEXT: /fluent-<skill>` | Next skill in plan | `-> NEXT: /fluent-e2e-test` |
| `-> BLOCKED: <reason>` | Cannot proceed | `-> BLOCKED: ENV_UNREACHABLE — Cannot connect to Fluent environment` |
| `-> SKIP: <reason>` | Read-only audit, no changes | `-> SKIP: Audit mode — no settings changes needed` |

### Error codes

| Code | Condition | Recovery |
|------|-----------|----------|
| `PLAN_REQUIRED` | Settings creation attempted without approved plan | Run `/fluent-feature-plan` first |
| `VALIDATION_FAILED` | Setting value format invalid or context missing | Fix setting values and retry |
| `ENV_UNREACHABLE` | Cannot connect to Fluent environment | Check credentials via `config.validate` |

## Ownership Boundary

This skill owns Settings CRUD, discovery, audit, and migration.

Workflow rule analysis is owned by `/fluent-workflow-analyzer`.
MCP tool syntax is owned by `/fluent-mcp-tools`.
Workflow creation and editing guidance is owned by `/fluent-workflow-builder`.
Entity test data is owned by `/fluent-test-data`.
Module deployment is owned by `/fluent-module-deploy`.

## When to Use

- After deploying a workflow or module — verify all required settings exist
- Setting up a new retailer — create required settings for workflows to function
- Debugging "rule did nothing" failures — missing settings are the #1 silent failure cause
- Migrating settings between environments (dev to staging to prod)
- Auditing settings completeness before go-live (Ready For Launch)
- Investigating webhook or carrier integration failures — the root cause is almost always a missing or malformed setting

## Settings Model in Fluent

Settings are key-value entities that configure rule behavior at runtime. They are NOT part of the workflow JSON — they live in the Fluent platform as separate entities queried by rules during execution.

### Entity Structure

A Setting has these queryable fields:

| Field | Type | Description |
|-------|------|-------------|
| `id` | String | Platform-assigned unique ID |
| `name` | String | The reference key (e.g., `webhook.order.created`) — this is what rules look up |
| `value` | String | The setting value (for STRING/INTEGER/BOOLEAN types). Returns empty string for LOB types |
| `lobValue` | String | Large object value — JSON string (for LOB-type settings only). Empty for non-LOB types |
| `valueType` | String | `STRING`, `LOB`, `INTEGER`, `BOOLEAN`, or `JSON` |
| `context` | String | Scope type: `RETAILER`, `ACCOUNT`, `GLOBAL`, `AGENT`, or `CUSTOMER` |
| `contextId` | Int | ID of the scoped entity (e.g., retailer ID `2` when context is `RETAILER`). `0` for GLOBAL/ACCOUNT |

**Important:** `context` and `contextId` are **separate fields**. The `context` is a plain string like `"RETAILER"` — NOT `"RETAILER:2"`. The entity ID goes in `contextId` as an integer.

### Context Types

Settings are scoped using two separate fields — `context` (scope type string) and `contextId` (scoped entity ID integer):

| Context | `contextId` | Use Case |
|---------|-------------|----------|
| `RETAILER` | retailer ID (e.g., `2`) | Most common — webhook URLs, carrier configs, routing strategy |
| `ACCOUNT` | `0` | Account-wide defaults that apply across retailers |
| `GLOBAL` | `0` | Platform-wide (e.g., sourcing criteria) |
| `AGENT` | agent/user ID | Agent-specific settings |
| `CUSTOMER` | customer ID | Customer-specific preferences |

When a rule reads a setting, it resolves in order of specificity: AGENT > RETAILER > ACCOUNT > GLOBAL. A more specific setting overrides a broader one with the same name.

### How Rules Access Settings

Rules access settings through the `SettingUtils.getSettingByRef(context, settingRef)` API. The rule reads a `setting` prop from the workflow ruleset configuration, then queries the platform for that setting name. If the setting does not exist, the rule typically throws a null pointer exception or silently skips its logic — both result in the entity not advancing.

## Phase 1: Discovery — List All Settings

### List All Settings for a Retailer

Use MCP `setting.get` for quick discovery (supports `%` wildcards), `graphql.query` for custom queries, or `graphql.queryAll` for retailers with many settings:

**Quick discovery with `setting.get`:**
```json
{ "name": "fc.mystique.manifest.%" }
```
Returns all manifest settings with metadata (name, context, valueType, size). Add `outputFile` to save large values to disk:
```json
{ "name": "fc.mystique.manifest.oms", "outputFile": "./accounts/<PROFILE>/manifests/backups/oms.json" }
```

**Full query with GraphQL:**

```graphql
query($cursor: String) {
  settings(first: 100, after: $cursor, context: "RETAILER") {
    edges {
      cursor
      node {
        id
        name
        value
        valueType
        context
        contextId
        lobValue
      }
    }
    pageInfo { hasNextPage }
  }
}
```

**Important:** The `context` argument is a plain **String** — not an object. Valid values: `"RETAILER"`, `"ACCOUNT"`, `"GLOBAL"`, `"AGENT"`, `"CUSTOMER"`. Using `{ contextType: "..." }` will fail with a `WrongType` validation error.

For auto-pagination across all settings:

```
graphql.queryAll({
  query: "query($cursor: String) { settings(first: 100, after: $cursor, context: \"RETAILER\") { edges { cursor node { id name value valueType context contextId lobValue } } pageInfo { hasNextPage } } }",
  variables: { cursor: null },
  maxRecords: 5000
})
```

### Search Settings by Exact Name

The `name` filter accepts an array of exact names:

```graphql
{
  settings(first: 1, name: ["webhook.order.created"], context: "RETAILER") {
    edges {
      node {
        id
        name
        value
        valueType
        context
        contextId
        lobValue
      }
    }
  }
}
```

### Search Settings by Name Pattern

Fluent does not support wildcard/regex in the `name` filter. To find settings matching a pattern (e.g., all webhook settings), query all settings and filter client-side:

```
1. graphql.queryAll to fetch all settings
2. Filter results where name starts with "webhook." or contains the target pattern
```

### List Settings at a Specific Context

```graphql
{
  settings(first: 50, context: "ACCOUNT") {
    edges {
      node {
        id
        name
        value
        valueType
        context
        contextId
      }
    }
  }
}
```

Replace `"ACCOUNT"` with `"GLOBAL"`, `"AGENT"`, or `"CUSTOMER"` as needed.

## Phase 2: Audit — Cross-Reference with Workflow Rules

This is the most valuable operation. Missing settings are the number one cause of silent rule failures in Fluent Commerce. After every workflow deployment, run this audit.

### Extraction Algorithm

Parse the workflow JSON file and extract all rule `props` that reference settings:

```
For each ruleset in workflow JSON:
  For each rule in ruleset.rules:
    1. If rule.name contains "SendWebhook" or "SendWebhookWithDynamicAttributes":
       Extract props["setting"] or props["webhookSettingName"] → required setting name
       Tag: expectedType=LOB (webhook settings are always JSON with a "url" field)
    2. If rule.name contains "UpdateEntityFromSetting":
       Extract props["setting"] or props["settingName"] → required setting name
       Tag: expectedType=LOB (entity update mappings are JSON)
    3. If any prop key matches: "setting", "settingName", "settingRef", "settingsKey", "webhookSettingName":
       Extract the prop value → required setting name
    4. If rule.name contains "SettingUtils" or "GetSetting":
       Extract the setting reference from props

  Record: { settingName, referencedBy: rulesetName, ruleName, propKey, expectedType? }
```

### Parameter-Level Discovery via plugin.list

The prop extraction above catches explicit setting references. To find implicit ones (settings accessed inside rule logic but not named in obvious prop keys), query the live rule registry:

```
For each unique rule name in the workflow:
  plugin.list({ name: "<RuleShortName>" })
  For each parameter in the response:
    If parameter.description mentions "setting" or "config":
      → Cross-reference parameter.name with the rule's props in the workflow
      → If the prop value looks like a setting name: add to extraction list
    If parameter.name matches: settingName, setting, settingRef, configSetting:
      → Extract the prop value from the workflow rule → add to extraction list
```

This catches rules that read settings internally but expose the setting name as a configurable parameter.

### Common Rule Patterns That Depend on Settings

| Rule Pattern | Prop That References Setting | What the Setting Contains |
|---|---|---|
| `SendWebhook` | `setting` or `webhookSettingName` | Webhook URL, headers, method |
| `SendWebhookWithDynamicAttributes` | `setting` | Webhook URL + dynamic attribute mapping |
| `UpdateEntityFromSetting` | `setting` | Entity field update mapping |
| `GetSettingValue` | `settingName` | Arbitrary config value |
| Any rule using `SettingUtils.getSettingByRef` | `setting` / `settingRef` | Rule-specific configuration |

### Verification Query

For each referenced setting name, query the platform to check if it exists:

```graphql
{
  settings(first: 1, name: ["<SETTING_NAME>"], context: "RETAILER") {
    edges {
      node {
        id
        name
        value
        lobValue
        valueType
      }
    }
  }
}
```

If the edges array is empty, the setting is **MISSING**.

### Extended Validation (Beyond Existence)

For each setting that exists, perform additional checks:

```
1. TYPE CHECK — Does the valueType match what the rule expects?
   - Webhook rules (SendWebhook*) expect LOB → if setting is STRING, flag TYPE_MISMATCH
   - Config rules (GetSettingValue) can accept STRING, BOOLEAN, or LOB
   - Entity update rules (UpdateEntityFromSetting) expect LOB → flag if STRING

2. CONTENT CHECK — Is the value meaningful?
   - LOB webhook settings: lobValue must contain a "url" field → flag if missing
   - STRING settings: value must be non-empty → flag EMPTY_VALUE if blank
   - BOOLEAN settings: value must be "true" or "false" → flag INVALID_VALUE otherwise

3. CONTEXT CHECK — Is the setting at the right scope?
   - Custom rules (e.g., SendWebhookWithDynamicAttributes) typically resolve settings using cascading scope: RETAILER first, then ACCOUNT
   - Module-installed settings often default to ACCOUNT scope (contextId=0)
   - **Always use cascading scope resolution when auditing**: query RETAILER first; if empty, query ACCOUNT; report which scope the setting was found at
   - If setting exists only at ACCOUNT scope, this is typically correct for module-installed settings — flag as INFO, not WRONG_CONTEXT
   - Query at multiple contexts:
     settings(name: ["<NAME>"], context: "RETAILER", contextId: <retailerId>)
     settings(name: ["<NAME>"], context: "ACCOUNT", contextId: 0)
```

### Audit Report Format

```
=== Settings Audit: ORDER::HD (v1.50) ===

Ruleset                    Rule                                Setting Name                 Status
────────────────────────────────────────────────────────────────────────────────────────────────────
OrderCreatedWebhook        *.SendWebhookWithDynamicAttributes  webhook.order.created        OK (LOB)
OrderCancelledWebhook      *.SendWebhookWithDynamicAttributes  webhook.order.cancelled      OK (LOB)
FulfilmentShippedWebhook   *.SendWebhookWithDynamicAttributes  webhook.fulfilment.shipped   MISSING       [HIGH]
UpdateOrderFromWebhook     *.UpdateEntityFromSetting           order.update.mapping         MISSING       [HIGH]
ConsignmentCreated         *.GetSettingValue                   consignment.prefix           OK (STRING)
CarrierRouting             *.GetSettingValue                   carrier.hd.config            TYPE_MISMATCH [MED]
  → Expected: LOB, Actual: STRING — rule reads lobValue which will be null

Summary: 3/6 valid, 2 MISSING, 1 TYPE_MISMATCH
Priority: 2 HIGH (silent failures), 1 MEDIUM (wrong type)
```

### Severity Classification

| Status | Severity | Impact |
|--------|----------|--------|
| OK | None | Setting will be read correctly |
| MISSING | HIGH | Rule will fail silently — entity gets stuck |
| TYPE_MISMATCH | MEDIUM | Rule reads wrong field (e.g., `lobValue` is null for STRING type) |
| EMPTY_VALUE | LOW | Setting exists but value is null/empty — behavior depends on rule |
| WRONG_CONTEXT | MEDIUM | Setting exists at ACCOUNT but rule expects RETAILER scope |

Flag every MISSING setting as HIGH priority. Missing settings cause rules to fail silently — the event processes as SUCCESS but the rule does nothing, leaving the entity stuck.

### Phase 2B: Conformance Mode (Workflow + Runtime Evidence)

Use this mode when you need more than existence checks. It compares:

1. **Static contract**: setting refs extracted from workflow rule props
2. **Live config**: settings currently stored in Fluent
3. **Runtime evidence**: what was actually used during orchestration/webhook actions

#### Conformance Statuses

| Status | Meaning |
|---|---|
| `FOUND` | Setting exists with plausible value shape |
| `MISSING` | Setting not found at expected context |
| `EMPTY` | Setting exists but value/lobValue empty |
| `INVALID_FORMAT` | Value exists but invalid shape (for example webhook LOB missing `url`) |
| `CONTRACT_MISMATCH` | Runtime behavior conflicts with configured value (for example observed webhook endpoint differs from setting URL) |

#### Runtime Cross-Check Pattern

After extracting setting refs from the workflow:

1. Query live setting values (`settings(... name: [...])`).
2. Query orchestration audit for webhook actions on a target entity:

   > **Event filter reference:** For complete filter syntax, see `/fluent-event-api`. The examples below show settings-specific patterns.

   ```
   event.list({
     "eventType": "ORCHESTRATION_AUDIT",
     "category": "ACTION",
     "context.rootEntityRef": "<ENTITY_REF>",
     "count": 100
   })
   ```
3. Compare configured URL vs observed `Request Endpoint` in audit attributes.
4. Emit `CONTRACT_MISMATCH` if they differ.

#### Conformance Output Format

```
=== Settings Conformance: ORDER::MULTI ===

Setting                               Referenced By                               Status               Evidence
-------------------------------------------------------------------------------------------------------------------
webhook.order.created                 ORDER.CREATE                                FOUND                id=365, valueType=LOB
webhook.carrier.shipment.request      FULFILMENT.RequestShipment                  CONTRACT_MISMATCH    configured=https://x, observed=https://y
update.fulfilment.confirmPick         FULFILMENT.ConfirmPick                      MISSING              no RETAILER setting found

Summary: FOUND=1 MISSING=1 CONTRACT_MISMATCH=1
```

### Phase 2B: Value Format Validation

Missing settings cause silent failures, but **wrong settings** are equally dangerous. A setting that exists but has the wrong `valueType`, malformed JSON, or a missing required field will also cause rules to fail silently. After confirming a setting exists, validate its value matches the expected format for the rule that references it.

#### Webhook Setting Validation

Settings referenced by `SendWebhook` or `SendWebhookWithDynamicAttributes` rules must meet all of the following:

| Check | Expected | Severity |
|-------|----------|----------|
| `valueType` is `LOB` | Required — webhook config is always JSON | HIGH |
| `lobValue` is valid JSON | Must parse without errors; must not be null or empty string | HIGH |
| `lobValue` contains `url` field | String, well-formed URL (starts with `http://` or `https://`) | HIGH |
| `url` uses HTTPS | HTTP URLs in non-dev environments are a security risk | WARN |
| `url` is not empty | Empty string URL causes silent delivery failure | HIGH |
| Optional: `headers` field | Object (key-value pairs) if present | INFO |
| Optional: `method` field | String, typically `POST` (default if absent) | INFO |

Example of a valid webhook setting value:

```json
{"url": "https://api.example.com/webhook/order-created", "headers": {"X-Api-Key": "key-123"}, "method": "POST"}
```

Common failures:
- `valueType` is `STRING` instead of `LOB` — rule reads `lobValue` which returns null
- `lobValue` is a stringified JSON (`"{\"url\":...}"`) instead of a parsed object — double-encoded
- `url` field is missing entirely — webhook sends to null endpoint
- `url` is placeholder (`https://example.com` or `TODO`) — webhook returns 404

#### Entity Update Setting Validation

Settings referenced by `UpdateEntityFromSetting` rules must meet:

| Check | Expected | Severity |
|-------|----------|----------|
| `valueType` is `LOB` | Required — field mappings are JSON | HIGH |
| `lobValue` is valid JSON | Must parse without errors | HIGH |
| `lobValue` contains field mapping structure | Should have recognizable field names or mapping keys | WARN |
| `lobValue` is not empty object `{}` | Empty mapping means rule does nothing | WARN |

#### General Format Checks (All Settings)

Apply these checks to every setting regardless of which rule references it:

| valueType | Validation Rule | Severity |
|-----------|----------------|----------|
| `STRING` | `value` is non-empty (not null, not `""`) | WARN |
| `LOB` | `lobValue` is valid JSON (not null, not empty string, not `"null"`) | HIGH |
| `BOOLEAN` | `value` is exactly `"true"` or `"false"` (case-sensitive) | HIGH |
| `INTEGER` | `value` is a numeric string (parseable as integer) | HIGH |
| `JSON` | `lobValue` is valid JSON | HIGH |

**valueType mismatch detection:** If a rule pattern expects LOB (e.g., `SendWebhook*`, `UpdateEntityFromSetting`) but the setting has `valueType: "STRING"`, flag as HIGH. The rule will read `lobValue` which returns null for STRING-type settings.

#### Enhanced Audit Report Format

When value validation is performed, extend the audit report with `valueType` and `Value Check` columns:

```
=== Settings Audit: ORDER::HD (v1.50) ===

Ruleset                              Rule                                          Setting Name                    Status    valueType  Value Check
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
OrderCreatedWebhook                  ACCT.ext.SendWebhookWithDynamicAttributes     webhook.order.created           FOUND     LOB        OK (url=https://api.example.com/order/created)
OrderCancelledWebhook                ACCT.ext.SendWebhookWithDynamicAttributes     webhook.order.cancelled         FOUND     LOB        WARN: url is HTTP not HTTPS
FulfilmentShippedWebhook             ACCT.ext.SendWebhookWithDynamicAttributes     webhook.fulfilment.shipped      MISSING   -          -  [HIGH]
UpdateOrderFromWebhook               ACCT.ext.UpdateEntityFromSetting              order.update.mapping            FOUND     STRING     ERROR: expected LOB for UpdateEntityFromSetting [HIGH]
ConsignmentCreated                   ACCT.ext.GetSettingValue                      consignment.prefix              FOUND     STRING     OK (value="A_")
FeatureFlagCheck                     ACCT.ext.GetSettingValue                      feature.express.enabled         FOUND     BOOLEAN    OK (value="true")

Summary: 5/6 settings present, 1 MISSING
Value issues: 1 ERROR (wrong valueType), 1 WARN (HTTP URL)
Priority: 2 HIGH — missing setting + valueType mismatch will cause silent rule failures
```

#### Validation Query

To validate values, the verification query from Phase 2 already returns `value`, `lobValue`, and `valueType`. After checking existence (edges not empty), apply format checks:

```
For each FOUND setting:
  1. Read valueType, value, lobValue from query result
  2. Apply general format checks for the valueType
  3. If referenced by SendWebhook* rule:
     - Verify valueType == "LOB"
     - Parse lobValue as JSON
     - Check for url field (non-empty, well-formed)
     - Flag HTTP URLs in non-dev environments
  4. If referenced by UpdateEntityFromSetting rule:
     - Verify valueType == "LOB"
     - Parse lobValue as JSON
     - Check for non-empty mapping structure
  5. Record: { settingName, valueType, valueCheck: "OK" | "WARN: ..." | "ERROR: ..." }
```

#### Cross-Reference with Connection Analysis

When invoked from `/fluent-connection-analysis`, use the webhook/setting dependency inventory from the connection analysis as the extraction source instead of re-parsing the workflow JSON. The connection analysis already identifies which rules depend on which settings and classifies them by type (webhook, entity-update, general). Pass that inventory directly into Phase 2 + 2B to avoid duplicate parsing:

```
1. Receive dependency inventory from connection analysis:
   [{ settingName, ruleType, ruleName, rulesetName }]
2. Skip the extraction algorithm (Phase 2 step 1)
3. Run existence check (Phase 2 verification query) for each setting
4. Run value format validation (Phase 2B) for each FOUND setting
5. Return combined audit report with Value Check column
```

## Phase 2D: Settings Conflict Detection

Before creating or updating settings, check for ownership conflicts across features.

### What is a Conflict?

A **settings conflict** occurs when:
1. Two or more features declare the same setting key in their plans
2. A setting being created already exists with a different value than expected
3. Multiple feature plans reference the same setting key with different intended values

### Detection Protocol

**Step 1: Cross-feature scan**
Scan all `accounts/<PROFILE>/features/*/plan.md` for settings tables (§9 Settings section). Extract all setting keys mentioned across all plans.

**Step 2: Identify overlaps**
If the same setting key appears in multiple feature plans:
- Flag as `CONFLICT` if the planned values differ
- Flag as `SHARED` if the planned values match (same value, multiple consumers — OK)
- Flag as `OWNED` if only one plan references it

**Step 3: Live environment check**
Before `setting.upsert`, query the current value using MCP `setting.get` (preferred) or GraphQL:

**Preferred — MCP `setting.get`:**
```
setting.get({ name: "<KEY>", context: "<CONTEXT>" })
```
For large settings (manifests), use `outputFile` to save to backup and keep out of context:
```
setting.get({ name: "<KEY>", outputFile: "accounts/<PROFILE>/settings/backups/<KEY>_<YYYY-MM-DD>.json" })
```

**Alternative — GraphQL:**
```graphql
{
  settings(first: 1, name: ["<KEY>"], context: "<CONTEXT>") {
    edges { node { id name value lobValue valueType contextId } }
  }
}
```
Compare existing value to planned value:
- If no existing value → safe to create (`NEW`)
- If existing value matches planned value → skip (`ALREADY_SET`)
- If existing value differs from planned → flag as `OVERWRITE_WARNING`

**Step 4: Present conflicts**
```markdown
### Settings Conflict Report

| Key | Context | Feature A (planned) | Feature B (planned) | Live Value | Status |
|-----|---------|--------------------|--------------------|------------|--------|
| WEBHOOK_ENDPOINT_ORDER | RETAILER:5 | https://a.com/hook | https://b.com/hook | https://a.com/hook | CONFLICT |
| ORDER_PREFIX | RETAILER:5 | HD- | HD- | HD- | SHARED |
| NEW_SETTING | RETAILER:5 | value1 | — | (none) | OWNED |
```

**Step 5: Resolution**
- `CONFLICT`: STOP. Present to user with both feature names. User must decide which value wins.
- `OVERWRITE_WARNING`: WARN. Show current → new diff. User must confirm.
- `SHARED` / `OWNED` / `NEW`: Proceed normally.

### Handoff
If conflicts are found:
```
-> BLOCKED: Settings conflict on <N> keys. User resolution required.
```

### Output Validation (Automated)

**The implementation critique protocol lives at: `knowledge/platform/implementation-critique-protocol.md`.** Select the **Settings Rubric** (ST-1 through ST-5) and follow the protocol exactly.

After preparing setting values (before any write/deploy), run the adversarial validation loop (max 2 rounds). The critic validates: setting parameters (JSON/ACCOUNT/0), lobValue for large values, dropdown format (`{ "active": [...] }` wrapper), no secrets in values, and plan alignment.

**This runs before the Backup Protocol and actual deployment.** Do not skip.

## Backup Protocol (MANDATORY before any write)

**Settings have NO versioning — updates overwrite immediately and the previous value is lost.** Always backup before writing.

### Before ANY create/update/upsert/batchMutate:

**Step 1 — Query and save current state:**

For each setting being modified, query the current value:

```graphql
{
  settings(first: 1, name: ["<SETTING_NAME>"], context: "<CONTEXT>") {
    edges { node { id name value lobValue valueType context contextId } }
  }
}
```

Save the full response to a local backup file:

```
accounts/<PROFILE>/settings/backups/<setting-name>_<YYYY-MM-DD_HHmmss>.json
```

For bulk operations, save all affected settings in a single backup file:

```
accounts/<PROFILE>/settings/backups/bulk_<slug>_<YYYY-MM-DD_HHmmss>.json
```

**If the setting does not exist yet (new setting), note "NEW — no backup needed" in the approval.**

**Step 2 — Show user what will change:**

For updates, present a clear diff:

```
Setting: fc.webhook.order.created (ID: 365)
Context: RETAILER:2
Current value: {"url": "https://old-endpoint.com/webhook"}
New value:     {"url": "https://new-endpoint.com/webhook"}
Backup: accounts/<PROFILE>/settings/backups/fc.webhook.order.created_2026-02-26_143000.json
```

**Wait for explicit user approval before proceeding.**

**Step 3 — Choose write method based on payload size (MANDATORY):**

| Setting size | Method | Why |
|---|---|---|
| **>4KB** (manifest fragments, large JSON configs) | CLI `fluent module install --include settings --force` | Reads from local file — zero LLM context cost. Fragments are 10-120KB. |
| **<4KB** (root manifests, small settings, feature flags) | MCP `setting.upsert` or GraphQL mutation | Inline is fine for small payloads. |
| **Multiple settings at once** | CLI `fluent module install --include settings --force` | 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 and may hit parameter size limits. Use CLI which reads JSON from local file on disk.

**CLI deploy pattern:**
```bash
DEPLOY=$(mktemp -d)
mkdir -p "$DEPLOY/assets/settings"
# Build manifests.json from local file content
node -e "const v=require('fs').readFileSync('<local-file>','utf8'); const s=[{name:'<KEY>',valueType:'JSON',context:'ACCOUNT',contextId:0,lobValue:JSON.parse(v)}]; require('fs').writeFileSync('$DEPLOY/assets/settings/manifests.json',JSON.stringify(s,null,2))"
echo '{"_schema":"1.0.0","name":"settings-deploy","version":"1.0.0"}' > "$DEPLOY/module.json"
echo '{}' > "$DEPLOY/module.config.json"
fluent module install "$DEPLOY" -p <PROFILE> -r <RETAILER> --include settings --force
rm -rf "$DEPLOY"
```

**Step 4 — Verify** by re-querying the setting to confirm the new value took effect.

### Rollback procedure

If a setting change causes issues:

1. Read the backup file from `accounts/<PROFILE>/settings/backups/<name>_<timestamp>.json`
2. Extract the `value` or `lobValue` and `id` from the backup
3. Execute `updateSetting` with the backup values (requires `id`, `valueType`, `context`, `contextId`)
4. Verify by re-querying

For new settings that need to be removed: check `graphql.introspect` for `deleteSetting`. If unavailable, update the value to empty string or a known "disabled" marker.

**Backups are never auto-deleted.** They accumulate for audit trail purposes. Use `/fluent-session export` to archive.

### Manifest settings (MANDATORY guardrails — a bad deploy breaks the entire UI)

Manifest settings (`fc.mystique.manifest.*`) have FIXED parameters that MUST NOT vary:

| Field | Value | Notes |
|---|---|---|
| `valueType` | `"JSON"` | ALWAYS JSON. Never LOB, never STRING. |
| `context` | `"ACCOUNT"` | ALWAYS ACCOUNT. Never RETAILER. |
| `contextId` | `0` | ALWAYS 0. |
| `lobValue` | `{...}` raw JSON object | NEVER use `value` (255 char limit, silently truncates). `lobValue` must be a raw JSON object, NOT `JSON.stringify()`'d. Double-encoding causes Mystique to reject the fragment silently ("invalid route"). |

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

**lobValue double-encoding trap:** When building GraphQL mutation variables or deploy scripts, pass `lobValue: parsedObject` (the raw JS object). NEVER pass `lobValue: JSON.stringify(parsedObject)`. The outer serialization handles encoding — pre-stringifying double-encodes the value as a string, which Mystique rejects with "Manifest contains an invalid route" and the sidebar section disappears.

Additional mandatory steps:
- **ALWAYS** backup the existing manifest before overwriting
- **ALWAYS** validate with `/fluent-mystique-validate` before deploying
- **ALWAYS** ask user for explicit approval before creating/updating — present: setting name, action, what changed, backup path, rollback instructions
- **ALWAYS** verify in browser after deploying (via `/fluent-ui-test` or manually)
- Prefer `fluent module install --include settings --force` for deploys — it's repeatable and CI/CD-friendly
- Existing settings with `valueType: "LOB"` or `context: "RETAILER"` are legacy — do NOT replicate their pattern

### Large payload transport protocol (MANDATORY)

For large JSON payloads (manifest fragments, long setting objects), use a local-file deployment path:

1. Query/export current value and save backup under `accounts/<PROFILE>/settings/backups/` (or `accounts/<PROFILE>/manifests/backups/` for manifests).
2. Edit JSON locally (file-based patching only).
3. Package into a settings-only module (`assets/settings/settings.json`).
4. Deploy with `fluent module install <module-dir> -p <PROFILE> -r <RETAILER> --include settings --force`.

Avoid sending very large inline mutation payloads through ad-hoc MCP/tool calls unless there is no alternative. Large inline payloads are harder to review, brittle in terminals, and risky for rollback.

See `/fluent-mystique-builder` for the full manifest safety guardrail protocol.

## Phase 3: CRUD Operations

### Schema Reference (from live introspection)

Both `CreateSettingInput` and `UpdateSettingInput` use `context` (String) + `contextId` (Int) as **separate fields**:

| Field | Create | Update | Type | Description |
|-------|:------:|:------:|------|-------------|
| `id` | — | **required** | `ID!` | Setting ID (query first to obtain) |
| `name` | **required** | optional | `String!` / `String` | Setting reference key |
| `valueType` | **required** | **required** | `String!` | `LOB`, `STRING`, `INTEGER`, `BOOLEAN`, or `JSON` |
| `value` | optional | optional | `String` | Use for non-JSON values (STRING, INTEGER, BOOLEAN) |
| `lobValue` | optional | optional | `Json` | Use for JSON values (LOB, JSON valueTypes). Pass as a JSON object, not a string |
| `context` | **required** | **required** | `String!` | `RETAILER`, `ACCOUNT`, `AGENT`, or `CUSTOMER` |
| `contextId` | **required** | **required** | `Int!` | ID of the context entity (e.g., retailer ID as integer) |

**Key differences from what you might expect:**
- `context` is a plain string (`"RETAILER"`), NOT `"RETAILER:2"` or `{ contextType: "RETAILER" }`
- `contextId` is a separate **Int** field (e.g., `2`), not embedded in the context string
- There is NO `retailer: { id }` field on settings — use `context` + `contextId` instead
- `valueType` is **required even for updates** — you must always specify it
- `lobValue` accepts a **Json object** on input (not a string) — pass `{"url": "..."}` not `"{\"url\": \"...\"}"`

### Create Setting

```graphql
mutation($input: CreateSettingInput!) {
  createSetting(input: $input) {
    id
    name
    value
    lobValue
    context
    contextId
    valueType
  }
}
```

**Variables for a LOB setting (webhook config):**

```json
{
  "input": {
    "name": "fc.webhook.order.created",
    "valueType": "LOB",
    "lobValue": {"url": "https://api.example.com/webhook/order-created", "headers": {"X-Api-Key": "your-api-key"}, "method": "POST"},
    "context": "RETAILER",
    "contextId": 2
  }
}
```

**Variables for a STRING setting:**

```json
{
  "input": {
    "name": "consignment.prefix",
    "valueType": "STRING",
    "value": "A_",
    "context": "RETAILER",
    "contextId": 2
  }
}
```

**Variables for a BOOLEAN setting:**

```json
{
  "input": {
    "name": "GLOBAL_INVENTORY_ENABLED",
    "valueType": "BOOLEAN",
    "value": "true",
    "context": "RETAILER",
    "contextId": 2
  }
}
```

**Variables for an ACCOUNT-level setting:**

```json
{
  "input": {
    "name": "CANCELLATION_REASONS",
    "valueType": "JSON",
    "lobValue": {"active": [{"name": "Wrong Size", "value": "WRONGSIZE"}, {"name": "Broken", "value": "BROKEN"}]},
    "context": "ACCOUNT",
    "contextId": 0
  }
}
```

Note: Account-level settings use `contextId: 0`.

### Update Setting

Updates overwrite the value immediately. There is no versioning — the previous value is lost. **You MUST complete the Backup Protocol (above) before calling updateSetting.**

**You must supply `id`, `valueType`, `context`, and `contextId` — all four are required for updates.**

```graphql
mutation($input: UpdateSettingInput!) {
  updateSetting(input: $input) {
    id
    name
    value
    lobValue
    context
    contextId
    valueType
  }
}
```

**Update a STRING value:**

```json
{
  "input": {
    "id": "368",
    "valueType": "STRING",
    "value": "new-value",
    "context": "RETAILER",
    "contextId": 2
  }
}
```

**Update a LOB value (e.g., change webhook URL):**

```json
{
  "input": {
    "id": "365",
    "valueType": "LOB",
    "lobValue": {"url": "https://updated-endpoint.com/webhook", "headers": {}, "method": "POST"},
    "context": "RETAILER",
    "contextId": 2
  }
}
```

**Important:** You need the setting `id` to update. Always query the setting by name first to get its ID:

```graphql
{
  settings(first: 1, name: ["fc.webhook.order.created"]) {
    edges { node { id name value lobValue valueType context contextId } }
  }
}
```

### Delete Setting

Check `graphql.introspect` for `deleteSetting` availability — not all Fluent versions expose this mutation. If unavailable, update the value to an empty string or a known "disabled" marker.

### Batch Create Settings

Use `graphql.batchMutate` for creating multiple settings at once (up to 50 per batch):

```json
{
  "mutation": "createSetting",
  "inputs": [
    {
      "name": "fc.webhook.order.created",
      "valueType": "LOB",
      "lobValue": {"url": "https://api.example.com/order/created"},
      "context": "RETAILER",
      "contextId": 2
    },
    {
      "name": "fc.webhook.order.cancelled",
      "valueType": "LOB",
      "lobValue": {"url": "https://api.example.com/order/cancelled"},
      "context": "RETAILER",
      "contextId": 2
    },
    {
      "name": "consignment.prefix",
      "valueType": "STRING",
      "value": "A_",
      "context": "RETAILER",
      "contextId": 2
    }
  ],
  "returnFields": ["id", "name", "value", "context", "contextId", "valueType"]
}
```

### Batch Update Settings

Use `graphql.batchMutate` with `updateSetting` for bulk updates. **All four required fields** (`id`, `valueType`, `context`, `contextId`) must be present on each input:

```json
{
  "mutation": "updateSetting",
  "inputs": [
    {
      "id": "365",
      "valueType": "LOB",
      "lobValue": {"url": "https://new-endpoint.com/order/created"},
      "context": "RETAILER",
      "contextId": 2
    },
    {
      "id": "370",
      "valueType": "STRING",
      "value": "B_",
      "context": "RETAILER",
      "contextId": 2
    }
  ],
  "returnFields": ["id", "name", "value", "context", "contextId", "valueType"]
}
```

### Discover Exact Input Types

Before creating settings, use `graphql.introspect` to discover the exact field requirements for your Fluent version:

```
graphql.introspect({ type: "CreateSettingInput" })
graphql.introspect({ type: "UpdateSettingInput" })
```

Field names and required/optional status can vary across Fluent versions. Always introspect first if a mutation returns a schema error.

## Phase 4: Common Settings Patterns

Document of the most common Fluent settings and their expected value format:

| Setting Pattern | Purpose | valueType | Example Value |
|---|---|---|---|
| `webhook.<entity>.<event>` | Webhook URL for SendWebhook rules | LOB | `{"url":"https://api.example.com/webhook","headers":{"X-Api-Key":"..."},"method":"POST"}` |
| `carrier.<carrier_ref>.config` | Carrier integration config | LOB | `{"apiUrl":"...","apiKey":"...","serviceTypes":["EXPRESS","STANDARD"]}` |
| `fulfilment.routing.config` | Sourcing/routing strategy | LOB | `{"strategy":"CLOSEST","maxDistance":50,"unit":"KM"}` |
| `consignment.prefix` | Consignment reference prefix | STRING | `A_` |
| `cancellation.reasons` | Valid cancellation reasons for UI (served via Transition API) | LOB | `["Out of stock","Customer request","Fraud","Duplicate"]` |
| `notification.<type>` | Email/SMS notification template config | LOB | `{"templateId":"...","channel":"EMAIL","provider":"sendgrid"}` |
| `buffer.time.<entity>` | Processing buffer times | LOB | `{"minutes":30}` |
| `feature.<name>` | Feature flags | STRING | `true` or `false` |
| `order.update.mapping` | Field mapping for UpdateEntityFromSetting rules | LOB | `{"fields":["status","attributes.customField"]}` |
| `sourcing.radius` | Maximum distance for inventory sourcing | STRING | `50` |
| `sourcing.strategy` | Inventory sourcing strategy | STRING | `CLOSEST` or `PRIORITY` |

### HD Workflow Settings Template

A typical Home Delivery workflow requires these settings (add `"context": "RETAILER", "contextId": <retailerId>` to each):

```json
[
  { "name": "webhook.order.created", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/order/created", "method": "POST"} },
  { "name": "webhook.order.booked", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/order/booked", "method": "POST"} },
  { "name": "webhook.order.cancelled", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/order/cancelled", "method": "POST"} },
  { "name": "webhook.fulfilment.created", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/fulfilment/created", "method": "POST"} },
  { "name": "webhook.fulfilment.shipped", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/fulfilment/shipped", "method": "POST"} },
  { "name": "webhook.consignment.created", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/consignment/created", "method": "POST"} },
  { "name": "consignment.prefix", "valueType": "STRING", "value": "A_" },
  { "name": "carrier.default.hd", "valueType": "STRING", "value": "STANDARD" }
]
```

### CC Workflow Settings Template

A typical Click & Collect workflow requires these settings (add `"context": "RETAILER", "contextId": <retailerId>` to each):

```json
[
  { "name": "webhook.order.created", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/order/created", "method": "POST"} },
  { "name": "webhook.order.ready_for_pickup", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/order/ready", "method": "POST"} },
  { "name": "webhook.order.collected", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/order/collected", "method": "POST"} }
]
```

## Phase 5: Migration Between Environments

Step-by-step process for migrating settings from one environment (e.g., dev) to another (e.g., staging or prod).

### Step 1: Export from Source Environment

Query all settings from the source environment:

```
graphql.queryAll({
  query: "query($cursor: String) { settings(first: 100, after: $cursor, context: \"RETAILER\") { edges { cursor node { id name value valueType context contextId lobValue } } pageInfo { hasNextPage } } }",
  variables: { cursor: null },
  maxRecords: 5000
})
```

Write the results to a JSON export file:

```json
{
  "exportedFrom": "dev.sandbox.api.fluentretail.com",
  "sourceRetailerId": 2,
  "exportedOn": "2026-02-20T10:00:00Z",
  "settingsCount": 42,
  "settings": [
    {
      "name": "webhook.order.created",
      "valueType": "LOB",
      "lobValue": {"url": "https://dev-api.example.com/order/created"},
      "context": "RETAILER",
      "contextId": 2
    },
    {
      "name": "consignment.prefix",
      "valueType": "STRING",
      "value": "A_",
      "context": "RETAILER",
      "contextId": 2
    }
  ]
}
```

**Important:** Strip the `id` field from each setting — IDs are environment-specific and cannot be reused.

### Step 2: Transform for Target Environment

Update environment-specific values:

1. Replace URLs: `dev-api.example.com` to `staging-api.example.com`
2. Update `contextId` with target retailer ID: `2` to `5`
3. Update any API keys, credentials, or endpoint-specific configuration
4. Create a mapping file for repeatable migrations:

```json
{
  "urlReplacements": {
    "dev-api.example.com": "staging-api.example.com",
    "dev.sandbox.api.fluentretail.com": "staging.sandbox.api.fluentretail.com"
  },
  "contextIdMapping": {
    "2": 5
  }
}
```

### Step 3: Import to Target Environment

For each setting in the export:

1. **Check if it already exists** in the target:
   ```graphql
   {
     settings(first: 1, name: ["<SETTING_NAME>"], context: "RETAILER") {
       edges { node { id name } }
     }
   }
   ```

2. **If it does NOT exist** — create it:
   ```
   graphql.batchMutate with createSetting
   ```

3. **If it already exists** — update it:
   ```
   graphql.batchMutate with updateSetting (using the discovered id)
   ```

Use `graphql.batchMutate` with `createSetting` for bulk creation (up to 50 per batch):

```json
{
  "mutation": "createSetting",
  "inputs": [
    {
      "name": "webhook.order.created",
      "valueType": "LOB",
      "lobValue": {"url": "https://staging-api.example.com/order/created"},
      "context": "RETAILER",
      "contextId": 5
    }
  ],
  "returnFields": ["id", "name", "value", "context", "contextId"]
}
```

### Step 4: Verify Migration

Query the target environment and compare:

```
1. graphql.queryAll to fetch all settings from target
2. Compare setting names: source count vs target count
3. Spot-check critical settings (webhooks, carrier configs) for correct values
4. Report any settings that failed to create
```

### Step 5: Handle Conflicts

If a setting already exists in the target and should be overwritten, use `updateSetting` with all required fields:

```json
{
  "mutation": "updateSetting",
  "inputs": [
    { "id": "<existing_setting_id>", "valueType": "LOB", "lobValue": {"url": "https://new-endpoint.com"}, "context": "RETAILER", "contextId": 5 },
    { "id": "<existing_setting_id>", "valueType": "STRING", "value": "new_value", "context": "RETAILER", "contextId": 5 }
  ],
  "returnFields": ["id", "name", "value", "context", "contextId"]
}
```

## Webhook Delivery Monitoring

After configuring webhook settings, verify delivery success using the event audit trail:

```
event.list({
  "eventType": "ORCHESTRATION_AUDIT",
  "category": "ACTION",
  "name": "Send Webhook",
  "context.rootEntityRef": "<ENTITY_REF>",
  "count": 50
})
```

Each result's attributes contain:
- `Request Endpoint` — the URL the webhook was sent to
- `Response code` — HTTP status code (200 = success, 4xx/5xx = failure)
- `Response Body` — the response from the webhook receiver
- `Response Headers` — response headers

**Cross-reference with settings:** Compare the `Request Endpoint` attribute with the `url` field in the webhook setting to confirm the correct endpoint was used. Mismatches indicate the wrong setting was loaded.

For full event query semantics → `/fluent-event-api` Pattern 7.

## Phase 2C: Webhook Payload Reconstruction

Reconstruct the full webhook payload that was sent to external systems by extracting dynamic attributes from ACTION "Send Webhook" audit events. This reveals exactly what data the external system received — useful when debugging webhook receiver failures or verifying integration contracts.

### Data Source

Query ORCHESTRATION_AUDIT events for webhook actions:

```
event.list({
  "eventType": "ORCHESTRATION_AUDIT",
  "context.rootEntityRef": "<ENTITY_REF>",
  "count": 200
})
```

Filter results where `name == "Send Webhook"`. Each webhook ACTION event contains:

| Attribute | Type | Description |
|-----------|------|-------------|
| `Request Endpoint` | String | Target URL the webhook was sent to |
| `Request Headers` | String | HTTP headers sent with the request |
| `Response code` | String | HTTP status code from the receiver |
| `Response Body` | String | Full response body |
| `Response Headers` | String | Response headers from the receiver |
| `Response reason` | String | HTTP status text |
| *(dynamic attributes)* | Various | Rule-injected attributes representing the webhook payload data |

### Dynamic Attribute Discovery

`SendWebhookWithDynamicAttributes` rules inject entity data as additional attributes on the ACTION event. These attributes represent the data that was included in the webhook payload body. Common dynamic attributes include:

```
orderType, orderId, orderRef, orderStatus, orderCreatedOn
fulfilmentId, fulfilmentRef, fulfilmentStatus, fulfilmentType
eventName (the triggering event name)
customerId, customerRef
locationRef, locationName
items (serialized item list)
```

The exact set of dynamic attributes depends on the rule's configuration and the entity type. To discover which attributes a specific webhook rule injects:

1. **Static analysis**: Check the rule's props in the workflow JSON for `dynamicAttributes` or similar configuration
2. **Runtime evidence**: Extract all non-standard attributes from the ACTION event (exclude `Request Endpoint`, `Request Headers`, `Response *` fields)
3. **Plugin registry**: `plugin.list({ name: "SendWebhookWithDynamicAttributes" })` → check `parameters` for dynamic attribute configuration

### Payload Reconstruction Algorithm

```
For each "Send Webhook" ACTION event:
  1. Extract standard fields: Request Endpoint, Response code, Response Body
  2. Extract dynamic attributes: all attributes NOT in the standard webhook field set
  3. Group dynamic attributes by the originating ruleset (infer from causal chain or timing)
  4. Reconstruct the payload:
     - Standard webhook body = entity snapshot (if configured by the rule)
     - Dynamic attributes = additional key-value pairs appended to the body
  5. Compare with the webhook receiver's expected contract
```

### Reconstruction Report Format

```
=== Webhook Payload Reconstruction: ORDER ref=HD-20260222-001 ===

Webhook #1: OrderCreatedWebhook
  Endpoint: https://api.example.com/order/created
  Method: POST (from setting lobValue)
  Response: 200 OK

  Reconstructed Payload:
    orderType: HD
    orderId: 143
    orderRef: HD-20260222-001
    orderStatus: CREATED
    eventName: CREATE
    customerId: 42
    fulfilmentChoiceRef: FC-HD-20260222-001
    sourceSystem: E2E_TEST

  Setting: webhook.order.created
  Setting URL: https://api.example.com/order/created
  URL Match: YES

---

Webhook #2: FulfilmentShippedWebhook
  Endpoint: https://api.example.com/fulfilment/shipped
  Method: POST
  Response: 404 Not Found

  Reconstructed Payload:
    orderType: HD
    fulfilmentId: 201
    fulfilmentRef: FUL-HD-001
    fulfilmentStatus: SHIPPED
    trackingNumber: E2E-TRACK-20260222
    carrierRef: E2E-CARRIER
    deliveredAt: (not present)

  Setting: webhook.fulfilment.shipped
  Setting URL: (MISSING — setting not found)
  Diagnosis: Webhook sent to URL from stale cache or hardcoded fallback. Create the setting.
```

### Integration with Settings Audit

Feed webhook payload reconstruction into the Phase 2 audit:

1. **URL validation**: Compare `Request Endpoint` from runtime against `lobValue.url` from settings
2. **Contract validation**: Compare dynamic attributes against the external system's expected payload schema (if documented)
3. **Missing setting detection**: If a webhook fired but the corresponding setting is MISSING, the rule may have used a hardcoded fallback URL
4. **Payload completeness**: Flag webhooks where expected dynamic attributes are missing (e.g., `trackingNumber` absent from a shipment webhook)

### Use Cases

- **Integration debugging**: When an external system reports "missing field X in webhook payload", check if the dynamic attribute was actually sent
- **Contract documentation**: Generate a webhook payload contract from actual runtime data (more accurate than reading rule source)
- **Environment comparison**: Compare webhook payloads between dev and prod to find configuration differences
- **Compliance audit**: Verify that sensitive data (PII, payment info) is not leaked through webhook payloads

## Troubleshooting

| Problem | Cause | Fix |
|---------|-------|-----|
| Rule executes but does nothing | Setting name in rule props does not match any setting | Run audit, create the missing setting |
| `createSetting` returns schema error | Input field mismatch | Use `graphql.introspect({ type: "CreateSettingInput" })` to discover exact fields |
| `updateSetting` returns validation error | Missing required fields — `valueType`, `context`, `contextId` are all required even for updates | Always include all four required fields: `id`, `valueType`, `context`, `contextId` |
| Setting exists but rule still fails | `valueType` mismatch — rule expects LOB but setting is STRING | Check `valueType` and recreate with correct type |
| Setting value is malformed JSON | `lobValue` contains invalid JSON | Validate JSON before saving; `lobValue` accepts a JSON object on input, not a string |
| Webhook returns 401/403 | API key or auth header in setting is wrong or expired | Update the setting with fresh credentials |
| Setting not found despite existing | Context mismatch — setting is ACCOUNT-scoped but rule queries RETAILER | Check context type; create at the correct scope |
| Migration creates duplicates | Setting already existed in target | Query first, then use `updateSetting` for existing, `createSetting` for new |
| `lobValue` is null but `value` has data | Setting was created as STRING but rule reads `lobValue` | Recreate with `valueType: "LOB"` |
| `context: { contextType: "RETAILER" }` fails | `context` is a plain String, not an object | Use `context: "RETAILER"` (string) in queries; use `context: "RETAILER", contextId: 2` in mutations |

## Integration with Other Skills

| Task | Skill |
|------|-------|
| Analyze which workflow rules need settings | `/fluent-workflow-analyzer` |
| Build workflow rulesets that reference settings | `/fluent-workflow-builder` |
| Discover retailer config before creating settings | `/fluent-test-data` |
| Deploy module containing rules that read settings | `/fluent-module-deploy` |
| Run E2E test after settings are configured | `/fluent-e2e-test` |
| Debug rule failures caused by missing settings | `/fluent-trace` |
| Query user actions that source options from settings | `/fluent-transition-api` |
| Expose a setting as an editable form in the OMS UI | `fc.settingForm` component (requires a matching `_SCHEMA` setting) |

## Session Tracking

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

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

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

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

## Handoff

| Output Artifact | Consumed By | Path |
|----------------|-------------|------|
| Settings created/updated in Fluent environment | `/fluent-pre-deploy-check` (Phase 5 validation) | Live environment via `graphql.query` |
| Settings audit report | `/fluent-workflow-builder` (dependency check) | `accounts/<PROFILE>/analysis/settings/settings-audit.json` |

## Error Reporting

Errors from this skill follow the standard format:

| Field | Description |
|-------|-------------|
| `phase` | Skill phase where error occurred (e.g., `audit`, `creation`, `update`, `migration`, `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 |

## Tips

- **Settings names are case-sensitive** — `webhook.Order.Created` and `webhook.order.created` are different settings
- **`lobValue` vs `value`** — Use `LOB` for JSON objects, URLs, and any value longer than a few words. Use `STRING` for simple scalar values. If in doubt, use LOB.
- **`lobValue` is a JSON object on input** — pass `{"url": "..."}` not `"{\"url\": \"...\"}"`. Fluent handles serialization. NEVER `JSON.stringify()` the lobValue before passing it — this double-encodes it as a string, breaking manifest fragment loading.
- **Settings do not have versions** — updates overwrite the value immediately with no rollback. The Backup Protocol (above) is MANDATORY before any write operation. Backups go to `accounts/<PROFILE>/settings/backups/`.
- **Missing settings cause silent rule failures** — the event may process as SUCCESS but the rule skips its logic entirely. Always audit settings after deploying a new workflow.
- **Use `graphql.introspect`** to discover exact `CreateSettingInput` and `UpdateSettingInput` fields before writing mutations. Field names can vary across Fluent versions.
- **Update requires 4 fields** — `id`, `valueType`, `context`, `contextId` are all required even if you only want to change the value. Omitting any will cause a validation error.
- **Transition API `source` field** references settings — when `source: "settings.cancellationReasons"` appears in a user action attribute, that means a setting named `cancellationReasons` (or similar) must exist for the dropdown to populate. The `source` prefix MUST be lowercase `"settings."` (NOT `"Settings."` — capital S silently fails). The setting value MUST use the `{ "active": [{ "name": "Display Label", "value": "SUBMITTED_VALUE" }] }` format — a plain array without the `"active"` wrapper silently renders an empty dropdown. The setting's retailer context must match the workflow's retailer.
- **Always query before creating** during migration — creating a setting with a name that already exists at the same context may fail or create a duplicate depending on the Fluent version.

## Post-Execution: Feedback Capture

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

1. **Classify outcome:** `SUCCESS` (settings applied), `PARTIAL_SUCCESS` (some settings failed), `FAILURE` (could not complete), `BLOCKED` (env unreachable), `USER_CORRECTED` (user overrode values/approach)
2. **Build record:** Create a `feedback-record-v1` JSON object with:
   - `skill`: `"fluent-settings"`
   - `feature`: feature slug if applicable
   - `entityType` / `entitySubtype`: entity type the settings configure (if applicable)
   - `phases`: trace of each phase (pre-flight, plan, audit, upsert, verify) with PASS/FAIL/SKIP
   - `learnings`: any gotchas discovered (wrong value type, missing context, silent failure)
   - `userCorrections`: any corrections the user provided
3. **Redact secrets:** Strip webhook URLs with tokens, API keys. Keep setting names and contexts.
4. **Append** one JSON line to `accounts/<PROFILE>/feedback/fluent-settings.jsonl`
5. **Update** `accounts/<PROFILE>/feedback/index.json` counters (create with `mkdir -p` if missing)

**Skip capture if:** The execution was read-only (audit, discover, list — no mutations).

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

_(No promoted learnings yet.)_

<!-- end feedback-promoted -->
