Information Architecture Base layer Flows

User flows

How each persona actually moves through Tendd: decision points, states, and honest dead ends, not only the happy path. Every screen node exists on the concept map; every route traces to a phase of the To-Be journey.

Colour carries meaning here: green is a closed job, red is a real dead end (no way left to the goal), grey is everything in between, including an error that recovers. Source of truth: docs/flows.md.

Flows

How the personas actually move, with decision points, states, and honest dead ends, not just the happy path. Rectangles are screens matching the sitemap; diamonds are decisions; stadium nodes are states and outcomes.

Closed in the prototype, 2026-08-05. All four dead ends are now a named control on a named page, and the list with the page and the control for each is on wireframes/overview.html under Flows. In short: Flow A by the third exit on connect-bank-error and by node 1.3.4, which had no page before; Flow B by the saved-as-you-go list and the retreat on guided-reveal-empty; Flow C by the alternative steps and the two non-terminal choices on cancel-guide-blocked; Flow D by node 2.7.3, which had no page before. The diagrams still draw them, because the drawing is the record of where the product can fail, not a list of open bugs.

Screen Loading / empty / error state Success end Dead end (a defect, see Critique)

Flow A. J-MAIN: see all recurring charges calmly (Emma, bank path)

The main job, first session, bank-connected path.

flowchart TD
  classDef state fill:#fff7e6,stroke:#d9a441,color:#5c4813;
  classDef success fill:#e9f5ee,stroke:#3f9c68,color:#1e4d33;
  classDef dead fill:#fdeaea,stroke:#c96a6a,color:#6b2b2b;

  Welcome["Welcome / Value Intro"] --> PathQ{"Bank or privacy path?"}
  PathQ -->|Privacy| AddSub["Add Subscription"]
  PathQ -->|Bank| ConnectBank["Connect Bank"]
  ConnectBank --> SyncLoad(["Loading: syncing your bank"])
  SyncLoad --> ConnOK{"Connection succeeded?"}
  ConnOK -->|no| ConnErr(["Error: could not connect"])
  ConnErr --> RetryQ{"Try again?"}
  RetryQ -->|yes| ConnectBank
  RetryQ -->|no| FallbackQ{"Use the manual path instead?"}
  FallbackQ -->|yes| AddSub
  FallbackQ -->|no| StuckExit(["Dead end: leaves without any list"])
  ConnOK -->|yes| FoundQ{"Any recurring charges found?"}
  FoundQ -->|no| NoneEmpty(["Empty: no recurring charges found"])
  NoneEmpty --> AddSub
  FoundQ -->|yes| Reveal["Guided Reveal"]
  AddSub --> Reveal
  Reveal --> Home["Home / Subscription List"]
  Home --> Success(["Success: sees all charges calmly"])

  class SyncLoad,ConnErr,NoneEmpty state
  class Success success
  class StuckExit dead
      

Dead end, closed: leaves without any list if both retry and the manual fallback are declined. connect-bank-error carries a third exit onto Home with both doors open, and node 1.3.4 connect-bank-cancelled does the same for backing out inside Link. The Guided Reveal is one node; internally it is gradual per D1 (count, categories with logos, then total paired with an action).

Flow B. J5: track without sharing bank data (Ravi, manual + presets)

The privacy path. The manual-entry trap is the risk it must survive.

flowchart TD
  classDef state fill:#fff7e6,stroke:#d9a441,color:#5c4813;
  classDef success fill:#e9f5ee,stroke:#3f9c68,color:#1e4d33;
  classDef dead fill:#fdeaea,stroke:#c96a6a,color:#6b2b2b;

  Welcome2["Welcome / Value Intro"] --> PathQ2{"Bank or privacy path?"}
  PathQ2 -->|Bank| BankRef(["Bank path: see Flow A"])
  PathQ2 -->|Privacy| AddSub2["Add Subscription"]
  AddSub2 --> PresetLoad(["Loading: preset library"])
  PresetLoad --> PresetOK{"Preset library loaded?"}
  PresetOK -->|no| LibErr(["Error: presets unavailable"])
  LibErr --> ManualCustom(["Step: add a custom subscription by hand"])
  PresetOK -->|yes| SearchQ{"Service in the presets?"}
  SearchQ -->|yes| AddFromPreset(["Step: add from a preset, prefilled"])
  SearchQ -->|no| ManualCustom
  AddFromPreset --> MoreQ{"Add another?"}
  ManualCustom --> MoreQ
  MoreQ -->|yes| AddSub2
  MoreQ -->|no| EnoughQ{"Anything added yet?"}
  EnoughQ -->|no| EmptyReveal(["Empty: nothing to reveal yet"])
  EmptyReveal --> AbandonRisk(["Dead end: abandons half-done, manual entry trap"])
  EnoughQ -->|yes| Reveal2["Guided Reveal"]
  Reveal2 --> Home2["Home / Subscription List"]
  Home2 --> Success2(["Success: private list, no bank"])

  class PresetLoad,LibErr,EmptyReveal state
  class Success2 success
  class AbandonRisk dead
      

Dead end, closed: abandons half-done. The list is saved as you go and says so, and guided-reveal-empty offers both a way on and a way out. Ravi has twice abandoned manual entry in the research; if nothing pulls him back he leaves with a partial list.

Flow C. J2 + E2: find, cancel, and feel the win (Claudia)

Basic cancel instruction is free (D3); the full guide and direct link are Pro. The external dark pattern is the risk.

flowchart TD
  classDef state fill:#fff7e6,stroke:#d9a441,color:#5c4813;
  classDef success fill:#e9f5ee,stroke:#3f9c68,color:#1e4d33;
  classDef dead fill:#fdeaea,stroke:#c96a6a,color:#6b2b2b;

  Home3["Home / Subscription List"] --> Detail3["Subscription Detail"]
  Detail3 --> CancelQ{"Cancel this one?"}
  CancelQ -->|no| Home3
  CancelQ -->|yes| Guide["Cancel Guide"]
  Guide --> DepthQ{"Free basic steps or Pro full guide?"}
  DepthQ -->|"Basic, free"| BasicSteps(["Step: basic instruction, link plus steps"])
  DepthQ -->|"Wants full guide"| Upgrade3["Upgrade / Tendd Pro"]
  Upgrade3 --> PayQ{"Upgrade now?"}
  PayQ -->|no| BasicSteps
  PayQ -->|yes| FullGuide(["Step: full guide plus direct link"])
  BasicSteps --> DoneQ{"Managed to cancel?"}
  FullGuide --> DoneQ
  DoneQ -->|yes| Win["Cancel Win Moment"]
  DoneQ -->|"no, blocked by retention flow"| BlockedErr(["Error: stuck in the service dark pattern"])
  BlockedErr --> HelpQ{"In-app help offered?"}
  HelpQ -->|no| StuckCancel(["Dead end: gives up, no in-app next step"])
  HelpQ -->|yes| Guide
  Win --> ShareQ{"Share the win?"}
  ShareQ -->|yes| Share["Share Snapshot"]
  ShareQ -->|no| Success3(["Success: saved money, calm"])
  Share --> Success3

  class BlockedErr state
  class Success3 success
  class StuckCancel dead
      

Dead end, closed: gives up with no in-app next step if a blocked cancellation is not caught. cancel-guide-blocked names the dark pattern as not the person fault, gives four alternative steps, and offers two non-terminal choices. Declining the upgrade always returns the free basic instruction, so the relief moment is never paywalled.

Flow D. J4: stay ahead of a surprise (alert to action)

Entry is a notification (the return hook). Price change and payment-failed alerts are free; trial-ending and unusual are Pro.

flowchart TD
  classDef state fill:#fff7e6,stroke:#d9a441,color:#5c4813;
  classDef success fill:#e9f5ee,stroke:#3f9c68,color:#1e4d33;
  classDef dead fill:#fdeaea,stroke:#c96a6a,color:#6b2b2b;

  Notif(["Event: notification, price change or payment failed"]) --> Alerts["Alerts / Activity"]
  Alerts --> AnyQ{"Any alerts to show?"}
  AnyQ -->|"load failed"| AlertErr(["Error: could not load alerts"])
  AlertErr --> RetryA{"Retry?"}
  RetryA -->|yes| Alerts
  RetryA -->|no| HomeA["Home / Subscription List"]
  AnyQ -->|no| AllClear(["Empty: all clear"])
  AllClear --> HomeA
  AnyQ -->|yes| TypeQ{"Alert type?"}
  TypeQ -->|"Trial ending or unusual, Pro"| UpgradeA["Upgrade / Tendd Pro"]
  TypeQ -->|"Price change or payment failed, free"| DetailA["Subscription Detail"]
  UpgradeA --> HomeA
  DetailA --> NeedQ{"Price change or payment failure?"}
  NeedQ -->|"price change"| ActQ{"Keep or cancel?"}
  ActQ -->|keep| SuccessA(["Success: caught it early, nothing to do"])
  ActQ -->|cancel| GuideA["Cancel Guide"]
  NeedQ -->|"payment failure"| FixQ{"Is the fix inside the app?"}
  FixQ -->|"yes"| SuccessA
  FixQ -->|"no, at the bank or merchant"| NoNext(["Dead end: no in-app next step for a failed payment"])

  class AlertErr,AllClear state
  class SuccessA success
  class NoNext dead
      

Dead end, closed: no in-app next step for a failed payment when the fix lives at the bank or merchant. Node 2.7.3 subscription-detail-payment-failed, which had no page before the rebuild, states what usually happens next, says the money itself is fine, and carries the cancel guide and the way to the full alert list.

Flow E. Coming back, and keeping a list made without an account

Added 2026-08-10 with the auth model. The only flow that starts outside a session, and deliberately the shortest in the map: everything persuasive already happened.

flowchart TD
  classDef state fill:#fff7e6,stroke:#d9a441,color:#5c4813;
  classDef success fill:#e9f5ee,stroke:#3f9c68,color:#1e4d33;

  Return["Returns to Tendd"] --> HasQ{"Signed in on this device?"}
  HasQ -->|yes| Home["Home / Subscription List"]
  HasQ -->|"no, has an account"| SignIn["Sign In"]
  SignIn --> Sent(["Check your email"])
  Sent --> LinkQ{"Link still valid?"}
  Sent -->|"send another"| Sent
  LinkQ -->|no| Expired(["That link has expired"])
  Expired -->|"send a new link"| Sent
  Expired --> SignIn
  LinkQ -->|yes| Home
  HasQ -->|"no account, list made by hand"| NoAcct["You, with no account yet"]
  NoAcct -->|"already have an account"| SignIn
  NoAcct --> Offer{"Create an account?"}
  Offer -->|"not now"| Home
  Offer -->|yes| Sent
  Home --> SuccessE(["Success: the same list, on any device"])

  class Sent,Expired,NoAcct state
  class SuccessE success
      

No dead end, and one thing deliberately absent. There is no "that email is not registered" anywhere in this flow: telling anybody which addresses have accounts tells everybody, so the screen behaves identically either way and the mail that arrives is the one that fits. The merge is the other quiet part, and the list built with no account joins the account the moment one exists, so nothing a person typed is the price of signing up. States: 1.6.1 check your email, naming the address it went to because a typo is invisible once the field is gone; 1.6.2 that link has expired, in the same register node 6.14.2 uses for a bank connection that needs reconnecting; 6.16.1 you, with no account yet, which is the steady state of the manual path and not an error.

Activation node: Guided Reveal. research/docs/aarrr.md defines activation as the first time a person sees a populated subscription list with a monthly total. In Flow A that happens inside the Guided Reveal, at its third step (the total paired with an action, per D1): three taps from the start, inside the ceiling the research set. Home is where that value is kept, not where it first arrives.

Where the account is created, added 2026-08-10. The auth model puts it inside Connect Bank as a block, not as a step: the email is asked on node 1.3 with its reason beside it, the account is created unverified and Plaid Link opens immediately, so Flow A gains no node and no tap. Flow B creates no account at all. That asymmetry is the decision and not an oversight: bank data needs an owner and three lines a person typed do not.