---
title: "Save a code rule's draft"
method: PUT
path: "/v25-10-beta/agentic-logic/rules/{ruleId}/draft"
tags: ["Agentic Logic"]
---

# Save a code rule's draft

`PUT /v25-10-beta/agentic-logic/rules/{ruleId}/draft`

## Request

- Base URL: `https://api.intelligems.io`
- URL: `https://api.intelligems.io/v25-10-beta/agentic-logic/rules/{ruleId}/draft`
- Auth: API key in header `intelligems-access-token`
- TypeScript: <https://skmtc.dev/intelligems/apis/intelligems-external-api-v25-10-beta/docs/v25-10-beta/agentic-logic/rules/:ruleId/draft/put.ts> (a function with typed inputs; its response is checked against the schema)

```http
PUT /v25-10-beta/agentic-logic/rules/{ruleId}/draft
```

Saves the draft of a code rule. The response is `{ draft }`. A builder rule
answers 400.

- `source` (string, required) — The rule source.
- `dependencies` (array, optional) — Must be empty: rules run with no npm
  packages.
- `userTestSuite` / `generatedTestSuite` (string, optional) — Omit to keep the
  stored suite; send `""` to clear it.
- `validationScenarios` (array, optional) — Scenarios the publish checks run
  the rule against. Omit to keep the stored ones; send `[]` to discard them.
- `graph` (object, optional) — The rule graph for this source, checked against
  it at save time. When omitted, the stored graph is kept only if the source
  is unchanged.
- `origin` (`chat` or `manual`, optional) — Where the source came from. Omit to
  keep the stored value.
- `autoAssist` (boolean, optional) — Whether the save starts AI assistance.
  Defaults to `true`. Send `false` to only store the draft (see below). Applies
  to this call only; it is not stored on the rule.

Every save replaces the previous draft. Publish snapshots the draft, so an
in-flight publish is not affected by a later save. Unless `autoAssist` is
`false`, a save also:

- Regenerates the rule graph with AI when the saved draft has no graph.
- Regenerates the AI-generated validation scenarios when they no longer cover
  the source's `attempt()` branches. Scenarios you authored are kept.

The first save of a `draft` rule that has never had a publish job, and that
does not send `autoAssist: false`, also starts a dry run of the publish checks. If the checks fail, an AI repair loop rewrites
the draft and runs them again, within its attempt, time, and cost limits. Once
the checks pass, the rule is published without any further call. If a rewrite
changed what the rule does, the loop stops instead and leaves the rewritten
draft for a person to publish.

Saving a draft does not stop a running repair loop. If you save while the loop
is rewriting, the loop's rewrite is dropped, that attempt counts against its
limit, and the loop decides its next step from your draft.

With `autoAssist: false`, the save starts neither the regeneration above nor
the dry run, and it does not stop a repair loop that is already running. A
later save without the flag still starts the dry run while the rule has never
had a publish job. A later publish still works: with no graph the graph
check passes with a note, with no scenarios the default scenario is used, and
the AI test suite is generated at publish time.

A save that omits `validationScenarios` keeps the stored ones unchanged, so
scenarios from an earlier save, including AI-generated ones, carry forward and
may no longer match the new source's branches. While they do not, the
[scenarios listing](#list-a-rules-scenarios) reports `pending` until a save
without `autoAssist: false` regenerates them, or a save sends a full generated
set: sent scenarios replace the stored ones rather than adding to them.

Use the `intelligems-access-token` header. The token selects the organization; do not send `orgId`. The organization must have Agentic Logic enabled.

**Rate Limit:** 50 requests per 30s window, refilling 25 tokens every 30s.

## Path parameters

- `ruleId` string, uuid, required

## Request body

- object: Overwrite the rule's single draft. Publishing snapshots the draft at job creation, so later saves never change an in-flight publish.
  - `source` string, required: Draft rule source code.
  - `userTestSuite` string: User-authored test suite to persist with the draft. Omit to leave the existing suite untouched; send an empty string to clear it.
  - `generatedTestSuite` string: Edits to the AI-generated test suite. Omit to leave the stored suite untouched; send an empty string to clear it and let the next publish generate a fresh one. An explicit suite is stored as hand-authored, so the next publish runs it to decide whether it still covers the rule rather than assuming either way.
  - `dependencies` object[]: Must be empty. Agentic Logic rules run with no third-party npm packages: the rule runtime's no-network, no-clock boundary is scoped to the rule's own files, and a bundled package would run inside the rule with that boundary drawn around nothing. Read commerce state through the injected SDK instead.
    - `name` string, required: npm package name.
    - `version` string, required: Requested semver range.
  - `validationScenarios` object[]: Request-side scenarios the publish gates evaluate the rule against. Omit to leave the draft's existing scenarios untouched; send an empty array to discard them. Publishing requires at least one. There is no read-side half to save: gate-time SDK reads are served live, and the scan-cost report is derived from the validation run itself rather than persisted here.
    - `name` string, required: Human-readable scenario name, shown in test results and gate details.
    - `request` object, required: The recommendation request the scenario evaluates. `placementId` and `surface` here are wire shape, not references: gates evaluate the rule directly, never an assignment, so no real placement is involved.
      - `surface` 'onsite' | 'checkout' | 'post-purchase', required: Surface the recommendation will render on. There is deliberately no 'headless' value — API callers declare the surface they render.
      - `cart` object, nullable, required: Snapshot of the visitor's current cart. An empty cart (zero lines) is a legal cart — distinct from cart: null, which means the surface has no cart at all (post-purchase).
        - `currency` string, required: ISO-4217 currency code all money fields in this cart are in.
        - `subtotal` integer, required: Cart merchandise subtotal in integer minor units of `currency`, before shipping and taxes.
        - `itemCount` integer, required: Total unit quantity across all lines (sum of line quantities) — units, not the count of distinct lines.
        - `lines` object[], required: The cart lines.
          - `productId` string, required: Shopify product GID (gid://shopify/Product/<n>). The bare numeric id the Shopify admin and most PIM exports show is accepted and promoted to GID form on the way in; anything that is neither is rejected.
          - `variantId` string, required: Shopify variant GID (gid://shopify/ProductVariant/<n>). The bare numeric id the Shopify admin and most PIM exports show is accepted and promoted to GID form on the way in; anything that is neither is rejected.
          - `quantity` integer, required: Number of units on the line. Positive integer.
          - `unitPrice` integer, required: Per-unit price actually in the cart, in integer minor units of the cart's currency.
          - `lineTotal` integer, required: Line total (unit price times quantity, after line-level adjustments), in integer minor units of the cart's currency.
          - `selectedOptions` object[], required: Selected product options for the line's variant.
            - `name` string, required: Option name as configured on the Shopify product (e.g. 'Size').
            - `value` string, required: Selected option value (e.g. 'Medium').
          - `isSubscriptionLine` boolean, required: True when the line carries a selling plan (subscription), false for a one-time purchase.
          - `flags` object, required: Provenance flags (gift card, synthetic gift, bundle child, Intelligems upsell). Which flags a surface can actually answer differs per surface — see the request contract's per-surface caveats.
            - `giftCard` boolean, nullable, required: True when the line is a Shopify gift-card product, false when known not to be one, or null when the surface exposes no authoritative marker. NULL on every checkout and post-purchase line — neither surface exposes a gift-card marker, so a clause keyed on this flag is inert there.
            - `syntheticGift` boolean, nullable, required: True when the line was injected as a gift-with-purchase by a promotion, false when known not to be one, or null when the surface exposes no authoritative marker.
            - `bundleChild` boolean, nullable, required: True when the line is a child component of an expanded bundle, false when known not to be one, or null when the surface exposes no authoritative marker. NULL on every post-purchase line, and never `true` onsite — `/cart.js` does not list bundle component lines at all.
            - `igUpsell` boolean, nullable, required: True when the line was added by an Intelligems upsell, false when known not to be one, or null when the surface exposes no authoritative marker.
          - `tags` string[]: The line product's Shopify tags, hydrated ONLY when an evaluated rule targets on tags — absent otherwise. Absent means 'not looked up' and an empty array means 'looked up, none'; the two are never interchangeable, and a rule reading an absent value must fail loudly rather than treat it as no tags.
          - `collections` string[]: Shopify collection GIDs the line product belongs to, hydrated ONLY when an evaluated rule targets on collections — absent otherwise. Bare numeric collection ids are accepted and promoted to GID form on the way in. Same absent-vs-empty rule as `tags`.
      - `order` object, nullable, required: The completed order a post-purchase surface is rendered against. First-class on the post-purchase surface only; null everywhere else. Deliberately carries NO customer field — the top-level request `customer` is the single customer reference.
        - `orderId` string, required: Shopify order GID (gid://shopify/Order/<n>).
        - `checkoutToken` string: Shopify checkout token for the checkout that produced this order, when available.
        - `createdAt` string, date-time, required: Order creation time as an ISO-8601 UTC timestamp.
        - `currencyCode` string, required: ISO-4217 shop currency code the order's money fields are denominated in.
        - `presentmentCurrency` string, required: ISO-4217 currency code the shopper saw at checkout, which may differ from `currencyCode`.
        - `itemsSubtotal` integer, required: Merchandise subtotal after line-level discounts but before order-level discounts, shipping, and taxes. Integer minor units.
        - `discountTotal` integer: Total order-level discount amount in integer minor units, when any.
        - `shippingTotal` integer: Shipping charged in integer minor units, when known.
        - `taxTotal` integer: Taxes charged in integer minor units, when known.
        - `total` integer, required: What the shopper was actually charged, in integer minor units.
        - `lines` object[], required: The purchased order lines.
          - `productId` string, required: Shopify product GID (gid://shopify/Product/<n>). The bare numeric id the Shopify admin and most PIM exports show is accepted and promoted to GID form on the way in; anything that is neither is rejected.
          - `variantId` string, required: Shopify variant GID (gid://shopify/ProductVariant/<n>). The bare numeric id the Shopify admin and most PIM exports show is accepted and promoted to GID form on the way in; anything that is neither is rejected.
          - `quantity` integer, required: Number of units on the line. Positive integer.
          - `unitPrice` integer, required: Per-unit price actually in the cart, in integer minor units of the cart's currency.
          - `lineTotal` integer, required: Line total (unit price times quantity, after line-level adjustments), in integer minor units of the cart's currency.
          - `selectedOptions` object[], required: Selected product options for the line's variant.
            - `name` string, required: Option name as configured on the Shopify product (e.g. 'Size').
            - `value` string, required: Selected option value (e.g. 'Medium').
          - `isSubscriptionLine` boolean, required: True when the line carries a selling plan (subscription), false for a one-time purchase.
          - `flags` object, required: Provenance flags (gift card, synthetic gift, bundle child, Intelligems upsell). Which flags a surface can actually answer differs per surface — see the request contract's per-surface caveats.
            - `giftCard` boolean, nullable, required: True when the line is a Shopify gift-card product, false when known not to be one, or null when the surface exposes no authoritative marker. NULL on every checkout and post-purchase line — neither surface exposes a gift-card marker, so a clause keyed on this flag is inert there.
            - `syntheticGift` boolean, nullable, required: True when the line was injected as a gift-with-purchase by a promotion, false when known not to be one, or null when the surface exposes no authoritative marker.
            - `bundleChild` boolean, nullable, required: True when the line is a child component of an expanded bundle, false when known not to be one, or null when the surface exposes no authoritative marker. NULL on every post-purchase line, and never `true` onsite — `/cart.js` does not list bundle component lines at all.
            - `igUpsell` boolean, nullable, required: True when the line was added by an Intelligems upsell, false when known not to be one, or null when the surface exposes no authoritative marker.
          - `tags` string[]: The line product's Shopify tags, hydrated ONLY when an evaluated rule targets on tags — absent otherwise. Absent means 'not looked up' and an empty array means 'looked up, none'; the two are never interchangeable, and a rule reading an absent value must fail loudly rather than treat it as no tags.
          - `collections` string[]: Shopify collection GIDs the line product belongs to, hydrated ONLY when an evaluated rule targets on collections — absent otherwise. Bare numeric collection ids are accepted and promoted to GID form on the way in. Same absent-vs-empty rule as `tags`.
          - `title` string: Product title at purchase time, when available.
          - `vendor` string: Product vendor at purchase time, when available.
          - `productType` string: Shopify product type at purchase time, when available.
          - `sellingPlanId` string: Shopify selling-plan GID (gid://shopify/SellingPlan/<n>) when the line was purchased on a subscription.
          - `discountAllocations` object[]: Line-level discount allocations applied to this line, when any.
            - `amount` integer, required: Discount amount allocated to the line, in integer minor units of the order's currency.
            - `title` string: Discount title or code as shown on the order, when available.
      - `pageProduct` object, nullable, required: The product a PDP placement is rendered on. Populated for onsite product-page placements only; null everywhere else.
        - `productId` string, required: Shopify product GID of the product being viewed (gid://shopify/Product/<n>). The bare numeric id the Shopify admin shows is accepted and promoted to GID form on the way in; anything that is neither is rejected.
        - `handle` string, required: Shopify product handle (URL slug) of the product being viewed.
        - `targeting` object, nullable: Theme product facts used for onsite product targeting. Null when the theme has no product data; product-targeted branches then do not match. Absent from clients that predate the field; serving then reads the facts from Shopify when the rule targets the product.
          - `tags` string[], required
          - `collectionIds` integer[], required
          - `inventory` integer, required
          - `lowestVariantPrice` integer, required
      - `customer` object, nullable, required: Minimal identified-customer context, or null for guests. PII-MINIMAL GUARANTEE: this object carries ids, counts, and money aggregates ONLY — no email, name, phone, or address field may ever be added here. That is a contract property, not a data-availability accident; richer profile data is fetched at rule runtime via sdk.getCustomer().
        - `id` string, required: Shopify customer GID (gid://shopify/Customer/<n>).
        - `ordersCount` integer, nullable, required: Number of orders this customer has placed, or null when the surface did not supply it — checkout supplies it for a verified buyer when the app's protected-customer-data grant covers it; onsite and post-purchase never do. Integer, >= 0, when present. Null-check it, or use sdk.getCustomer() for a count that does not depend on the grant.
        - `amountSpent` object: Lifetime spend aggregate, when known.
          - `amount` integer, required: Lifetime amount spent in integer minor units of `currency`.
          - `currency` string, required: ISO-4217 currency code `amount` is denominated in.
      - `shipping` object, required: Shipping-incentive context. Required — 'not-applicable' is expressed in-band via its state field, never by omitting the object.
        - `state` 'resolved' | 'assumed' | 'not-applicable' | 'unavailable', required: 'resolved' = thresholds are known facts for this visitor; 'assumed' = best-effort defaults that may be wrong, so treat them as a hint rather than a fact when steering; 'not-applicable' = the store has no shipping incentive here, and the order may not be shipping at all; 'unavailable' = the shipping facts could not be read before the serving deadline (a Shopify read failed or timed out), so no threshold field is present and nothing is known — never read it as 'no incentive'.
        - `currency` string, required: ISO-4217 currency code the threshold fields are denominated in. Always present, even when state is 'not-applicable'.
        - `freeShippingThreshold` integer: Cart subtotal at which shipping becomes free, in integer minor units — pre-resolved to the visitor's assigned variation's threshold. Absent when there is no free-shipping incentive.
        - `remainingToFreeShipping` integer, nullable: How much more the visitor must add to qualify for free shipping, in integer minor units. 0 = already qualifies; absent or null = unresolvable.
        - `tiers` object[]: Tiered incentive ladder in ascending threshold order, when the store runs tiered incentives.
          - `threshold` integer, required: Cart subtotal at which this tier unlocks, in integer minor units of the shipping context's currency.
          - `label` string, required: Shopper-facing label for the tier (e.g. 'Free shipping').
        - `delivery` object: The delivery options Shopify computed for this checkout, as observed facts rather than merchant settings. Present only at checkout, and only once Shopify has computed options (an address or a pickup selection exists); absent onsite, post-purchase, and on first render. Present under every state, including 'not-applicable' for local pickup.
          - `isLocalPickup` boolean, required: True when every selected delivery option is a pickup option — the buyer is not shipping anything.
          - `selected` object: The buyer's currently selected option, when exactly one delivery group has a selection. Absent before the buyer picks, and when multiple groups (a subscription plus a one-time purchase) select different options — read `options` then.
            - `title` string: Shopper-facing option title, when Shopify reports one.
            - `isPickup` boolean, required: True for a pickup option (pickup point or pickup location) rather than a shipping rate. Pickup options carry no cost and no transit estimate.
            - `selected` boolean, required: True when this is the buyer's currently selected option in its delivery group.
            - `cost` integer: The option's cost after shipping discounts, in integer minor units of `currency`. 0 = free. Absent on pickup options, which have no cost.
            - `currency` string: ISO-4217 currency code of `cost`. Absent with it.
            - `carrierName` string: Carrier presenting the option, when Shopify reports one.
            - `transitDays` object: Estimated time in transit in whole days, when Shopify reports one — `min` rounded down, `max` rounded up. Shipping options only.
              - …
          - `options` object[], required: Every option Shopify computed, flattened across delivery groups, in Shopify's order. A zero-cost non-pickup option means the buyer already qualifies for free shipping.
            - `title` string: Shopper-facing option title, when Shopify reports one.
            - `isPickup` boolean, required: True for a pickup option (pickup point or pickup location) rather than a shipping rate. Pickup options carry no cost and no transit estimate.
            - `selected` boolean, required: True when this is the buyer's currently selected option in its delivery group.
            - `cost` integer: The option's cost after shipping discounts, in integer minor units of `currency`. 0 = free. Absent on pickup options, which have no cost.
            - `currency` string: ISO-4217 currency code of `cost`. Absent with it.
            - `carrierName` string: Carrier presenting the option, when Shopify reports one.
            - `transitDays` object: Estimated time in transit in whole days, when Shopify reports one — `min` rounded down, `max` rounded up. Shipping options only.
              - …
      - `country` string, nullable: OPTIONAL. ISO-3166-1 alpha-2 code of the market this request should be evaluated in, e.g. 'CA'. Omit it (or send null) and the platform evaluates in its default market, 'US'. Name it whenever the cart is denominated in a non-shop currency, so the catalog reads run in the same market the money does.
      - `cartFingerprint` string: Opaque client-supplied fingerprint of the cart state this request was built from. The engine echoes it verbatim on the result envelope so the rendering surface can compare and discard late results.
      - `placementId` string: Optional at authoring time; omit it and the server fills the rule's own id when the draft is saved. Wire shape only — publish gates evaluate the rule directly, never a placement assignment, so no real placement is needed and the author should never be asked for one.
    - `context` object: Optional evaluation context override. When absent the harness supplies a representative preview context.
      - `isPreview` boolean, required: True when the evaluation is a preview (editor/test-lab) run rather than live traffic.
      - `slotsNeeded` integer, required: How many recommendation slots the placement wants filled. Non-negative integer, at most 50 — ZERO is reachable, because a placement can resolve to no slots, so a rule must answer zero with an empty envelope before it reads anything.
      - `excludeOutOfStock` boolean: The rule's out-of-stock setting. When true, every SDK catalog read and product recommendation drops products with no variant for sale, so rule code never filters stock itself. Defaults to true, the rule setting's own default. A read that passes its own `excludeOutOfStock` overrides it.
    - `evaluatedAt` integer: Optional author-pinned evaluation instant, epoch milliseconds UTC. When absent the harness stamps the publish job's own instant. Pin it to make a time-dependent scenario stable: a case like 'a 30-day supply ordered 34 days ago' needs both ends fixed, or its expected verdict drifts with the calendar.
    - `description` string: Display copy for scenario pickers, phrased for a merchant (e.g. 'A cart that matches your matching cover-ups rule'). Absent on scenarios authored before display copy existed.
    - `targetBranchId` string, nullable: The branch this scenario is designed to fire: the graph branch node id (=== the attempt() base label). Null marks the nothing-matches scenario whose cart clears no branch; absent means unannotated (typically hand-authored).
    - `origin` 'authored' | 'generated': 'authored' = written by a person or authoring agent alongside the rule; 'generated' = produced by the scenario-generation service. Regeneration replaces only 'generated' scenarios and never clobbers authored ones.
  - `graph` object: The rule-graph artifact emitted WITH this source. Validated at save time against the submitted code (branch node ids must equal the attempt() base labels; structure must be a capped DAG) and rejected with mismatch details on disagreement. Omit to clear any previously saved graph — a graph never outlives the source it described.
    - `version` 1, required: Graph wire-format version. Currently 1.
    - `nodes` object[], required: Graph nodes, at most RULE_GRAPH_MAX_NODES. Large inputs must be aggregated into node meta, never expanded per item.
      - `id` string, required: Stable node id, unique within the graph. For kind 'branch', MUST equal the attempt() base label (a static string literal in the code). Runtime attempt instances are drill-in data on the node, never nodes of their own.
      - `kind` 'input' | 'branch' | 'generator' | 'filter' | 'sort' | 'limit' | 'exclusions' | 'output' | 'fallback-terminal', required: 'input' | 'branch' | 'generator' | 'filter' | 'sort' | 'limit' | 'exclusions' | 'output' | 'fallback-terminal'. Branch nodes mirror attempt() bases; pipeline kinds describe the flow inside or after a branch, while Exclusions is the platform-owned final filter.
      - `label` string, required: Short card title shown on the node.
      - `summary` string: One plain-language sentence describing what the node does.
      - `branchId` string: Owning branch node id, set ONLY on generator/filter/sort/limit nodes that run inside that branch; absent for shared pipeline nodes and always absent on 'input', 'branch', 'exclusions', 'output' and 'fallback-terminal' nodes — a branch never names itself.
      - `meta` object: Kind-specific structured details (e.g. generator source, aggregate itemCount + sampleItems, limit N). Aggregates belong here — never one node per item.
        - `itemCount` integer: Aggregate count of items the node covers (e.g. a generator drawing from a 1000-product list).
        - `sampleItems` string[]: Up to five representative item names for drill-in display; never the full list.
        - `condition` string: Plain-language matching clause for branch/filter nodes, phrased for a merchant ('in stock right now', 'cart contains a tagged cover-up'). The MATCHING card body.
        - `conditionSubject` 'cart' | 'delivery' | 'shipping' | 'customer' | 'page' | 'order' | 'time': Which request context a branch node's condition chip is about — 'cart' (the default when absent), 'delivery' (request.shipping.delivery: selected option, local pickup, cost, carrier, transit days), 'shipping' (gap, threshold, tiers, state), 'customer' (ordersCount, tags), 'page' (request.pageProduct), 'order' (request.order), 'time' (request.evaluatedAt). Drives the row-header sentence; absent on always-matching branches and on every non-branch node.
        - `sortKey` string: Plain-language sort criterion for sort nodes ('Best sellers'). The SORTED BY card body.
        - `limit` integer: Maximum number of items a limit node lets through. Rendered as 'UP TO n products'.
    - `edges` object[], required: Directed edges between node ids, at most RULE_GRAPH_MAX_EDGES.
      - `from` string, required: Source node id.
      - `to` string, required: Target node id.
      - `kind` 'flow' | 'fallback', required: 'flow' = data/control flow; 'fallback' = taken when the source produced nothing (branch miss or empty output). Fallback edges may only terminate at a branch or the fallback-terminal.
  - `origin` 'chat' | 'manual': Provenance of the submitted source: 'chat' when the editor buffer was applied from the workspace AI chat and untouched since, 'manual' when a person authored or last edited it. Omit to leave the stored provenance untouched.
  - `autoAssist` boolean: Whether the save starts AI assistance. Defaults to true. False stores the draft without starting AI graph or scenario regeneration, the initial dry run, or a repair loop; a repair loop already running is not stopped. Applies to this call only.

## Response `200`

OK

- object
  - `draft` object, required: The rule's single mutable draft (exactly one per rule, overwritten on save). Source and user tests live in S3; dependencies live on the row.
    - `ruleId` string, required: Owning rule id (uuid).
    - `source` string, nullable, required: Draft rule source code, or null when nothing has been saved yet.
    - `userTestSuite` string, nullable, required: User-authored test suite saved with the draft, or null when none exists.
    - `generatedTestSuite` string, nullable, required: The AI-generated test suite carried with the draft, or null when none has been generated yet. Reused across publishes and dry runs: the publish pipeline regenerates it only when the rule's inputs or contract surface have moved, and writes the result back here.
    - `dependencies` object[], required: Always empty for a draft saved under the current contract. Agentic Logic rules run with no third-party npm packages — rules read commerce state through the injected SDK — so the field only ever carries what an older draft stored.
      - `name` string, required: npm package name.
      - `version` string, required: Requested semver range.
    - `updatedByUserId` string, nullable, required: User who last saved the draft, or null.
    - `updatedByUser` object, nullable, required: Display summary for the user who last saved the draft, or null when the user is not known.
      - `name` string, required
    - `lastUpdateTs` number, required: Last save time, Unix epoch-ms.
    - `origin` 'chat' | 'manual', nullable, required: How the draft source came to be: 'chat' when applied from the workspace AI chat and untouched since, 'manual' when a person authored or last edited it, null for drafts saved before provenance was recorded.
    - `graph` object, nullable, required: The rule's graph artifact: a renderer-agnostic node/edge description of the rule's selection logic, versioned alongside the code. Carries NO geometry — layout is computed client-side. Produced deterministically for template rules and emitted by the authoring agent for custom-code rules, then verified by the graph_consistency gate.
      - `version` 1, required: Graph wire-format version. Currently 1.
      - `nodes` object[], required: Graph nodes, at most RULE_GRAPH_MAX_NODES. Large inputs must be aggregated into node meta, never expanded per item.
        - `id` string, required: Stable node id, unique within the graph. For kind 'branch', MUST equal the attempt() base label (a static string literal in the code). Runtime attempt instances are drill-in data on the node, never nodes of their own.
        - `kind` 'input' | 'branch' | 'generator' | 'filter' | 'sort' | 'limit' | 'exclusions' | 'output' | 'fallback-terminal', required: 'input' | 'branch' | 'generator' | 'filter' | 'sort' | 'limit' | 'exclusions' | 'output' | 'fallback-terminal'. Branch nodes mirror attempt() bases; pipeline kinds describe the flow inside or after a branch, while Exclusions is the platform-owned final filter.
        - `label` string, required: Short card title shown on the node.
        - `summary` string: One plain-language sentence describing what the node does.
        - `branchId` string: Owning branch node id, set ONLY on generator/filter/sort/limit nodes that run inside that branch; absent for shared pipeline nodes and always absent on 'input', 'branch', 'exclusions', 'output' and 'fallback-terminal' nodes — a branch never names itself.
        - `meta` object: Kind-specific structured details (e.g. generator source, aggregate itemCount + sampleItems, limit N). Aggregates belong here — never one node per item.
          - `itemCount` integer: Aggregate count of items the node covers (e.g. a generator drawing from a 1000-product list).
          - `sampleItems` string[]: Up to five representative item names for drill-in display; never the full list.
          - `condition` string: Plain-language matching clause for branch/filter nodes, phrased for a merchant ('in stock right now', 'cart contains a tagged cover-up'). The MATCHING card body.
          - `conditionSubject` 'cart' | 'delivery' | 'shipping' | 'customer' | 'page' | 'order' | 'time': Which request context a branch node's condition chip is about — 'cart' (the default when absent), 'delivery' (request.shipping.delivery: selected option, local pickup, cost, carrier, transit days), 'shipping' (gap, threshold, tiers, state), 'customer' (ordersCount, tags), 'page' (request.pageProduct), 'order' (request.order), 'time' (request.evaluatedAt). Drives the row-header sentence; absent on always-matching branches and on every non-branch node.
          - `sortKey` string: Plain-language sort criterion for sort nodes ('Best sellers'). The SORTED BY card body.
          - `limit` integer: Maximum number of items a limit node lets through. Rendered as 'UP TO n products'.
      - `edges` object[], required: Directed edges between node ids, at most RULE_GRAPH_MAX_EDGES.
        - `from` string, required: Source node id.
        - `to` string, required: Target node id.
        - `kind` 'flow' | 'fallback', required: 'flow' = data/control flow; 'fallback' = taken when the source produced nothing (branch miss or empty output). Fallback edges may only terminate at a branch or the fallback-terminal.
    - `contentFingerprint` string, nullable, required: SHA-256 identity of the code workspace (source, user tests, generated tests). Equal draft and active-version fingerprints mean the draft has nothing to apply. Null for template rules, and for rows not yet fingerprinted.

## Other responses

- `429`: Too Many Requests

## Changes

- **2026-11-06** `7b769b2f90b1`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/intelligems/apis/intelligems-external-api-v25-10-beta/changes/v25-10-beta/agentic-logic/rules/:ruleId/draft/put.md)

---

[API](https://skmtc.dev/intelligems/apis/intelligems-external-api-v25-10-beta.md) - [All operations](https://skmtc.dev/intelligems/apis/intelligems-external-api-v25-10-beta/llms.txt) - [OpenAPI document](https://skmtc.dev/intelligems/apis/intelligems-external-api-v25-10-beta/revisions/7b769b2f90b1?raw)
