> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usefini.com/llms.txt
> Use this file to discover all available pages before exploring further.

# End-to-end: order status and changes

> Build a single Rulebook rule that handles three e-commerce intents (status check, cancel, change address) using a Fallback-rooted tree, a shared read-only Lookup Order action, and shipping-state branching.

This walkthrough builds the most common e-commerce support flow on Fini, end to end: *"where's my order?"*, *"can I cancel it?"*, *"can you change the shipping address?"*, in a single rule. By the time you finish, your agent will reliably handle all three intents, branch on the order's actual shipping state (not the customer's perception), execute the right destructive Action when allowed, and respond with a real tracking link, real refund amount, or a real updated delivery estimate, every value pulled live from your order-management system, every step recorded in the AI Steps trace.

The rule you'll build is **100% deterministic** at every branch: same conversation context, same tree walk, same outcome. That property is what makes it safe to put cancellation and address-change Actions behind an LLM-routed conversation, the LLM picks the intent, but every step after the pick is encoded in the tree.

This walkthrough leans on a Rulebook pattern the [Cancellation flow](/en/walkthroughs/cancellation-flow) walkthrough didn't show: a **Fallback at the root**, with one child per intent. Multiple sibling intents handled in a single rule, each child Steps gates on its own intent, the Fallback walks through them until one matches.

<Info>
  **Use this walkthrough when** you need one rule to handle multiple related intents. The Fallback-root pattern is also the right shape for *FAQ-with-fallback-to-agent* rules, *eligibility check before destructive Action* flows, and any case where you want to *"try path A; if that doesn't apply, try path B; otherwise ask"*. For single-intent flows, the simpler Steps-rooted [Cancellation flow](/en/walkthroughs/cancellation-flow) is a cleaner template.
</Info>

## Before you start

This walkthrough configures the **Behavior** layer of your Fini agent (a Fallback-rooted rule that branches across three intents, plus the read-only and destructive Actions). It assumes the other three layers your agent needs are already in place: **Knowledge** so the agent can back up the blocked-Reply paths (return policies, carrier-redirect instructions, general "what statuses mean" articles), **Prompts** so the Reply nodes inherit your tone, and **Deployment** so customers can reach the agent on the channels you serve. If you're starting from scratch, run [Quickstart](/en/quickstart) first.

This rule specifically also needs the following:

| What | Why | Where |
| - | - | - |
| **A configured agent** | The attribute, actions, rule, and Reply Behavior settings all attach to a specific agent. The agent needs **at least one [Knowledge](/en/knowledge/overview) attachment**, **a [Prompt](/en/configuration/prompts) set**, and **at least one [Deployment](/en/deploy/overview) live**. The Form node in the Change-address branch needs the [Widget](/en/deploy/widget); the Status and Cancel branches work on any channel. | [AI Agents](/en/configuration/agent-home) |
| **An order-management API you can call** | The walkthrough configures real HTTP calls to look up orders, cancel orders, and update shipping addresses. Shopify, Commerce Cloud, BigCommerce, in-house, anything works. Follow Fini's [API contract](/en/api-reference/api-contract) (HTTPS, JSON, `x-api-key`, 2s reads / 5s writes) for endpoints you built yourself. | Your OMS |
| **A way to pass the API token to Fini** | Fini has no separate "workspace secrets" store. Either hardcode the credential in the Headers JSON of each Data Step, or, if your OMS is reachable via the same credentials as a [connected integration](/en/api-reference/attributes#integration-metadata-fields), use that integration's Connection Settings cross-source. | [Attributes → Connection Settings](/en/api-reference/attributes#integration-metadata-fields) |
| **A Rulebook-enabled tag group covering three intents** | Each branch of the Fallback gates on a different tag value: status, cancel, change. Either use the default `Type of Issue` group with three relevant tags, or create a custom `Topic` group. Whichever you pick, **Tag Group available in Rulebooks** must be on, **Tag Selection** set to "Exactly one tag," and the group assigned to your agent. The three tag values need clear, distinguishing `AI Instructions`. | [Configuration → Tags](/en/configuration/tags) |
| **At least one test order in your OMS** | You'll need test orders in three shipping states (not\_shipped, in\_transit, delivered) to verify the branching logic. | Your OMS |

If you don't have an OMS ready and want to follow along anyway, swap the URLs for your own test endpoints and the rest of the walkthrough still works.

<Tip>
  **The Lookup Order Action is reusable.** The Action you build in Step 2 is read-only and useful far beyond this rule, exposure-tag automations, return-eligibility checks, and analytics queries can all reference it. Build it cleanly here once and reuse the workspace-level Action wherever you need order context downstream.
</Tip>

## What you'll build

A rule that handles three intents in a single Fallback-rooted tree:

1. **Order status** (*"where's my order?"*, *"has it shipped yet?"*): looks up the order, replies with shipping status, expected delivery, and tracking link.
2. **Cancel order** (*"please cancel my order"*, *"I want to cancel order #12345"*): looks up the order, checks `eligible_for_cancel`, if eligible, calls the cancel Action and confirms the refund; if not, explains why (already shipped) and offers the return path.
3. **Change shipping address** (*"can you redirect my package?"*, *"change my shipping address"*): looks up the order, checks `eligible_for_change`, if eligible, renders a Form to collect the new address, updates it, and confirms; if not (already shipped), explains and offers the return-to-sender path.

A single `Customer Identity` attribute provides `customer_id` and `recent_orders` (the customer's last few order numbers, so the Read can disambiguate when the customer says "my order" without a number). A single `Lookup Order` Action is the read-only common dependency every branch starts with, no destructive work happens before the order's actual state is loaded.

The final rule tree:

<Frame>
  <img src="https://mintcdn.com/fini/zef_RDWlJqADKDlS/images/en/home/order-status-and-changes-tree.svg?fit=max&auto=format&n=zef_RDWlJqADKDlS&q=85&s=b998ffd8572c5077c55fc4cd04789ea5" alt="Order status and changes rule tree. Root Fallback node contains four children in order. Child 1: a Steps labeled Status branch with three children, Check Topic Equals Order Status, Tool Lookup Order with order_id input and full order output, Reply composing a status response with tracking url. Child 2: a Steps labeled Cancel branch with five children, Check Topic Equals Cancel Order, Tool Lookup Order, Check eligible_for_cancel Equals True, Tool Cancel Order with order_id input and refund output, Reply confirming the cancellation. Child 3: a Steps labeled Change branch with five children, Check Topic Equals Change Order, Tool Lookup Order, Check eligible_for_change Equals True, Form Shipping address collecting four fields, Tool Update Shipping Address binding the form fields and order_id, Reply confirming the new estimated delivery. Child 4: a Reply labeled Fallback ask asking the customer to clarify which action they want." width="920" height="1100" data-path="images/en/home/order-status-and-changes-tree.svg" />
</Frame>

Reading the tree: the root **Fallback** evaluates its children in order. Each of the first three children is a `Steps` block gated by its own intent `Check` at the top, when the LLM routes a *"where's my order?"* message to this rule, the **Status** branch's Check passes and that branch runs to completion; the Fallback succeeds; the other two branches never evaluate. If the customer's message doesn't match any of the three intents (unusual but possible because the LLM routed to this rule on a borderline message), the **Fallback ask** Reply runs as the final child and asks the customer to clarify.

The four pieces and where each is configured:

| Piece | Page | What it owns |
| - | - | - |
| `Customer Identity` attribute | [API Setup → Attributes](/en/api-reference/attributes) | Per-message lookup that exposes `customer_id` and `recent_orders`. |
| `Lookup Order` action | [API Setup → Actions](/en/api-reference/actions) | Read-only lookup that returns the full order state, status, items, totals, shipping fields, plus computed eligibility booleans. |
| `Cancel Order` action | [API Setup → Actions](/en/api-reference/actions) | Destructive cancel call. Returns refund amount and confirmation. |
| `Update Shipping Address` action | [API Setup → Actions](/en/api-reference/actions) | Destructive shipping-address update. Returns new estimated delivery. |
| `Order status and changes` rule | [Automations → Rulebook](/en/automations/rulebook) | The Fallback-rooted tree that routes between the three intents. |

Build in this order. The Action used most (Lookup Order) is shared across all three Rulebook branches, so it ships once.

## Step 1: Configure the User Attribute

A single attribute supplies the two fields the rule needs upstream: the `customer_id` to scope all order lookups, and a `recent_orders` array so the Read can map *"my order"* to a specific id when the customer doesn't quote one.

<Steps>
  <Step title="Create the attribute">
    On **API Setup → Attributes**, click **New Attribute**. Name it `Customer Identity`. Pick a **Source**:

    * `widget` if your storefront's widget signs the shopper's identity into a JWT (the recommended pattern for authenticated e-commerce sessions, see [Widget → Identify logged-in users](/en/deploy/widget) for the JWT payload shape).
    * `ui` if your storefront passes the shopper's email through embed metadata.
    * The integration's own name (`zendesk`, `intercom`, `gorgias`, `front`, `hubspot`, `salesforce`, `livechat`) if your support traffic flows through that helpdesk, the integration's user identifier becomes the lookup key.
  </Step>

  <Step title="Declare the input field">
    Under **Passed-in fields** (or **Connection Settings** if integration-sourced), list the lookup key with **Use as Input in API** turned on. For most e-commerce setups this is `email`. For Shopify, you may also have a `customer_id` already on the conversation, in which case use that directly and skip the email-based lookup below.
  </Step>

  <Step title="Add a Data Collection Step">
    * **Step Name:** `Lookup Customer`
    * **Method:** `GET`
    * **URL:** `https://api.yourshop.com/customers?email=${email}&include=recent_orders`
    * **Headers:** `{"x-api-key": "your-oms-api-key"}` (or your OMS's native scheme, Shopify uses `X-Shopify-Access-Token`).
    * **Save From Response:** `{"customer_id": "id", "recent_orders": "recent_orders"}`

    Pull the whole `recent_orders` array unchanged, the Read node downstream can pick the first element when needed. Fini's Save From Response documentation uses dotted JSON paths (`order.status`, `user.organization_id`); whether bracket-star projection (`recent_orders[*].id`) is supported isn't documented, so the safe pattern is to pull the array and access elements in Read or Tool input instructions.

    <Note>
      **Where the API token lives.** Fini doesn't have a separate "workspace secrets" store for custom Attribute calls, the credential is hardcoded here in the Headers JSON. The Fini workspace itself is the secret boundary. Native integrations expose their own connection fields only for the matching integration source, and selected active integration fields can be used by widget-source attributes.
    </Note>
  </Step>

  <Step title="Toggle the collected fields">
    Collected fields show **two** switches, **Use in Rulebooks** and **Visible to AI** (no input switch, collected fields are automatically available to later steps in the same chain).

    | Field | Use in Rulebooks | Visible to AI |
    | - | - | - |
    | `customer_id` | ✓ *(verify it's not null)* | |
    | `recent_orders` | | ✓ *(agent surfaces the list when asking for an order id)* |

    `recent_orders` is **Visible to AI** so when a customer says *"can you check my order?"* without specifying which one, the agent can naturally reply *"Sure, are you asking about order #1002 or #1003?"*. Without this visibility, the agent would either guess (bad) or use opaque language (worse). The Read node downstream consumes `recent_orders` to default the order\_id; the Tool's input binding picks up the value via the tree variable the Read populates.
  </Step>

  <Step title="Attach and verify">
    Pick the agent from the top-right dropdown, toggle `Customer Identity` on, leave both **Chat** and **Email** selected (this rule works across both channels, with one Form-vs-Read caveat covered in Step 3). Click **Save**, then **Play** the Data Collection Step with a real email to confirm both fields populate correctly.
  </Step>
</Steps>

<AccordionGroup>
  <Accordion title="What can go wrong at this stage" icon="circle-info">
    * **`recent_orders` comes back as something other than an array:** the path you used on the right side of Save From Response doesn't point at the array. Click **Play** on the Data Step to inspect the raw response, then adjust the path. Some APIs nest it under `data.orders` or `customer.recent_orders`, the Save From Response value is a JSON path into the response, not a Fini-specific transform.
    * **Customers with no orders cause the attribute to return null:** the lookup may need to be `Allow Empty` on the array field. If your OMS returns `recent_orders: []` for new customers, the empty array is fine; null is the problem.
    * **The attribute fires twice per message:** you may have an upstream attribute that depends on this one. Order matters; if the chain is misconfigured, Fini retries. Inspect the attribute order in the editor.
  </Accordion>
</AccordionGroup>

## Step 2: Configure the three Actions

Three Actions: one read-only that every Rulebook branch invokes first, and two destructive ones that only specific branches invoke. Build the read-only one first, it's the dependency.

### 2a. `Lookup Order` (read-only)

<Steps>
  <Step title="Create the action">
    On **API Setup → Actions** (page header reads *External Actions*), click **New Action**.

    * **Name:** `Lookup Order`
    * **Description:** `Looks up an order's full state and returns the fields downstream branches need: shipping status, totals, tracking, plus computed booleans for cancel and address-change eligibility. Read-only.`
  </Step>

  <Step title="Define the inputs">
    | Field | Type | Required |
    | - | - | - |
    | `order_id` | string | ✓ |

    Single input. The Rulebook's Read node will resolve `order_id` from the customer's message or, when the customer didn't specify, from the first entry of `recent_orders` (the most recent order).
  </Step>

  <Step title="Define the outputs">
    | Field | Type | What it represents |
    | - | - | - |
    | `order_status` | string | `pending`, `paid`, `cancelled`, `refunded`. |
    | `shipping_status` | string | `not_shipped`, `label_created`, `in_transit`, `out_for_delivery`, `delivered`. |
    | `tracking_url` | string | Carrier's tracking page. |
    | `expected_delivery` | string | ISO 8601 date. |
    | `item_summary` | string | A short human-readable summary, e.g., *"2× Wireless Headphones, 1× Charging Cable"*. |
    | `total` | number | Order total in your store's currency. |
    | `eligible_for_cancel` | boolean | True when the OMS will accept a cancel call. Usually `order_status == paid AND shipping_status == not_shipped`. |
    | `eligible_for_change` | boolean | True when the OMS will accept a shipping-address update. Usually `shipping_status IN (not_shipped, label_created)`. |
    | `cancel_blocker_reason` | string | Human-readable reason when `eligible_for_cancel` is false. E.g., *"Already shipped on 2024-01-15."* |
    | `change_blocker_reason` | string | Same idea for address change. |

    The two eligibility booleans are the load-bearing output fields, the Cancel and Change branches Check on them before invoking the destructive Tools. **Compute these on your OMS side**, not in Fini. The OMS already knows the policy windows, the carrier handoff rules, the *"shipping label printed but not picked up"* edge cases. Letting Fini reconstruct that logic in Rulebook duplicates rules that should live in one place.
  </Step>

  <Step title="Add the Data Step">
    * **Step Name:** `Fetch order via OMS`
    * **Method:** `GET`
    * **URL:** `https://api.yourshop.com/orders/${order_id}`
    * **Headers:** `{"x-api-key": "your-oms-api-key"}` (or your OMS's native scheme).
    * **Save From Response:** map each output to the OMS's actual field paths. For example:

      ```json theme={null}
      {
        "order_status": "status",
        "shipping_status": "shipping.status",
        "tracking_url": "shipping.tracking.url",
        "expected_delivery": "shipping.estimated_delivery",
        "item_summary": "summary",
        "total": "totals.grand_total",
        "eligible_for_cancel": "policy.cancellable",
        "eligible_for_change": "policy.address_changeable",
        "cancel_blocker_reason": "policy.cancel_blocker",
        "change_blocker_reason": "policy.change_blocker"
      }
      ```

    If your OMS doesn't compute `policy.cancellable` natively, build it as a derived field in a small middleware layer between Fini and the OMS, anywhere except inside Fini's Rulebook.
  </Step>

  <Step title="Test, then save">
    Click **Play**. Plug in a real `order_id` from your dev OMS and confirm every output field populates with the right type. Pay attention to the two `*_blocker_reason` fields, they're populated only when their respective eligibility booleans are false, so on a fully eligible order they'll be empty strings or null. Both should still pass through the response without throwing.
  </Step>
</Steps>

### 2b. `Cancel Order` (destructive)

<Steps>
  <Step title="Create the action">
    * **Name:** `Cancel Order`
    * **Description:** `Cancels an order that is eligible for cancellation. Returns the confirmation id and refund amount. Will error if the order is already shipped; downstream Rulebook must check eligible_for_cancel before invoking.`
  </Step>

  <Step title="Define the inputs">
    | Field | Type | Required |
    | - | - | - |
    | `order_id` | string | ✓ |
    | `reason` | string | optional |
  </Step>

  <Step title="Define the outputs">
    | Field | Type |
    | - | - |
    | `cancel_confirmation_id` | string |
    | `refund_amount` | number |
    | `refund_eta_days` | number |
  </Step>

  <Step title="Add the Data Step">
    * **Step Name:** `Cancel via OMS`
    * **Method:** `POST`
    * **URL:** `https://api.yourshop.com/orders/${order_id}/cancel`
    * **Headers:** `{"x-api-key": "your-oms-api-key"}` (or your OMS's native scheme).
    * **Body:** `{"reason": "${reason}", "idempotency_key": "${order_id}-cancel"}`
    * **Save From Response:** `{"cancel_confirmation_id": "id", "refund_amount": "refund.amount", "refund_eta_days": "refund.eta_days"}`
  </Step>

  <Step title="Test, then save">
    **Play** the Action against an actual cancellable test order. Confirm 200 and that all three output fields populate. The refund flow on your OMS may be async, `refund_eta_days` of 5-10 is normal for credit-card refunds.
  </Step>
</Steps>

### 2c. `Update Shipping Address` (destructive)

<Steps>
  <Step title="Create the action">
    * **Name:** `Update Shipping Address`
    * **Description:** `Updates the shipping address on a pre-shipment order. Returns the new estimated delivery date. Will error if the order is already shipped; downstream Rulebook must check eligible_for_change before invoking.`
  </Step>

  <Step title="Define the inputs">
    | Field | Type | Required |
    | - | - | - |
    | `order_id` | string | ✓ |
    | `shipping_street` | string | ✓ |
    | `shipping_city` | string | ✓ |
    | `shipping_postal_code` | string | ✓ |
    | `shipping_country` | string | ✓ |
  </Step>

  <Step title="Define the outputs">
    | Field | Type |
    | - | - |
    | `change_confirmation_id` | string |
    | `new_expected_delivery` | string |
  </Step>

  <Step title="Add the Data Step">
    * **Step Name:** `Update address via OMS`
    * **Method:** `PATCH`
    * **URL:** `https://api.yourshop.com/orders/${order_id}/shipping_address`
    * **Headers:** `{"x-api-key": "your-oms-api-key"}` (or your OMS's native scheme).
    * **Body:**
      ```json theme={null}
      {
        "street": "${shipping_street}",
        "city": "${shipping_city}",
        "postal_code": "${shipping_postal_code}",
        "country": "${shipping_country}",
        "idempotency_key": "${order_id}-addresschange"
      }
      ```
    * **Save From Response:** `{"change_confirmation_id": "id", "new_expected_delivery": "shipping.estimated_delivery"}`
  </Step>

  <Step title="Test, then save">
    **Play** with a pre-shipment test order and a deliberately different address. Verify the OMS records the new address and recalculates the delivery date.
  </Step>
</Steps>

For the full reference on Input/Output schemas, chaining Tools, and the workspace-vs-rule scoping model, see [API Setup → Actions](/en/api-reference/actions). Actions are workspace-level, there's no per-agent toggle, an Action becomes effective for an agent only when a Rulebook rule referencing it is assigned to that agent.

## Step 3: Configure the Rulebook rule

This is where the Fallback-root pattern lives. One rule, three branches, one read-only Tool shared across all branches.

<Tip>
  **Tree reference.** Final shape you'll build below:

  ```
  Fallback  (root, picks the first child whose intent Check passes)
  │
  ├── Steps  (Status branch)
  │   ├── Check: Topic Equals Order Status
  │   ├── Read: extract `order_id` (String), default to recent_orders[0]
  │   ├── Tool: Lookup Order
  │   │     in:  order_id
  │   │     out: shipping_status, tracking_url, expected_delivery, item_summary, ...
  │   └── Reply: status summary with tracking_url and expected_delivery
  │
  ├── Steps  (Cancel branch)
  │   ├── Check: Topic Equals Cancel Order
  │   ├── Read: extract `order_id` and optional `reason`
  │   ├── Tool: Lookup Order   (same Action as above)
  │   ├── Fallback
  │   │   ├── Steps  (cancel-allowed path)
  │   │   │   ├── Check: eligible_for_cancel Equals True
  │   │   │   ├── Tool: Cancel Order
  │   │   │   │     out: cancel_confirmation_id, refund_amount, refund_eta_days
  │   │   │   └── Reply: confirm with refund details
  │   │   └── Reply: cancel-blocked path, explains cancel_blocker_reason
  │   │              and offers the return path
  │   └── (end)
  │
  ├── Steps  (Change-address branch)
  │   ├── Check: Topic Equals Change Order
  │   ├── Read: extract `order_id`
  │   ├── Tool: Lookup Order
  │   ├── Fallback
  │   │   ├── Steps  (change-allowed path)
  │   │   │   ├── Check: eligible_for_change Equals True
  │   │   │   ├── Form: Shipping address (street, city, postal, country)
  │   │   │   ├── Tool: Update Shipping Address
  │   │   │   │     out: new_expected_delivery
  │   │   │   └── Reply: confirm with new_expected_delivery
  │   │   └── Reply: change-blocked path, explains change_blocker_reason
  │   │              and offers return-to-sender
  │   └── (end)
  │
  └── Reply: disambiguation, "What would you like to do with your order?"
  ```

  The pattern at each branch: gate on intent, run the read-only `Lookup Order`, then for destructive branches, gate on eligibility and either run the destructive Tool or explain the blocker. The two destructive branches use a nested `Fallback` to express the *"if eligible, do the work; else explain"* alternative.
</Tip>

<Steps>
  <Step title="Create a new rule">
    On **Automations → Rulebook**, click **+ Create Rule**. Set the Name to `Order status and changes`.
  </Step>

  <Step title="Write the Description">
    The Description routes the LLM. For a multi-intent rule like this, you describe the **rule's scope** (orders) rather than a single intent:

    > *"Use this rule when the customer asks about the status of an order they placed, wants to cancel an order, or wants to change the shipping address on an order. Example queries: 'where is my order?', 'has my order shipped?', 'can you cancel order #12345?', 'I need to change my shipping address', 'can you redirect my package?'. Do not use this rule for: refunds on already-delivered orders (use Refund Request), returns (use Return Request), or general product questions (let the knowledge base handle these)."*

    The negative examples matter, e-commerce has many adjacent rules (returns, refunds, exchanges), and you want the Planner to route to the most specific one.

    <Warning>
      Because this rule covers three intents, the **Topic** tag group must distinguish between them. The Check at the top of each branch reads a specific Topic value, so your `Topic` group needs values like `Order Status`, `Cancel Order`, `Change Order` configured under [Configuration → Tags](/en/configuration/tags). Without the granular tags, the LLM routes correctly but all three branches' intent Checks read the same broad tag and the wrong branch may fire.
    </Warning>
  </Step>

  <Step title="Change the root from Steps to Fallback">
    Every new rule starts with a `Steps` root. For this rule, change it to `Fallback`. Click the root node in the editor; the configuration panel offers a node-type switcher. Switch to `Fallback`. The semantics shift: now children evaluate in order until one succeeds.
  </Step>

  <Step title="Build the Status branch">
    Click **+ Add step** on the Fallback root. Pick `Steps`. Inside that Steps, add four children:

    * **Check** `Intent is status`: in the field picker, expand **Tag Groups** → pick your routing group (`Type of Issue` or your custom `Topic` group). Operator `Equals`. Value `Order Status`. The three subsequent branch Checks follow the same pattern with different values.
    * **Read** `Extract order_id`:
      * field `order_id`, type `String`, required.
      * Instructions: *"Extract the order id mentioned by the customer (looks like #12345, order #12345, or just a number). If no order id is mentioned and the customer has more than one recent order, return the first id from `recent_orders` (the most recent). If the customer has no recent orders, fail the Read."*
    * **Tool** `Lookup Order`: bind `order_id` input to the Read's `order_id`.
    * **Reply**:
      > *"Tell the customer their order status. Include: the order's current shipping status (${shipping_status}), the expected delivery date (${expected_delivery}), and the tracking link (${tracking_url}) if it's not empty. If shipping_status is 'delivered', say so cheerfully and mention the date; if 'not_shipped', let them know it's still being processed; if 'in_transit' or 'out_for_delivery', share the tracking link. Mention the items being shipped (${item_summary}) so they're certain it's the order they meant."*

    <Note>
      **Voice control lives elsewhere.** Reply instructions across all three branches are for **content directives** ("include the tracking link", "explain the blocker"). For **voice** (tone, register, brand vocabulary), edit your agent's [Main Guidelines → Tone](/en/configuration/prompts), those rules apply to every reply your agent sends. The cancel-blocked and change-blocked Replies in the destructive branches in particular lean on your agent's general escalation guidance from [Planning Prompt → Escalation Topics](/en/configuration/prompts), make sure that's tuned for cases like "customer is very upset about a delivery delay."
    </Note>
  </Step>

  <Step title="Build the Cancel branch">
    Add another `Steps` to the Fallback root. Inside:

    * **Check** `Intent is cancel`: `Topic Equals Cancel Order`.
    * **Read** `Extract order_id and reason`:
      * field `order_id`, type `String`, required (same instructions as the Status branch).
      * field `reason`, type `String`, optional. Instructions: *"Extract the reason the customer wants to cancel, if they volunteer one. Examples: 'changed my mind', 'found it cheaper elsewhere', 'shipping is taking too long', 'ordered by mistake'. Empty string if no reason given."*
    * **Tool** `Lookup Order`: bind `order_id` to the Read.
    * **Fallback**: this is the nested *"if eligible, do; else explain"* fork. Inside the Fallback, add:
      * **Steps** (cancel-allowed):
        * **Check** `Eligible to cancel`: `eligible_for_cancel Equals True`.
        * **Tool** `Cancel Order`: bind `order_id` (Read), `reason` (Read).
        * **Reply**:
          > *"Confirm the cancellation. Tell the customer their order has been cancelled and a refund of ${refund_amount} will be processed within ${refund_eta_days} business days. Share the confirmation id (\${cancel_confirmation_id}) for their records. Reassure them and ask if there's anything else they need help with."*
      * **Reply** (cancel-blocked):
        > *"Politely explain that the order can no longer be cancelled because \${cancel_blocker_reason}. Acknowledge the inconvenience, and offer the next-best option: if the order hasn't been delivered yet, suggest waiting and then initiating a return; if it has been delivered, walk them through the return process. Mention that returns typically refund within 5-10 business days."*
  </Step>

  <Step title="Build the Change-address branch">
    Add a third `Steps` to the Fallback root. Inside:

    * **Check** `Intent is change`: `Topic Equals Change Order`.
    * **Read** `Extract order_id`:
      * field `order_id`, type `String`, required.
    * **Tool** `Lookup Order`: bind `order_id` to the Read.
    * **Fallback** (nested if-else):
      * **Steps** (change-allowed):
        * **Check** `Eligible to change`: `eligible_for_change Equals True`.
        * **Form** `New shipping address`:
          * Title: *New shipping address*.
          * Fields: `shipping_street`, `shipping_city`, `shipping_postal_code`, `shipping_country` (all String, all required). Pre-fill from the order's current `shipping_address.*` fields if your `Lookup Order` Action exposes them, the customer typically only changes one or two fields, so a pre-filled form drops friction.
          * Form Error: `shipping_postal_code invalid for country`.
        * **Tool** `Update Shipping Address`: bind `order_id`, plus the four `shipping_*` fields from the Form.
        * **Reply**:
          > *"Confirm the address change. Tell the customer the new address is locked in and the order will now arrive by ${new_expected_delivery}. Share the confirmation id (${change_confirmation_id}). Mention that no further action is needed; the carrier will route to the new address automatically."*
      * **Reply** (change-blocked):
        > *"Politely explain that the shipping address can no longer be changed because ${change_blocker_reason}. If the order is already in transit, mention that they can refuse delivery (the carrier will return it to us, and we'll refund or reship to the new address) or contact the carrier directly with their tracking link (${tracking_url}). Apologize for the inconvenience."*

    <Note>
      **Channel notes.** The Form node renders inside the [Fini widget](/en/deploy/widget). On email-only deploys (most Salesforce, Zendesk-email-only), use four sequential Read + Reply pairs instead of the Form (one field per pair). The [Rulebook reference's address-change example](/en/automations/rulebook#a-worked-example-address-change-with-form) shows the email-channel pattern.
    </Note>
  </Step>

  <Step title="Build the disambiguation fallback">
    Add the **final child** to the Fallback root. This child fires only when all three intent Checks above failed (the LLM routed to this rule but the Topic tag didn't match any of the three values). Pick `Reply`:

    > *"You routed here without a clear intent. Acknowledge that you can help with orders and ask what they'd like to do: check status, cancel, or change shipping address. Mention their recent orders (\${recent_orders}) if available to help them give you context."*

    This is the safety valve. In normal operation it almost never fires (the Planner's routing is accurate enough that the three intent Checks usually catch the message); when it does, you've avoided a silent failure.
  </Step>

  <Step title="Assign to your agent">
    Under **Assigned Bots**, pick the same agent you attached `Customer Identity` to. A rule with no agents assigned never runs.
  </Step>

  <Step title="Test each branch before publishing">
    Open **▶ Test Run**. Test all three branches plus the blocked variants:

    **Status branch, in\_transit order:**

    ```json theme={null}
    {
      "message": "where is my order #1002?",
      "user_attributes": {
        "customer_id": "cus_42",
        "recent_orders": [{"id": "1002"}, {"id": "1001"}, {"id": "999"}]
      },
      "tag_groups": { "Topic": "Order Status" }
    }
    ```

    **Cancel branch, eligible order:**

    ```json theme={null}
    {
      "message": "please cancel order #1003, I changed my mind",
      "user_attributes": {
        "customer_id": "cus_42",
        "recent_orders": [{"id": "1003"}]
      },
      "tag_groups": { "Topic": "Cancel Order" }
    }
    ```

    **Cancel branch, blocked (already shipped):**

    ```json theme={null}
    {
      "message": "cancel order #1001",
      "user_attributes": {
        "customer_id": "cus_42",
        "recent_orders": [{"id": "1001"}]
      },
      "tag_groups": { "Topic": "Cancel Order" }
    }
    ```

    **Change-address branch, eligible:**

    ```json theme={null}
    {
      "message": "can you change the shipping address on order #1003?",
      "user_attributes": {
        "customer_id": "cus_42",
        "recent_orders": [{"id": "1003"}]
      },
      "tag_groups": { "Topic": "Change Order" },
      "form_submission": {
        "shipping_street": "2 Apple Park Way",
        "shipping_city": "Cupertino",
        "shipping_postal_code": "95014",
        "shipping_country": "US"
      }
    }
    ```

    **Disambiguation fallback (no Topic match):**

    ```json theme={null}
    {
      "message": "hi I need help with my order",
      "user_attributes": {
        "customer_id": "cus_42",
        "recent_orders": [{"id": "1002"}, {"id": "1001"}]
      },
      "tag_groups": { "Topic": "Other" }
    }
    ```

    A successful trace for each branch:

    <Frame>
      <img src="https://mintcdn.com/fini/zef_RDWlJqADKDlS/images/en/home/order-status-and-changes-ai-steps.svg?fit=max&auto=format&n=zef_RDWlJqADKDlS&q=85&s=3a8dc1fdc1685ab450e942d3b019eb89" alt="AI Steps panel showing five execution traces stacked. Trace 1, status branch on order 1002, four green nodes ending in a Reply that interpolates tracking_url and expected_delivery. Trace 2, cancel branch eligible, all six green nodes ending in Cancel Order success and a refund confirmation Reply. Trace 3, cancel branch blocked, the eligible_for_cancel Check shows red, the Fallback advances to the cancel-blocked Reply with a green dot interpolating cancel_blocker_reason. Trace 4, change-address branch eligible, six green nodes ending in Update Shipping Address success and a Reply with new_expected_delivery. Trace 5, disambiguation Fallback, three intent Checks show red and the final Reply fires green." width="480" height="880" data-path="images/en/home/order-status-and-changes-ai-steps.svg" />
    </Frame>

    What to verify for each trace:

    1. The right branch's intent Check is the only one of the three that passes (Test 5 shows all three red, and the final Reply fires instead).
    2. `Lookup Order` runs successfully on every test where an intent matched.
    3. The eligibility Checks pass or fail correctly based on the test order's state.
    4. Destructive Tools (`Cancel Order`, `Update Shipping Address`) only fire when their eligibility Check passes.
    5. Replies interpolate the right variables for the right path.
  </Step>

  <Step title="Publish">
    Save commits the draft; Publish promotes it to live.
  </Step>
</Steps>

<AccordionGroup>
  <Accordion title="What can go wrong at this stage" icon="circle-info">
    * **The Status branch fires on cancel intents (or vice versa):** your `Topic` tag group has overlapping values, or the AI Instructions on the Topic tag group aren't specific enough about *"how to distinguish a status check from a cancel request"*. Refine the Topic tag's AI Instructions, not the Rulebook description.
    * **The disambiguation fallback fires too often:** the LLM is routing to your rule on borderline messages where the customer didn't really ask about orders. Tighten the rule's Description with more negative examples. The Disambiguation Reply is your safety net, but it should be a rare path, not a common one.
    * **The Cancel Tool runs on already-shipped orders:** the eligibility Check is missing or wired against the wrong field. The Check should read `eligible_for_cancel Equals True`. If your OMS doesn't compute this server-side, the Check would have to combine multiple raw fields, which is error-prone, fix it upstream in your OMS.
    * **The Form pre-fill shows blank fields:** the Form's pre-fill bindings reference fields the Lookup Order Action doesn't expose. Either extend `Lookup Order` to return `shipping_street`, `shipping_city`, etc., or drop the pre-fill and accept the customer retyping the whole address.
  </Accordion>
</AccordionGroup>

For the full reference on Fallback semantics, nested composites, and tree variables, see [Automations → Rulebook](/en/automations/rulebook).

## Step 4: Optional Reply Behavior gating

Most e-commerce deployments don't gate this rule's replies, the customer-facing replies are safe to send directly. The complication when you do want to gate is that Reply Behavior's condition builder offers three field categories only: **System fields, Tag Groups, User Attributes**. There's no "Tree Variables" category, the values your Lookup Order Tool returns (`total`, `eligible_for_cancel`, etc.) live only inside the Rulebook tree for that one execution; Reply Behavior runs as a separate decision layer and can't see them.

So the only way to gate on order-specific properties via Reply Behavior is to surface them via a User Attribute that fetches *the customer's most-recent-order context* on every message (or via a tag group that the LLM populates based on the order). Two patterns that work:

* **Customer-type gating.** Expose `customer_type` (e.g., `b2c`, `b2b`, `wholesale`) on your `Customer Identity` attribute. Configure under **Automations → Reply Behavior → Internal Comment**: condition `Tag Groups → Type of Issue Equals Cancel Order` AND `User Attributes → customer_type Equals b2b`. Every B2B cancellation routes to internal review regardless of order details.
* **Pre-emptive tagging.** Configure a custom tag group called *Order Risk* whose AI Instructions tell the model to apply `high_value` when the conversation references an order over your threshold (the model can infer from the customer's wording or from prior messages). Gate Internal Comment on that tag instead of on `total`.

The Reply Behavior gate doesn't stop the destructive Action from firing, the cancel or address change still goes through. Reply Behavior only controls whether the customer sees the agent's confirmation. If you need to hold the Action itself for human approval, split the rule into two as covered in the [Card replacement walkthrough's Reply Behavior step](/en/walkthroughs/card-replacement#step-4-configure-the-reply-behavior-fraud-gate).

## Verify the flow end-to-end

With everything published, run the five scenarios above as real conversations on a deploy channel that supports Forms (the widget). Use a test customer whose recent orders cover all the shipping states you need (one not\_shipped, one in\_transit, one delivered).

The five tests:

1. **Status, in\_transit order.** *"where is my order #1002"* → direct reply with tracking link and expected delivery.
2. **Cancel, eligible order (not\_shipped).** *"cancel #1003"* → cancel Tool fires; reply confirms the refund amount and ETA.
3. **Cancel, ineligible order (in\_transit).** *"cancel #1002"* → cancel-blocked Reply explains the order has already shipped and offers the return path; no Tool fires.
4. **Change address, eligible.** *"change shipping address on #1003"* → Form renders; customer fills it; address-update Tool fires; reply confirms new ETA.
5. **Change address, ineligible (out\_for\_delivery).** *"redirect my package #1002"* → change-blocked Reply explains the carrier handoff and offers refuse-delivery; no Tool fires.

**Verify in the AI Steps trace.** Open each test conversation in the [Inbox](/en/testing/inbox). Each trace should show only the relevant branch's nodes green; the other two branches' intent Checks should never have run (the Fallback short-circuits once a branch succeeds).

### Lock it down with a regression test

Multi-intent rules are uniquely fragile: a Description tweak that helps one intent can hurt another. Add this rule to your [Test Suite](/en/testing/test-suite) with at least three scenarios per branch (eligible, ineligible, and one edge case per intent). Bind **Tool use correctness** as the judge for "did the right Tool fire on the right input?", or pair it with **Goal resolution** to also grade whether the customer ended up with their stated outcome. Scenarios run as simulated multi-turn conversations; write expected outcomes that describe the arc, not verbatim wording. Re-run the set every time you change the Description, the routing tag's AI Instructions, or any Action's Data Step.

<Warning>
  **Test Suite runs hit your real APIs.** Each scenario replay actually invokes the Tools, the Cancel Order and Update Shipping Address Tools will fire your OMS for real on whatever test customer the scenario uses. Point the Actions at a sandbox OMS while iterating on the test set, or accept that each run produces real cancels and address changes.
</Warning>

After a week in production, open [Analytics → Overview](/en/analytics) and find your three intents in the **Category breakdown** table (Analytics doesn't expose a generic tag filter; intent-level metrics live in the Category breakdown view on each page). Each row shows volume, AI resolve rate, and escalated rate, the answers will tell you which branch deserves the next refinement. Click into a row to spot-check conversations on that intent. If escalations are climbing on a specific intent, the most-likely [Escalation Reason](/en/analytics) tags to filter for are **Missing API Access** (when the OMS rejected a Tool that should have succeeded) and **Ambiguous or Unclear Input** (when the disambiguation Reply fired too often).

<Note>
  **Knowledge handles the fall-through paths.** When all three intent Checks fail (the disambiguation Reply fires), or when `eligible_for_cancel` / `eligible_for_change` is false and the blocked-Reply path runs, the agent leans on Knowledge to back up the prose. For this walkthrough to feel complete, make sure your [Articles](/en/knowledge/articles) cover: the return policy your cancel-blocked Reply mentions, the carrier-redirect process the change-blocked Reply offers, and the general "what statuses mean" customer-facing explanations. [Background AI](/en/knowledge/review) watches conversations and proposes Articles for gaps it detects, check the Review queue weekly.
</Note>

## What to vary

Common adaptations once the basic rule is in place:

* **Add a Return Request branch.** Extend the Fallback root with a fourth Steps for `Topic Equals Return Request`. Look up the order, check `eligible_for_return` (a new output from `Lookup Order`), and either spawn a Form to collect the return reason and items or post an internal comment for the returns team.
* **Add a Reorder branch.** *"Order the same thing again"* is a common intent. Add a fifth Steps that looks up the order, presents a *"Confirm reorder?"* Form summarizing the items, and posts a new order via a `Create Order` Action.
* **Pre-emptive proactive notifications.** Add a Check on `expected_delivery` against today's date. If the order is overdue by more than 2 days, escalate to Internal Comment so a human investigates with the carrier before the customer escalates themselves.
* **Multi-order disambiguation.** When the customer asks *"cancel my orders"* (plural) and `recent_orders` has more than one, the Read won't extract a single id. Add a `Fallback` at the top of each destructive branch: if the Read fails, fall back to a `Reply` that lists the order ids and asks the customer to specify (the customer's next message goes through the rule again with the chosen `order_id` populated).
* **Tag the ticket in Gorgias.** If your support deployment is on [Gorgias](/en/deploy/gorgias), configure Tag Sync so the `Cancel Order` / `Change Order` Fini tags map to Gorgias tags (e.g., `cancelled-via-ai`, `address-changed-via-ai`). Your Gorgias views and macros can then triage off them. Tag Sync is currently a Gorgias-only feature; other integrations will get it as it rolls out.
* **Carrier-aware change-blocker messaging.** Some carriers (UPS, FedEx) let you redirect a package mid-transit via their own portal even after the OMS no longer accepts changes. Extend the change-blocked Reply with carrier-specific instructions: *"Your package is with UPS now. You can redirect it through UPS My Choice using the tracking number above."*

The pattern (single Lookup Action shared across branches → gated destructive Tools → blocked-path Replies that surface the actual blocker reason) generalizes to most e-commerce workflows. Returns, exchanges, partial refunds, and re-shipments all follow the same shape: lookup, check eligibility, branch into *do-it* or *explain-why-not*.

## Troubleshooting

Stages where this walkthrough commonly gets stuck.

<AccordionGroup>
  <Accordion title="The rule fires on a refund or return message" icon="route">
    Your Description or the Topic tag's AI Instructions overlap with adjacent rules. Tighten both. The Description's *"do not use this rule for"* section should explicitly call out refunds and returns by name.
  </Accordion>

  <Accordion title="A branch's intent Check fails when it should pass" icon="ban">
    The Topic tag for this conversation isn't set to the value the Check reads. Topic tagging happens after the model classifies the conversation; on synthetic tests, you must inject the Topic in the Custom JSON. In production, check the **Output Tag Selection** panel of AI Steps, if Topic isn't being applied as expected, refine the Topic tag's AI Instructions (not the rule).
  </Accordion>

  <Accordion title="The Read can't find an order_id and falls through" icon="magnifying-glass">
    The customer didn't quote an order number, and the fallback to `recent_orders[0]` isn't working. Two possible causes: (1) `recent_orders` is empty for this customer, the fallback to "most recent" gives nothing; (2) your Read instructions don't reference the `recent_orders` attribute. Edit the Read's instructions to explicitly say *"if no order id was mentioned, return the first element of recent\_orders"*, and verify the attribute is fetched and visible at this point in the tree.
  </Accordion>

  <Accordion title="Lookup Order succeeds but downstream eligibility Check fails unexpectedly" icon="circle-question">
    Click into the Check in the AI Steps trace, it shows the actual value it read. `eligible_for_cancel` may be coming back as `false` when you expected `true`, which means the OMS computed the policy differently than you expected. The fix is on the OMS side, not in Fini, refine the policy logic where it actually lives.
  </Accordion>

  <Accordion title="The Cancel Tool fires but the order isn't cancelled in the OMS" icon="plug-circle-xmark">
    The Tool returned 200 but the side effect didn't happen, usually means your OMS's cancel endpoint is async and returns success on enqueue. Confirm with your OMS's docs: are cancels synchronous or eventual? If eventual, the agent's confirmation is *"your cancel request was received"*, not *"your order has been cancelled"*. Adjust the Reply wording.
  </Accordion>

  <Accordion title="The Form's pre-fill is one address change behind" icon="window-restore">
    The pre-fill is reading from a cached version of `Lookup Order`. Attributes and Actions don't share a cache, but if you have a User Attribute exposing the order's shipping address (instead of using the Action's output), that attribute may be stale on the same message. Switch the pre-fill to read from the Tool's output (`Tool: Lookup Order → shipping_address.*`) rather than a User Attribute.
  </Accordion>

  <Accordion title="The Fallback root keeps falling through to the disambiguation Reply" icon="layer-group">
    None of the three intent Checks are passing. Three likely causes: (1) the Topic tag values in your tag group don't match the Checks (case-sensitive: `Order Status` vs `order status`); (2) the AI Instructions on the Topic tag aren't classifying customer messages into your three buckets; (3) the rule's Description is overly broad so the Planner routes off-topic messages to this rule.
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="Cancellation flow" icon="route" href="/en/walkthroughs/cancellation-flow">
    The simpler single-intent template, refunds, subscription cancellations, account closures.
  </Card>

  <Card title="Card replacement" icon="credit-card" href="/en/walkthroughs/card-replacement">
    The fintech companion: chained destructive Tools, Form for shipping, and a fraud-gating Reply Behavior.
  </Card>

  <Card title="Rulebook" icon="diagram-project" href="/en/automations/rulebook">
    Full reference for Fallback semantics, tree variables, and rule selection.
  </Card>

  <Card title="Actions" icon="bolt" href="/en/api-reference/actions">
    Designing read-only vs destructive Actions, chaining, and the workspace-vs-rule scoping model.
  </Card>

  <Card title="Attributes" icon="id-card" href="/en/api-reference/attributes">
    Sources, fields, switches, and the per-message attribute chain.
  </Card>

  <Card title="Tags" icon="tag" href="/en/configuration/tags">
    Configure the `Topic` tag group whose three values gate the three branches.
  </Card>

  <Card title="Inbox" icon="inbox" href="/en/testing/inbox">
    Open any conversation and read the AI Steps trace to see exactly which branch fired.
  </Card>

  <Card title="Test Suite" icon="vial" href="/en/testing/test-suite">
    Regression-test all three branches before publishing changes; multi-intent rules are especially sensitive.
  </Card>
</CardGroup>


## Related topics

- [End-to-end: card replacement](/en/walkthroughs/card-replacement.md)
- [End-to-end: cancellation flow](/en/walkthroughs/cancellation-flow.md)
- [List sources](/en/api-reference/list-sources.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.