@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.
- package/LICENSE +21 -0
- package/README.md +622 -0
- package/bin/cli.mjs +1973 -0
- package/content/cli/agents/fluent-cli/agent.json +149 -0
- package/content/cli/agents/fluent-cli.md +132 -0
- package/content/cli/skills/fluent-bootstrap/SKILL.md +181 -0
- package/content/cli/skills/fluent-cli-index/SKILL.md +63 -0
- package/content/cli/skills/fluent-cli-mcp-cicd/SKILL.md +77 -0
- package/content/cli/skills/fluent-cli-reference/SKILL.md +1031 -0
- package/content/cli/skills/fluent-cli-retailer/SKILL.md +85 -0
- package/content/cli/skills/fluent-cli-settings/SKILL.md +106 -0
- package/content/cli/skills/fluent-connect/SKILL.md +886 -0
- package/content/cli/skills/fluent-module-deploy/SKILL.md +349 -0
- package/content/cli/skills/fluent-profile/SKILL.md +180 -0
- package/content/cli/skills/fluent-workflow/SKILL.md +310 -0
- package/content/dev/agents/fluent-dev/agent.json +88 -0
- package/content/dev/agents/fluent-dev.md +525 -0
- package/content/dev/reference-modules/catalog.json +4754 -0
- package/content/dev/skills/fluent-build/SKILL.md +192 -0
- package/content/dev/skills/fluent-connection-analysis/SKILL.md +386 -0
- package/content/dev/skills/fluent-custom-code/SKILL.md +895 -0
- package/content/dev/skills/fluent-data-module-scaffold/SKILL.md +714 -0
- package/content/dev/skills/fluent-e2e-test/SKILL.md +394 -0
- package/content/dev/skills/fluent-event-api/SKILL.md +945 -0
- package/content/dev/skills/fluent-feature-explain/SKILL.md +603 -0
- package/content/dev/skills/fluent-feature-plan/PLAN_TEMPLATE.md +695 -0
- package/content/dev/skills/fluent-feature-plan/SKILL.md +227 -0
- package/content/dev/skills/fluent-job-batch/SKILL.md +138 -0
- package/content/dev/skills/fluent-mermaid-validate/SKILL.md +86 -0
- package/content/dev/skills/fluent-module-scaffold/SKILL.md +1928 -0
- package/content/dev/skills/fluent-module-validate/SKILL.md +775 -0
- package/content/dev/skills/fluent-pre-deploy-check/SKILL.md +1108 -0
- package/content/dev/skills/fluent-retailer-config/SKILL.md +1111 -0
- package/content/dev/skills/fluent-rule-scaffold/SKILL.md +385 -0
- package/content/dev/skills/fluent-scope-decompose/SKILL.md +1021 -0
- package/content/dev/skills/fluent-session-audit-export/SKILL.md +632 -0
- package/content/dev/skills/fluent-session-summary/SKILL.md +195 -0
- package/content/dev/skills/fluent-settings/SKILL.md +1058 -0
- package/content/dev/skills/fluent-source-onboard/SKILL.md +632 -0
- package/content/dev/skills/fluent-system-monitoring/SKILL.md +767 -0
- package/content/dev/skills/fluent-test-data/SKILL.md +513 -0
- package/content/dev/skills/fluent-trace/SKILL.md +1143 -0
- package/content/dev/skills/fluent-transition-api/SKILL.md +346 -0
- package/content/dev/skills/fluent-version-manage/SKILL.md +744 -0
- package/content/dev/skills/fluent-workflow-analyzer/SKILL.md +959 -0
- package/content/dev/skills/fluent-workflow-builder/SKILL.md +319 -0
- package/content/dev/skills/fluent-workflow-deploy/SKILL.md +267 -0
- package/content/mcp-extn/agents/fluent-mcp.md +69 -0
- package/content/mcp-extn/skills/fluent-mcp-tools/SKILL.md +461 -0
- package/content/mcp-official/agents/fluent-mcp-core.md +91 -0
- package/content/mcp-official/skills/fluent-mcp-core/SKILL.md +94 -0
- package/content/rfl/agents/fluent-rfl.md +56 -0
- package/content/rfl/skills/fluent-rfl-assess/SKILL.md +172 -0
- package/docs/CAPABILITY_MAP.md +77 -0
- package/docs/CLI_COVERAGE.md +47 -0
- package/docs/DEV_WORKFLOW.md +802 -0
- package/docs/FLOW_RUN.md +142 -0
- package/docs/USE_CASES.md +404 -0
- package/metadata.json +156 -0
- 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.
|