> ## 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.

# Fini for password reset and account access

> How Fini handles password reset and login requests by triggering your own reset flow or linking to self-serve recovery, without ever handling passwords or MFA codes in chat.

export const TreeWalker = ({title = "Try it: run the example tree", nodes = [], scenarios = [], note}) => {
  const FV = {
    lime: "#C3EE5E",
    ink: "#131415",
    line: "rgba(127,127,127,0.28)",
    soft: "rgba(127,127,127,0.07)",
    softer: "rgba(127,127,127,0.04)",
    muted: "rgba(127,127,127,0.95)",
    pass: "#C3EE5E",
    warn: "#FFB020",
    fail: "#FF4D4D",
    radius: 14
  };
  const fvCard = {
    border: `1px solid ${FV.line}`,
    borderRadius: FV.radius,
    padding: 18,
    margin: "20px 0",
    background: FV.softer
  };
  const fvChip = active => ({
    border: `1px solid ${active ? FV.lime : FV.line}`,
    background: active ? FV.lime : "transparent",
    color: active ? FV.ink : "inherit",
    borderRadius: 999,
    padding: "6px 12px",
    fontSize: 13,
    fontWeight: 600,
    cursor: "pointer",
    lineHeight: 1.2
  });
  const fvBtn = primary => ({
    border: `1px solid ${primary ? FV.lime : FV.line}`,
    background: primary ? FV.lime : "transparent",
    color: primary ? FV.ink : "inherit",
    borderRadius: 10,
    padding: "7px 14px",
    fontSize: 13,
    fontWeight: 600,
    cursor: "pointer"
  });
  const fvLabel = {
    fontSize: 11,
    fontWeight: 700,
    letterSpacing: "0.08em",
    textTransform: "uppercase",
    opacity: 0.6,
    marginBottom: 8
  };
  const [sc, setSc] = useState(0);
  const [pos, setPos] = useState(-1);
  const [running, setRunning] = useState(false);
  const cur = scenarios[sc] || ({});
  const stop = typeof cur.stopAt === "number" ? cur.stopAt : null;
  const last = stop === null ? nodes.length - 1 : stop;
  useEffect(() => {
    if (!running) return;
    if (pos >= last) {
      setRunning(false);
      return;
    }
    const t = setTimeout(() => setPos(p => p + 1), 650);
    return () => clearTimeout(t);
  }, [running, pos, last]);
  const run = () => {
    setPos(-1);
    setRunning(true);
    setTimeout(() => setPos(0), 50);
  };
  const pick = k => {
    setSc(k);
    setPos(-1);
    setRunning(false);
  };
  const done = !running && pos >= last && pos >= 0;
  const typeColor = {
    Check: "#E8E8E8",
    Read: "#E8E8E8",
    Form: "#E8E8E8",
    Tool: "#E8E8E8",
    Reply: FV.lime
  };
  return <div style={fvCard}>
      <div style={fvLabel}>{title}</div>
      <div style={{
    display: "flex",
    gap: 8,
    flexWrap: "wrap",
    marginBottom: 14
  }}>
        {scenarios.map((s, k) => <button key={k} style={fvChip(k === sc)} onClick={() => pick(k)}>{s.name}</button>)}
      </div>
      <div style={{
    fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
    fontSize: 13.5
  }}>
        {nodes.map((n, k) => {
    const reached = pos >= k;
    const failed = done || running ? stop === k && reached : false;
    const skipped = stop !== null && k > stop && (done || running);
    const state = failed ? "fail" : reached ? "pass" : "idle";
    return <div key={k} style={{
      display: "flex",
      alignItems: "stretch",
      gap: 10,
      opacity: skipped ? 0.35 : 1,
      transition: "opacity .3s"
    }}>
              <div style={{
      width: 18,
      display: "flex",
      flexDirection: "column",
      alignItems: "center"
    }}>
                <div style={{
      width: 2,
      flex: 1,
      background: k === 0 ? "transparent" : FV.line
    }} />
                <div style={{
      width: 12,
      height: 12,
      borderRadius: 999,
      background: state === "fail" ? FV.fail : state === "pass" ? FV.pass : FV.line,
      transition: "background .3s"
    }} />
                <div style={{
      width: 2,
      flex: 1,
      background: k === nodes.length - 1 ? "transparent" : FV.line
    }} />
              </div>
              <div style={{
      flex: 1,
      margin: "4px 0",
      padding: "8px 12px",
      borderRadius: 10,
      border: `1px solid ${state === "fail" ? FV.fail : state === "pass" ? FV.pass : FV.line}`,
      background: state === "pass" ? "rgba(195,238,94,0.14)" : state === "fail" ? "rgba(255,77,77,0.08)" : "transparent",
      transition: "all .3s"
    }}>
                <span style={{
      fontSize: 11,
      fontWeight: 700,
      padding: "2px 8px",
      borderRadius: 6,
      marginRight: 10,
      background: typeColor[n.type] || FV.line,
      color: FV.ink
    }}>{n.type}</span>
                {n.label}
                {failed && n.onFail && <div style={{
      fontFamily: "inherit",
      fontSize: 12.5,
      marginTop: 6,
      color: FV.fail
    }}>Short-circuits: {n.onFail}</div>}
              </div>
            </div>;
  })}
      </div>
      <div style={{
    display: "flex",
    justifyContent: "space-between",
    alignItems: "center",
    marginTop: 14,
    gap: 10,
    flexWrap: "wrap"
  }}>
        <button style={fvBtn(true)} onClick={run} disabled={running}>{running ? "Running..." : done ? "Run again" : "Run scenario"}</button>
        {done && cur.outcome && <div style={{
    flex: 1,
    minWidth: 220,
    fontSize: 14,
    padding: "8px 12px",
    borderRadius: 10,
    border: `1px solid ${cur.escalates ? FV.warn : FV.pass}`
  }}>
            <b>{cur.escalates ? "Escalates: " : cur.resolved ? "Resolved: " : "Outcome: "}</b>{cur.outcome}
          </div>}
      </div>
      {note && <div style={{
    fontSize: 12,
    opacity: 0.6,
    marginTop: 10
  }}>{note}</div>}
    </div>;
};

Fini (usefini.com) handles password reset and account-access requests by triggering your existing reset flow through an [Action](/en/api-reference/actions), or by pointing the customer to your self-serve reset page, and it never asks for, accepts, or sets a password or MFA code in the conversation. Your identity system stays the only place where a user is verified and a credential changes; Fini's job is to recognize the request, start the right flow for that account, and explain the next step clearly.

That split is the whole design. A reset link sent to the email address already on file is safe to trigger from a chat, because only the owner of that inbox can use it. Changing the email on file, removing MFA, or unlocking an account on someone's word is not, so those go to your team.

## What Fini handles and what it hands to your team

| Request | Who handles it | How |
| - | - | - |
| "I forgot my password" | Fini | A **Tool** node calls your `Send Password Reset` Action for the login email; the reset link goes only to the address on file. |
| "The reset link expired" / "I never got the email" | Fini | Re-triggers the same Action within a limit you set, and explains link expiry and spam folders from [Knowledge](/en/knowledge/overview). |
| "How do I change my password?" (signed in) | Fini | Answered from Knowledge with a link to your account settings page. |
| "I sign in with my company account" | Fini | Explains that the password is managed by the customer's company identity provider and that their IT admin can reset it. |
| "My account is locked" | Fini, if your system has a self-service unlock flow; otherwise your team | Fini triggers the unlock-by-email flow as an Action, or escalates. |
| Lost MFA device, new phone, no backup codes | Fini, after identity verification; otherwise your team | There is no separate feature to switch on: identity verification and MFA resets are built as Agentic Actions, an Intent Rule plus Attributes and Actions that call your identity and MFA systems (see [KYC and account changes](/en/use-cases/kyc-and-account-changes)). Without that built, escalate for identity verification; Fini can still share your published recovery steps (for example, backup codes) from Knowledge. |
| Change of login email or phone number | Your team | A common account-takeover path; never automated on an unverified request. |
| "Someone else is in my account" | Your team, immediately | Escalated at the Planning step through Escalation Topics. |

## How it works in Fini

| Piece | What it does for account access | Where |
| - | - | - |
| **Tags** | A Rulebook-enabled group (for example `Account Access Intent`) with `forgot_password`, `reset_link_issue`, `account_locked`, `mfa_issue`, `change_login_email`, `account_takeover`, and `other`. | [Tags](/en/configuration/tags) |
| **User Attributes** | When the requester can be identified (email channel, helpdesk requester, or a logged-in widget user), an `Account Status` attribute exposes `auth_method`, `account_locked`, and `recent_reset_requests`. | [Attributes](/en/api-reference/attributes) |
| **Actions** | `Send Password Reset` and, if you have one, `Send Unlock Email`. Both call your identity system's existing endpoints. | [Actions](/en/api-reference/actions) |
| **Intent Rule** | A **Fallback**-rooted tree that sends SSO users to their admin, triggers the reset for everyone else, and falls back to the self-serve link. | [Intent Rules](/en/automations/rulebook) |
| **Prompts** | **Escalation Topics** for MFA loss, login-email changes, and takeover reports, so the planner routes those to a human and skips knowledge search. | [Prompts](/en/configuration/prompts) |
| **Reply Rules** | **Internal Comment** for `change_login_email`, so your team sees the draft but the customer doesn't receive an automated answer. | [Reply Rules](/en/automations/reply-behavior) |
| **Guardrails** | A custom rule that fails any reply asking for a password or code, plus a URL allowlist so reset links only point at your domains. | [Guardrails](/en/configuration/guardrails) |
| **Knowledge** | Articles on reset-link expiry, spam folders, SSO sign-in, and your published MFA recovery steps. | [Articles](/en/knowledge/articles) |

### Design your reset endpoint for chat

Your `Send Password Reset` endpoint should behave the same way your public "forgot password" form does:

* **Send only to the address on file.** The Action passes a login email; your system sends the link to that account's registered email, never to an address supplied in the conversation that differs from it.
* **Return the same response whether or not the account exists.** The Reply then uses neutral wording ("if an account exists for that email, a reset link is on its way"), so the conversation can't be used to discover which emails have accounts.
* **Rate-limit on your side too.** The tree checks `recent_reset_requests` when it can identify the account, but your endpoint should enforce its own limit.

### Example behavior tree

<Note>
  **Example to adapt.** Tag values, field names, and Action names below are placeholders; replace them with the ones in your workspace.
</Note>

```text theme={null}
Fallback  (root)
├── Steps  (Company SSO accounts: send to their admin)
│   ├── Check: Account Access Intent In forgot_password, reset_link_issue, account_locked
│   ├── Check: auth_method Equals sso
│   └── Reply: their password is managed by their company sign-in; contact their IT admin
├── Steps  (Over the reset limit)
│   ├── Check: Account Access Intent In forgot_password, reset_link_issue, account_locked
│   ├── Check: recent_reset_requests Greater Than or Equal To 3
│   └── Reply: explain the limit; link to the self-serve reset page
├── Steps  (Trigger your reset flow)
│   ├── Check: Account Access Intent In forgot_password, reset_link_issue, account_locked
│   ├── Read:  extract `login_email` (String, required)
│   ├── Tool:  Send Password Reset
│   │     in:  login_email
│   │     out: request_accepted, link_expires_in_minutes
│   └── Reply: neutral confirmation; link expires in ${link_expires_in_minutes} minutes;
│              check spam; never ask for the password or a code
├── Steps  (Ask for the login email)
│   ├── Check: Account Access Intent In forgot_password, reset_link_issue, account_locked
│   └── Reply: ask for the email they sign in with
└── Reply: link to the self-serve reset page
```

<TreeWalker
  title="Try it: run the reset branch"
  nodes={[
{ type: "Check", label: "Reset branch: Account Access Intent In forgot_password, reset_link_issue, account_locked", onFail: "the next branch needs the same intent, so the last Reply links to the self-serve reset page" },
{ type: "Read", label: "extract login_email (String, required)", onFail: "the branch fails and the next branch asks for the email they sign in with" },
{ type: "Tool", label: "Send Password Reset", onFail: "the reset branch fails, login_email is discarded, and the Fallback moves on to the ask-for-email branch" },
{ type: "Reply", label: "neutral confirmation, link expiry in minutes, check spam, never ask for the password or a code" }
]}
  scenarios={[
{ name: "Login email in the message", outcome: "Send Password Reset runs. The reply says that if an account exists for that email a reset link is on its way, when it expires, and to check spam." },
{ name: "No email given yet", stopAt: 1, outcome: "The next branch asks for the email they sign in with. Tree variables don't carry over, so when the planner selects the rule on the next message it runs again from the root, with the email now in the conversation." },
{ name: "Reset call fails", stopAt: 2, outcome: "No confirmation is sent. The ask-for-email branch passes its intent Check, so the agent asks for the sign-in email again. Add a failure branch around the Tool if you want a handoff here." }
]}
  note="Example tree to adapt. This walks the third branch; SSO accounts and customers over the reset limit are caught by the two branches before it, shown in the diagram below."
/>

How this runs: SSO users are caught first, so the agent never sends a reset email to an account whose password your system doesn't own. Customers who already requested several resets get the self-serve link instead of another email. When `auth_method` or `recent_reset_requests` isn't populated (the requester couldn't be identified yet), those Checks fail and their branches are skipped. The reset branch needs a login email; if the Read can't find one, the branch fails and the next branch asks for it. Tree variables don't carry over between messages, so on the customer's next message the rule runs again from the root with the email now in the conversation.

If `Send Password Reset` fails, the reset branch fails too and the Fallback moves on to the ask-for-email branch, whose intent Check passes, so the customer is asked for the email again. To hand off instead, wrap the Tool and its Reply in a Fallback whose second child is a handoff Reply saying a teammate will follow up.

```mermaid theme={null}
---
title: How an account-access request is handled
---
flowchart TD
    MSG(["Customer message"]) --> ET{"Lost MFA, login-email change,<br/>or someone else in the account?"}
    ET -->|"Yes, Escalation Topics"| TEAM(["Your team"])
    ET -->|"No"| INT{"Reset, link or<br/>locked-account intent?"}
    INT -->|"No"| SELF["Reply links the<br/>self-serve reset page"]
    INT -->|"Yes"| SSO{"auth_method<br/>Equals sso?"}
    SSO -->|"Yes"| ADMIN["Reply: contact<br/>your IT admin"]
    SSO -->|"No or not populated"| LIM{"3 or more<br/>recent resets?"}
    LIM -->|"Yes"| LIMR["Reply explains the limit,<br/>links self-serve reset"]
    LIM -->|"No or not populated"| EMAIL{"login_email<br/>in the message?"}
    EMAIL -->|"Yes"| TOOL["Send Password Reset<br/>through your API"]
    TOOL -->|"Success"| NEU(["Neutral confirmation<br/>and spam reminder"])
    TOOL -->|"Fails"| ASK
    EMAIL -->|"No"| ASK["Reply asks for the<br/>sign-in email"]
    ASK -.->|"next message runs<br/>from the root"| MSG

    classDef source fill:#F7F7F7,color:#131415,stroke:#E8E8E8
    classDef agent fill:#131415,color:#FFFFFF,stroke:#131415,stroke-width:3px
    classDef surface fill:#FFFFFF,color:#131415,stroke:#131415
    classDef human fill:#C3EE5E,color:#131415,stroke:#131415,stroke-width:2px

    class MSG,NEU source
    class SELF,ADMIN,LIMR,TOOL,ASK agent
    class ET,INT,SSO,LIM,EMAIL surface
    class TEAM human
```

<Tip>
  If the reset flow asks the customer to go to their inbox and the conversation goes quiet, a **Reply** node can send one follow-up after a delay with **Follow up after inactivity**. See [Rulebook → Inactivity follow-ups](/en/automations/rulebook#inactivity-follow-ups).
</Tip>

## Guardrails and escalation

<Steps>
  <Step title="Tell the agent never to handle credentials">
    In **Main Guidelines → Guardrails**, add a subsection: never ask for a password, one-time code, MFA code, recovery code, or security answer; if the customer shares one, don't repeat it and tell them to change it. See [Prompts → Main Guidelines](/en/configuration/prompts).
  </Step>

  <Step title="Check every reply for it">
    On **Guardrails**, add a **Custom rule** with an evaluation instruction such as "Fail if the reply asks the customer to share a password, one-time code, MFA code, recovery code, or security answer, or repeats one back." Add pass and fail examples to calibrate it. Add a **URL allowlist** with your app and identity domains so the agent can only link to real reset pages.
  </Step>

  <Step title="Escalate the high-risk requests">
    In the Planning Prompt, add **Escalation Topics** for lost MFA devices, requests to change the login email or phone, and any report that someone else has access to the account. The Planning Prompt guidance lists identity issues and account-takeover signals as high-risk operations for exactly this reason.
  </Step>

  <Step title="Route escalations to the right queue">
    For widget conversations, a [Business Rule](/en/automations/business-rules) with the **On Escalation** trigger creates the ticket in your helpdesk. For native tickets, map `mfa_issue` and `account_takeover` to your identity or trust team with [Agent groups](/en/configuration/agent-groups). See [Fini for escalation and human handoff](/en/use-cases/escalation-and-handoff) for the full handoff setup.
  </Step>
</Steps>

<Warning>
  Guardrails check what the agent sends; they don't remove what a customer types. Fini automatically masks sensitive data, including card numbers and health details, everywhere it stores conversation data (transcripts, Inbox and **AI Steps** traces). Fields you hide from the AI are also redacted in **AI Steps**; Guardrails stay an extra layer on top. If a customer pastes a password into the conversation, your Main Guidelines instruction should still tell the agent to advise the customer to change it. See [Data handling](/en/security/data-handling) for how conversation data is stored.
</Warning>

## What you need

| What | Why | Where |
| - | - | - |
| **A reset endpoint** | The `Send Password Reset` Action calls it. It should send to the address on file, return a uniform response, and rate-limit. | Your identity system |
| **A credential scoped to that endpoint** | The Action's Data Step sends it in Headers. Issue a key that can trigger reset and unlock emails and nothing else, never one that can change credentials or MFA settings. | Your identity system |
| **A self-serve reset page URL** | The fallback in the tree and the link in Knowledge articles. | Your app |
| **A Rulebook-enabled tag group** | The intent Checks read it. Write tag descriptions that separate `forgot_password` from `mfa_issue`. | [Tags](/en/configuration/tags) |
| **Knowledge articles** | Link expiry, spam folders, SSO sign-in, and MFA recovery steps you publish to customers. | [Articles](/en/knowledge/articles) |
| **A test account** | Verify each branch, including the SSO branch and the rate limit, before publishing. | Your identity system |

## What to measure

* **Intent rule breakdown** in [Analytics](/en/analytics): the reset rule's **AI Resolve Rate**, **Escalated Rate**, and **Volume**.
* **Conversation status.** Reset conversations often end with the customer leaving to check their inbox. Compare **Resolved by AI** with **Waiting for Customer** for this intent; a large waiting share inflates deflection rate without telling you whether customers got back in.
* **Escalation reasons.** The **Customer requested human** (after attempt) reason on reset conversations usually means the email didn't arrive or the Reply wording was unclear.
* **Guardrail activity.** Hits on the credential custom rule over **7d** and **30d**. Any hit is worth opening in [Inbox](/en/testing/inbox).

Turn reset conversations into [Test Suite](/en/testing/test-suite) test cases, including one where the customer volunteers a password (create it in Inbox with test data). Put the shared checks in a criteria group: an AI judgement that the agent never repeats or asks for the password, and an exact check that the reset Action ran. Test Suite uses recorded or mock Action responses, so runs never call your identity provider. For a live end-to-end check, point the Action at a test identity environment or run a controlled pilot.

## Related

<CardGroup cols={2}>
  <Card title="End-to-end: cancellation flow" icon="route" href="/en/walkthroughs/cancellation-flow">
    The closest walkthrough: intent, identify, one API call, confirm. Password resets share this shape.
  </Card>

  <Card title="Intent Rules" icon="diagram-project" href="/en/automations/rulebook">
    Fallback trees, Read nodes, and inactivity follow-ups.
  </Card>

  <Card title="Guardrails" icon="shield-check" href="/en/configuration/guardrails">
    Custom rules and the URL allowlist.
  </Card>

  <Card title="Prompts" icon="message" href="/en/configuration/prompts">
    Escalation Topics and Main Guidelines.
  </Card>

  <Card title="Widget" icon="message" href="/en/deploy/widget">
    Identify logged-in users with a signed JWT when the request comes from inside your app.
  </Card>
</CardGroup>


## Related topics

- [Setting up Fini for healthcare](/en/industry-setup/healthcare.md)
- [Changelog](/en/changelog.md)
- [End-to-end: cancellation flow](/en/walkthroughs/cancellation-flow.md)


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