> ## 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: cancellation flow

> Build a complete subscription-cancellation workflow combining User Attributes, Actions, Rulebook, and Reply Behavior, the canonical end-to-end example of Fini's automation surface.

This walkthrough builds a real subscription-cancellation workflow on Fini, end to end. By the time you finish, a customer typing *"cancel my subscription"* will get a deterministic response: the agent verifies their intent, extracts their cancellation reason, looks up their account, calls your billing API to execute the cancellation, and replies with the real refund amount and effective date, every value pulled from your live system, every step recorded in the AI Steps trace.

The flow you'll build is **100% deterministic**: given the same conversation context, it produces the same tree walk, the same API call, the same Reply every time. That's the whole point of Rulebook, for workflows like cancellations, refunds, identity verification, or claims, you don't want an LLM improvising. You want a precise, auditable sequence the LLM only kicks off when the intent matches.

This walkthrough configures the **Behavior** surface of your agent (the deterministic rules) and touches **Observability** (the AI Steps trace). Every field, button label, and condition value below is what you'll actually see and type in the dashboard.

<Info>
  **Use this walkthrough as the canonical reference** for any *intent → identify customer → execute one API call → confirm* flow. Refunds, account closures, subscription pauses, and password resets all share the same four-piece shape. Pick the closest companion walkthrough if your flow needs different machinery: [Card replacement](/en/walkthroughs/card-replacement) for chained Tools + Form input, [Order status and changes](/en/walkthroughs/order-status-and-changes) for `Fallback`-rooted multi-intent rules.
</Info>

## Before you start

This walkthrough configures the **Behavior** layer of your Fini agent (the deterministic rules in Rulebook and the Reply Behavior gates around them). It assumes the other three layers your agent needs to be operational are already in place: **Knowledge** so the agent has something to fall back on when this rule doesn't fire, **Prompts** so the Reply nodes inherit your tone and escalation rules, and **Deployment** so customers can actually reach the agent. If you're starting from scratch, run [Quickstart](/en/quickstart) first, it walks you through all three in order.

You'll also need a handful of cancellation-specific things in place:

| What | Why | Where |
| - | - | - |
| **A configured agent** | The rule, attribute, and Reply Behavior settings all attach to a specific agent. The agent needs **at least one [Knowledge](/en/knowledge/overview) attachment** (so the agent has fallback content when this rule doesn't fire), **a [Prompt](/en/configuration/prompts) set** (so the Reply nodes inherit your tone), and **at least one [Deployment](/en/deploy/overview) live** (so customers can reach the agent). Pick or create the agent you want to handle cancellations. | [AI Agents](/en/configuration/agent-home) |
| **A billing API you can call** | The walkthrough configures real HTTP calls to look up customers and cancel subscriptions. You need an endpoint, an API token, and a test customer with an active subscription. If the endpoint is one you built, follow Fini's [API contract](/en/api-reference/api-contract) (HTTPS, JSON, `x-api-key` auth, \< 2s reads / \< 5s writes). | Your billing system (Stripe, Recurly, Chargebee, in-house, etc.) |
| **A way to pass the API token to Fini** | If your billing system 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). Otherwise, hardcode the token in the Headers JSON of the Data Collection Step, treat the Fini workspace itself as the secret boundary. | [Attributes → Connection Settings](/en/api-reference/attributes#integration-metadata-fields) |
| **A `Cancellation` tag in a Rulebook-enabled tag group** | The intent `Check` in Step 3 reads `Type of Issue Equals Cancellation` (or whichever tag group you've set up for routing). The group needs **Tag Group available in Rulebooks** turned on, an `AI Instructions` field that teaches the model when to apply each tag, and `Tag Selection` set to "Exactly one tag." Default `Type of Issue` is the simplest fit; you can also create a custom `Topic` group. | [Configuration → Tags](/en/configuration/tags) |

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

<Tip>
  **Idempotency.** Cancellations are destructive. If your billing API supports idempotency keys (Stripe, Recurly, Chargebee all do), pass one in the Action's body, that way a duplicate Tool call (e.g., the customer sends the cancel message twice before the first reply arrives) won't double-cancel. The Tool's Body field accepts any JSON you write, so add `"idempotency_key": "${customer_id}-${effective_date}"` or similar.
</Tip>

## What you'll build

A subscription-cancellation flow that:

1. **Identifies the customer** on every message by looking up their email in your billing system, exposing their `customer_id`, `plan`, and an `is_vip` flag to the agent.
2. **Cancels the subscription** when the customer asks to, by calling your billing API with the customer's ID and reason.
3. **Orchestrates the conversation** with a Rulebook tree that gates on intent, extracts the cancellation reason, calls the cancel Action, and replies with the confirmation details.
4. **Holds VIP cancellations for human review** by posting an internal note instead of replying directly when the customer is on an Enterprise plan.

The final rule looks like this:

<Frame>
  <img src="https://mintcdn.com/fini/zef_RDWlJqADKDlS/images/en/home/cancellation-flow-tree.svg?fit=max&auto=format&n=zef_RDWlJqADKDlS&q=85&s=7618bcc2a11e4f9d4325ac97e5f73efc" alt="Cancellation flow rule tree. Root Steps node contains five children in order: Check verifying intent is cancellation, Read extracting the cancellation reason, Check verifying customer_id is not null, Tool calling the Cancel Subscription action with customer_id and reason as input and binding confirmation_id, refund_amount, and effective_date as output, and Reply composing a confirmation message with those output values interpolated." width="880" height="620" data-path="images/en/home/cancellation-flow-tree.svg" />
</Frame>

Reading the tree top to bottom: the root **Steps** node runs children in order and short-circuits on the first failure. The intent **Check** at the top filters out non-cancellation messages, a deterministic backstop on top of the LLM's intent routing. The **Read** uses the LLM to extract the customer's stated reason into a tree variable. The customer\_id **Check** is a precondition, without an identified customer, the rule stops cleanly. The **Tool** calls the `Cancel Subscription` action with two typed inputs and binds three typed outputs as new tree variables. The **Reply** at the bottom composes the confirmation message, interpolating the Tool's outputs into the final text.

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`, `plan`, `is_vip` to the agent. |
| `Cancel Subscription` action | [API Setup → Actions](/en/api-reference/actions) | The deterministic API call to your billing system. |
| `Cancellation flow` rule | [Rulebook → Intent Rules](/en/automations/rulebook) | The behavior tree that gates on intent and orchestrates the work. |
| `Internal Comment for VIP cancellations` rule | [Automations → Reply Rules](/en/automations/reply-behavior) | Optional safety net that holds VIP cancellations for human review. |

The order below is also the order you should build in, each piece references the prior one.

## Step 1: Configure the User Attribute

The first thing the agent needs to know is *who is this person?*. User Attributes are Fini's mechanism for pulling that context in. On every message, Fini calls your configured HTTP endpoint, parses the response, and exposes the named fields to the agent, so the agent always sees the current `plan`, `is_vip`, and `customer_id` without you having to push updates to Fini.

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

    * `ui` if your app passes the customer's email through UI metadata.
    * `widget` if you sign the email into a JWT for the Fini widget.
    * The integration's own name (e.g., `zendesk`, `intercom`, `front`, `gorgias`, `hubspot`, `salesforce`, `livechat`) if your traffic flows through a connected helpdesk, the helpdesk's user identifier becomes the lookup key.
  </Step>

  <Step title="Declare the input field">
    Under **UI Metadata Attributes** (or **JWT Token Attributes** for widget source), list `email` with **Use as Input in API** turned on. This tells Fini to pass `email` to the HTTP call as `${email}`. The other two switches can stay off for the email itself, you don't need to match rules on it or surface it to the LLM, you just need to use it to look up the customer.
  </Step>

  <Step title="Add a Data Collection Step">
    A Data Collection Step is the HTTP call that fetches the customer data. Configure it:

    * **Name:** `Lookup Customer`
    * **Method:** `GET`
    * **URL:** `https://api.yourbilling.com/customers?email=${email}`
    * **Headers:** `{"x-api-key": "your-billing-api-key"}` (per Fini's [API contract](/en/api-reference/api-contract) when calling your own endpoint; if you're calling a third-party SaaS billing API like Stripe or Recurly, use their auth scheme instead, typically `{"Authorization": "Bearer ..."}`).
    * **Save From Response:** `{"customer_id": "id", "plan": "plan", "is_vip": "is_vip"}`

    The **Save From Response** block turns your API's JSON response into named fields. Keys on the left are the field names you'll reference in the rest of Fini; values on the right are the JSON paths into the API response.

    <Note>
      **Where the API token lives.** Fini doesn't have a separate "workspace secrets" store for custom Attribute calls; the credential is hardcoded in the Headers JSON here. 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 (those populated by your Data Collection Step) have **two** switches, **Use in Rulebooks** and **Visible to AI**. They're automatically available to subsequent Data Steps in the same chain by name, so there's no separate "Use as Input in API" switch on collected fields.

    | Field | Use in Rulebooks | Visible to AI |
    | - | - | - |
    | `customer_id` | ✓ *(so Rulebook Checks can verify it)* | |
    | `plan` | ✓ *(so Reply Behavior can scope on plan)* | ✓ *(so the agent can reference the plan in replies)* |
    | `is_vip` | ✓ *(Reply Behavior reads this)* | |

    The pattern: turn on **Use in Rulebooks** when Rulebook or Reply Behavior conditions need to match on the field. Turn on **Visible to AI** only when you want the LLM to know the value and potentially reference it in replies. `customer_id` stays out of LLM context, the agent should never quote an internal ID back to the customer. `is_vip` is a system flag for rules, not customer-facing.

    Downstream Actions that consume `customer_id` pick it up via the Tool node's input binding in the Rulebook (covered in Step 3), not via a Use-as-Input-in-API toggle here.
  </Step>

  <Step title="Attach to your agent">
    Pick the agent from the top-right dropdown, toggle `Customer Identity` on, leave **Chat** and **Email** both selected (so the attribute fetches on both channels), and click **Save**.
  </Step>

  <Step title="Verify the lookup">
    Click **Play** on the Data Collection Step. Plug in a real email from your dev environment and run the lookup. Confirm the response includes all three expected fields (`customer_id`, `plan`, `is_vip`) with the right values. If anything's off, fix it here, every downstream stage assumes this lookup works.
  </Step>
</Steps>

<AccordionGroup>
  <Accordion title="What can go wrong at this stage" icon="circle-info">
    * **The Data Step returns 401 / 403:** the secret token isn't configured or doesn't have the right permissions. Re-check the workspace secret value and the API's auth requirements.
    * **The response mapping returns null fields:** the JSON path on the right side of the mapping doesn't match your API's actual response shape. Run the API call directly (curl, Postman) and check the field names.
    * **The attribute works in test but not in production:** the agent isn't attached. Confirm the attribute is toggled on for the right agent under the agent picker.
  </Accordion>
</AccordionGroup>

For the full reference on attribute switches, sources, and Data Collection Steps, see [Configuration → Attributes](/en/api-reference/attributes).

## Step 2: Configure the Action

The Attribute pulls customer state in; the Action pushes a change out. In this case, the change is *cancel this customer's subscription*. Actions are workspace-level, you configure them once, and any rule in any agent can invoke them via a Tool node.

Each Action declares an **Input schema** (the typed parameters it accepts) and an **Output schema** (the typed fields it returns). Those schemas are how the Rulebook validates Tool nodes at design time, type mismatches surface in the editor, not at runtime.

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

    * **Name:** `Cancel Subscription`
    * **Description:** `Cancels the customer's active subscription via the billing API. Returns confirmation id, refund amount, and effective date.`

    The Description here is for your team, it shows up when teammates browse available Actions. Unlike a Rulebook rule description, it's not used for LLM routing.
  </Step>

  <Step title="Define the Input schema">
    The Input schema declares what the Rulebook must provide when invoking this Action:

    | Field | Type | Required |
    | - | - | - |
    | `customer_id` | string | ✓ |
    | `reason` | string | optional |

    Marking `customer_id` required means a Tool node referencing this Action won't validate unless it binds something to `customer_id`. `reason` being optional lets the rule call the Action even when the customer didn't volunteer one.
  </Step>

  <Step title="Define the Output schema">
    The Output schema declares the typed fields the Action returns. These become tree variables in Rulebook, available to downstream nodes.

    | Field | Type |
    | - | - |
    | `confirmation_id` | string |
    | `refund_amount` | number |
    | `effective_date` | string |

    Note the types: `refund_amount` as `number` means downstream Checks can use numeric operators on it (e.g., `refund_amount Greater Than 100`). `effective_date` as `string` because most billing APIs return ISO date strings; downstream Replies can interpolate it as-is.
  </Step>

  <Step title="Add the Data Step">
    The Data Step is the actual HTTP call:

    * **Step Name:** `Cancel via Billing API`
    * **Method:** `POST`
    * **URL:** `https://api.yourbilling.com/v1/subscriptions/${customer_id}/cancel`
    * **Headers:** `{"x-api-key": "your-billing-api-key"}` (or your third-party billing system's Bearer scheme).
    * **Body:** `{"reason": "${reason}"}`
    * **Save From Response:** `{"confirmation_id": "confirmation_id", "refund_amount": "refund.amount", "effective_date": "effective_at"}`

    Save From Response handles common shape mismatches: your billing API might return `refund.amount` (nested) but you want to expose it as a flat `refund_amount` to Rulebook.
  </Step>

  <Step title="Test, then save">
    Click **Play** in the Data Step. Plug in a real `customer_id` from your dev environment (one with an active test subscription you can safely cancel) and run the step. Confirm:

    * The HTTP call returns 200.
    * All three output fields populate with the expected types.
    * The response shape matches your declared Output schema.

    If the shape doesn't match, fix the response mapping here. Don't move on until the test passes, every downstream stage assumes the Action works.
  </Step>
</Steps>

<AccordionGroup>
  <Accordion title="What can go wrong at this stage" icon="circle-info">
    * **The test returns 404 / 422:** the URL or body shape is wrong for your billing API. Compare against your API's docs.
    * **The Play test won't accept your customer\_id:** the Input schema is more strict than the test panel. Verify `customer_id` is marked as a string, not a number.
    * **Outputs don't bind:** the response mapping references a JSON path that doesn't exist. Pull a real response from your API and update the right-hand-side paths.
  </Accordion>
</AccordionGroup>

For the full reference on Input/Output schemas, chaining Tool nodes, and the workspace-vs-rule scoping model, see [Configuration → Actions](/en/api-reference/actions).

## Step 3: Configure the Rulebook rule

The Attribute brings customer context in; the Action pushes a change out; the Rulebook decides *when* the change happens and *how* the conversation flows around it. This is the orchestration layer.

Each rule is a **behavior tree** that the LLM may invoke when the customer's message matches the rule's Description. Inside the tree, deterministic node types compose the work, Checks gate, Reads extract, Tools execute, Replies compose.

<Tip>
  **Tree reference.** You'll build this node by node in the steps below. Final shape:

  ```text theme={null}
  Steps  (root)
  ├── Check: Topic Equals Cancellation
  ├── Read:  extract `reason` (String)
  ├── Check: customer_id Is Not Null
  ├── Tool:  Cancel Subscription
  │     in:  customer_id, reason
  │     out: confirmation_id, refund_amount, effective_date
  └── Reply: confirm cancellation with refund details
  ```
</Tip>

<Steps>
  <Step title="Create a new rule">
    On **Rulebook → Intent Rules**, click **+ Create Rule**. Set the Name to `Cancellation flow`.
  </Step>

  <Step title="Write the Description carefully">
    The Description is *how the LLM decides whether to fire this rule on a given customer message*. A vague description (*"Handles billing things"*) will misroute messages or skip yours entirely. Write it like a trigger specification.

    For this walkthrough, paste this as your starting Description:

    > *"Use this rule when the customer wants to cancel their subscription. The query could look like: 'cancel my subscription,' 'I want to close my account,' 'stop charging me,' or 'I'd like to terminate my plan.'"*

    Tighten the example phrases over time to match how your customers actually phrase cancellation requests. Imagine you're teaching a new teammate what triggers this rule, that's the level of specificity to aim for.

    <Warning>
      The Description field is the rule's only routing signal. If a customer's message doesn't seem to fire your rule in production, the Description is the first thing to revisit. You can confirm what the LLM did in the **Planning** section of the AI Steps trace, see the **Test before publishing** step below.
    </Warning>
  </Step>

  <Step title="Add the intent Check">
    Click **+ Add step** on the root Steps node. Pick `Check` as the node type. Configure:

    * **Name:** *Intent is cancellation*
    * **Condition group:** in the field picker, expand **Tag Groups** and pick `Type of Issue` (the default group) or your custom routing group (e.g., `Topic`). Operator `Equals`. Value `Cancellation`.

    This is the deterministic backstop on the LLM's routing. Even if the Description matches and the LLM picks this rule, the Check short-circuits if the conversation's tag isn't `Cancellation`. This protects against edge cases where the LLM routes overconfidently.

    <Note>
      **Set up the routing tag group first.** Tag Groups are populated by the LLM after it classifies each conversation. For this Check to work, the tag group needs four things in place under [Configuration → Tags](/en/configuration/tags): (1) **Tag Group available in Rulebooks** turned on; (2) **Tag Selection** set to "Exactly one tag" (otherwise use `Contains`, not `Equals`); (3) a `Cancellation` tag defined, with its description teaching the model when to apply it; (4) the group assigned to your agent under the agent selector. The default `Type of Issue` group ships with a Cancellation-style tag and is the path of least resistance; create a custom `Topic` group only if you need different categories.
    </Note>
  </Step>

  <Step title="Add the Read for the cancellation reason">
    Click **+ Add step** again. Pick `Read`. Configure:

    * **Name:** *Extract cancellation reason*
    * **Look at:** Interaction History
    * **Find these details:** field `reason`, type `String`
    * **Instructions:** *"Extract the customer's stated reason for cancelling. Examples include 'too expensive,' 'not using it,' 'product not working,' 'switching to competitor.' If no reason is given, return an empty string."*

    Concrete examples in the instructions dramatically improve extraction reliability. The Read populates a tree variable named `reason` that downstream nodes can reference as `${reason}`.

    <Tip>
      **Read or Form?** Use `Read` when the customer typically volunteers the reason in their message (the cancellation case). Use `Form` when you need the reason structured for downstream reporting (e.g., dropdown of *Too expensive / Not using it / Switching competitor / Other*), or when the cancellation is sensitive enough that you want a typed, auditable record of *the customer chose Reason X*. The [address-change example in Rulebook](/en/automations/rulebook#a-worked-example-address-change-with-form) shows the Form pattern in full.
    </Tip>
  </Step>

  <Step title="Add the customer_id Check">
    Click **+ Add step**. Pick `Check`. Configure:

    * **Name:** *Customer is identified*
    * **Condition group:** in the field picker, expand **User Attributes** → pick `customer_id`. Operator `Is Not Null`.

    This Check references the `customer_id` field that was exposed by the `Customer Identity` attribute in Step 1 (which is why it had **Use in Rulebooks** turned on). If the customer's email lookup didn't return a customer\_id, they're not in your billing system, or the API call failed silently, this Check short-circuits the rule cleanly instead of calling the cancel API with a null ID.
  </Step>

  <Step title="Add the Tool node and bind its inputs">
    Click **+ Add step**. Pick `Tool`. In the configuration panel:

    * **Action:** select `Cancel Subscription` from the dropdown (the Action you created in Step 2).
    * **Inputs:** the panel shows the Action's required inputs. For each one, pick where the value comes from:
      * `customer_id` → bind to `customer_id` (from the User Attribute)
      * `reason` → bind to `reason` (from the upstream Read node)

    The binding is the load-bearing part. The dropdown for each input shows every tree variable available at this point in the tree, User Attributes, upstream Read outputs, upstream Tool outputs. Pick the one whose name and type match the Action's input.

    Once you've bound the inputs, the Tool's outputs (`confirmation_id`, `refund_amount`, `effective_date`) automatically become tree variables available to nodes below this one.
  </Step>

  <Step title="Add the Reply node">
    Click **+ Add step**. Pick `Reply`. In the **Instructions** field, write:

    > *"Tell the customer their subscription has been cancelled. Mention that a refund of* ${refund_amount} will be processed by ${effective_date}*, and give them the confirmation ID \$*{confirmation_id} *for their records."*

    The `${variable}` placeholders interpolate from the Tool's outputs. The Reply node injects this as instructions into the agent's reply-composition pass, the agent writes the final wording in its own voice, but the content (refund amount, dates, confirmation ID) comes from the bound variables.

    <Tip>
      Reply instructions don't have to be the verbatim message to the customer. They're guidance for the agent. *"Tell the customer..."* works better than copy-pasting the exact reply text, because it lets the agent maintain its voice while reliably surfacing the facts you specified.
    </Tip>

    <Note>
      **Voice control lives elsewhere.** Reply instructions are for **content directives** ("mention the refund amount", "share the confirmation id"). For **voice** ("be formal", "never apologize twice", brand vocabulary), edit your agent's [Main Guidelines → Tone](/en/configuration/prompts) instead, those rules apply to every reply your agent sends, including the one this rule produces.
    </Note>
  </Step>

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

  <Step title="Test before publishing">
    Open the **▶ Test Run** panel from the rule editor. Two input modes:

    * **Conversation Link**, paste a URL to a real conversation from your Inbox. Use this when you want to verify the rule behaves correctly on conversations you've already seen.
    * **Custom JSON**, paste a JSON object describing a synthetic conversation context. Use this for edge cases.

    Start with Custom JSON. Paste something like:

    ```json theme={null}
    {
      "message": "please cancel my subscription, the product just isn't working for us",
      "user_attributes": {
        "customer_id": "cus_abc123",
        "plan": "Pro",
        "is_vip": false
      },
      "tag_groups": {
        "Topic": "Cancellation"
      }
    }
    ```

    Use the `customer_id` from the test you ran in Step 2's Action, that's a customer your billing API actually knows about. To test the VIP path you'll configure in Step 4, set `"is_vip": true` instead and re-run. Click **Run Test** and walk the execution trace. A successful run looks like this:

    <Frame>
      <img src="https://mintcdn.com/fini/zef_RDWlJqADKDlS/images/en/home/cancellation-flow-ai-steps.svg?fit=max&auto=format&n=zef_RDWlJqADKDlS&q=85&s=070ba12340f5990a0f700aeb084e8114" alt="AI Steps panel showing the execution trace for a successful cancellation. Five collapsible sections: Planning (collapsed, showing it routed to Cancellation flow), Executed Rule (expanded), Generate Answer, Input Tag Selection, and Output Tag Selection. Inside Executed Rule, five rows, each with a green status dot, the node title, and a colored type pill. The five rows in order are: Intent is cancellation (Check), Extract cancellation reason (Read) showing reason value, Customer is identified (Check), Cancel Subscription (Tool) showing typed inputs and outputs with real values, and Confirm cancellation (Reply)." width="480" height="720" data-path="images/en/home/cancellation-flow-ai-steps.svg" />
    </Frame>

    What to verify:

    1. The intent Check passes (green dot).
    2. The Read populates `reason` with something like *"product just isn't working"*.
    3. The customer\_id Check passes.
    4. The Tool fires with the expected inputs and returns three output fields with real values.
    5. The Reply interpolates the values correctly.

    If a node shows a red dot, click into it to see what value it had. Reads not populating means the extraction instructions aren't clear enough; Checks failing means the upstream data isn't what you expected.
  </Step>

  <Step title="Publish">
    Save commits the draft. Publish promotes it to live. From the next conversation onward, your agent will run this rule on messages whose Description matches.
  </Step>
</Steps>

<AccordionGroup>
  <Accordion title="What can go wrong at this stage" icon="circle-info">
    * **The rule doesn't fire on test conversations:** the LLM didn't route to your rule's Description. Check the **Planning** section of the AI Steps trace to see which rule it picked (if any) and rewrite the Description with more example trigger phrases.
    * **The Read returns nothing:** the extraction prompt isn't specific enough. Edit the Read to say, e.g., *"Extract the customer's reason for cancellation, or return an empty string if no reason is given."*
    * **The customer\_id Check fails:** the User Attribute from Step 1 isn't populated for this conversation. Verify the agent is attached to the attribute and the email lookup is returning customer\_id.
    * **The Tool fails with auth errors:** the workspace secret isn't set or isn't readable from this surface. Re-check the secret config.
  </Accordion>
</AccordionGroup>

For the full reference on node types, tree variables, the AI Steps trace, and tool chaining, see [Rulebook → Intent Rules](/en/automations/rulebook).

## Step 4: Configure the Reply Behavior safety net

You've now got a working cancellation flow. This step adds an optional safety override on top: for Enterprise customers (the `is_vip` flag from Step 1), the agent should *draft* the cancellation reply but post it as an internal note instead of sending it to the customer, so a human can review before it goes out.

This step is optional. The flow you built above works without it. But for high-stakes intents like cancellations, refunds, or account closures, especially with high-value customers, having Reply Behavior gate the final send is a common pattern in regulated industries.

<Steps>
  <Step title="Open the Internal Comment card">
    On **Automations → Reply Rules**, click the **Internal Comment** card to expand it.
  </Step>

  <Step title="Add the two predicates">
    In the first condition group on the card, add two predicates:

    * `User Attributes → is_vip` `Equals` `True`
    * `Tag Groups → Type of Issue` `Equals` `Cancellation` (or your custom routing group, using `Equals` if **Tag Selection** is "Exactly one tag" or `Contains` if "Multiple tags can be selected").

    Predicates inside the same group are joined with AND, so both must be true for the rule to apply. Don't click **+ Add alternative condition**, that creates a new group that's OR'd with the existing one, which isn't what we want here.

    The first predicate uses the `is_vip` field you exposed in Step 1 (which is why it had **Use in Rulebooks** turned on). The second uses the same tag group the Rulebook Check reads, the LLM populates it after classifying the conversation. The three cards (No Reply, Internal Comment, Direct Reply) evaluate independently; the stricter behavior wins (No Reply > Internal Comment > Direct Reply).
  </Step>

  <Step title="Enable and save">
    Toggle the Internal Comment switch on in the card's top-right. Click **Save**.
  </Step>
</Steps>

<Note>
  Reply Behavior gates the agent's final send decision, which doesn't surface in the Rulebook Test Run. You'll verify it works in the next section by sending a real test message from a VIP customer account and confirming the agent posts an internal note rather than a direct reply.
</Note>

**What this rule does at runtime:** when a VIP asks to cancel, the agent still runs the Rulebook flow from Step 3, the cancellation API call still fires, and the Reply node still drafts the confirmation message. But because Reply Behavior matched the Internal Comment card, the drafted message gets posted as an **internal note** visible only to your team. A human can read it, optionally call the customer, and post the customer-facing response themselves.

The exact terminology varies by integration, *Internal Note* in Zendesk and Intercom, *Internal Comment* in Front, *Case Comment* in Salesforce. The behavior is the same: customer-invisible.

**Why Internal Comment, not No Reply?** Both prevent a direct customer reply, but they do different work:

* **Internal Comment** lets the agent run the full Rulebook flow and draft a reply your team can review. The API call still happens. Useful when you want the work done but a human to vet the wording.
* **No Reply** would stop the agent from generating anything at all. The API call wouldn't fire, the conversation would sit untouched, and you'd handle everything manually.

For VIP cancellations specifically, Internal Comment gives your team a head start (the API is done; the message is drafted) without the risk of a tone-deaf auto-reply going to a customer who might be saved by a phone call.

<AccordionGroup>
  <Accordion title="What can go wrong at this stage" icon="circle-info">
    * **The card doesn't enable:** there's a predicate row with no value selected. Walk through and confirm every field, operator, and value is filled in.
    * **The Tag Group predicate doesn't fire on test conversations:** Tag Groups are populated by the LLM *after* it classifies the conversation. For brand-new test conversations, the tag may not exist yet. Use the **AI Steps** panel in the Inbox to check whether the Topic tag was applied.
    * **Internal Comment fires but No Reply was also expected:** if you have a No Reply rule that also matches, it takes priority. No Reply > Internal Comment > Direct Reply.
  </Accordion>
</AccordionGroup>

The interaction with the Rulebook is the key insight: **Rulebook decides what happens; Reply Behavior decides who sees what the agent generated.** They're orthogonal layers.

<Tip>
  **Reply Behavior is one of two safety surfaces.** The other is your agent's [Planning Prompt → Escalation Topics](/en/configuration/prompts), which routes whole conversations to a human *before* any rule fires. For high-stakes intents like cancellations, you can layer both: Escalation Topics catches the conversations the LLM judges as fraught (e.g., "customer is threatening churn for a competitor"); Reply Behavior catches the deterministic predicates (e.g., `is_vip Equals True`). Use the Prompt for fuzzy judgment; use Reply Behavior for hard rules.
</Tip>

For the full reference on reply types, priority order, and the condition builder, see [Automations → Reply Rules](/en/automations/reply-behavior).

## Verify the flow end-to-end

With everything saved and published, send three test messages from a real conversation surface, the widget on your site, a test Zendesk ticket, an Intercom test conversation, whatever channel you've integrated your agent into. Use a test customer account whose email resolves through your billing API so the `Customer Identity` attribute populates real values.

1. **Non-VIP, cancellation intent.** Send *"please cancel my subscription, the product isn't working for us"* from a test customer whose `is_vip` is false. Within seconds, you should see a **direct reply** in the channel with the real refund amount interpolated from the Action's output.
2. **VIP, cancellation intent.** Same message, but from a test customer whose `is_vip` is true. The Rulebook still runs, the cancellation API call still fires, the reply is still drafted, but the reply posts as an **internal note** instead of a direct customer reply. A human teammate can review and follow up.
3. **Non-cancellation intent.** Send *"how do I add a new card?"* from any customer. The LLM doesn't route to your `Cancellation flow` rule (because the Description doesn't match), the rule's tree never runs, and the agent answers from its knowledge base instead. The intent Check inside your tree is a backstop for cases where the LLM *does* route here on a borderline message, it almost never triggers in normal operation.

<Note>
  **The Knowledge fallback matters more than people realize.** When the intent Check fails, when `customer_id Is Not Null` fails, when the Tool errors, when the customer's message routes away from this rule entirely, the agent answers from your [Knowledge base](/en/knowledge/overview). For this walkthrough to feel complete to customers, make sure you have Articles covering the fall-through paths: *"We can't process cancellations for closed accounts"*, *"We can't find your account, please reach out from the registered email"*, and the actual cancellation / refund policy text the agent will reference if the Tool fails mid-flow. Background AI watches conversations and proposes Articles for gaps it detects, check the [Review queue](/en/knowledge/review) weekly.
</Note>

**Verify in the AI Steps trace.** Open each test conversation in the [Inbox](/en/testing/inbox) and look at the **AI Steps** panel on the right. You should see the same trace shape as the one shown in Step 3's Test step:

* For tests 1 and 2: a green-dot trace through all five rule nodes, with the Tool node showing the real API output values.
* For test 3: the **Planning** section shows the LLM didn't pick the Cancellation flow rule, and the **Executed Rule** section is empty, no rule ran, the agent answered from knowledge.

The trace is your source of truth in production. Any time the rule behaves unexpectedly, open AI Steps and find the first red dot.

<Note>
  **What happens on follow-up messages in the same conversation?** Each customer message is evaluated independently, the rule re-runs from the root every time. So if the customer cancels in one message and then sends *"actually wait, can I get a discount instead?"* in a follow-up, the rule will fire again on the second message (assuming the intent still matches). For cancellation specifically, the API call from the first message has already executed by the time the second message arrives. Two defensive patterns:

  1. **Confirm before cancelling.** Add a `Form` node before the Tool that requires explicit customer confirmation (a single "Confirm cancellation" button). The rule short-circuits cleanly on the first message if the customer hasn't confirmed.
  2. **Idempotent cancel API.** Pass a deterministic idempotency key (e.g., `${customer_id}-${effective_date}`) in the Action body so a duplicate Tool call returns the same `confirmation_id` without performing the cancellation twice. Most modern billing APIs support this header.
</Note>

### Lock it down with a regression test

Once the rule is live, add it to your [Test Suite](/en/testing/test-suite). Create a test case from a real cancellation conversation (or use the flask icon in the [Inbox](/en/testing/inbox)), then add criteria: an exact check that the agent called the `Cancel Subscription` Action, plus an AI judgement that the reply confirms the cancellation with the refund amount and effective date. Describe the expected outcome rather than verbatim wording, because each run generates new agent replies. Run the case again every time you change the Description, the Read instructions, or the Action's Data Step.

<Note>
  **Test Suite does not call your billing API.** External Actions in a test run use the recorded or mock responses saved in the test case's **Test setup**, so a run never cancels a real subscription. If a response is missing, the case reports an execution error instead of calling the live service. To check the live API call end to end, use a sandbox billing environment or a controlled pilot with test accounts.
</Note>

After a week in production, open [Analytics](/en/analytics) and find the row for your `Cancellation flow` rule in the **Intent rule breakdown** table. The row shows volume, AI resolve rate, and escalated rate for that rule specifically; use the row arrow to spot-check conversations. If the escalated rate is climbing while the rule keeps firing, the issue is in the Reply, the Action, or downstream Reply Behavior, open those conversations in the [Inbox](/en/testing/inbox) and walk the AI Steps trace for each one.

## What to vary

Once the basic flow is in place, common adaptations:

* **Add a Form for structured reason collection.** Replace the Read node with a Form node that gives the customer a dropdown of cancellation reasons plus a free-text "other" field. **Read works when the customer volunteers the reason in their message; Form is better when you need structured, required input you can analyze later** (e.g., reporting on cancellation reasons). The form's submitted values become tree variables the Tool can reference. See [Rulebook → A worked example: address change with Form](/en/automations/rulebook#a-worked-example-address-change-with-form).
* **Chain multiple Tools.** Add a `Verify Refund Eligibility` Tool before `Cancel Subscription`, gate the cancel Tool with a Check on the eligibility output, and handle the denial path with a Reply explaining why. See [Rulebook → Chaining Tools](/en/automations/rulebook#chaining-tools).
* **Different VIP definition.** Replace the `is_vip Equals True` predicate with a tier check on `plan` (e.g., `plan Equals Enterprise`), or layer multiple conditions for tiered escalation.
* **Stay silent for VIPs instead of drafting.** Switch the Reply Behavior rule from Internal Comment to **No Reply** if you want the agent to not engage at all for VIPs, leaving the conversation entirely to humans.
* **Add graceful recovery.** Wrap the cancellation Tool in a Fallback whose second child is a Reply explaining what happened, so a failed API call gives the customer a useful response rather than escalating silently.

The pattern (User Attribute for context → Action for work → Rulebook for orchestration → Reply Behavior for global overrides) generalizes to most workflows: refunds, identity verification, account upgrades, address changes, claims intake, password resets, and so on. Anything where you'd write a runbook for a human agent maps cleanly to this four-piece structure.

## Troubleshooting

Stages where this walkthrough commonly gets stuck, ordered by likelihood.

<AccordionGroup>
  <Accordion title="Everything works in test but not in production" icon="circle-pause">
    The most common cause: the rule is still a Draft. Drafts don't run on live conversations, only Published rules do. Open the rule and click **Publish**. Less common but worth checking: the agent isn't actually receiving messages (deployment / integration issue), or the Attribute isn't fetching at runtime (test the attribute lookup from the agent's deployed channel, not just the dashboard).
  </Accordion>

  <Accordion title="The Rulebook rule doesn't fire on cancellation messages" icon="route">
    The LLM didn't route to your rule's Description. Open the conversation in the Inbox and check the **Planning** section of the AI Steps trace, it shows which rule (if any) the LLM picked. If the picked rule is wrong or none was picked, the Description on your rule needs tightening. Rewrite it with concrete example trigger phrases (see Step 3's Description guidance).
  </Accordion>

  <Accordion title="The customer_id Check fails on every conversation" icon="circle-question">
    The User Attribute from Step 1 isn't populating `customer_id` for this agent. Three things to check:

    1. The attribute is toggled on for the correct agent under **Configuration → Attributes** with that agent selected in the picker.
    2. The email used in the lookup matches what's actually being passed (UI metadata vs widget JWT vs integration user ID, depending on your Source).
    3. The API endpoint is returning a `customer_id` value, not null or undefined. Test the Data Collection Step's lookup with a known-good email.
  </Accordion>

  <Accordion title="The Tool node fails with auth errors at runtime" icon="key">
    `BILLING_API_TOKEN` isn't configured as a workspace secret, or the secret value is wrong, or the secret isn't accessible from the Actions Data Step (separate from Attributes). Verify under workspace settings → Secrets that the value exists and matches what your API expects. The same `${SECRET_NAME}` syntax works for both Attributes and Actions, but they reference the same workspace-level secret store, there's no per-feature scoping.
  </Accordion>

  <Accordion title="The Read node returns an empty string for `reason`" icon="magnifying-glass">
    The LLM read the conversation but didn't find a reason it could extract. Two possibilities:

    1. The customer genuinely didn't say why, *"please cancel"* is a valid cancellation message with no reason. The Read's instructions should allow this: *"Extract the customer's reason if given; return an empty string otherwise."*
    2. The extraction prompt is too narrow. If the customer wrote *"this product just isn't working for us anymore"*, the LLM might not classify that as a "reason." Broaden the instructions with examples of what counts.
  </Accordion>

  <Accordion title="The Tool succeeds but downstream Reply doesn't interpolate the values" icon="code">
    Your Reply node is referencing variables that don't match the Tool's output schema. Common mistakes:

    * Typos: `${refundAmount}` vs `${refund_amount}`, variable names are case-sensitive and exact.
    * Schema mismatch: the response mapping in Step 2 didn't expose the field, so it's not available as a tree variable. Re-verify the mapping.
    * Path errors: `refund.amount` in the response wasn't flattened to `refund_amount` in the mapping. Check Step 2's response mapping.
  </Accordion>

  <Accordion title="The Reply Behavior rule fires when it shouldn't (or doesn't when it should)" icon="layer-group">
    Two common causes:

    * **A higher-priority card matched first.** No Reply beats Internal Comment beats Direct Reply. If you have a No Reply rule that also matches (e.g., *human agent assigned*), it overrides Internal Comment. Audit your No Reply conditions.
    * **The Topic tag isn't populated yet.** Tag Groups are populated after classification. On brand-new test conversations, the Topic tag may not exist, so the predicate evaluates to false. Open AI Steps → **Output Tag Selection** to confirm the tag was applied.
  </Accordion>

  <Accordion title="The cancellation API call fires but I want to roll it back" icon="rotate-left">
    Once the Action calls your billing API, the cancellation is real. Fini doesn't add an undo layer on top of your API. If you want a "soft cancel with grace period" pattern, that's an API-side change, the Action just invokes whatever endpoint you point it at. Two safety practices for cancellation flows specifically:

    1. **Use Reply Behavior to gate destructive Actions** (Step 4's pattern) so a human reviews before the customer sees the confirmation.
    2. **Add an "Are you sure?" Form** before the Tool node, requiring an explicit confirmation step.
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="Rulebook" icon="diagram-project" href="/en/automations/rulebook">
    Full reference for behavior trees, node types, the AI Steps trace, Test Run, and rule selection.
  </Card>

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

  <Card title="Analytics" icon="chart-line" href="/en/analytics">
    Once your rule is live, monitor how often it fires, success/failure rates, and escalation counts.
  </Card>

  <Card title="Reply Rules" icon="turn-down-right" href="/en/automations/reply-behavior">
    The three reply types, condition builder, operators, and priority order.
  </Card>

  <Card title="Attributes" icon="id-card" href="/en/api-reference/attributes">
    Sources, fields, switches, and Data Collection Steps.
  </Card>

  <Card title="Actions" icon="bolt" href="/en/api-reference/actions">
    Input/Output schemas, Tool chaining, and the workspace-vs-rule scoping model.
  </Card>

  <Card title="Test Suite" icon="vial" href="/en/testing/test-suite">
    Run this rule against a curated set of conversations as a regression check before publishing changes.
  </Card>

  <Card title="Tags" icon="tag" href="/en/configuration/tags">
    Configure the `Topic` tag group the Check at the top of the tree references.
  </Card>

  <Card title="Card replacement" icon="credit-card" href="/en/walkthroughs/card-replacement">
    Companion walkthrough for fintech: chained Tools, Form input for shipping address, and a Reply Behavior fraud gate.
  </Card>

  <Card title="Order status and changes" icon="boxes-packing" href="/en/walkthroughs/order-status-and-changes">
    Companion walkthrough for e-commerce: a Fallback-rooted rule that handles three intents in one tree.
  </Card>
</CardGroup>


## Related topics

- [Intent Rules](/en/automations/rulebook.md)
- [Reply Rules](/en/automations/reply-behavior.md)
- [Behavior](/en/configuration/prompts.md)


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