@fluentcommerce/ai-skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +622 -0
  3. package/bin/cli.mjs +1973 -0
  4. package/content/cli/agents/fluent-cli/agent.json +149 -0
  5. package/content/cli/agents/fluent-cli.md +132 -0
  6. package/content/cli/skills/fluent-bootstrap/SKILL.md +181 -0
  7. package/content/cli/skills/fluent-cli-index/SKILL.md +63 -0
  8. package/content/cli/skills/fluent-cli-mcp-cicd/SKILL.md +77 -0
  9. package/content/cli/skills/fluent-cli-reference/SKILL.md +1031 -0
  10. package/content/cli/skills/fluent-cli-retailer/SKILL.md +85 -0
  11. package/content/cli/skills/fluent-cli-settings/SKILL.md +106 -0
  12. package/content/cli/skills/fluent-connect/SKILL.md +886 -0
  13. package/content/cli/skills/fluent-module-deploy/SKILL.md +349 -0
  14. package/content/cli/skills/fluent-profile/SKILL.md +180 -0
  15. package/content/cli/skills/fluent-workflow/SKILL.md +310 -0
  16. package/content/dev/agents/fluent-dev/agent.json +88 -0
  17. package/content/dev/agents/fluent-dev.md +525 -0
  18. package/content/dev/reference-modules/catalog.json +4754 -0
  19. package/content/dev/skills/fluent-build/SKILL.md +192 -0
  20. package/content/dev/skills/fluent-connection-analysis/SKILL.md +386 -0
  21. package/content/dev/skills/fluent-custom-code/SKILL.md +895 -0
  22. package/content/dev/skills/fluent-data-module-scaffold/SKILL.md +714 -0
  23. package/content/dev/skills/fluent-e2e-test/SKILL.md +394 -0
  24. package/content/dev/skills/fluent-event-api/SKILL.md +945 -0
  25. package/content/dev/skills/fluent-feature-explain/SKILL.md +603 -0
  26. package/content/dev/skills/fluent-feature-plan/PLAN_TEMPLATE.md +695 -0
  27. package/content/dev/skills/fluent-feature-plan/SKILL.md +227 -0
  28. package/content/dev/skills/fluent-job-batch/SKILL.md +138 -0
  29. package/content/dev/skills/fluent-mermaid-validate/SKILL.md +86 -0
  30. package/content/dev/skills/fluent-module-scaffold/SKILL.md +1928 -0
  31. package/content/dev/skills/fluent-module-validate/SKILL.md +775 -0
  32. package/content/dev/skills/fluent-pre-deploy-check/SKILL.md +1108 -0
  33. package/content/dev/skills/fluent-retailer-config/SKILL.md +1111 -0
  34. package/content/dev/skills/fluent-rule-scaffold/SKILL.md +385 -0
  35. package/content/dev/skills/fluent-scope-decompose/SKILL.md +1021 -0
  36. package/content/dev/skills/fluent-session-audit-export/SKILL.md +632 -0
  37. package/content/dev/skills/fluent-session-summary/SKILL.md +195 -0
  38. package/content/dev/skills/fluent-settings/SKILL.md +1058 -0
  39. package/content/dev/skills/fluent-source-onboard/SKILL.md +632 -0
  40. package/content/dev/skills/fluent-system-monitoring/SKILL.md +767 -0
  41. package/content/dev/skills/fluent-test-data/SKILL.md +513 -0
  42. package/content/dev/skills/fluent-trace/SKILL.md +1143 -0
  43. package/content/dev/skills/fluent-transition-api/SKILL.md +346 -0
  44. package/content/dev/skills/fluent-version-manage/SKILL.md +744 -0
  45. package/content/dev/skills/fluent-workflow-analyzer/SKILL.md +959 -0
  46. package/content/dev/skills/fluent-workflow-builder/SKILL.md +319 -0
  47. package/content/dev/skills/fluent-workflow-deploy/SKILL.md +267 -0
  48. package/content/mcp-extn/agents/fluent-mcp.md +69 -0
  49. package/content/mcp-extn/skills/fluent-mcp-tools/SKILL.md +461 -0
  50. package/content/mcp-official/agents/fluent-mcp-core.md +91 -0
  51. package/content/mcp-official/skills/fluent-mcp-core/SKILL.md +94 -0
  52. package/content/rfl/agents/fluent-rfl.md +56 -0
  53. package/content/rfl/skills/fluent-rfl-assess/SKILL.md +172 -0
  54. package/docs/CAPABILITY_MAP.md +77 -0
  55. package/docs/CLI_COVERAGE.md +47 -0
  56. package/docs/DEV_WORKFLOW.md +802 -0
  57. package/docs/FLOW_RUN.md +142 -0
  58. package/docs/USE_CASES.md +404 -0
  59. package/metadata.json +156 -0
  60. package/package.json +51 -0
@@ -0,0 +1,1058 @@
1
+ ---
2
+ name: fluent-settings
3
+ 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".
4
+ user-invocable: true
5
+ allowed-tools: Bash, Read, Write, Edit, Glob, Grep
6
+ argument-hint: [--audit <workflow-file>] [--context RETAILER|ACCOUNT|LOCATION] [--migrate --from <profile> --to <profile>]
7
+ ---
8
+
9
+ # Settings Lifecycle Management
10
+
11
+ 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.
12
+
13
+ ## Planning Gate
14
+
15
+ **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.
16
+
17
+ **Settings-specific emphasis — ensure these are covered:**
18
+
19
+ 1. **Business Context (Section 1)** — why these settings are needed; what workflow behavior they enable
20
+ 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
21
+ 3. **Webhooks (Section 4.5)** — if creating webhook URL settings: endpoint, HTTP method, payload shape, which ruleset fires it
22
+ 4. **Impacted rulesets (Section 4.3)** — which workflow rules depend on each setting; what behavior changes
23
+ 5. **Impacted retailers (Section 4.8)** — settings take effect immediately; confirm target environment
24
+ 6. **Risks (Section 7)** — wrong values can break live flows; note immediate-effect risk
25
+
26
+ **Write the plan to:** `accounts/<PROFILE>/plans/<YYYY-MM-DD>-settings-<slug>.md`. Set `Status: PENDING`.
27
+
28
+ 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).
29
+
30
+ ## Ownership Boundary
31
+
32
+ This skill owns Settings CRUD, discovery, audit, and migration.
33
+
34
+ Workflow rule analysis is owned by `/fluent-workflow-analyzer`.
35
+ MCP tool syntax is owned by `/fluent-mcp-tools`.
36
+ Workflow creation and editing guidance is owned by `/fluent-workflow-builder`.
37
+ Entity test data is owned by `/fluent-test-data`.
38
+ Module deployment is owned by `/fluent-module-deploy`.
39
+
40
+ ## When to Use
41
+
42
+ - After deploying a workflow or module — verify all required settings exist
43
+ - Setting up a new retailer — create required settings for workflows to function
44
+ - Debugging "rule did nothing" failures — missing settings are the #1 silent failure cause
45
+ - Migrating settings between environments (dev to staging to prod)
46
+ - Auditing settings completeness before go-live (Ready For Launch)
47
+ - Investigating webhook or carrier integration failures — the root cause is almost always a missing or malformed setting
48
+
49
+ ## Settings Model in Fluent
50
+
51
+ 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.
52
+
53
+ ### Entity Structure
54
+
55
+ A Setting has these queryable fields:
56
+
57
+ | Field | Type | Description |
58
+ |-------|------|-------------|
59
+ | `id` | String | Platform-assigned unique ID |
60
+ | `name` | String | The reference key (e.g., `webhook.order.created`) — this is what rules look up |
61
+ | `value` | String | The setting value (for STRING/INTEGER/BOOLEAN types). Returns empty string for LOB types |
62
+ | `lobValue` | String | Large object value — JSON string (for LOB-type settings only). Empty for non-LOB types |
63
+ | `valueType` | String | `STRING`, `LOB`, `INTEGER`, `BOOLEAN`, or `JSON` |
64
+ | `context` | String | Scope type: `RETAILER`, `ACCOUNT`, `GLOBAL`, `AGENT`, or `CUSTOMER` |
65
+ | `contextId` | Int | ID of the scoped entity (e.g., retailer ID `2` when context is `RETAILER`). `0` for GLOBAL/ACCOUNT |
66
+
67
+ **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.
68
+
69
+ ### Context Types
70
+
71
+ Settings are scoped using two separate fields — `context` (scope type string) and `contextId` (scoped entity ID integer):
72
+
73
+ | Context | `contextId` | Use Case |
74
+ |---------|-------------|----------|
75
+ | `RETAILER` | retailer ID (e.g., `2`) | Most common — webhook URLs, carrier configs, routing strategy |
76
+ | `ACCOUNT` | `0` | Account-wide defaults that apply across retailers |
77
+ | `GLOBAL` | `0` | Platform-wide (e.g., sourcing criteria) |
78
+ | `AGENT` | agent/user ID | Agent-specific settings |
79
+ | `CUSTOMER` | customer ID | Customer-specific preferences |
80
+
81
+ 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.
82
+
83
+ ### How Rules Access Settings
84
+
85
+ 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.
86
+
87
+ ## Phase 1: Discovery — List All Settings
88
+
89
+ ### List All Settings for a Retailer
90
+
91
+ Use `graphql.query` for a quick look, or `graphql.queryAll` for retailers with many settings:
92
+
93
+ ```graphql
94
+ query($cursor: String) {
95
+ settings(first: 100, after: $cursor, context: "RETAILER") {
96
+ edges {
97
+ cursor
98
+ node {
99
+ id
100
+ name
101
+ value
102
+ valueType
103
+ context
104
+ contextId
105
+ lobValue
106
+ }
107
+ }
108
+ pageInfo { hasNextPage }
109
+ }
110
+ }
111
+ ```
112
+
113
+ **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.
114
+
115
+ For auto-pagination across all settings:
116
+
117
+ ```
118
+ graphql.queryAll({
119
+ query: "query($cursor: String) { settings(first: 100, after: $cursor, context: \"RETAILER\") { edges { cursor node { id name value valueType context contextId lobValue } } pageInfo { hasNextPage } } }",
120
+ variables: { cursor: null },
121
+ maxRecords: 5000
122
+ })
123
+ ```
124
+
125
+ ### Search Settings by Exact Name
126
+
127
+ The `name` filter accepts an array of exact names:
128
+
129
+ ```graphql
130
+ {
131
+ settings(first: 1, name: ["webhook.order.created"], context: "RETAILER") {
132
+ edges {
133
+ node {
134
+ id
135
+ name
136
+ value
137
+ valueType
138
+ context
139
+ contextId
140
+ lobValue
141
+ }
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ ### Search Settings by Name Pattern
148
+
149
+ 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:
150
+
151
+ ```
152
+ 1. graphql.queryAll to fetch all settings
153
+ 2. Filter results where name starts with "webhook." or contains the target pattern
154
+ ```
155
+
156
+ ### List Settings at a Specific Context
157
+
158
+ ```graphql
159
+ {
160
+ settings(first: 50, context: "ACCOUNT") {
161
+ edges {
162
+ node {
163
+ id
164
+ name
165
+ value
166
+ valueType
167
+ context
168
+ contextId
169
+ }
170
+ }
171
+ }
172
+ }
173
+ ```
174
+
175
+ Replace `"ACCOUNT"` with `"GLOBAL"`, `"AGENT"`, or `"CUSTOMER"` as needed.
176
+
177
+ ## Phase 2: Audit — Cross-Reference with Workflow Rules
178
+
179
+ 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.
180
+
181
+ ### Extraction Algorithm
182
+
183
+ Parse the workflow JSON file and extract all rule `props` that reference settings:
184
+
185
+ ```
186
+ For each ruleset in workflow JSON:
187
+ For each rule in ruleset.rules:
188
+ 1. If rule.name contains "SendWebhook" or "SendWebhookWithDynamicAttributes":
189
+ Extract props["setting"] or props["webhookSettingName"] → required setting name
190
+ Tag: expectedType=LOB (webhook settings are always JSON with a "url" field)
191
+ 2. If rule.name contains "UpdateEntityFromSetting":
192
+ Extract props["setting"] or props["settingName"] → required setting name
193
+ Tag: expectedType=LOB (entity update mappings are JSON)
194
+ 3. If any prop key matches: "setting", "settingName", "settingRef", "settingsKey", "webhookSettingName":
195
+ Extract the prop value → required setting name
196
+ 4. If rule.name contains "SettingUtils" or "GetSetting":
197
+ Extract the setting reference from props
198
+
199
+ Record: { settingName, referencedBy: rulesetName, ruleName, propKey, expectedType? }
200
+ ```
201
+
202
+ ### Parameter-Level Discovery via plugin.list
203
+
204
+ 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:
205
+
206
+ ```
207
+ For each unique rule name in the workflow:
208
+ plugin.list({ name: "<RuleShortName>" })
209
+ For each parameter in the response:
210
+ If parameter.description mentions "setting" or "config":
211
+ → Cross-reference parameter.name with the rule's props in the workflow
212
+ → If the prop value looks like a setting name: add to extraction list
213
+ If parameter.name matches: settingName, setting, settingRef, configSetting:
214
+ → Extract the prop value from the workflow rule → add to extraction list
215
+ ```
216
+
217
+ This catches rules that read settings internally but expose the setting name as a configurable parameter.
218
+
219
+ ### Common Rule Patterns That Depend on Settings
220
+
221
+ | Rule Pattern | Prop That References Setting | What the Setting Contains |
222
+ |---|---|---|
223
+ | `SendWebhook` | `setting` or `webhookSettingName` | Webhook URL, headers, method |
224
+ | `SendWebhookWithDynamicAttributes` | `setting` | Webhook URL + dynamic attribute mapping |
225
+ | `UpdateEntityFromSetting` | `setting` | Entity field update mapping |
226
+ | `GetSettingValue` | `settingName` | Arbitrary config value |
227
+ | Any rule using `SettingUtils.getSettingByRef` | `setting` / `settingRef` | Rule-specific configuration |
228
+
229
+ ### Verification Query
230
+
231
+ For each referenced setting name, query the platform to check if it exists:
232
+
233
+ ```graphql
234
+ {
235
+ settings(first: 1, name: ["<SETTING_NAME>"], context: "RETAILER") {
236
+ edges {
237
+ node {
238
+ id
239
+ name
240
+ value
241
+ lobValue
242
+ valueType
243
+ }
244
+ }
245
+ }
246
+ }
247
+ ```
248
+
249
+ If the edges array is empty, the setting is **MISSING**.
250
+
251
+ ### Extended Validation (Beyond Existence)
252
+
253
+ For each setting that exists, perform additional checks:
254
+
255
+ ```
256
+ 1. TYPE CHECK — Does the valueType match what the rule expects?
257
+ - Webhook rules (SendWebhook*) expect LOB → if setting is STRING, flag TYPE_MISMATCH
258
+ - Config rules (GetSettingValue) can accept STRING, BOOLEAN, or LOB
259
+ - Entity update rules (UpdateEntityFromSetting) expect LOB → flag if STRING
260
+
261
+ 2. CONTENT CHECK — Is the value meaningful?
262
+ - LOB webhook settings: lobValue must contain a "url" field → flag if missing
263
+ - STRING settings: value must be non-empty → flag EMPTY_VALUE if blank
264
+ - BOOLEAN settings: value must be "true" or "false" → flag INVALID_VALUE otherwise
265
+
266
+ 3. CONTEXT CHECK — Is the setting at the right scope?
267
+ - Custom rules (e.g., SendWebhookWithDynamicAttributes) typically resolve settings using cascading scope: RETAILER first, then ACCOUNT
268
+ - Module-installed settings often default to ACCOUNT scope (contextId=0)
269
+ - **Always use cascading scope resolution when auditing**: query RETAILER first; if empty, query ACCOUNT; report which scope the setting was found at
270
+ - If setting exists only at ACCOUNT scope, this is typically correct for module-installed settings — flag as INFO, not WRONG_CONTEXT
271
+ - Query at multiple contexts:
272
+ settings(name: ["<NAME>"], context: "RETAILER", contextId: <retailerId>)
273
+ settings(name: ["<NAME>"], context: "ACCOUNT", contextId: 0)
274
+ ```
275
+
276
+ ### Audit Report Format
277
+
278
+ ```
279
+ === Settings Audit: ORDER::HD (v1.50) ===
280
+
281
+ Ruleset Rule Setting Name Status
282
+ ────────────────────────────────────────────────────────────────────────────────────────────────────
283
+ OrderCreatedWebhook *.SendWebhookWithDynamicAttributes webhook.order.created OK (LOB)
284
+ OrderCancelledWebhook *.SendWebhookWithDynamicAttributes webhook.order.cancelled OK (LOB)
285
+ FulfilmentShippedWebhook *.SendWebhookWithDynamicAttributes webhook.fulfilment.shipped MISSING [HIGH]
286
+ UpdateOrderFromWebhook *.UpdateEntityFromSetting order.update.mapping MISSING [HIGH]
287
+ ConsignmentCreated *.GetSettingValue consignment.prefix OK (STRING)
288
+ CarrierRouting *.GetSettingValue carrier.hd.config TYPE_MISMATCH [MED]
289
+ → Expected: LOB, Actual: STRING — rule reads lobValue which will be null
290
+
291
+ Summary: 3/6 valid, 2 MISSING, 1 TYPE_MISMATCH
292
+ Priority: 2 HIGH (silent failures), 1 MEDIUM (wrong type)
293
+ ```
294
+
295
+ ### Severity Classification
296
+
297
+ | Status | Severity | Impact |
298
+ |--------|----------|--------|
299
+ | OK | None | Setting will be read correctly |
300
+ | MISSING | HIGH | Rule will fail silently — entity gets stuck |
301
+ | TYPE_MISMATCH | MEDIUM | Rule reads wrong field (e.g., `lobValue` is null for STRING type) |
302
+ | EMPTY_VALUE | LOW | Setting exists but value is null/empty — behavior depends on rule |
303
+ | WRONG_CONTEXT | MEDIUM | Setting exists at ACCOUNT but rule expects RETAILER scope |
304
+
305
+ 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.
306
+
307
+ ### Phase 2B: Conformance Mode (Workflow + Runtime Evidence)
308
+
309
+ Use this mode when you need more than existence checks. It compares:
310
+
311
+ 1. **Static contract**: setting refs extracted from workflow rule props
312
+ 2. **Live config**: settings currently stored in Fluent
313
+ 3. **Runtime evidence**: what was actually used during orchestration/webhook actions
314
+
315
+ #### Conformance Statuses
316
+
317
+ | Status | Meaning |
318
+ |---|---|
319
+ | `FOUND` | Setting exists with plausible value shape |
320
+ | `MISSING` | Setting not found at expected context |
321
+ | `EMPTY` | Setting exists but value/lobValue empty |
322
+ | `INVALID_FORMAT` | Value exists but invalid shape (for example webhook LOB missing `url`) |
323
+ | `CONTRACT_MISMATCH` | Runtime behavior conflicts with configured value (for example observed webhook endpoint differs from setting URL) |
324
+
325
+ #### Runtime Cross-Check Pattern
326
+
327
+ After extracting setting refs from the workflow:
328
+
329
+ 1. Query live setting values (`settings(... name: [...])`).
330
+ 2. Query orchestration audit for webhook actions on a target entity:
331
+ ```
332
+ event.list({
333
+ "eventType": "ORCHESTRATION_AUDIT",
334
+ "category": "ACTION",
335
+ "context.rootEntityRef": "<ENTITY_REF>",
336
+ "count": 100
337
+ })
338
+ ```
339
+ 3. Compare configured URL vs observed `Request Endpoint` in audit attributes.
340
+ 4. Emit `CONTRACT_MISMATCH` if they differ.
341
+
342
+ #### Conformance Output Format
343
+
344
+ ```
345
+ === Settings Conformance: ORDER::MULTI ===
346
+
347
+ Setting Referenced By Status Evidence
348
+ -------------------------------------------------------------------------------------------------------------------
349
+ webhook.order.created ORDER.CREATE FOUND id=365, valueType=LOB
350
+ webhook.carrier.shipment.request FULFILMENT.RequestShipment CONTRACT_MISMATCH configured=https://x, observed=https://y
351
+ update.fulfilment.confirmPick FULFILMENT.ConfirmPick MISSING no RETAILER setting found
352
+
353
+ Summary: FOUND=1 MISSING=1 CONTRACT_MISMATCH=1
354
+ ```
355
+
356
+ ### Phase 2B: Value Format Validation
357
+
358
+ 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.
359
+
360
+ #### Webhook Setting Validation
361
+
362
+ Settings referenced by `SendWebhook` or `SendWebhookWithDynamicAttributes` rules must meet all of the following:
363
+
364
+ | Check | Expected | Severity |
365
+ |-------|----------|----------|
366
+ | `valueType` is `LOB` | Required — webhook config is always JSON | HIGH |
367
+ | `lobValue` is valid JSON | Must parse without errors; must not be null or empty string | HIGH |
368
+ | `lobValue` contains `url` field | String, well-formed URL (starts with `http://` or `https://`) | HIGH |
369
+ | `url` uses HTTPS | HTTP URLs in non-dev environments are a security risk | WARN |
370
+ | `url` is not empty | Empty string URL causes silent delivery failure | HIGH |
371
+ | Optional: `headers` field | Object (key-value pairs) if present | INFO |
372
+ | Optional: `method` field | String, typically `POST` (default if absent) | INFO |
373
+
374
+ Example of a valid webhook setting value:
375
+
376
+ ```json
377
+ {"url": "https://api.example.com/webhook/order-created", "headers": {"X-Api-Key": "key-123"}, "method": "POST"}
378
+ ```
379
+
380
+ Common failures:
381
+ - `valueType` is `STRING` instead of `LOB` — rule reads `lobValue` which returns null
382
+ - `lobValue` is a stringified JSON (`"{\"url\":...}"`) instead of a parsed object — double-encoded
383
+ - `url` field is missing entirely — webhook sends to null endpoint
384
+ - `url` is placeholder (`https://example.com` or `TODO`) — webhook returns 404
385
+
386
+ #### Entity Update Setting Validation
387
+
388
+ Settings referenced by `UpdateEntityFromSetting` rules must meet:
389
+
390
+ | Check | Expected | Severity |
391
+ |-------|----------|----------|
392
+ | `valueType` is `LOB` | Required — field mappings are JSON | HIGH |
393
+ | `lobValue` is valid JSON | Must parse without errors | HIGH |
394
+ | `lobValue` contains field mapping structure | Should have recognizable field names or mapping keys | WARN |
395
+ | `lobValue` is not empty object `{}` | Empty mapping means rule does nothing | WARN |
396
+
397
+ #### General Format Checks (All Settings)
398
+
399
+ Apply these checks to every setting regardless of which rule references it:
400
+
401
+ | valueType | Validation Rule | Severity |
402
+ |-----------|----------------|----------|
403
+ | `STRING` | `value` is non-empty (not null, not `""`) | WARN |
404
+ | `LOB` | `lobValue` is valid JSON (not null, not empty string, not `"null"`) | HIGH |
405
+ | `BOOLEAN` | `value` is exactly `"true"` or `"false"` (case-sensitive) | HIGH |
406
+ | `INTEGER` | `value` is a numeric string (parseable as integer) | HIGH |
407
+ | `JSON` | `lobValue` is valid JSON | HIGH |
408
+
409
+ **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.
410
+
411
+ #### Enhanced Audit Report Format
412
+
413
+ When value validation is performed, extend the audit report with `valueType` and `Value Check` columns:
414
+
415
+ ```
416
+ === Settings Audit: ORDER::HD (v1.50) ===
417
+
418
+ Ruleset Rule Setting Name Status valueType Value Check
419
+ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
420
+ OrderCreatedWebhook ACCT.ext.SendWebhookWithDynamicAttributes webhook.order.created FOUND LOB OK (url=https://api.example.com/order/created)
421
+ OrderCancelledWebhook ACCT.ext.SendWebhookWithDynamicAttributes webhook.order.cancelled FOUND LOB WARN: url is HTTP not HTTPS
422
+ FulfilmentShippedWebhook ACCT.ext.SendWebhookWithDynamicAttributes webhook.fulfilment.shipped MISSING - - [HIGH]
423
+ UpdateOrderFromWebhook ACCT.ext.UpdateEntityFromSetting order.update.mapping FOUND STRING ERROR: expected LOB for UpdateEntityFromSetting [HIGH]
424
+ ConsignmentCreated ACCT.ext.GetSettingValue consignment.prefix FOUND STRING OK (value="A_")
425
+ FeatureFlagCheck ACCT.ext.GetSettingValue feature.express.enabled FOUND BOOLEAN OK (value="true")
426
+
427
+ Summary: 5/6 settings present, 1 MISSING
428
+ Value issues: 1 ERROR (wrong valueType), 1 WARN (HTTP URL)
429
+ Priority: 2 HIGH — missing setting + valueType mismatch will cause silent rule failures
430
+ ```
431
+
432
+ #### Validation Query
433
+
434
+ To validate values, the verification query from Phase 2 already returns `value`, `lobValue`, and `valueType`. After checking existence (edges not empty), apply format checks:
435
+
436
+ ```
437
+ For each FOUND setting:
438
+ 1. Read valueType, value, lobValue from query result
439
+ 2. Apply general format checks for the valueType
440
+ 3. If referenced by SendWebhook* rule:
441
+ - Verify valueType == "LOB"
442
+ - Parse lobValue as JSON
443
+ - Check for url field (non-empty, well-formed)
444
+ - Flag HTTP URLs in non-dev environments
445
+ 4. If referenced by UpdateEntityFromSetting rule:
446
+ - Verify valueType == "LOB"
447
+ - Parse lobValue as JSON
448
+ - Check for non-empty mapping structure
449
+ 5. Record: { settingName, valueType, valueCheck: "OK" | "WARN: ..." | "ERROR: ..." }
450
+ ```
451
+
452
+ #### Cross-Reference with Connection Analysis
453
+
454
+ 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:
455
+
456
+ ```
457
+ 1. Receive dependency inventory from connection analysis:
458
+ [{ settingName, ruleType, ruleName, rulesetName }]
459
+ 2. Skip the extraction algorithm (Phase 2 step 1)
460
+ 3. Run existence check (Phase 2 verification query) for each setting
461
+ 4. Run value format validation (Phase 2B) for each FOUND setting
462
+ 5. Return combined audit report with Value Check column
463
+ ```
464
+
465
+ ## Phase 3: CRUD Operations
466
+
467
+ ### Schema Reference (from live introspection)
468
+
469
+ Both `CreateSettingInput` and `UpdateSettingInput` use `context` (String) + `contextId` (Int) as **separate fields**:
470
+
471
+ | Field | Create | Update | Type | Description |
472
+ |-------|:------:|:------:|------|-------------|
473
+ | `id` | — | **required** | `ID!` | Setting ID (query first to obtain) |
474
+ | `name` | **required** | optional | `String!` / `String` | Setting reference key |
475
+ | `valueType` | **required** | **required** | `String!` | `LOB`, `STRING`, `INTEGER`, `BOOLEAN`, or `JSON` |
476
+ | `value` | optional | optional | `String` | Use for non-JSON values (STRING, INTEGER, BOOLEAN) |
477
+ | `lobValue` | optional | optional | `Json` | Use for JSON values (LOB, JSON valueTypes). Pass as a JSON object, not a string |
478
+ | `context` | **required** | **required** | `String!` | `RETAILER`, `ACCOUNT`, `AGENT`, or `CUSTOMER` |
479
+ | `contextId` | **required** | **required** | `Int!` | ID of the context entity (e.g., retailer ID as integer) |
480
+
481
+ **Key differences from what you might expect:**
482
+ - `context` is a plain string (`"RETAILER"`), NOT `"RETAILER:2"` or `{ contextType: "RETAILER" }`
483
+ - `contextId` is a separate **Int** field (e.g., `2`), not embedded in the context string
484
+ - There is NO `retailer: { id }` field on settings — use `context` + `contextId` instead
485
+ - `valueType` is **required even for updates** — you must always specify it
486
+ - `lobValue` accepts a **Json object** on input (not a string) — pass `{"url": "..."}` not `"{\"url\": \"...\"}"`
487
+
488
+ ### Create Setting
489
+
490
+ ```graphql
491
+ mutation($input: CreateSettingInput!) {
492
+ createSetting(input: $input) {
493
+ id
494
+ name
495
+ value
496
+ lobValue
497
+ context
498
+ contextId
499
+ valueType
500
+ }
501
+ }
502
+ ```
503
+
504
+ **Variables for a LOB setting (webhook config):**
505
+
506
+ ```json
507
+ {
508
+ "input": {
509
+ "name": "fc.webhook.order.created",
510
+ "valueType": "LOB",
511
+ "lobValue": {"url": "https://api.example.com/webhook/order-created", "headers": {"X-Api-Key": "your-api-key"}, "method": "POST"},
512
+ "context": "RETAILER",
513
+ "contextId": 2
514
+ }
515
+ }
516
+ ```
517
+
518
+ **Variables for a STRING setting:**
519
+
520
+ ```json
521
+ {
522
+ "input": {
523
+ "name": "consignment.prefix",
524
+ "valueType": "STRING",
525
+ "value": "A_",
526
+ "context": "RETAILER",
527
+ "contextId": 2
528
+ }
529
+ }
530
+ ```
531
+
532
+ **Variables for a BOOLEAN setting:**
533
+
534
+ ```json
535
+ {
536
+ "input": {
537
+ "name": "GLOBAL_INVENTORY_ENABLED",
538
+ "valueType": "BOOLEAN",
539
+ "value": "true",
540
+ "context": "RETAILER",
541
+ "contextId": 2
542
+ }
543
+ }
544
+ ```
545
+
546
+ **Variables for an ACCOUNT-level setting:**
547
+
548
+ ```json
549
+ {
550
+ "input": {
551
+ "name": "CANCELLATION_REASONS",
552
+ "valueType": "JSON",
553
+ "lobValue": {"active": [{"name": "Wrong Size", "value": "WRONGSIZE"}, {"name": "Broken", "value": "BROKEN"}]},
554
+ "context": "ACCOUNT",
555
+ "contextId": 0
556
+ }
557
+ }
558
+ ```
559
+
560
+ Note: Account-level settings use `contextId: 0`.
561
+
562
+ ### Update Setting
563
+
564
+ Updates overwrite the value immediately. There is no versioning — the previous value is lost.
565
+
566
+ **You must supply `id`, `valueType`, `context`, and `contextId` — all four are required for updates.**
567
+
568
+ ```graphql
569
+ mutation($input: UpdateSettingInput!) {
570
+ updateSetting(input: $input) {
571
+ id
572
+ name
573
+ value
574
+ lobValue
575
+ context
576
+ contextId
577
+ valueType
578
+ }
579
+ }
580
+ ```
581
+
582
+ **Update a STRING value:**
583
+
584
+ ```json
585
+ {
586
+ "input": {
587
+ "id": "368",
588
+ "valueType": "STRING",
589
+ "value": "new-value",
590
+ "context": "RETAILER",
591
+ "contextId": 2
592
+ }
593
+ }
594
+ ```
595
+
596
+ **Update a LOB value (e.g., change webhook URL):**
597
+
598
+ ```json
599
+ {
600
+ "input": {
601
+ "id": "365",
602
+ "valueType": "LOB",
603
+ "lobValue": {"url": "https://updated-endpoint.com/webhook", "headers": {}, "method": "POST"},
604
+ "context": "RETAILER",
605
+ "contextId": 2
606
+ }
607
+ }
608
+ ```
609
+
610
+ **Important:** You need the setting `id` to update. Always query the setting by name first to get its ID:
611
+
612
+ ```graphql
613
+ {
614
+ settings(first: 1, name: ["fc.webhook.order.created"]) {
615
+ edges { node { id name value lobValue valueType context contextId } }
616
+ }
617
+ }
618
+ ```
619
+
620
+ ### Delete Setting
621
+
622
+ 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.
623
+
624
+ ### Batch Create Settings
625
+
626
+ Use `graphql.batchMutate` for creating multiple settings at once (up to 50 per batch):
627
+
628
+ ```json
629
+ {
630
+ "mutation": "createSetting",
631
+ "inputs": [
632
+ {
633
+ "name": "fc.webhook.order.created",
634
+ "valueType": "LOB",
635
+ "lobValue": {"url": "https://api.example.com/order/created"},
636
+ "context": "RETAILER",
637
+ "contextId": 2
638
+ },
639
+ {
640
+ "name": "fc.webhook.order.cancelled",
641
+ "valueType": "LOB",
642
+ "lobValue": {"url": "https://api.example.com/order/cancelled"},
643
+ "context": "RETAILER",
644
+ "contextId": 2
645
+ },
646
+ {
647
+ "name": "consignment.prefix",
648
+ "valueType": "STRING",
649
+ "value": "A_",
650
+ "context": "RETAILER",
651
+ "contextId": 2
652
+ }
653
+ ],
654
+ "returnFields": ["id", "name", "value", "context", "contextId", "valueType"]
655
+ }
656
+ ```
657
+
658
+ ### Batch Update Settings
659
+
660
+ Use `graphql.batchMutate` with `updateSetting` for bulk updates. **All four required fields** (`id`, `valueType`, `context`, `contextId`) must be present on each input:
661
+
662
+ ```json
663
+ {
664
+ "mutation": "updateSetting",
665
+ "inputs": [
666
+ {
667
+ "id": "365",
668
+ "valueType": "LOB",
669
+ "lobValue": {"url": "https://new-endpoint.com/order/created"},
670
+ "context": "RETAILER",
671
+ "contextId": 2
672
+ },
673
+ {
674
+ "id": "370",
675
+ "valueType": "STRING",
676
+ "value": "B_",
677
+ "context": "RETAILER",
678
+ "contextId": 2
679
+ }
680
+ ],
681
+ "returnFields": ["id", "name", "value", "context", "contextId", "valueType"]
682
+ }
683
+ ```
684
+
685
+ ### Discover Exact Input Types
686
+
687
+ Before creating settings, use `graphql.introspect` to discover the exact field requirements for your Fluent version:
688
+
689
+ ```
690
+ graphql.introspect({ type: "CreateSettingInput" })
691
+ graphql.introspect({ type: "UpdateSettingInput" })
692
+ ```
693
+
694
+ Field names and required/optional status can vary across Fluent versions. Always introspect first if a mutation returns a schema error.
695
+
696
+ ## Phase 4: Common Settings Patterns
697
+
698
+ Document of the most common Fluent settings and their expected value format:
699
+
700
+ | Setting Pattern | Purpose | valueType | Example Value |
701
+ |---|---|---|---|
702
+ | `webhook.<entity>.<event>` | Webhook URL for SendWebhook rules | LOB | `{"url":"https://api.example.com/webhook","headers":{"X-Api-Key":"..."},"method":"POST"}` |
703
+ | `carrier.<carrier_ref>.config` | Carrier integration config | LOB | `{"apiUrl":"...","apiKey":"...","serviceTypes":["EXPRESS","STANDARD"]}` |
704
+ | `fulfilment.routing.config` | Sourcing/routing strategy | LOB | `{"strategy":"CLOSEST","maxDistance":50,"unit":"KM"}` |
705
+ | `consignment.prefix` | Consignment reference prefix | STRING | `A_` |
706
+ | `cancellation.reasons` | Valid cancellation reasons for UI (served via Transition API) | LOB | `["Out of stock","Customer request","Fraud","Duplicate"]` |
707
+ | `notification.<type>` | Email/SMS notification template config | LOB | `{"templateId":"...","channel":"EMAIL","provider":"sendgrid"}` |
708
+ | `buffer.time.<entity>` | Processing buffer times | LOB | `{"minutes":30}` |
709
+ | `feature.<name>` | Feature flags | STRING | `true` or `false` |
710
+ | `order.update.mapping` | Field mapping for UpdateEntityFromSetting rules | LOB | `{"fields":["status","attributes.customField"]}` |
711
+ | `sourcing.radius` | Maximum distance for inventory sourcing | STRING | `50` |
712
+ | `sourcing.strategy` | Inventory sourcing strategy | STRING | `CLOSEST` or `PRIORITY` |
713
+
714
+ ### HD Workflow Settings Template
715
+
716
+ A typical Home Delivery workflow requires these settings (add `"context": "RETAILER", "contextId": <retailerId>` to each):
717
+
718
+ ```json
719
+ [
720
+ { "name": "webhook.order.created", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/order/created", "method": "POST"} },
721
+ { "name": "webhook.order.booked", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/order/booked", "method": "POST"} },
722
+ { "name": "webhook.order.cancelled", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/order/cancelled", "method": "POST"} },
723
+ { "name": "webhook.fulfilment.created", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/fulfilment/created", "method": "POST"} },
724
+ { "name": "webhook.fulfilment.shipped", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/fulfilment/shipped", "method": "POST"} },
725
+ { "name": "webhook.consignment.created", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/consignment/created", "method": "POST"} },
726
+ { "name": "consignment.prefix", "valueType": "STRING", "value": "A_" },
727
+ { "name": "carrier.default.hd", "valueType": "STRING", "value": "STANDARD" }
728
+ ]
729
+ ```
730
+
731
+ ### CC Workflow Settings Template
732
+
733
+ A typical Click & Collect workflow requires these settings (add `"context": "RETAILER", "contextId": <retailerId>` to each):
734
+
735
+ ```json
736
+ [
737
+ { "name": "webhook.order.created", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/order/created", "method": "POST"} },
738
+ { "name": "webhook.order.ready_for_pickup", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/order/ready", "method": "POST"} },
739
+ { "name": "webhook.order.collected", "valueType": "LOB", "lobValue": {"url": "https://api.example.com/order/collected", "method": "POST"} }
740
+ ]
741
+ ```
742
+
743
+ ## Phase 5: Migration Between Environments
744
+
745
+ Step-by-step process for migrating settings from one environment (e.g., dev) to another (e.g., staging or prod).
746
+
747
+ ### Step 1: Export from Source Environment
748
+
749
+ Query all settings from the source environment:
750
+
751
+ ```
752
+ graphql.queryAll({
753
+ query: "query($cursor: String) { settings(first: 100, after: $cursor, context: \"RETAILER\") { edges { cursor node { id name value valueType context contextId lobValue } } pageInfo { hasNextPage } } }",
754
+ variables: { cursor: null },
755
+ maxRecords: 5000
756
+ })
757
+ ```
758
+
759
+ Write the results to a JSON export file:
760
+
761
+ ```json
762
+ {
763
+ "exportedFrom": "dev.sandbox.api.fluentretail.com",
764
+ "sourceRetailerId": 2,
765
+ "exportedOn": "2026-02-20T10:00:00Z",
766
+ "settingsCount": 42,
767
+ "settings": [
768
+ {
769
+ "name": "webhook.order.created",
770
+ "valueType": "LOB",
771
+ "lobValue": {"url": "https://dev-api.example.com/order/created"},
772
+ "context": "RETAILER",
773
+ "contextId": 2
774
+ },
775
+ {
776
+ "name": "consignment.prefix",
777
+ "valueType": "STRING",
778
+ "value": "A_",
779
+ "context": "RETAILER",
780
+ "contextId": 2
781
+ }
782
+ ]
783
+ }
784
+ ```
785
+
786
+ **Important:** Strip the `id` field from each setting — IDs are environment-specific and cannot be reused.
787
+
788
+ ### Step 2: Transform for Target Environment
789
+
790
+ Update environment-specific values:
791
+
792
+ 1. Replace URLs: `dev-api.example.com` to `staging-api.example.com`
793
+ 2. Update `contextId` with target retailer ID: `2` to `5`
794
+ 3. Update any API keys, credentials, or endpoint-specific configuration
795
+ 4. Create a mapping file for repeatable migrations:
796
+
797
+ ```json
798
+ {
799
+ "urlReplacements": {
800
+ "dev-api.example.com": "staging-api.example.com",
801
+ "dev.sandbox.api.fluentretail.com": "staging.sandbox.api.fluentretail.com"
802
+ },
803
+ "contextIdMapping": {
804
+ "2": 5
805
+ }
806
+ }
807
+ ```
808
+
809
+ ### Step 3: Import to Target Environment
810
+
811
+ For each setting in the export:
812
+
813
+ 1. **Check if it already exists** in the target:
814
+ ```graphql
815
+ {
816
+ settings(first: 1, name: ["<SETTING_NAME>"], context: "RETAILER") {
817
+ edges { node { id name } }
818
+ }
819
+ }
820
+ ```
821
+
822
+ 2. **If it does NOT exist** — create it:
823
+ ```
824
+ graphql.batchMutate with createSetting
825
+ ```
826
+
827
+ 3. **If it already exists** — update it:
828
+ ```
829
+ graphql.batchMutate with updateSetting (using the discovered id)
830
+ ```
831
+
832
+ Use `graphql.batchMutate` with `createSetting` for bulk creation (up to 50 per batch):
833
+
834
+ ```json
835
+ {
836
+ "mutation": "createSetting",
837
+ "inputs": [
838
+ {
839
+ "name": "webhook.order.created",
840
+ "valueType": "LOB",
841
+ "lobValue": {"url": "https://staging-api.example.com/order/created"},
842
+ "context": "RETAILER",
843
+ "contextId": 5
844
+ }
845
+ ],
846
+ "returnFields": ["id", "name", "value", "context", "contextId"]
847
+ }
848
+ ```
849
+
850
+ ### Step 4: Verify Migration
851
+
852
+ Query the target environment and compare:
853
+
854
+ ```
855
+ 1. graphql.queryAll to fetch all settings from target
856
+ 2. Compare setting names: source count vs target count
857
+ 3. Spot-check critical settings (webhooks, carrier configs) for correct values
858
+ 4. Report any settings that failed to create
859
+ ```
860
+
861
+ ### Step 5: Handle Conflicts
862
+
863
+ If a setting already exists in the target and should be overwritten, use `updateSetting` with all required fields:
864
+
865
+ ```json
866
+ {
867
+ "mutation": "updateSetting",
868
+ "inputs": [
869
+ { "id": "<existing_setting_id>", "valueType": "LOB", "lobValue": {"url": "https://new-endpoint.com"}, "context": "RETAILER", "contextId": 5 },
870
+ { "id": "<existing_setting_id>", "valueType": "STRING", "value": "new_value", "context": "RETAILER", "contextId": 5 }
871
+ ],
872
+ "returnFields": ["id", "name", "value", "context", "contextId"]
873
+ }
874
+ ```
875
+
876
+ ## Webhook Delivery Monitoring
877
+
878
+ After configuring webhook settings, verify delivery success using the event audit trail:
879
+
880
+ ```
881
+ event.list({
882
+ "eventType": "ORCHESTRATION_AUDIT",
883
+ "category": "ACTION",
884
+ "name": "Send Webhook",
885
+ "context.rootEntityRef": "<ENTITY_REF>",
886
+ "count": 50
887
+ })
888
+ ```
889
+
890
+ Each result's attributes contain:
891
+ - `Request Endpoint` — the URL the webhook was sent to
892
+ - `Response code` — HTTP status code (200 = success, 4xx/5xx = failure)
893
+ - `Response Body` — the response from the webhook receiver
894
+ - `Response Headers` — response headers
895
+
896
+ **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.
897
+
898
+ For full event query semantics → `/fluent-event-api` Pattern 7.
899
+
900
+ ## Phase 2C: Webhook Payload Reconstruction
901
+
902
+ 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.
903
+
904
+ ### Data Source
905
+
906
+ Query ORCHESTRATION_AUDIT events for webhook actions:
907
+
908
+ ```
909
+ event.list({
910
+ "eventType": "ORCHESTRATION_AUDIT",
911
+ "context.rootEntityRef": "<ENTITY_REF>",
912
+ "count": 200
913
+ })
914
+ ```
915
+
916
+ Filter results where `name == "Send Webhook"`. Each webhook ACTION event contains:
917
+
918
+ | Attribute | Type | Description |
919
+ |-----------|------|-------------|
920
+ | `Request Endpoint` | String | Target URL the webhook was sent to |
921
+ | `Request Headers` | String | HTTP headers sent with the request |
922
+ | `Response code` | String | HTTP status code from the receiver |
923
+ | `Response Body` | String | Full response body |
924
+ | `Response Headers` | String | Response headers from the receiver |
925
+ | `Response reason` | String | HTTP status text |
926
+ | *(dynamic attributes)* | Various | Rule-injected attributes representing the webhook payload data |
927
+
928
+ ### Dynamic Attribute Discovery
929
+
930
+ `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:
931
+
932
+ ```
933
+ orderType, orderId, orderRef, orderStatus, orderCreatedOn
934
+ fulfilmentId, fulfilmentRef, fulfilmentStatus, fulfilmentType
935
+ eventName (the triggering event name)
936
+ customerId, customerRef
937
+ locationRef, locationName
938
+ items (serialized item list)
939
+ ```
940
+
941
+ 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:
942
+
943
+ 1. **Static analysis**: Check the rule's props in the workflow JSON for `dynamicAttributes` or similar configuration
944
+ 2. **Runtime evidence**: Extract all non-standard attributes from the ACTION event (exclude `Request Endpoint`, `Request Headers`, `Response *` fields)
945
+ 3. **Plugin registry**: `plugin.list({ name: "SendWebhookWithDynamicAttributes" })` → check `parameters` for dynamic attribute configuration
946
+
947
+ ### Payload Reconstruction Algorithm
948
+
949
+ ```
950
+ For each "Send Webhook" ACTION event:
951
+ 1. Extract standard fields: Request Endpoint, Response code, Response Body
952
+ 2. Extract dynamic attributes: all attributes NOT in the standard webhook field set
953
+ 3. Group dynamic attributes by the originating ruleset (infer from causal chain or timing)
954
+ 4. Reconstruct the payload:
955
+ - Standard webhook body = entity snapshot (if configured by the rule)
956
+ - Dynamic attributes = additional key-value pairs appended to the body
957
+ 5. Compare with the webhook receiver's expected contract
958
+ ```
959
+
960
+ ### Reconstruction Report Format
961
+
962
+ ```
963
+ === Webhook Payload Reconstruction: ORDER ref=HD-20260222-001 ===
964
+
965
+ Webhook #1: OrderCreatedWebhook
966
+ Endpoint: https://api.example.com/order/created
967
+ Method: POST (from setting lobValue)
968
+ Response: 200 OK
969
+
970
+ Reconstructed Payload:
971
+ orderType: HD
972
+ orderId: 143
973
+ orderRef: HD-20260222-001
974
+ orderStatus: CREATED
975
+ eventName: CREATE
976
+ customerId: 42
977
+ fulfilmentChoiceRef: FC-HD-20260222-001
978
+ sourceSystem: E2E_TEST
979
+
980
+ Setting: webhook.order.created
981
+ Setting URL: https://api.example.com/order/created
982
+ URL Match: YES
983
+
984
+ ---
985
+
986
+ Webhook #2: FulfilmentShippedWebhook
987
+ Endpoint: https://api.example.com/fulfilment/shipped
988
+ Method: POST
989
+ Response: 404 Not Found
990
+
991
+ Reconstructed Payload:
992
+ orderType: HD
993
+ fulfilmentId: 201
994
+ fulfilmentRef: FUL-HD-001
995
+ fulfilmentStatus: SHIPPED
996
+ trackingNumber: E2E-TRACK-20260222
997
+ carrierRef: E2E-CARRIER
998
+ deliveredAt: (not present)
999
+
1000
+ Setting: webhook.fulfilment.shipped
1001
+ Setting URL: (MISSING — setting not found)
1002
+ Diagnosis: Webhook sent to URL from stale cache or hardcoded fallback. Create the setting.
1003
+ ```
1004
+
1005
+ ### Integration with Settings Audit
1006
+
1007
+ Feed webhook payload reconstruction into the Phase 2 audit:
1008
+
1009
+ 1. **URL validation**: Compare `Request Endpoint` from runtime against `lobValue.url` from settings
1010
+ 2. **Contract validation**: Compare dynamic attributes against the external system's expected payload schema (if documented)
1011
+ 3. **Missing setting detection**: If a webhook fired but the corresponding setting is MISSING, the rule may have used a hardcoded fallback URL
1012
+ 4. **Payload completeness**: Flag webhooks where expected dynamic attributes are missing (e.g., `trackingNumber` absent from a shipment webhook)
1013
+
1014
+ ### Use Cases
1015
+
1016
+ - **Integration debugging**: When an external system reports "missing field X in webhook payload", check if the dynamic attribute was actually sent
1017
+ - **Contract documentation**: Generate a webhook payload contract from actual runtime data (more accurate than reading rule source)
1018
+ - **Environment comparison**: Compare webhook payloads between dev and prod to find configuration differences
1019
+ - **Compliance audit**: Verify that sensitive data (PII, payment info) is not leaked through webhook payloads
1020
+
1021
+ ## Troubleshooting
1022
+
1023
+ | Problem | Cause | Fix |
1024
+ |---------|-------|-----|
1025
+ | Rule executes but does nothing | Setting name in rule props does not match any setting | Run audit, create the missing setting |
1026
+ | `createSetting` returns schema error | Input field mismatch | Use `graphql.introspect({ type: "CreateSettingInput" })` to discover exact fields |
1027
+ | `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` |
1028
+ | Setting exists but rule still fails | `valueType` mismatch — rule expects LOB but setting is STRING | Check `valueType` and recreate with correct type |
1029
+ | Setting value is malformed JSON | `lobValue` contains invalid JSON | Validate JSON before saving; `lobValue` accepts a JSON object on input, not a string |
1030
+ | Webhook returns 401/403 | API key or auth header in setting is wrong or expired | Update the setting with fresh credentials |
1031
+ | Setting not found despite existing | Context mismatch — setting is ACCOUNT-scoped but rule queries RETAILER | Check context type; create at the correct scope |
1032
+ | Migration creates duplicates | Setting already existed in target | Query first, then use `updateSetting` for existing, `createSetting` for new |
1033
+ | `lobValue` is null but `value` has data | Setting was created as STRING but rule reads `lobValue` | Recreate with `valueType: "LOB"` |
1034
+ | `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 |
1035
+
1036
+ ## Integration with Other Skills
1037
+
1038
+ | Task | Skill |
1039
+ |------|-------|
1040
+ | Analyze which workflow rules need settings | `/fluent-workflow-analyzer` |
1041
+ | Build workflow rulesets that reference settings | `/fluent-workflow-builder` |
1042
+ | Discover retailer config before creating settings | `/fluent-test-data` |
1043
+ | Deploy module containing rules that read settings | `/fluent-module-deploy` |
1044
+ | Run E2E test after settings are configured | `/fluent-e2e-test` |
1045
+ | Debug rule failures caused by missing settings | `/fluent-trace` |
1046
+ | Query user actions that source options from settings | `/fluent-transition-api` |
1047
+
1048
+ ## Tips
1049
+
1050
+ - **Settings names are case-sensitive** — `webhook.Order.Created` and `webhook.order.created` are different settings
1051
+ - **`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.
1052
+ - **`lobValue` is a JSON object on input** — pass `{"url": "..."}` not `"{\"url\": \"...\"}"`. Fluent handles serialization.
1053
+ - **Settings do not have versions** — updates overwrite the value immediately with no rollback. Export before updating if you need a backup.
1054
+ - **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.
1055
+ - **Use `graphql.introspect`** to discover exact `CreateSettingInput` and `UpdateSettingInput` fields before writing mutations. Field names can vary across Fluent versions.
1056
+ - **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.
1057
+ - **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.
1058
+ - **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.