Funnel
How many people reach each step of a flow and where they drop off, against the previous period or split by segment.
≈ 1,389 tokens per use (tool 890 · guidance 319 · typical payload 180)How token costs are estimated
Example: Funnel
How's our checkout funnel this week?
Checkout funnel
Oct 1–7 vs Sep 24–30
- Viewed product25,060 · 100% of the first step
34% continue, 16,540 leave
Added to cart8,520 · 34% of the first step55% continue, 3,830 leave
Started checkout4,690 · 19% of the first step30% continue, 3,280 leaveBiggest drop
Purchased1,410 · 5.6% of the first step
| Step | Count | % of first step | Step conversion | Sep 24–30 |
|---|---|---|---|---|
| Viewed product | 25,060 | 100% | – | 24,200 |
| Added to cart | 8,520 | 34% | 34% | 8,090 |
| Started checkout | 4,690 | 19% | 55% | 4,520 |
| Purchased | 1,410 | 5.6% | 30% | 1,240 |
Checkout loses the most: 70% who start don't finish.
Recovering 5% after Started checkout ≈ +$10,500 per period: 3,280 people leave there; at $64 per conversion, if they then convert like everyone else.
When to use
- The user asks how a multi-step flow converts or where people drop off: checkout, signup, onboarding, a trial-to-paid path.
- You have per-step counts from analytics, optionally for a previous period or by segment (device, channel, plan).
When not to use
- The steps aren't sequential, or people can enter at any step. Use a Chart or a Data Table.
- The question is about one number over time. Use a KPI Card or a chart.
- You don't have real counts. Don't invent or estimate them.
How the agent should use it
- Every count must come from the tool result, in funnel order. Percentages, drop-offs, the biggest drop and the overall conversion are computed by the component, so the agent never does the arithmetic.
- Set
previousper step only when you have the same steps for the comparison period, and name that period inperiod.compareLabel. - Give segments the same steps in the same order, at most 4. A segment whose
countsdoesn't have one value per step is left out, with a note saying why. - Add
value.perConversiononly when you know the real value of one conversion (e.g. average order value). The card then sizes the biggest drop in one line. - Keep
insightto one sentence that names the step and a number from the data. - Drill-down and export are the user's requests, not actions you've taken: fetch the step's detail when asked.
- Lifecycle: this is analytics data, so set
fetchedAt(andstaleAfterMs, e.g. an hour for a live dashboard or a day for weekly reports) on the call. When the user re-queries the same funnel (a new period, another segment split), mark the old call superseded by the new one. NoexpiresAt.
What it shows
| Part | Notes |
|---|---|
| Header | Title, period ("Oct 1–7 vs Sep 24–30"), and the overall conversion, large, with its change in percentage points when every step has previous, then "Visited to Purchased" as one quiet line |
| Rows | One per step: label, count (compact, e.g. "25.1K"), share of the first step and a bar whose width is the count relative to the first step (with a minimum so small steps still show) |
| Previous period | Not drawn per step: it drives the header's change, and each step's previous count is in the hidden table |
| Connectors | Between steps: step-to-step conversion, e.g. "52% continue" (the number who leave is read by screen readers). The highest drop rate is marked "Biggest drop" with an icon |
| Data check | When a step has more people than the step before, the connector says "Step counts go up at Purchased: check the data" instead of hiding it |
| Opportunity | With value.perConversion, one line: "Recovering 5% after Started checkout ≈ +$10,500", per period like the one shown. It assumes the recovered people convert like everyone else from that step on, at perConversion each; that assumption is read by screen readers and documented here rather than printed |
| Insight, source | The agent's sentence under the bars; the source (linked when url is http(s)) and the as-of date share one quiet footer line |
Segments
With segments, the header gets a segmented control: All, one option per segment, and Compare. A segment view shows the same funnel with that segment's counts and its own overall conversion (no previous period, since segments don't carry one). Compare leads with each segment's overall conversion in one row, then small grouped bars per step, one per segment, each as a share of that segment's own first step, so conversion rates line up even when the segments differ in size. Segments are told apart by their name next to each bar and the legend order, not only by fill.
The view is local to the card. When the user drills into a step while a segment is selected, its id is included in the result.
Interaction
The card never locks: it's a view of the data with ways into it.
- Drill into a step: the footer's "Drill into step" menu lists every step.
- Export CSV: builds the CSV in the browser (one row per step: count, % of first step, step conversion, previous, and each segment's count), downloads it, and tells the agent. Fields are escaped, and text that a spreadsheet could run as a formula is prefixed with
'.
Accessibility
The bars are hidden from screen readers. Each view is a figure with a one-sentence summary (aria-label) and a visually hidden table of steps × count, % of first step, step conversion and previous (or per-segment counts in Compare). Compact counts carry the full number in a visually hidden span, and the rolling overall conversion keeps its real value in one too. Changes show a sign, an arrow and text, and "Biggest drop" is text with an icon, so color is never the only cue. The segment control is a native radio group (arrow keys move between options) with a visible focus ring, and view switches and exports are announced in a polite live region. Bar animations are skipped with reduced motion.
Installation
npx shadcn@latest add https://your-site.com/r/funnel.json
Usage
import { tool } from "ai"
import { funnelTool } from "@/components/genui/funnel.schema"
const tools = {
[funnelTool.name]: tool({
description: funnelTool.description,
inputSchema: funnelTool.inputSchema,
}),
}Without actions the funnel is display-only, so give the tool an execute that returns immediately. With them, the agent (or your host) receives one result per press:
{ action: "drill-down", stepId: "checkout", segmentId: "mobile" } // segmentId only in a segment view
{ action: "export" } // the CSV is already downloaded in the browser<Funnel
{...part.input}
status={part.state === "input-streaming" ? "streaming" : "ready"}
onResult={(r) => sendToAgent(part.toolCallId, r)}
/>With LangGraph
Push it from a graph node with ui.push({ name: "show_funnel", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
There's no finished state to restore. Render the stored tool input again; the card stays live and opens on All.
Schema
| Prop | Type | Notes |
|---|---|---|
title | string | Up to 60 characters |
steps | { id, label, count, previous? }[] | 2–8, in funnel order. Data-bound |
period | { label, compareLabel? }? | e.g. "Oct 1–7" and "Sep 24–30" |
segments | { id, label, counts }[]? | Up to 4; one count per step. Data-bound |
value | { perConversion?, currency }? | Value of one conversion, for the opportunity line. Data-bound |
insight | string? | One sentence, up to 200 characters |
source | { label, url? }? | Data-bound |
asOf | string? | ISO date or date-time. Data-bound |
actions | ("drill-down" | "export")[]? | Omit for display only |
Host props: status (streaming, ready), onResult and className.