# Profiles and Edit Loop

> Read, create, edit, dry-run, preview, publish, roll back, assign and archive cart profiles through the Storefront MCP.

Canonical URL: https://77865786950d:3005/cart/profiles

The MCP supports the same cart lifecycle as the app. Design and promotion
changes save a draft. Publishing changes the live profile and synchronizes
managed Shopify discounts, so it requires explicit approval of the exact draft
version. See [Cart Overview](/cart#mcp-actions) for the action table.

Call `lexsis_discover` with the router and action for the current schema, and
put action parameters inside `args`. Validation errors contain the relevant
paths and issues.

These features require the updated MCP, API, renderer and island runtime.
Documentation availability is not deployment proof.

## Read a profile

Call `lexsis_cart.get` before assigning or editing a cart.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `page_id` | UUID | No | Resolve the cart a shopper receives on this page |
| `cart_profile_id` | UUID | No | Fetch one editable profile |
| `store_id` | UUID | No | Optional store hint for a multi-store account |
| `include_available_profiles` | boolean | No | Include a compact profile list, defaults to `true` |
| `fields` | string[] | No | Explicit profile fields or dotted paths; unknown fields fail |
| `response` | `summary` or `full` | No | Compact metadata by default; full source is opt-in |
| `include_history` | boolean | No | Include lifecycle audit history |

Pass either `page_id` or `cart_profile_id`, not both. Passing only `store_id`
lists the available profiles.

Read only what you need:

```json
{
  "cart_profile_id": "PROFILE_UUID",
  "fields": ["id", "version", "design_spec", "layout_schema"]
}
```

Field paths are relative to the profile, such as `version` or
`design_spec.shell.title`; the `profile.` prefix is also accepted. Invalid
fields return `VALIDATION_ERROR` and `valid_fields`.

When resolving a page, the response includes
`effective_profile.resolution_source`: `page`, `campaign`, `default`,
`legacy`, or `none`. The effective profile is the published snapshot used by
the renderer. An individually fetched profile is the editable draft and
includes `has_unpublished_changes`.

`cart_edit`, `cart_create`, `cart_duplicate`, `cart_promotions_edit` and
`cart_rollback` also return compact summaries with `cart_profile_id` and
`version` by default. Set `response: "full"` for the complete profile.

## Create or duplicate

Use `lexsis_drafts.cart_create`:

```json
{
  "store_id": "STORE_UUID",
  "name": "Campaign cart",
  "from_preset": "default"
}
```

Presets are `default`, `subscription`, and `aov_booster`. Alternatively provide
`from_profile_id` to copy a profile in that store. The two source options are
mutually exclusive. Store currency comes from the connected store.

To duplicate directly, call `lexsis_drafts.cart_duplicate` with
`cart_profile_id` and `name`. Neither creation path publishes or creates
discounts.

## Edit design and composition

`lexsis_drafts.cart_edit` applies a partial update to a profile draft. It never
publishes.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `cart_profile_id` | UUID | Yes | Profile to edit |
| `store_id` | UUID | No | Optional store hint |
| `patch` | object | Yes | Partial cart configuration |
| `change_note` | string | No | Short reason for the draft change |
| `expected_version` | positive integer | Yes | Profile version returned by `lexsis_cart.get` |
| `dry_run` | boolean | No | Compile and preview without saving a version |
| `response` | `summary` or `full` | No | Summary by default; full profile is opt-in |
| `page_id` | UUID | No | Page on which to preview the edit |

The patch accepts:

| Field | Purpose |
|---|---|
| `cart_mode` | Drawer, sheet, modal, fullscreen, page, or `headless` presentation |
| `composition_ops` | Ordered `upsert`, `move`, `set_enabled`, or custom-module `remove` operations |
| `custom_css` | Replaces all custom CSS and named blocks; `null` clears it |
| `css_ops` | Ordered named block upsert/remove; cannot accompany `custom_css` |
| `design_patch` | Typed `CartDesignSpecV1` JSON Merge Patch |
| `commerce_config` | Free-shipping threshold, offer slots and cart style |

Use `lexsis_drafts.cart_promotions_edit` for promotion, payment and cart-rule
configuration. See [Cart Styling](/cart/styling) for `design_patch`,
`custom_css` and `css_ops`, and [Custom Cart Modules](/cart/custom-modules)
for `composition_ops` source.

```json
{
  "cart_profile_id": "PROFILE_UUID",
  "expected_version": 1,
  "patch": {
    "design_patch": {"shell": {"title": "Your cart"}}
  }
}
```

### Update design and free shipping

```json
{
  "cart_profile_id": "PROFILE_UUID",
  "expected_version": 8,
  "change_note": "Use the campaign cart treatment",
  "patch": {
    "cart_mode": "drawer-right",
    "commerce_config": {
      "free_shipping_threshold": 7500,
      "free_shipping_celebration": true,
      "cart_style": {
        "width": "440px",
        "responsive": {
          "mobile": "bottom-sheet"
        },
        "line_spacing": "comfortable"
      }
    }
  }
}
```

Money thresholds use the store currency and are expressed in minor units. For
example, `7500` is 75.00 for a two-decimal currency.

### Configure an offer

```json
{
  "cart_profile_id": "PROFILE_UUID",
  "expected_version": 8,
  "patch": {
    "commerce_config": {
      "offer_slots": [
        {
          "id": "pairs-well",
          "placement": "after_line",
          "source": "shopify_recommendations",
          "recommendation_intent": "COMPLEMENTARY",
          "heading": "Pairs well with",
          "trigger_product_ids": [],
          "recommend_product_ids": [],
          "max_items": 1,
          "enabled": true
        }
      ]
    }
  }
}
```

### Rendering warnings

Presentation edits regenerate the rendered cart. Disabled optional modules
stay hidden, including offers beside individual cart items. `shell.title`
controls both the visible title and the dialog's accessible label.

`cart_edit` returns `warnings` with `path`, `code: "setting_not_rendered"`,
and a message for stored settings that do not render. Settings diagnostics
refer only to fields authored in the current patch. Responsive overrides
currently support tokens, drawer width, and module/state CSS. Set structural
options such as title, variant, icons, and payment placement at the base level.
The legacy gift `lock_quantity` setting and unused `warning` color token are
deprecated; confirmed gifts always follow reward quantity rules.

If `expected_version` is stale, reload the profile and reapply the intended
patch to the latest draft rather than overwriting another edit.

## Compile before saving

Add `dry_run: true` to `cart_edit`:

```json
{
  "cart_profile_id": "PROFILE_UUID",
  "expected_version": 10,
  "page_id": "PAGE_UUID",
  "dry_run": true,
  "patch": {
    "design_patch": {"shell": {"title": "Your cart"}}
  }
}
```

The result contains `version`, `dry_run`, `updated_fields`, `module_tree`,
`compile_errors`, `warnings`, and a temporary `preview_url`. The module tree
reports IDs, regions, enabled state and visibility against an empty sample
cart. If static evaluation is unavailable, conditional entries remain
`"conditional"` and `visibility_error` explains the limitation.

The draft version and history do not change. Invalid source returns
`compile_errors` and no preview URL. A stale `expected_version` still fails.

Open the returned URL to review it. To save, repeat the edit with
`dry_run: false` and the same expected version. Saving returns:

```json
{
  "cart_profile_id": "PROFILE_UUID",
  "version": 11,
  "updated_fields": ["design_spec"],
  "warnings": [],
  "preview_url": "SIGNED_PREVIEW_URL",
  "readiness_summary": {
    "status": "not_checked",
    "enabled_rewards": 2,
    "message": "Run promotions readiness before publishing."
  }
}
```

Readiness is not rechecked against Shopify after each design edit. Run
`lexsis_cart.promotions` before publishing. Summary warnings are bounded;
`warnings_omitted` tells you when to request the full response. If an edit
saves but its preview cannot be created, `preview_error` says it was saved:
retry `preview`, not the edit.

## Edit promotions

Call `lexsis_drafts.cart_promotions_edit` with the latest version:

```json
{
  "cart_profile_id": "PROFILE_UUID",
  "expected_version": 2,
  "patch": {
    "rewards": [{
      "id": "shipping",
      "type": "free_shipping",
      "title": "Free shipping",
      "threshold": 150000,
      "discount_source": "managed",
      "enabled": true
    }],
    "coupon_settings": {
      "enabled": true,
      "allow_manual_entry": false,
      "coupons": []
    },
    "payment_settings": {
      "enabled": true,
      "placement": "below_checkout",
      "providers": ["visa", "mastercard"]
    }
  }
}
```

`threshold` and fixed monetary values use minor currency units: `150000` means
₹1,500 for an INR store. Use the actual store currency and verified catalog
IDs. Managed reward codes follow the saved reward contract; existing Shopify
codes are verified rather than overwritten.

| Patch field | Patch semantics |
|---|---|
| `rewards` | Merge rows by `id`; supports reward type, threshold, title, enabled state, gift products/variants and qualifying products/collection |
| `offer_slots` | Merge rows by `id`, including placement, product source and enabled state |
| `quantity_promotions` | Merge rows by `id`; managed or merchant automatic promotions |
| `cart_rules` | Merge rules by `id`; conditions and actions use the existing cart-rule contract |
| `coupon_settings` | Merge settings; `coupons` replaces the allow-list |
| `payment_settings` | Merge settings; `providers` replaces the ordered provider list |

Omitted rows remain unchanged. An empty row-patch array is a no-op. Remove a
row explicitly:

```json
{"rewards": [{"id": "shipping", "operation": "remove"}]}
```

Payment placement accepts `inside_checkout`, `below_checkout`, or `hidden`.
Design controls remain available through `cart_edit`. An explicit promotion
placement/provider edit also updates an existing design override for that field.

The response includes `profile` and `readiness`. Malformed input or stale
versions fail before saving. Incomplete promotion setup can remain a draft;
the readiness preview explains what must be fixed. If Shopify readiness cannot
be checked, the saved draft returns `ready:false` with `readiness_unavailable`.
No Shopify discount writes occur during this edit.

Whole-cart milestone programs intentionally do not require qualifying products:
eligible paid merchandise contributes to spend, excluding reward items and gift
cards. Gift *choices* still require valid products and variants. Readiness rows
identify `qualification_basis:"whole_cart"` and the managed program code; a code
that has not been assigned is `null`, rather than an empty string.

`lexsis_cart.promotions` accepts `{"cart_profile_id":"PROFILE_UUID"}` without
`store_id`. An explicit store remains an optional scope; store-only calls list
Shopify discounts without profile-specific readiness.

## Preview a sample cart

Call `lexsis_cart.preview`:

```json
{
  "page_id": "PAGE_UUID",
  "cart_profile_id": "PROFILE_UUID",
  "fixture": "all_unlocked"
}
```

The response contains a version-pinned `preview_url` valid for 15 minutes.
It creates no browser session. Open it using your browser or your agent's
existing browser tool. Omit `page_id` for an isolated cart preview. A preview
does not prove Shopify discounts have been provisioned.

Available states are `empty`, `below_first_reward`, `between_rewards`,
`all_unlocked`, `gift_chosen`, and `code_applied`. They use the profile's
thresholds, coupons and real synced catalogue variants. An unavailable gift,
missing coupon or unreachable threshold is reported explicitly. A profile
without the relevant feature cannot demonstrate that state.

For exact merchandise selection, provide a fixture object:

```json
{
  "page_id": "PAGE_UUID",
  "cart_profile_id": "PROFILE_UUID",
  "fixture": {
    "lines": [
      {"variantId": "gid://shopify/ProductVariant/123", "quantity": 2}
    ],
    "codes": ["SAVE10"]
  }
}
```

Replace the variant and code with real store values. Each line may include
`sellingPlanId`. The limits are 12 lines, quantity 1–999, and 10 codes.
`fixture_products` supplies explicit variant GIDs, `simulate_rewards:false`
disables automatic gift simulation, and `transition:{from:"below_first_reward",
to:"all_unlocked"}` replays unlocks. Responses include render order, visible
modules, total, savings, gift lines and visibility reasons.

The page displays **Preview: sample cart**. Quantity, remove, add and custom
cart commands operate in memory. Checkout is blocked, tracking is suppressed,
and the fixture never reads or writes the shopper's `lx_cartId`. Reloading
restores the signed sample. The top badge is dismissible; add
`preview_badge=0` to the URL for screenshots. A tampered or expired token logs
`fixture_invalid` and falls back to the normal cart. Badge visibility alone
does not establish whether fixture mode is active.

### Fixture accuracy

Named sample states prefer the page's bound product and synced variant prices.
A missing bound product is reported as unavailable. If the page has no
unambiguous binding, catalogue fallback is explicit in warnings. Country and
language come from Shopify's configured primary region and primary locale; an
unavailable lookup is reported as unknown in fixture context.

Fixtures run cart changes in memory. `lifecycle.cart`, `onCart`, `data-lx-text`,
`data-lx-if` and `visible_when` share the same settled snapshot as the built-in
cart modules. Fixture mode neither resolves products through the Storefront
API nor sends Shopify cart mutations.

Pricing is a simulation, reported as `source: "computed"`; discount estimates
are not Shopify checkout quotes. A coupon with unknown or restricted scope is
marked `evaluation:"not_evaluated"` and adds no savings. Native discount
combinations are not assumed. The managed milestone simulation uses its
configured best-offer policy. Preview-only reward simulation can show
unpublished rewards; readiness warnings remain visible and publishing still
requires readiness. Savings intentionally include the original value of a
chosen free gift, as declared by `pricing.savings_include_gift_value`.

## Inspect all six states

Call `lexsis_cart.preview_states`:

```json
{
  "page_id": "PAGE_UUID",
  "cart_profile_id": "PROFILE_UUID"
}
```

The default compact result (`format_version:3`) contains six entries with
`url`, `total`, `savings`, `visible_modules`, `warnings`, and a `pass`,
`warn`, `fail` or `not_run` status. With `response:"full"`, `format_version:2`
hoists invariant checks and module definitions into `shared`. Each full state
retains its snapshot, status, `preview_url`, module `id`/`enabled`/`visible`
and any checks that vary. Combine `shared.modules` with each state's
visibility by module ID. Informational settings appear in `static_checks.info`
and do not cause `status:"warn"`.

Static checks cover:

- Stored settings without a rendered effect.
- Configured text/surface, button-text/accent, muted/surface and reward color
  pairs against the 4.5:1 contrast threshold. Inherited or unresolved colors
  are `not_checked`.
- Unready rewards in visible reward modules.
- Custom module compilation errors.
- Hardcoded checkout links.

These checks do not measure DOM layout, overflow, actual font size, touch
targets or computed styles, and they do not produce screenshots. Open the
URLs at mobile and desktop sizes for visual review. No browser or screenshot
service is started by Lexsis.

## Publish

After the user explicitly approves the exact version, call
`lexsis_live_ops.cart_publish`:

```json
{
  "cart_profile_id": "PROFILE_UUID",
  "expected_version": 3,
  "allow_partial": false,
  "skip_reward_ids": []
}
```

The response includes `published_version`, `shopify.discounts_created`,
`shopify.discounts_updated`, `shopify.discounts_disabled`, and `warnings`.
Resolving Shopify contracts can create an additional version; use the returned
version rather than assuming it equals the input.

- `VERSION_CONFLICT`: re-read the draft and obtain approval for its current version.
- `CART_NOT_READY`: inspect `issues`, fix the draft and preview again.
- `allow_partial:true` only permits the explicitly listed `skip_reward_ids` to
  publish disabled. Remaining reward or non-reward issues still block publication.
- Discounts shared by another published profile remain active and return a warning.

The previous published profile stays live until Shopify work succeeds and the
database transaction commits. Failed writes trigger cleanup of new discounts
and restoration of retired discounts. Shopify does not support a transaction
across multiple mutations: if cleanup cannot be confirmed, the tool returns
`CART_PUBLISH_RECOVERY_REQUIRED` with recovery details. Resolve that condition
before retrying; do not treat it as a successful publish.

Publishing a profile refreshes affected published Shopify pages. This boundary
prevents an agent edit from changing a shopper-facing cart without merchant
review.

## Roll back

Restore a previous configuration with `lexsis_live_ops.cart_rollback`:

```json
{"cart_profile_id": "PROFILE_UUID", "target_version": 1}
```

This creates a new draft version and keeps the existing live pointer unchanged.
Review it, then separately approve `cart_publish`. Managed discounts retired
by a later version are verified and reactivated during publication when needed.

## Assign a page, campaign or default

All targets must belong to the same authorized store, and each change requires
publish access and explicit approval.

Assign a published profile to one page with `lexsis_drafts.cart_set`. A new
assignment replaces the previous page assignment; pass `cart_profile_id: null`
to restore campaign, store-default, or legacy fallback resolution:

```json
{"page_id": "PAGE_UUID", "cart_profile_id": "PUBLISHED_PROFILE_UUID"}
```

The action returns the effective profile after the change, and Lexsis refreshes
the affected published Shopify page automatically.

Assign a campaign with `lexsis_drafts.cart_assign_campaign`; pass
`cart_profile_id: null` to clear the override:

```json
{"campaign_id": "CAMPAIGN_UUID", "cart_profile_id": "PUBLISHED_PROFILE_UUID"}
```

Set the store default with `lexsis_live_ops.cart_set_default`:

```json
{"store_id": "STORE_UUID", "cart_profile_id": "PUBLISHED_PROFILE_UUID"}
```

## Archive

Archive with `lexsis_drafts.cart_archive`:

```json
{"cart_profile_id": "OLD_PROFILE_UUID", "reassign_to": "PUBLISHED_REPLACEMENT_UUID"}
```

Omit `reassign_to` only when the profile is neither default nor assigned;
archiving a default or assigned profile without a replacement fails.
Reassignment changes live resolution, requires explicit approval, and
preserves page/campaign assignments and their priority/enabled settings.
Archival does not delete historical snapshots or mutate Shopify.

## Audit history

Call `lexsis_cart.get` with `cart_profile_id` and `include_history:true`.
It returns the latest 100 lifecycle events, including actor kind/user ID,
authenticated MCP client, timestamp, version and a diff summary. Custom source
and credentials are excluded from summaries. Existing snapshots remain
available in the app's version history.

## Request smaller island schemas

`lexsis_design.island_schema` returns a compact prop overview by default.
Repeated icon names appear once under `$defs.iconName`. Nested props marked
`expand: true` can be requested explicitly:

```json
{
  "name": "CartRewardProgress",
  "fields": ["rewards", "visuals"],
  "verbose": false
}
```

Use `verbose: true` for the full guidance, styling and examples. Shared
definitions use `$ref`; a `type_expression` preserves the authoring schema's
shape notation and can contain `$ref(#/$defs/iconName)` aliases. This is an
authoring contract, not a standalone JSON Schema validator.

## Related

- [Cart Overview](/cart)
- [Cart Styling](/cart/styling)
- [Custom Cart Modules](/cart/custom-modules)
- [Open Cart profiles in the Lexsis app](https://app.trylexsis.com/storefront/settings/cart)
