---
name: fluent-account-snapshot
description: Lightweight account state snapshot — retailers, workflows, modules, catalogues, settings, UI customizations. MCP-first data collection with LLM summary. Triggers on "account snapshot", "account state", "account inventory", "what's in this account", "environment summary".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: [--profile <name>] [--retailer <ref>] [--include <sections>] [--json] [--refresh]
category: discovery
tags: [account, inventory, discovery, snapshot, environment]
---

# Account Snapshot

Produce a structured inventory of what exists in a Fluent Commerce account — workflows, modules, catalogues, locations, settings, and UI customizations. Designed for cost-efficient data collection: MCP tools gather the data, the LLM only summarizes at the end.

## Design Principle: MCP-First, LLM-Last

**Phase 1–2 are pure MCP tool calls and local file reads.** Do NOT load large payloads (workflow JSON, manifest JSON, settings values) into LLM context. Use `outputFile`/`outputDir` to save to disk. The LLM reads only summaries, counts, and small metadata.

**Phase 3 uses the LLM** only to produce the final human-readable summary paragraph.

## GraphQL Constraint: No Counts

Fluent Commerce GraphQL has **no aggregate/count queries**. You cannot get "total orders = 5,234". You can only:
- Query `first: 1` to check **existence** (does this entity type have any records?)
- Query `first: 100` to **enumerate** items (but you won't know the total without full pagination)
- Use `pageInfo.hasNextPage` to indicate "more than N exist"

**Implication:** The snapshot reports **what types exist** and **samples up to 100**, not exact totals. For workflows, modules, settings, and catalogues this is usually exhaustive (< 100 items). For orders, locations, and inventory positions it may not be.

## Pre-flight

1. Determine profile: `--profile` flag, or `FLUENT_PROFILE` env var, or ask the user
2. Determine retailer: `--retailer` flag, or `FLUENT_RETAILER_REF` env var, or discover from profile
3. **Freshness check:** If `accounts/<PROFILE>/snapshot/account-snapshot.json` exists and `generatedAt` is < 24 hours ago AND `--refresh` was NOT passed, report the existing snapshot instead of re-collecting. Print: "Snapshot from <timestamp> is still fresh. Use --refresh to force re-collection."
4. Create output directory: `accounts/<PROFILE>/snapshot/`

## Token Budget Awareness

MCP tool responses flow through the LLM context window. Every tool call costs tokens even if you "just save the data." The skill minimizes this by:
- Using `outputFile`/`outputDir` where the MCP tool supports it (response = metadata only, ~200 tokens)
- Using `keysOnly: true` on `plugin.list` (or `compact: true` fallback)
- Splitting `environment.discover` into focused sections to avoid one massive response
- Running workflow/manifest parsing via Bash scripts (tool output = extracted counts, not raw JSON)

**Target: < 8,000 tokens total for Phase 1 MCP responses in context.**

## Phase 1: Collect (MCP Tool Calls)

Run these MCP tool calls. Responses still enter LLM context — keep them small by using `outputFile`, `keysOnly`/`compact`, and focused queries.

### Step 1.1: Validate and Get Retailer Context

```
config.validate()
```

If this fails, STOP. Then get core retailer info:

```
environment.discover({
  include: ["retailer", "users"]
})
```

**Response size:** ~300 tokens. Gives retailer name/ref/status and authenticated user. Do NOT request all sections in one call — locations and settings return up to 100 items each and bloat the response.

### Step 1.2: Locations and Networks (separate call, controlled)

```
environment.discover({
  include: ["locations", "networks"]
})
```

**Response size:** ~2,000-5,000 tokens depending on location count. If locations array length = 100, flag as truncated.

### Step 1.3: Catalogues, Modules, and Settings (separate call)

```
environment.discover({
  include: ["catalogues", "modules", "settings"]
})
```

**Response size:** ~1,000-3,000 tokens. Catalogues and modules are typically < 20 items each. Settings returns first 100 with name/context/valueType (no values) — if length = 100, flag as truncated.

### Step 1.4: Definitive Workflow List

```
workflow.list({ outputDir: "accounts/<PROFILE>/snapshot/workflows/" })
```

Downloads ALL workflows as individual JSON files. Returns only summary metadata (names, versions, file sizes) — full JSON stays on disk. **Response size:** ~500-1,000 tokens (metadata only).

### Step 1.5: Workflow Summaries + Status Catalog (Bash — not LLM)

Extract counts and the allowed status list per entity type using a **single** Node.js script (not per-file invocations):

```bash
node -e "
  const fs = require('fs'), path = require('path');
  const dir = 'accounts/<PROFILE>/snapshot/workflows';
  const uniq = values => [...new Set(values.filter(Boolean))].sort();
  const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
  const results = files.map(f => {
    const w = JSON.parse(fs.readFileSync(path.join(dir, f)));
    const rules = (w.rulesets || []).reduce((n,r) => n + (r.rules?.length || 0), 0);
    const uas = (w.rulesets || []).flatMap(r => (r.userActions || []).map(u => u.name));
    const ruleNames = (w.rulesets || []).flatMap(r => (r.rules || []).map(ru => ru.name));
    const statusCatalog = {};
    for (const status of (w.statuses || [])) {
      const entityType = status.entityType || w.entityType;
      (statusCatalog[entityType] ||= []).push(status.name);
    }
    return {
      name: w.name, version: w.version,
      entityType: w.entityType, entitySubtype: w.entitySubtype,
      statuses: (w.statuses || []).length, rulesets: (w.rulesets || []).length,
      rules,
      userActions: uniq(uas),
      ruleNames: uniq(ruleNames),
      subEntityTypes: uniq((w.rulesets || []).map(r => r.type).filter(t => t && t !== w.entityType)),
      statusCatalog: Object.fromEntries(
        Object.entries(statusCatalog).map(([entityType, names]) => [entityType, uniq(names)])
      )
    };
  });
  console.log(JSON.stringify(results));
"
```

**Response size:** ~2,000-3,000 tokens for 10-15 workflows (compact JSON). Single process, works cross-platform.

**Important:** Fluent statuses are flexible. Do NOT assume `COMPLETE`, `BOOKED`, or any other status exists for a given entity. The downloaded workflow files are the source of truth for which statuses are valid for which entity types.

### Step 1.6: Rule Registry (keysOnly mode — MANDATORY)

```
plugin.list({ keysOnly: true })
```

**MUST use `keysOnly: true`.** Returns only the full list of rule key names plus a `byPrefix` breakdown (e.g., `{ "ACME": 402, "FLUENTRETAIL": 200 }`). No rule details — avoids response budget truncation entirely.

**Response size:** ~2,000-4,000 tokens (key names only) vs ~500,000+ tokens (full with all definitions).

Use this to:
- Count custom rules vs OOTB rules from the `byPrefix` breakdown
- Group custom rules by module context (second segment of key: `ACCOUNT.context.RuleName`)
- Cross-reference rule names from Step 1.5 workflow parsing

**Fallback:** If `keysOnly` is not available (older MCP extension), use `compact: true` instead.

### Step 1.7: UI Customization Discovery

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

**Note:** `outputFile` with a trailing `/` saves each matched setting as a separate file in that directory. If no settings match, the directory will be empty — this is not an error.

Downloads all Mystique manifest settings. For each, extract (from local file):
- Setting name → which app/fragment
- `plugins[]` → custom component bundles
- Route count → number of sections and pages
- Custom component aliases = aliases absent from `/fluent-mystique-validate`'s built-in registry. Do NOT classify standard non-`fc.*` aliases such as `select`, `retailer`, `AttributeInput.list`, or `ExtendedAddressInput` as custom.

### Step 1.8: Webhook and Integration Settings

Collect integration-related settings separately (small payloads — inline is fine, no outputFile needed):

```
setting.get({ name: "WEBHOOK_%", first: 50 })
setting.get({ name: "fc.integration.%", first: 50 })
```

Record: setting name, context, value (URL or config). These reveal which external systems are wired into the account.

### Step 1.9: Entity Type Existence Probe (Schema-Validated)

For entity types NOT covered by `environment.discover`, probe existence with `first: 1` queries.

**GraphQL Pre-Flight (MANDATORY):** See `knowledge/platform/graphql-preflight.md`. Do NOT hardcode query strings. Use `graphql.buildQuery` to construct each probe — the returned query is guaranteed schema-valid.

1. Build `STATUS_CATALOG[entityType]` from Step 1.5 using the workflow `statuses[]` arrays.
2. For each entity type to probe, construct the query via MCP:
   ```
   graphql.buildQuery({ rootField: "orders", fields: ["edges { node { id ref status } }", "pageInfo { hasNextPage }"], args: { first: 1 } })
   ```
   Use the relay connection pattern (`edges { node { ... } }`) — flat fields like `["id"]` fail on connection types.
   If the entity type supports a `status` filter AND the status catalog has entries, add it to the args.
3. If `graphql.buildQuery` is unavailable, construct the query manually and validate it:
   ```
   graphql.validate({ query: "{ orders(first: 1) { edges { node { id status } } pageInfo { hasNextPage } } }" })
   ```
   Check the `valid` field (NOT `ok`). Fix any errors before executing.
4. If a status-filtered probe returns no rows, rerun once without the status filter and flag a `status catalog mismatch` if the unfiltered probe returns data.
5. If an entity type has no workflow-derived status catalog, omit the status filter entirely rather than guessing.

**Important:** Fluent statuses are flexible. Derive statuses from downloaded workflows, show that list in the snapshot, and use those for the probes.

Record: entity type → exists (yes/no) + hasNextPage (indicates more than 1 exist) + `queriedStatuses`.

## Phase 2: Analyze (Local File Processing)

Process the collected data **locally** (Bash/Node scripts, Grep, Read). Do NOT load raw JSON into LLM context.

### Step 2.1: Build Inventory Tables

From the data collected in Phase 1, build these tables:

**Workflow Inventory:**

| Entity Type | Subtype | Version | Description | Statuses | Rulesets | Rules | User Actions | Sub-Entity Types |
|---|---|---|---|---|---|---|---|---|

**Allowed Statuses by Entity (build this BEFORE live entity probes):**

| Entity Type | Allowed Statuses | Source Workflows |
|---|---|---|

**Module Inventory:**

| Module | Type | Version | Custom Rules | OOTB Rules |
|---|---|---|---|---|

Where Type = REFERENCE (if module name starts with standard prefixes like `fluentretail-`, `fluent-`) or CUSTOM.

**Catalogue Inventory:**

| Type | Ref | Status | Items |
|---|---|---|---|

Where Type = Product Catalogue, Inventory Catalogue, Virtual Catalogue, Control Group.

**Location Summary:**

| Type | Count (up to 100) | Has More? | Sample Refs |
|---|---|---|---|

**Settings Summary:**

| Scope | Count (up to 100) | Has More? | Key Settings |
|---|---|---|---|

Where Key Settings = settings matching patterns: `fc.mystique.*`, `*.webhook.*`, `*.integration.*`, `*.api.*`, sourcing profiles.

**UI Customizations:**

| App | Fragment Setting | Sections | Pages | Custom Components | Plugins |
|---|---|---|---|---|---|

### Step 2.2: Detect Patterns

From the inventory, detect and flag:
- **Multi-entity workflows** — workflows with rulesets for different entity types (e.g., ORDER workflow containing FULFILMENT rulesets)
- **Custom UI** — any manifest with `plugins[]` or aliases absent from the validator built-in component registry
- **Status catalog mismatch** — entity data exists, but unfiltered probe returned rows that the workflow-derived status-filtered probe missed
- **Integration signals** — settings or rules referencing webhooks, HTTP calls, external APIs
- **Sourcing configuration** — presence of `FULFILMENT_OPTIONS` workflows, sourcing profile settings

## Phase 3: Report (LLM Summarizes)

### Step 3.1: Generate Structured Report

Write `accounts/<PROFILE>/snapshot/account-snapshot.md`:

```markdown
# Account Snapshot: <ACCOUNT> / <RETAILER>

Generated: <date>
Profile: <profile>  |  Retailer: <ref> (ID: <id>)  |  User: <username>

## Executive Summary

<LLM-generated summary answering exactly these questions:
1. What fulfilment model does this account run? (HD only, CC only, HD+CC, marketplace, etc.)
2. How customized is it? (N custom rules, N custom UI plugins, N extension modules)
3. What integrations exist? (webhook endpoints, external systems)
4. What entity types are active? (orders, fulfilments, waves, returns, etc.)
5. Any notable gaps? (no returns workflow, no inventory, no waves = no store fulfilment)
>

## Workflows (<count>)

<Workflow Inventory table from Step 2.1>

### Allowed Statuses by Entity

<Status catalog table from Step 2.1. Show the exact statuses used for entity probes>

### Entity Type Coverage

<List which entity types have workflows and which don't>

## Modules (<count>)

<Module Inventory table>

### Custom Rules (<count>)

<Grouped by module, with rule names>

## Catalogues

<Catalogue Inventory table>

## Locations & Networks

<Location Summary table>
<Network list with location count per network>

## Settings (<count>)

<Settings Summary table>

### Key Settings

<List of important settings: mystique manifests, webhooks, integrations, sourcing>

## UI Customizations

<UI Customizations table>

### Custom Components

<List of aliases absent from the validator built-in registry. Exclude standard aliases such as select/retailer/AttributeInput.list>

## Entity Existence

| Entity Type | Exists? | Has Data? | Queried Statuses |
|---|---|---|---|

## Patterns Detected

<Bullet list from Step 2.2>
```

### Step 3.2: Generate Machine-Readable JSON

Write `accounts/<PROFILE>/snapshot/account-snapshot.json`:

```json
{
  "profile": "<PROFILE>",
  "retailer": { "id": "", "ref": "", "tradingName": "" },
  "baseUrl": "https://<account>.api.fluentcommerce.com",
  "generatedAt": "<ISO timestamp>",
  "workflows": [
    { "name": "", "version": "", "entityType": "", "entitySubtype": "",
      "statuses": 0, "rulesets": 0, "rules": 0,
      "userActions": [], "subEntityTypes": [],
      "statusCatalog": { "ORDER": ["CREATED", "COMPLETE"] } }
  ],
  "statusCatalog": {
    "ORDER": ["CREATED", "BOOKED", "COMPLETE", "CANCELLED"]
  },
  "modules": [
    { "name": "", "type": "REFERENCE|CUSTOM", "version": "", "customRules": 0, "ootbRules": 0 }
  ],
  "catalogues": {
    "product": [],
    "inventory": [],
    "virtual": [],
    "controlGroup": []
  },
  "locations": { "count": 0, "hasMore": false, "byType": {} },
  "networks": [],
  "settings": { "count": 0, "hasMore": false, "keySettings": [] },
  "ui": {
    "manifests": [],
    "customComponents": [],
    "plugins": []
  },
  "entityExistence": {
    "ORDER": { "exists": true, "queriedStatuses": ["CREATED", "BOOKED", "COMPLETE", "CANCELLED"] }
  },
  "patterns": []
}
```

## Output

| Artifact | Path | Purpose |
|---|---|---|
| Snapshot report | `accounts/<PROFILE>/snapshot/account-snapshot.md` | Human-readable account state |
| Machine-readable | `accounts/<PROFILE>/snapshot/account-snapshot.json` | CI/CD, diffing, tracking |
| Workflows (raw) | `accounts/<PROFILE>/snapshot/workflows/*.json` | Local analysis without re-downloading |
| Manifests (raw) | `accounts/<PROFILE>/snapshot/manifests/*.json` | Local UI analysis |
| Environment data | `accounts/<PROFILE>/snapshot/environment-*.json` | Raw MCP discovery responses (3 files: retailer, locations, catalogues) |

## Progress

Emit this block at each phase transition:

```
▸ /fluent-account-snapshot [1/3]
  → Collect (MCP calls)    ○ Analyze (local)    ○ Report
```

Update as each phase completes.

## Cost Profile

| Phase | LLM context tokens (responses) | MCP calls | Time |
|---|---|---|---|
| Phase 1: Collect | ~6,000-8,000 (tool responses enter context) | 8-12 calls | 15-40s |
| Phase 2: Analyze | ~2,000 (Bash script outputs) | 0 | 5-10s |
| Phase 3: Report | ~3,000 (report generation) | 0 | 5-10s |

**Total: ~11,000-13,000 tokens.** The critical optimization is `keysOnly: true` on `plugin.list` (or `compact: true` fallback) plus splitting `environment.discover` into focused calls instead of requesting every section at once.

## Comparison with Other Skills

| Skill | Overlap | Difference |
|---|---|---|
| `/fluent-implementation-map` | Also discovers workflows, rules, settings | Implementation-map clusters into features, scores customization, generates Mermaid. Much heavier LLM usage. Use snapshot for quick inventory, implementation-map for deep analysis. |
| `/fluent-connect` | Also discovers profiles, retailers, workflows | Connect is an onboarding wizard (wires MCP, downloads source). Use connect for first-time setup, snapshot for recurring state checks. |
| `/fluent-module-validate` | Also inventories modules and rules | Module-validate does per-module structural validation. Use snapshot for overview, module-validate for build-readiness. |
| `/fluent-mystique-analyze` | Also analyzes manifest structure | Mystique-analyze does deep component/complexity scoring. Use snapshot for manifest overview, analyze for detailed UI audit. |

## Downstream Consumption

Other skills could read `account-snapshot.json` to avoid re-querying. This is **not yet implemented** — currently aspirational. When a skill adds snapshot-awareness, document it here.

| Skill | Would Use | Status |
|---|---|---|
| `/fluent-implementation-map` | workflows, modules, rules | Not yet — still does own collection |
| `/fluent-pre-deploy-check` | modules, workflows, settings | Not yet |
| `/fluent-rfl-assess` | full snapshot | Not yet |

**To add snapshot-awareness to a skill:** At pre-flight, check if `accounts/<PROFILE>/snapshot/account-snapshot.json` exists and `generatedAt` < 24h. If so, read it instead of re-querying. If stale, warn and re-collect.

## Error Handling

| Error | Action |
|---|---|
| `config.validate` fails | STOP — report config error, do not proceed |
| `workflow.list` returns 401 | Fall back to `workflow.get` for known entity types (ORDER, FULFILMENT, INVENTORY_CATALOGUE, RETURN_ORDER, WAVE, ARTICLE) |
| `environment.discover` section truncated (100 items) | Flag in report as "possibly truncated — use graphql.queryAll for full list" |
| `setting.get` returns no manifests | Report "No custom UI manifests found" — valid for API-only accounts |
| Entity presence query fails (auth/permission) | Record `{ exists: "unknown", error: "..." }` — do not block the snapshot |
| MCP extension not connected | STOP — this skill requires the MCP extension server |
| `plugin.list` fails | Skip module/rule inventory, flag as "rule registry unavailable" |

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

## Known Pitfalls

- **No count queries in Fluent GraphQL** — Do NOT try `orders { totalCount }` or `aggregate` queries. They don't exist. Use `first: 1` + `pageInfo.hasNextPage` for existence checks.
- **Statuses are flexible in Fluent** — Never hardcode `COMPLETE` or any other status in entity probes. Derive statuses from downloaded workflows, show that list in the snapshot, query on those statuses, and only fall back to an unfiltered probe as a mismatch check.
- **plugin.list without keysOnly/compact = token bomb** — `plugin.list({})` returns massive payloads. Prefer `plugin.list({ keysOnly: true })`; if unavailable, fall back to `compact: true`.
- **environment.discover ALL sections = token bomb** — Requesting all 8 sections in one call can return 15,000-25,000 tokens. Split into focused calls: `["retailer","users"]`, then `["locations","networks"]`, then `["catalogues","modules"]`.
- **MCP responses enter LLM context** — Even though Phase 1 is "MCP tool calls", every response flows through the conversation. Budget accordingly. Use `outputFile`/`outputDir` wherever the tool supports it.
- **environment.discover truncates at 100** — Locations, settings, and similar sections cap at 100 items. The snapshot flags this but cannot report exact totals.
- **workflow.list may 401** — Some accounts restrict workflow listing. Fall back to individual `workflow.get` calls for known entity types.
- **Manifest lobValue double-encoding** — When reading manifest settings, `lobValue` may already be parsed JSON or a JSON string. Always check `typeof` before parsing.
- **Stale snapshots** — Snapshots are a point-in-time capture. Always check `generatedAt` before relying on the data for deployment decisions.
- **Custom component over-reporting** — Built-in aliases like `select`, `retailer`, `AttributeInput.list`, `ExtendedAddressInput`, etc. don't start with `fc.` but are NOT custom components. Filter these out before reporting.

<!-- end feedback-promoted -->

## Tips

- Run snapshot after `/fluent-connect` to get a baseline state of a new account
- Re-run snapshot periodically to detect drift (diff the JSON outputs)
- Use `--include workflows,modules` to limit scope when you only need part of the picture
- The downloaded workflow files in `snapshot/workflows/` can be used directly by `/fluent-workflow-analyzer` and `/fluent-workflow-validate` — no re-download needed
