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 first. This rule specifically also needs the following:What you’ll build
A rule that handles three intents in a single Fallback-rooted tree:- Order status (“where’s my order?”, “has it shipped yet?”): looks up the order, replies with shipping status, expected delivery, and tracking link.
- 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. - 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.
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:
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:
Step 1: Configure the User Attribute
A single attribute supplies the two fields the rule needs upstream: thecustomer_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.
Create the attribute
Customer Identity. Pick a Source:widgetif 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 for the JWT payload shape).uiif 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.
Declare the input field
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.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 usesX-Shopify-Access-Token). - Save From Response:
{"customer_id": "id", "recent_orders": "recent_orders"}
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.Toggle the collected fields
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.Attach and verify
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.What can go wrong at this stage
What can go wrong at this stage
recent_orderscomes 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 underdata.ordersorcustomer.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 Emptyon the array field. If your OMS returnsrecent_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.
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)
Create the 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.
Define the inputs
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).Define the outputs
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:
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.Test, then save
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.2b. Cancel Order (destructive)
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.
Define the inputs
Define the outputs
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"}
Test, then save
refund_eta_days of 5-10 is normal for credit-card refunds.2c. Update Shipping Address (destructive)
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.
Define the inputs
Define the outputs
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:
- Save From Response:
{"change_confirmation_id": "id", "new_expected_delivery": "shipping.estimated_delivery"}
Test, then save
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.Create a new rule
Order status and changes.Write the Description
“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.
Change the root from Steps to Fallback
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.Build the Status branch
Steps. Inside that Steps, add four children:- Check
Intent is status: in the field picker, expand Tag Groups → pick your routing group (Type of Issueor your customTopicgroup). OperatorEquals. ValueOrder Status. The three subsequent branch Checks follow the same pattern with different values. - Read
Extract order_id:- field
order_id, typeString, 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.”
- field
- Tool
Lookup Order: bindorder_idinput to the Read’sorder_id. - Reply:
“Tell the customer their order status. Include: the order’s current shipping status (), and the tracking link () so they’re certain it’s the order they meant.”
Build the Cancel branch
Steps to the Fallback root. Inside:- Check
Intent is cancel:Topic Equals Cancel Order. - Read
Extract order_id and reason:- field
order_id, typeString, required (same instructions as the Status branch). - field
reason, typeString, 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.”
- field
- Tool
Lookup Order: bindorder_idto 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: bindorder_id(Read),reason(Read). - Reply:
“Confirm the cancellation. Tell the customer their order has been cancelled and a refund of business days. Share the confirmation id ($) for their records. Reassure them and ask if there’s anything else they need help with.”
- Check
- Reply (cancel-blocked):
“Politely explain that the order can no longer be cancelled because $. 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.”
- Steps (cancel-allowed):
Build the Change-address branch
Steps to the Fallback root. Inside:- Check
Intent is change:Topic Equals Change Order. - Read
Extract order_id:- field
order_id, typeString, required.
- field
- Tool
Lookup Order: bindorder_idto 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 currentshipping_address.*fields if yourLookup OrderAction 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: bindorder_id, plus the fourshipping_*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 ). Mention that no further action is needed; the carrier will route to the new address automatically.”
- Check
- Reply (change-blocked):
“Politely explain that the shipping address can no longer be changed because ). Apologize for the inconvenience.”
- Steps (change-allowed):
Build the disambiguation fallback
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 ($) 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.
Assign to your agent
Customer Identity to. A rule with no agents assigned never runs.Test each branch before publishing
- 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).
Lookup Orderruns successfully on every test where an intent matched.- The eligibility Checks pass or fail correctly based on the test order’s state.
- Destructive Tools (
Cancel Order,Update Shipping Address) only fire when their eligibility Check passes. - Replies interpolate the right variables for the right path.
Publish
What can go wrong at this stage
What can go wrong at this stage
- The Status branch fires on cancel intents (or vice versa): your
Topictag 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 Orderto returnshipping_street,shipping_city, etc., or drop the pre-fill and accept the customer retyping the whole address.
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 yourCustomer Identityattribute. Configure under Automations → Reply Behavior → Internal Comment: conditionTag Groups → Type of Issue Equals Cancel OrderANDUser 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_valuewhen 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 ontotal.
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:- Status, in_transit order. “where is my order #1002” → direct reply with tracking link and expected delivery.
- Cancel, eligible order (not_shipped). “cancel #1003” → cancel Tool fires; reply confirms the refund amount and ETA.
- Cancel, ineligible order (in_transit). “cancel #1002” → cancel-blocked Reply explains the order has already shipped and offers the return path; no Tool fires.
- Change address, eligible. “change shipping address on #1003” → Form renders; customer fills it; address-update Tool fires; reply confirms new ETA.
- Change address, ineligible (out_for_delivery). “redirect my package #1002” → change-blocked Reply explains the carrier handoff and offers refuse-delivery; no Tool fires.
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 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. After a week in production, open Analytics → Overview 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 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).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 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 watches conversations and proposes Articles for gaps it detects, check the Review queue weekly.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, checkeligible_for_return(a new output fromLookup 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 OrderAction. - Pre-emptive proactive notifications. Add a Check on
expected_deliveryagainst 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_ordershas more than one, the Read won’t extract a single id. Add aFallbackat the top of each destructive branch: if the Read fails, fall back to aReplythat lists the order ids and asks the customer to specify (the customer’s next message goes through the rule again with the chosenorder_idpopulated). - Tag the ticket in Gorgias. If your support deployment is on Gorgias, configure Tag Sync so the
Cancel Order/Change OrderFini 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.”
Troubleshooting
Stages where this walkthrough commonly gets stuck.The rule fires on a refund or return message
The rule fires on a refund or return message
A branch's intent Check fails when it should pass
A branch's intent Check fails when it should pass
The Read can't find an order_id and falls through
The Read can't find an order_id and falls through
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.Lookup Order succeeds but downstream eligibility Check fails unexpectedly
Lookup Order succeeds but downstream eligibility Check fails unexpectedly
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.The Cancel Tool fires but the order isn't cancelled in the OMS
The Cancel Tool fires but the order isn't cancelled in the OMS
The Form's pre-fill is one address change behind
The Form's pre-fill is one address change behind
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.The Fallback root keeps falling through to the disambiguation Reply
The Fallback root keeps falling through to the disambiguation Reply
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.Related
Cancellation flow
Card replacement
Rulebook
Actions
Attributes
Tags
Topic tag group whose three values gate the three branches.
