> ## 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: card replacement

> Build a complete lost/stolen card replacement workflow combining a chained User Attribute, two chained Actions, a Form for shipping address, a Rulebook tree, and a Reply Behavior fraud gate.

This walkthrough builds a real lost-or-stolen card replacement workflow on Fini, end to end. By the time you finish, a customer typing *"I lost my card"* or *"my card was stolen, please send a new one"* will get a deterministic response: the agent confirms which card to freeze (if the customer has more than one), collects a shipping address with a structured form, freezes the compromised card via your card-management API, orders a replacement, and replies with the expected delivery date and a tracking link, every value pulled from your live system, every step recorded in the AI Steps trace.

The flow is **100% deterministic**: given the same conversation context, it produces the same tree walk, the same two API calls in the same order, the same Reply every time. That property is non-negotiable for fintech, anything touching a payment instrument has to be auditable end to end, and the [AI Steps trace](/en/automations/rulebook#observability-the-ai-steps-trace) is what makes it auditable.

This walkthrough configures the **Behavior** surface of your agent (the deterministic rules) plus a fraud safety net via **Reply Behavior**. Every field, button label, and condition value below is what you'll actually see and type in the dashboard.

<Info>
  **Use this walkthrough when** your flow needs more than one API call wired together, structured input from the customer (not just LLM extraction), and a fraud or risk gate before the destructive Action fires. If your flow is *intent → single API call → confirm*, see the simpler [Cancellation flow](/en/walkthroughs/cancellation-flow) walkthrough first.
</Info>

## Before you start

This walkthrough configures the **Behavior** layer of your Fini agent (the Rulebook tree, the two chained Actions, the Reply Behavior fraud gate). It assumes the other three layers your agent needs are already in place: **Knowledge** so the agent has substance for the fall-through paths (closed accounts, frozen-but-not-lost cards, security guidance), **Prompts** so the Reply nodes inherit your tone and your Planning Prompt's escalation rules, and **Deployment** so customers can actually reach the agent. If you're starting from scratch, run [Quickstart](/en/quickstart) first.

Card replacement specifically also needs the following:

| What | Why | Where |
| - | - | - |
| **A configured agent** | The attributes, 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**. For card replacement specifically, the Form node in Step 3 needs the [Widget](/en/deploy/widget) deployment, see the *Channel notes* under Step 3 for the email-channel alternative. | [AI Agents](/en/configuration/agent-home) |
| **A card-management API you can call** | The walkthrough configures real HTTP calls to look up customers, list their cards, freeze a card, and order a replacement. You need endpoints, an API token, and a test account whose card you can safely freeze. If you built the endpoints yourself, follow Fini's [API contract](/en/api-reference/api-contract): HTTPS, JSON in/out, `x-api-key` header, 2s reads / 5s writes. | Your card processor (Marqeta, Galileo, Lithic, in-house, etc.) |
| **A way to pass the API token to Fini** | Fini has no separate "workspace secrets" store. Either hardcode the credential in the Data Step's Headers JSON (the Fini workspace is the secret boundary), or, if your card-management endpoint 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 `Card Lost or Stolen` value in a Rulebook-enabled tag group** | The intent `Check` near the top of the tree reads `Type of Issue Equals Card Lost or Stolen` (or your custom routing group). The group needs **Tag Group available in Rulebooks** on, **Tag Selection** "Exactly one tag," a `Card Lost or Stolen` tag whose description teaches the model when to apply it, and the group assigned to your agent. The default `Type of Issue` group is the path of least resistance. | [Configuration → Tags](/en/configuration/tags) |
| **The Fini widget configured for Forms** | The shipping-address Form node renders reliably inside the [Fini widget](/en/deploy/widget). Form-rendering on helpdesk chat surfaces isn't documented per-integration; the widget is the recommended primary test surface. Email channels (Salesforce email cases, Zendesk email-only) cannot render Forms, see the *Channel notes* callout under Step 3. | [Deploy → Widget](/en/deploy/widget) |

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

<Warning>
  **This is a destructive flow against a financial product.** Freezing the wrong card or shipping a replacement to the wrong address creates customer pain and potential fraud loss. Build this walkthrough end to end in a sandbox or staging environment first; do not point the Actions at production card-management endpoints until the Test Suite (Step 6) has passed against a representative set of conversations. The Reply Behavior fraud gate in Step 5 is a safety net, not a substitute for testing.
</Warning>

## What you'll build

A card-replacement flow that:

1. **Identifies the customer and their cards** on every message via a single `Customer Identity` attribute with two chained Data Collection Steps: step 1 looks up the customer (`account_id`, `account_status`, `is_high_risk`, `recent_replacement_count`); step 2 uses that `account_id` to list the customer's active cards (`active_cards`). One attribute, two HTTP calls in dependency order.
2. **Confirms which card to replace** when the customer has more than one active card, by extracting a `card_last_four` from the message or asking via a quick disambiguation reply.
3. **Collects a shipping address with a Form node** so the address is typed, validated, and recorded as a structured artifact on the conversation transcript.
4. **Freezes the compromised card** via a `Freeze Card` Action, then **orders a replacement** via an `Order Replacement Card` Action, two chained Tools, with the freeze's `confirmation_id` flowing into the order's input.
5. **Gates fraud-risk replacements for human review** by routing high-risk customers or repeat-replacement requests through Reply Behavior's Internal Comment.

The final rule looks like this:

<Frame>
  <img src="https://mintcdn.com/fini/zef_RDWlJqADKDlS/images/en/home/card-replacement-tree.svg?fit=max&auto=format&n=zef_RDWlJqADKDlS&q=85&s=0e501e3be5489901ae46270ab70e756a" alt="Card replacement rule tree. Root Steps node contains seven children in order: Check verifying intent is card-lost-or-stolen, Check verifying account_status is active, Read extracting card_last_four and replacement_reason, Form collecting shipping_street/shipping_city/shipping_postal_code/shipping_country with Form Error children for invalid postal code and missing fields, Tool calling Freeze Card with card_id input and freeze_confirmation_id output, Tool calling Order Replacement Card with account_id and freeze_confirmation_id and shipping address inputs and new_card_id/expected_delivery/tracking_url outputs, and Reply composing a confirmation message with those output values interpolated." width="880" height="920" data-path="images/en/home/card-replacement-tree.svg" />
</Frame>

Reading the tree top to bottom: the root **Steps** node runs children in order and short-circuits on the first failure. Two **Checks** at the top gate on intent and account state (an inactive or closed account cannot order a card). A **Read** extracts the card's last four digits (if the customer mentioned them) and the replacement reason. A **Form** collects the shipping address with validation. Two **Tools** run in sequence: freeze first, then order, the order Tool waits on the freeze's confirmation so we never ship a replacement against an unfrozen card. The final **Reply** composes the confirmation with the new card's expected delivery date and tracking URL.

The pieces and where each is configured:

| Piece | Page | What it owns |
| - | - | - |
| `Customer Identity` attribute (two Data Steps) | [API Setup → Attributes](/en/api-reference/attributes) | Per-message chain that exposes `account_id`, `account_status`, `is_high_risk`, `recent_replacement_count`, and `active_cards` to the agent. |
| `Freeze Card` action | [API Setup → Actions](/en/api-reference/actions) | The deterministic API call that freezes a specific card. |
| `Order Replacement Card` action | [API Setup → Actions](/en/api-reference/actions) | The deterministic API call that orders a new card to a typed shipping address. |
| `Card replacement` rule | [Automations → Rulebook](/en/automations/rulebook) | The behavior tree that gates, collects, freezes, orders, and replies. |
| `Internal Comment for high-risk replacements` rule | [Automations → Reply Behavior](/en/automations/reply-behavior) | Fraud safety net that holds risky replacements for human review. |

Build in this order, each piece references the prior ones.

<Note>
  **Bot Routing reminder.** If your fintech serves cards via the widget (Form path) *and* via Zendesk email tickets (Reads-in-sequence path), those are two separate Fini agents. You'll need to either assign this rule to both agents (and accept that the Form step degrades on the email agent), or maintain two variants, one Form-based for the widget, one Read-based for the email channel. See [Deploy → Widget](/en/deploy/widget) and the integration page for whichever helpdesk you use.
</Note>

## Step 1: Configure the `Customer Identity` attribute

The agent needs two pieces of context on every message: *who* the customer is (account state, risk flags) and *which cards* they have (so the replacement Tool can address the right card). Both come from a **single attribute** with two **Data Collection Steps** chained in dependency order, step 2 references a field collected by step 1.

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

    * `widget` if your widget signs the customer's identity into a JWT (the standard pattern for authenticated fintech, see [Widget → Identify logged-in users](/en/deploy/widget) for the payload shape).
    * `ui` if your app passes the customer's email through UI metadata.
    * The integration's own name (`zendesk`, `intercom`, `front`, `gorgias`, `hubspot`, `salesforce`, `livechat`) if you're routing through a helpdesk.
  </Step>

  <Step title="Declare the input field">
    Under the **Passed-in fields** group (UI Metadata Attributes or JWT Token Attributes depending on Source), list `email` (or `customer_token`, whichever your widget signs) with **Use as Input in API** turned on. This is the lookup key step 1 will pass to your customer-management API.
  </Step>

  <Step title="Add Data Collection Step 1: Lookup Customer">
    * **Step Name:** `Lookup Customer`
    * **Method:** `GET`
    * **URL:** `https://api.yourbank.com/v1/customers?email=${email}`
    * **Headers:** `{"x-api-key": "your-cards-api-key"}` (per Fini's [API contract](/en/api-reference/api-contract) when calling your own endpoint; use the third-party scheme if you're calling a processor's SaaS API directly).
    * **Save From Response:** `{"account_id": "id", "account_status": "status", "is_high_risk": "risk_flags.kyc_recheck_required", "recent_replacement_count": "stats.card_replacements_30d"}`

    The four fields step 1 yields:

    | Field | Type | Why we need it |
    | - | - | - |
    | `account_id` | string | Step 2's URL uses it, and Order Replacement Card's input binds to it. |
    | `account_status` | string | Top-of-tree `Check` rejects closed / suspended accounts before any API call fires. |
    | `is_high_risk` | boolean | Reply Behavior reads this to route fraud-flagged customers to an internal note. |
    | `recent_replacement_count` | number | Reply Behavior reads this to flag customers who replaced more than one card in the last 30 days. |
  </Step>

  <Step title="Add Data Collection Step 2: List Active Cards">
    Add a second Data Collection Step below the first:

    * **Step Name:** `List Active Cards`
    * **Method:** `GET`
    * **URL:** `https://api.yourbank.com/v1/customers/${account_id}/cards?status=active`
    * **Headers:** same as step 1.
    * **Save From Response:** `{"active_cards": "cards"}`

    The `${account_id}` in step 2's URL is the field step 1 just collected, the docs are explicit about this: *"later steps can use any field collected by earlier steps as input."* The fields collected by step 1 are automatically available to step 2 by name, no input switch needed.

    Your processor's response should be an array shaped like:

    ```json theme={null}
    [
      {"id": "card_abc", "last_four": "4242", "type": "physical", "product": "Debit"},
      {"id": "card_xyz", "last_four": "1881", "type": "virtual", "product": "Credit"}
    ]
    ```

    The Read node downstream will use this array to disambiguate when the customer has multiple cards.
  </Step>

  <Step title="Toggle the collected fields">
    Collected fields (those populated by Data Collection Steps) have **two** switches, **Use in Rulebooks** and **Visible to AI**. They don't show a "Use as Input in API" toggle, that switch only exists on passed-in fields and credentials.

    | Field | Use in Rulebooks | Visible to AI |
    | - | - | - |
    | `account_id` | ✓ *(Check verifies it's not null; Tool input binds to it)* | |
    | `account_status` | ✓ *(Check verifies it equals `active`)* | |
    | `is_high_risk` | ✓ *(Reply Behavior reads this)* | |
    | `recent_replacement_count` | ✓ *(Reply Behavior reads this)* | |
    | `active_cards` | ✓ *(Read node references the array)* | ✓ *(when the customer doesn't mention a card and has multiple, the agent uses the list to ask a disambiguation question)* |

    None of the operational flags get **Visible to AI**, they shouldn't appear in customer-facing replies. `active_cards` does need to be visible so the agent can verbalize the disambiguation question naturally.
  </Step>

  <Step title="Attach to your agent">
    Pick the agent from the top-right dropdown, toggle `Customer Identity` on, leave **Chat** selected (Forms render reliably in the widget chat surface), and click **Save**.
  </Step>

  <Step title="Verify the chain">
    Click the **Play** button at the top of the detail view. Provide a real email from your dev environment and step through the chain. Confirm step 1 returns the four customer fields and step 2 returns a non-empty `active_cards` array. Each Data Collection Step also has its own per-step Play button you can use to test in isolation.
  </Step>
</Steps>

<AccordionGroup>
  <Accordion title="What can go wrong at this stage" icon="circle-info">
    * **Step 2's URL renders with literal `${account_id}`:** step 1 didn't populate `account_id`. Click the per-step Play on step 1 and inspect the response, the `Save From Response` mapping's right-hand path doesn't match your API's actual field name.
    * **The cards array comes back empty for a customer you know has cards:** your `status=active` filter is too narrow, or the processor treats *frozen* / *blocked* cards as non-active even when the customer regards them as their card. Adjust the URL filter, or expose multiple statuses in the array so the agent can show the customer their *frozen* card too.
    * **Multiple cards but no `last_four`:** your processor's response uses a different key (`pan_last_4`, `last4`, `mask`). Fix the Save From Response mapping; the Read node below depends on the key being `last_four` exactly.
  </Accordion>
</AccordionGroup>

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

## Step 2: Configure the two Actions

The Attribute pulls customer state in; two Actions push two changes out, in order. Both Actions are workspace-level, you configure them once, and any rule in any agent can invoke them via a Tool node.

### 2a. `Freeze Card`

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

    * **Name:** `Freeze Card`
    * **Description:** `Freezes a specific card so it can no longer authorize new transactions. Returns a confirmation id for downstream actions to reference.`
  </Step>

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

    `reason` is required by most card processors for compliance (the freeze gets recorded with a reason in the card's audit log). Acceptable values are usually `lost`, `stolen`, `compromised`, `customer_request`.
  </Step>

  <Step title="Define the outputs">
    | Field | Type |
    | - | - |
    | `freeze_confirmation_id` | string |
    | `frozen_at` | string |

    `frozen_at` is an ISO 8601 timestamp. The Reply doesn't need it directly, but the next Action (`Order Replacement Card`) records it for the new card's audit trail.
  </Step>

  <Step title="Add the Data Step">
    * **Step Name:** `Freeze card via Cards API`
    * **Method:** `POST`
    * **URL:** `https://api.yourbank.com/v1/cards/${card_id}/freeze`
    * **Headers:** `{"x-api-key": "your-cards-api-key"}` (or your processor's native scheme).
    * **Body:** `{"reason": "${reason}", "idempotency_key": "${card_id}-freeze"}`
    * **Save From Response:** `{"freeze_confirmation_id": "id", "frozen_at": "frozen_at"}`

    Fini doesn't enforce an idempotency convention, idempotency is a customer-side concern. The idempotency\_key in the body is just an example pattern: a deterministic key keyed on the card so retries don't double-freeze. Most processors take this from a header (`Idempotency-Key`) rather than the body, check your processor's docs.
  </Step>

  <Step title="Test, then save">
    Click **Play**. Use a real `card_id` from your sandbox processor (with a test card you can safely freeze). Confirm the response returns 200 and both output fields populate.
  </Step>
</Steps>

### 2b. `Order Replacement Card`

<Steps>
  <Step title="Create the action">
    * **Name:** `Order Replacement Card`
    * **Description:** `Orders a replacement card on the customer's account, shipped to the address provided. Records the prior freeze confirmation for audit. Returns the new card id, expected delivery date, and tracking URL.`
  </Step>

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

    Marking `prior_freeze_confirmation_id` required is the critical link: it forces the Rulebook tree to bind this input from an upstream Tool's output, which means the freeze Tool must have run successfully before this Tool can validate. This is how Fini's type system enforces "don't ship a card unless the old one is frozen."
  </Step>

  <Step title="Define the outputs">
    | Field | Type |
    | - | - |
    | `new_card_id` | string |
    | `expected_delivery` | string |
    | `tracking_url` | string |
    | `last_four` | string |

    `expected_delivery` is ISO 8601 again. `tracking_url` is the carrier's tracking page (UPS, FedEx, USPS, depending on your processor's shipping partner). `last_four` is the new card's last four digits, surfaced so the customer can match it when the card arrives.
  </Step>

  <Step title="Add the Data Step">
    * **Step Name:** `Order via Cards API`
    * **Method:** `POST`
    * **URL:** `https://api.yourbank.com/v1/customers/${account_id}/cards`
    * **Headers:** `{"x-api-key": "your-cards-api-key"}` (or your processor's native scheme).
    * **Body:**
      ```json theme={null}
      {
        "type": "replacement",
        "prior_freeze_id": "${prior_freeze_confirmation_id}",
        "reason": "${replacement_reason}",
        "shipping_address": {
          "street": "${shipping_street}",
          "city": "${shipping_city}",
          "postal_code": "${shipping_postal_code}",
          "country": "${shipping_country}"
        },
        "idempotency_key": "${prior_freeze_confirmation_id}-replacement"
      }
      ```
    * **Save From Response:** `{"new_card_id": "id", "expected_delivery": "shipping.estimated_delivery", "tracking_url": "shipping.tracking_url", "last_four": "last_four"}`

    The idempotency key is keyed on the freeze confirmation, so a retry against the same freeze produces the same replacement card rather than two.
  </Step>

  <Step title="Test, then save">
    Click **Play**. Plug in a real `account_id` and a recent freeze confirmation from your sandbox. Confirm the response returns 200 and all four output fields populate. Inspect the response carefully, shipping fields tend to be nested under different keys depending on the processor; adjust `Save From Response` accordingly.
  </Step>
</Steps>

<AccordionGroup>
  <Accordion title="What can go wrong at this stage" icon="circle-info">
    * **The order test returns 422 with *card\_already\_replaced* :** most processors block duplicate replacements against the same freeze. Use a fresh sandbox freeze, or wait for the processor's cooldown.
    * **`tracking_url` is null:** some processors only populate `tracking_url` once the card actually ships (hours or days later). For the immediate confirmation reply, fall back to the `expected_delivery` date alone; document this in your Reply wording so customers don't expect a live link the moment they get the message.
    * **Shipping fails with *invalid\_country*:** processors typically use ISO 3166-1 alpha-2 codes (`US`, `GB`, `IN`). The Form node in Step 3 should constrain the country field to these codes, see the Form configuration below.
  </Accordion>
</AccordionGroup>

For the full reference on Action I/O schemas, chaining Tools, and the workspace-vs-rule scoping model, see [Configuration → Actions](/en/api-reference/actions).

## Step 3: Configure the Rulebook rule

The `Customer Identity` attribute brings customer and card context in; the two Actions push the freeze and the order out; the Rulebook decides *when* the work happens, *which* card to act on, and *how* the conversation flows.

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

  ```
  Steps  (root)
  ├── Check: Topic Equals Card Lost or Stolen
  ├── Check: account_status Equals active
  ├── Read:  extract `card_last_four` (String, optional), `replacement_reason` (String)
  ├── Form:  "Shipping address for replacement"
  │     ├── Field: shipping_street (String, required)
  │     ├── Field: shipping_city (String, required)
  │     ├── Field: shipping_postal_code (String, required)
  │     ├── Field: shipping_country (String, required, dropdown)
  │     ├── Form Error: shipping_postal_code invalid for country
  │     └── Form Error: shipping_street fewer than 3 characters
  ├── Tool:  Freeze Card
  │     in:  card_id (resolved from card_last_four + active_cards), reason
  │     out: freeze_confirmation_id, frozen_at
  ├── Tool:  Order Replacement Card
  │     in:  account_id, prior_freeze_confirmation_id (= freeze_confirmation_id),
  │          replacement_reason, shipping_street, shipping_city,
  │          shipping_postal_code, shipping_country
  │     out: new_card_id, expected_delivery, tracking_url, last_four
  └── Reply: confirm replacement with expected_delivery, tracking_url, last_four
  ```
</Tip>

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

  <Step title="Write the Description carefully">
    The Description is *how the LLM decides whether to fire this rule on a given customer message*. For this walkthrough, paste this as your starting Description:

    > *"Use this rule when the customer reports a lost, stolen, damaged, or otherwise compromised card and wants a replacement. Example queries: 'I lost my debit card', 'my card was stolen', 'someone took my wallet, please send a new card', 'my card stopped working, can you ship me a new one', 'I need a replacement card'. Do not use this rule for activation, PIN reset, or transaction disputes, those have their own rules."*

    The negative examples at the end (*"do not use this rule for..."*) matter a lot when other card-related rules exist in the same workspace. The LLM uses the Description as a positive-and-negative signal.

    <Warning>
      The Description is the rule's only routing signal. If a customer's message doesn't fire your rule in production, the Description is the first thing to revisit. Confirm what the LLM did in the **Planning** section of the AI Steps trace.
    </Warning>
  </Step>

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

    * **Name:** *Intent is card lost or stolen*
    * **Condition group:** in the field picker, expand **Tag Groups** → pick `Type of Issue` (the default group) or your custom routing group. Operator `Equals`. Value `Card Lost or Stolen`.

    The deterministic backstop on the LLM's routing. Even if the LLM matches the Description and picks this rule, the Check short-circuits if the conversation's tag isn't `Card Lost or Stolen`, which catches false positives like *"my card stopped working at the ATM"* (an activation or PIN issue, not a replacement).
  </Step>

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

    * **Name:** *Account is active*
    * **Condition group:** in the field picker, expand **User Attributes** → pick `account_status`. Operator `Equals`. Value `active`.

    Closed, suspended, or frozen accounts cannot order new cards. This Check short-circuits cleanly before any side-effect-bearing Tool fires, so a customer whose account was closed yesterday and is asking for a replacement today gets a clean "we can't" path (default agent reply) rather than a half-executed transaction.

    <Tip>
      **Refine this Check for your business.** Some processors allow card orders on accounts in `pending_kyc` (the account exists but isn't fully verified). If yours does, add an OR group: `account_status Equals active OR account_status Equals pending_kyc`. The condition builder supports OR via **+ Add alternative condition**.
    </Tip>
  </Step>

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

    * **Name:** *Extract card details*
    * **Look at:** Interaction History
    * **Find these details:**
      * field `card_last_four`, type `String`, **not required**
      * field `replacement_reason`, type `String`, **required**, allowed values: `lost`, `stolen`, `damaged`, `compromised`
    * **Instructions:** *"Extract the last four digits of the card the customer is reporting on, if they mentioned a number like '4242' or 'card ending in 1881'. If they didn't mention a number, return an empty string, do not guess. Also extract the replacement\_reason from these allowed values: `lost`, `stolen`, `damaged`, `compromised`. Examples: 'I lost my wallet' → lost. 'someone stole my card at the gas station' → stolen. 'my card got cut in half by the ATM' → damaged. 'I saw a charge I didn't make' → compromised. If unclear, default to compromised."*

    `card_last_four` is optional because customers often say *"my card was stolen"* without naming which card. When the customer has only one active card, the Tool will use that card's id directly; when they have multiple, the agent will ask a disambiguation question before the Form renders.

    <Note>
      **Disambiguation when multi-card and no last\_four:** the dropdown logic happens in the Tool's input binding (next step), not here. The Read just gathers what the customer volunteered.
    </Note>
  </Step>

  <Step title="Add the Form node">
    Click **+ Add step**. Pick `Form`. Configure the **Title** as *Shipping address for your replacement card*, and add four required `String` fields (the Form node documents `String`, `Number`, `Date` as supported types, country is a single-line String that you'll validate via Form Error):

    | Name | Type | Required |
    | - | - | - |
    | `shipping_street` | String | ✓ |
    | `shipping_city` | String | ✓ |
    | `shipping_postal_code` | String | ✓ |
    | `shipping_country` | String | ✓ |

    Add two **Form Error** children to the Form node:

    * **Form Error 1:** *Postal code doesn't match country format.* Triggered when the entered `shipping_postal_code` fails the regex for the selected `shipping_country`. The Form Error specifies which field's validation it represents; the form re-renders showing the error message until the customer corrects it.
    * **Form Error 2:** *Street address too short.* Triggered when `shipping_street` is fewer than 3 characters.

    The customer fills the form, submits, and the four fields become tree variables (`${shipping_street}`, `${shipping_city}`, `${shipping_postal_code}`, `${shipping_country}`) available to the Tools downstream.

    <Note>
      **Channel notes.** Forms render reliably inside the [Fini widget](/en/deploy/widget). Form rendering on helpdesk chat surfaces (Zendesk chat, Intercom Messenger, etc.) isn't documented per-integration; default to the widget for Form-driven flows and confirm in your test channel before relying on it. On **email** deployments (Salesforce, Zendesk email-only), the Form node can't render at all, replace the Form with **four Read nodes** in sequence (one per address field) and add a `Reply` between each to prompt for the next. Less elegant, but it works. The address-change pattern in [Rulebook → A worked example: address change with Form](/en/automations/rulebook#a-worked-example-address-change-with-form) is the canonical reference.
    </Note>
  </Step>

  <Step title="Resolve `card_id` and add the Freeze Card Tool">
    Tool inputs in Rulebook bind to a *single tree variable*, not a conditional expression. To pick the right `card_id` from a customer's potentially-multi-card account, do the resolution upstream in a `Read` node and bind the Tool to the Read's output.

    Add a second `Read` between the existing `Extract card details` Read and the Form:

    * **Name:** *Resolve card\_id*
    * **Look at:** Interaction History + User Attributes
    * **Find these details:** field `card_id`, type `String`, required.
    * **Instructions:** *"Given the user\_attribute `active_cards` (an array of cards with `id` and `last_four`) and the tree variable `card_last_four`: if `card_last_four` is non-empty and exactly one card in `active_cards` has a matching `last_four`, return that card's `id`. If `card_last_four` is empty but `active_cards` has exactly one entry, return that entry's `id`. Otherwise, fail (return empty) so the Fallback below can prompt the customer for clarification."*

    Then wrap the Freeze Tool plus everything downstream in a `Fallback` whose second child is a `Reply`:

    ```
    Fallback (try the freeze + order chain; if card_id couldn't be resolved, ask)
    ├── Steps
    │   ├── Tool: Freeze Card (in: card_id from the Resolve Read)
    │   ├── Form: Shipping address
    │   ├── Tool: Order Replacement Card
    │   └── Reply: confirmation
    └── Reply: "You have multiple cards on file. Which one would you like to replace, the one ending in [list active_cards.last_four]?"
    ```

    The Read fails when it can't unambiguously pick a card, the Steps fails, the Fallback advances to the disambiguation Reply, and the customer's next message goes through the rule again with `card_last_four` populated. This is the documented *"try the primary path; fall back if it doesn't apply"* pattern from the [Rulebook reference](/en/automations/rulebook#how-the-tree-runs).

    Now configure the Freeze Tool itself:

    * **Action:** select `Freeze Card`.
    * **Inputs:**
      * `card_id` → bind to the `card_id` tree variable from the Resolve Read.
      * `reason` → bind to `replacement_reason` (from the earlier Read).

    Once the inputs are bound, the Tool's outputs (`freeze_confirmation_id`, `frozen_at`) become tree variables.
  </Step>

  <Step title="Add the Order Replacement Card Tool">
    Click **+ Add step**. Pick `Tool`. Configure:

    * **Action:** select `Order Replacement Card`.
    * **Inputs:**
      * `account_id` → bind to `account_id` (User Attribute).
      * `prior_freeze_confirmation_id` → bind to `freeze_confirmation_id` (upstream Tool output).
      * `replacement_reason` → bind to `replacement_reason`.
      * `shipping_street`, `shipping_city`, `shipping_postal_code`, `shipping_country` → bind each to the matching Form output.

    The `prior_freeze_confirmation_id` binding is the load-bearing link: this Tool cannot fire unless the upstream Freeze Tool succeeded, the dropdown will only show `freeze_confirmation_id` once the Freeze Tool is in place above this node.
  </Step>

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

    > *"Confirm to the customer that their old card has been frozen and a replacement is on the way. Mention the new card's last four digits (${last_four}), the expected delivery date (${expected_delivery}), and include the tracking URL (\${tracking_url}) so they can monitor delivery. Reassure them that their old card can no longer be used. Suggest they keep an eye out for any unauthorized transactions on the frozen card and reach out if they see any."*

    The `${variable}` placeholders interpolate from the Order Tool's outputs. The Reply node passes this as guidance to the agent's reply-composition pass; the agent writes the final wording in its voice.

    <Tip>
      **Handle null tracking\_url gracefully.** If your processor doesn't always return a tracking URL at order time, your Reply instructions should say so: *"...include the tracking URL if available (\${tracking_url}); if it's empty, tell the customer the tracking link will arrive in a separate email when the card ships."* The reply-composition pass handles conditional language naturally.
    </Tip>

    <Note>
      **Voice control lives elsewhere.** Reply instructions are for **content directives** ("mention the new card's last four", "include the tracking link"). For **voice** (tone, vocabulary, how to phrase security reassurance), edit your agent's [Main Guidelines → Tone](/en/configuration/prompts), those rules apply to every reply your agent sends, including this one. For high-stakes flows like fraud-adjacent card replacement, also revisit the [Planning Prompt → Escalation Topics](/en/configuration/prompts) to make sure the right edge cases (e.g., "customer mentions identity theft") escalate to a human before the rule fires.
    </Note>
  </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 before publishing">
    Open the **▶ Test Run** panel from the rule editor. Start with Custom JSON:

    ```json theme={null}
    {
      "message": "my wallet was stolen yesterday, my debit card ending in 4242 was in it, please send a replacement",
      "user_attributes": {
        "account_id": "acc_abc123",
        "account_status": "active",
        "is_high_risk": false,
        "recent_replacement_count": 0,
        "active_cards": [
          {"id": "card_42", "last_four": "4242", "type": "physical", "product": "Debit"},
          {"id": "card_88", "last_four": "1881", "type": "virtual", "product": "Credit"}
        ]
      },
      "tag_groups": {
        "Topic": "Card Lost or Stolen"
      },
      "form_submission": {
        "shipping_street": "1 Infinite Loop",
        "shipping_city": "Cupertino",
        "shipping_postal_code": "95014",
        "shipping_country": "US"
      }
    }
    ```

    The `form_submission` object simulates the Form submission, the test panel injects these values as if the customer had filled the form. Click **Run Test** and walk the trace. A successful run looks like this:

    <Frame>
      <img src="https://mintcdn.com/fini/zef_RDWlJqADKDlS/images/en/home/card-replacement-ai-steps.svg?fit=max&auto=format&n=zef_RDWlJqADKDlS&q=85&s=fcbe928c08691ef6eb898569b8ad8d94" alt="AI Steps panel for a successful card replacement. Sections labeled Planning (collapsed, showing it routed to Card replacement), Executed Rule (expanded), Generate Answer, Input Tag Selection, Output Tag Selection. Inside Executed Rule, seven rows in order: Intent is card lost or stolen (Check, green), Account is active (Check, green), Extract card details (Read, green) showing card_last_four 4242 and replacement_reason stolen, Shipping address (Form, green) showing the four submitted fields, Freeze Card (Tool, green) showing card_id card_42 input and freeze_confirmation_id output, Order Replacement Card (Tool, green) showing prior_freeze_confirmation_id flowing in and new_card_id and expected_delivery outputs, Confirm replacement (Reply, green)." width="480" height="880" data-path="images/en/home/card-replacement-ai-steps.svg" />
    </Frame>

    What to verify:

    1. Both top Checks pass.
    2. The Read populates `card_last_four = 4242` and `replacement_reason = stolen`.
    3. The Form node passes (with the injected `form_submission` values).
    4. The Freeze Tool fires with `card_id = card_42` and produces a `freeze_confirmation_id`.
    5. The Order Tool fires with that confirmation flowing into `prior_freeze_confirmation_id`, plus all four shipping fields from the Form.
    6. The Reply interpolates `new_card_id`, `expected_delivery`, and `tracking_url` from the Order Tool's outputs.

    If any node shows a red dot, click into it to see what value it had. The most common cause of a red Order Tool is the `prior_freeze_confirmation_id` binding being missing, the dropdown only populates once the upstream Freeze Tool is correctly configured.
  </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 rule fires on activation messages:** your Description's negative examples weren't strong enough, or the `Topic` tag is too broad. Tighten both. Reuse the AI Steps trace's **Planning** section to confirm which rule the Planner picked.
    * **The Form node doesn't render:** the deploy channel is email-only. See the *Channel notes* callout above for the Read-based alternative.
    * **The Freeze Tool fires but the Order Tool short-circuits:** the `prior_freeze_confirmation_id` binding is missing or the upstream Tool failed. Open the trace and look at the Freeze Tool's output values; if `freeze_confirmation_id` is empty, the freeze API itself errored (most often a 404 on the `card_id` because the card doesn't belong to that account).
    * **The customer's address in the Form doesn't match what the processor accepts:** US processors generally require five-digit zip codes; international processors accept various formats. The Form Error children should catch obvious mismatches, but processor-side validation may still reject. Pass the error message back to the customer in a Fallback child.
  </Accordion>
</AccordionGroup>

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

## Step 4: Configure the Reply Behavior fraud gate

You've now got a working card-replacement flow. This step adds a safety net: for **high-risk customers** or customers with **repeat replacements in the last 30 days**, the agent should still run the full flow (freeze the card, draft the confirmation) but post the confirmation as an internal note instead of replying to the customer directly. A human reviews the case, optionally calls the customer to verify identity, and then either approves the replacement or reverses it before the new card actually ships.

This is the canonical fraud-prevention pattern for fintech: don't refuse to help the customer (refusing makes the agent useless when fraud is most likely), but require human approval before the side effect customers actually feel (the replacement card landing in someone's mailbox).

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

  <Step title="Add the first condition group: high-risk customers">
    In the first condition group, add two predicates:

    * `User Attributes → is_high_risk` `Equals` `True`
    * `Tag Groups → Type of Issue` `Equals` `Card Lost or Stolen` (use `Contains` instead if your tag group has **Tag Selection** set to "Multiple tags can be selected")

    Predicates in the same group are AND'd, both must be true.
  </Step>

  <Step title="Add the second condition group: repeat replacements">
    Click **+ Add alternative condition** to create a second group (OR'd with the first). Add:

    * `User Attributes → recent_replacement_count` `Greater Than` `1`
    * `Tag Groups → Type of Issue` `Equals` `Card Lost or Stolen`

    Either group's matching is enough to gate the reply, fraud-flagged customers OR customers replacing their second-or-later card in 30 days. The three Reply Behavior 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>

<Warning>
  **The Order Tool still fires.** Reply Behavior gates only the agent's **final reply** (does the customer see it, does a teammate see it, or does nobody see it). It does not gate Tools inside the Rulebook tree. So when this Internal Comment rule fires, the card is **still frozen** and the replacement **is still ordered**, the customer just doesn't see the confirmation. For a true "freeze but hold replacement for human approval" pattern, see the *Holding the destructive Action itself* note below.
</Warning>

**Holding the destructive Action itself.** Reply Behavior is the right tool when freezing the card is always safe (it is, freezing is reversible) but the customer-facing confirmation needs human review. If you want to hold the **replacement order** itself, split the rule into two: an *immediate* rule that freezes the card and posts an internal note, and a *follow-up* rule (triggered manually by a human or by a different intent like *"I've verified the customer"*) that calls the Order Action. This is heavier to maintain but matches the operating model some banks require.

<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. Walk through each row and confirm every field, operator, and value is filled in.
    * **The Internal Comment fires when it shouldn't:** check whether your `recent_replacement_count` field is being correctly populated by the Customer Identity attribute, a default-of-null behaves like zero in some processors and like *unknown* in others. If unsure, add a third predicate: `recent_replacement_count Is Not Null` AND `Greater Than 1`.
    * **Internal Comment doesn't fire when it should:** a higher-priority No Reply rule may be catching the conversation first. Audit your No Reply rules per [Reply Behavior → When multiple cards match](/en/automations/reply-behavior#when-multiple-cards-match). Priority order: No Reply > Internal Comment > Direct Reply.
  </Accordion>
</AccordionGroup>

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

## Verify the flow end-to-end

With everything saved and published, send four test messages from a real conversation surface (your widget, primarily, since the Form needs the chat channel). Use test customer accounts whose accounts resolve through your card-management API and whose cards you can safely freeze.

1. **Standard, single-card, with last\_four mentioned.** *"I lost my card ending in 4242, please send a replacement."* Within seconds, the agent freezes the card, asks for the shipping address via the Form, the customer fills it in, the replacement ships, and the customer sees a **direct reply** confirming the new card's last four and expected delivery.

2. **Standard, multi-card, no last\_four.** *"My wallet was stolen."* The agent disambiguates by asking which card. The customer replies with *"my debit card"* or *"the one ending in 1881"*, the rule re-fires with the new context, the Form renders, and the replacement ships.

3. **High-risk customer.** Same as test 1, but the customer's `is_high_risk` is true. The Rulebook still runs the full tree, the card is frozen, the order is placed, the reply is drafted, but the reply posts as an **internal note** instead of going to the customer. A teammate reviews and follows up.

4. **Account closed.** Same message, but a customer whose `account_status` is `closed`. The account-state Check at the top fails, the rule short-circuits cleanly, and the agent falls through to its default knowledge-base reply. No Tool fires, no card is touched.

**Verify in the AI Steps trace.** Open each test conversation in the [Inbox](/en/testing/inbox). For tests 1 and 2, expect a green-dot trace through all seven nodes; for test 3, the same trace plus a Reply Behavior decision routing to Internal Comment; for test 4, the trace stops at the account\_status Check with a red dot. The trace is your source of truth in production.

### Lock it down with a regression test

Card replacement is one of the highest-stakes flows in fintech support; regress this rule and you ship cards to the wrong people. Add it to your [Test Suite](/en/testing/test-suite) immediately after publishing.

A minimum useful test set:

| Scenario | Expected outcome |
| - | - |
| Single card, last\_four mentioned, low-risk customer | Direct reply with new card's last\_four and expected\_delivery |
| Multi-card, last\_four mentioned, low-risk | Direct reply, correct card frozen |
| Multi-card, no last\_four mentioned, low-risk | Agent asks disambiguation question, no Tool fires |
| Single card, high-risk customer | Internal note posted; customer sees nothing |
| Single card, account\_status closed | Default reply; no Tool fires |
| Single card, customer mentions wrong last\_four (no match in active\_cards) | Agent surfaces the mismatch; no Tool fires |

Bind one of Fini's six pre-built judges, **Tool use correctness** is the natural fit for card replacement (did the Freeze and Order Tools fire with the right inputs, in the right order?), pair it with **Goal resolution** if you want to also grade whether the customer ended up with their stated outcome. Scenarios run as simulated multi-turn conversations, so write expected outcomes that describe the arc, not verbatim wording.

<Warning>
  **Test Suite runs hit your real APIs.** Each scenario replay actually invokes the Tools, which means a Freeze Card + Order Replacement Card run will fire your card-management API for real on the bound test customer. Point the Actions at a sandbox processor while iterating on the test set, or accept that each run produces real freezes and real replacement orders.
</Warning>

After a week in production, open [Analytics → Overview](/en/analytics) and find your card-replacement intent's row in the **Category breakdown** table (Analytics doesn't have a generic tag filter; intent breakdowns are surfaced via the Category breakdown view). Click the row to spot-check conversations. Spikes in the escalated rate usually mean the disambiguation prompt isn't working or the Form is failing for a country your processor doesn't ship to; open a sample of those conversations in the Inbox and read the trace.

<Note>
  **Knowledge handles the fall-through paths.** When the intent Check fails, when `account_status Equals active` fails, when the disambiguation Reply runs because the Resolve Read couldn't pick a card, the agent answers from your [Knowledge base](/en/knowledge/overview). For this walkthrough to feel complete to customers, make sure your Articles cover: *"We can't replace cards for closed or suspended accounts"*, *"What to do if you have a card that's frozen but not lost"*, and the security guidance you'd want a customer to hear if they're confused about whether they were compromised. [Background AI](/en/knowledge/review) watches conversations and proposes Articles for gaps it detects, check the Review queue weekly.
</Note>

<Note>
  **Identity verification.** This walkthrough assumes the customer is already authenticated (the widget is signed with a JWT, or the helpdesk integration carries an authenticated user id). If your channel is unauthenticated (a public form, a chat on a marketing page), insert an identity-verification step before the Freeze Tool, typically a short-lived OTP sent to the customer's email or phone, with a Read or Form node capturing the OTP and a Check verifying it against your auth service. Don't let an unauthenticated channel freeze cards; the fraud risk outweighs the convenience.
</Note>

## What to vary

Once the basic flow is in place, common adaptations:

* **Add Apple/Google Wallet provisioning.** Chain a third Tool after the Order Action that calls your processor's provisioning endpoint to issue a virtual card the customer can use immediately on a digital wallet, useful when the customer needs to keep transacting while waiting for the physical replacement.
* **Skip the Form for known-address customers.** If `Customer Identity` exposes a verified billing address, gate the Form behind a Check: `billing_address_verified Equals False`. When the address is already verified, jump straight from Read to the Tools using the attribute's address fields.
* **Expedited shipping for VIPs.** Add a Check on `is_vip` between the Form and the Order Tool. If `is_vip Equals True`, set the Order Tool's body to include `"shipping_speed": "overnight"`. The processor charges your account the upcharge automatically.
* **Two-card pre-emptive freeze.** If the customer says *"my wallet was stolen"* and has multiple cards, you may want to freeze all active cards (not just the one they identified). The Rulebook doesn't have a generic iteration node, so the cleanest pattern is to extend your `Freeze Card` Action server-side to accept either a single `card_id` or an `account_id` (freezing all cards on that account), and bind the Tool to `account_id` when the customer's wording is "wallet" / "all my cards" / etc., gated by a Check on a `freeze_all` boolean populated by an upstream Read.
* **Suspicious activity sweep.** After ordering the replacement, chain a `Lookup Recent Transactions` Action that pulls the last 24 hours of activity on the frozen card and includes any flagged transactions in the Reply, so the customer can immediately flag any they didn't authorize.
* **Localized shipping countries.** Pre-populate the Form's `shipping_country` dropdown with the customer's known country first (from `Customer Identity`), then a sorted full list. Reduces form friction for international customers.

The pattern (Attributes for context → chained Actions for work → Rulebook with Form for orchestration → Reply Behavior for fraud gating) generalizes to most fintech workflows: dispute filing, payment skip / deferral, identity verification re-runs, account closures with grace periods. Anything where you'd write a multi-step runbook for a human fraud or support agent maps cleanly to this five-piece structure.

## Troubleshooting

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

<AccordionGroup>
  <Accordion title="The rule fires on the wrong intent (activation, PIN reset)" icon="route">
    The Description isn't disambiguating well enough. Open three or four such conversations in the Inbox; copy the customer's actual phrasings into the negative-examples section of the Description (*"do not use this rule for: 'my card isn't working at the ATM', 'I need to set my PIN', ..."*). Re-publish and re-test.
  </Accordion>

  <Accordion title="The Read populates the wrong card_last_four" icon="magnifying-glass">
    Customers sometimes mention amounts that look like card numbers (*"a \$4242 charge"*) or order numbers. Tighten the Read's instructions: *"Only extract a number as card\_last\_four if the customer explicitly says 'card', 'debit', 'credit', or 'ending in'. Do not extract amounts in currency or other reference numbers."*
  </Accordion>

  <Accordion title="The Form looks broken on a specific deploy channel" icon="window-restore">
    Forms only render in the widget. Salesforce email cases, Zendesk email-only, and similar render as a placeholder. If you need this rule to work cross-channel, build a Read-only variant of the rule (no Form) for email and a Form variant for widget; assign each to the relevant agent.
  </Accordion>

  <Accordion title="The Order Tool runs but ships to the wrong address" icon="map-location-dot">
    Two common causes: (1) the Form's pre-fill is wiring stale customer data instead of the current submission, untoggle pre-fill or remap the Tool inputs to read from the Form's submission, not the attribute; (2) your processor's address field names don't match your Save From Response. Test the Order Action's **Play** panel with deliberate values and confirm what the processor actually shipped.
  </Accordion>

  <Accordion title="A duplicate freeze creates two freeze records in the audit log" icon="copy">
    Idempotency isn't taking. Three things to check: (1) the idempotency\_key in the Body is non-empty (some processors silently treat empty as "not idempotent"), (2) the processor reads idempotency from the body field name you used, some want a header (`Idempotency-Key`) instead, (3) the key is deterministic across retries, `${card_id}-freeze` is deterministic; `${now}` is not.
  </Accordion>

  <Accordion title="The Internal Comment fires for VIPs but not high-risk customers" icon="layer-group">
    Either your `is_high_risk` attribute isn't populating, or the predicate has a value mismatch (`true` vs `True`, type coercion). Open the conversation in the Inbox, expand **AI Steps → Executed User Attributes**, and click the `Customer Identity` row to see the actual returned values. Compare against the predicate.
  </Accordion>

  <Accordion title="A replacement was ordered to a wrong address, can it be reversed?" icon="rotate-left">
    Most processors allow card order cancellation within a short window (typically minutes to a few hours, before the card enters the production queue). The reversal is an API call on the processor side, not a Fini concept; document the operational runbook for your team. To prevent recurrences, tighten the Form's validation (postal-code regex per country) and consider adding an *"Are you sure?"* confirmation Reply between the Form and the Order Tool, the customer sees the full shipping address one last time and types **yes** to proceed.
  </Accordion>

  <Accordion title="The Order Tool succeeds but the Reply shows null for tracking_url" icon="link-slash">
    Some processors don't issue a tracking URL until the card physically ships (hours later). Adjust the Reply instructions to handle the null case: *"Include the tracking URL if available; otherwise tell the customer the tracking link will arrive in a separate email when the card ships."*
  </Accordion>
</AccordionGroup>

## Related

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

  <Card title="Order status and changes" icon="boxes-packing" href="/en/walkthroughs/order-status-and-changes">
    The e-commerce companion: a Fallback-rooted rule handling three intents in one tree.
  </Card>

  <Card title="Rulebook" icon="diagram-project" href="/en/automations/rulebook">
    Full reference for behavior trees, node types, the AI Steps trace, Test Run, Form nodes, and rule selection.
  </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="Attributes" icon="id-card" href="/en/api-reference/attributes">
    Sources, fields, switches, and the dependency order Fini uses when fetching attributes.
  </Card>

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

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

  <Card title="Tags" icon="tag" href="/en/configuration/tags">
    Configure the `Topic` tag group the intent Check references.
  </Card>
</CardGroup>


## Related topics

- [End-to-end: cancellation flow](/en/walkthroughs/cancellation-flow.md)
- [End-to-end: order status and changes](/en/walkthroughs/order-status-and-changes.md)
- [Overview](/en/api-reference/actions.md)


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