Simulation

An interactive what-if model that runs over time. The agent declares the model; the component runs it as the user moves the inputs.

Pattern

≈ 1,720 tokens per use (tool 1,273 · guidance 242 · typical payload 205)How token costs are estimated

Example: Simulation

How does my savings grow with $500 a month?

Savings over 10 years

Balance, month 120
$80,935
Savings over 10 years by month, every 3 months
MonthBalanceDeposited
0$2,000$2,000
3$3,531$3,500
6$5,082$5,000
9$6,652$6,500
12$8,242$8,000
15$9,851$9,500
18$11,481$11,000
21$13,132$12,500
24$14,803$14,000
27$16,495$15,500
30$18,208$17,000
33$19,943$18,500
36$21,700$20,000
39$23,478$21,500
42$25,279$23,000
45$27,103$24,500
48$28,949$26,000
51$30,819$27,500
54$32,712$29,000
57$34,629$30,500
60$36,570$32,000
63$38,535$33,500
66$40,525$35,000
69$42,540$36,500
72$44,580$38,000
75$46,646$39,500
78$48,738$41,000
81$50,856$42,500
84$53,000$44,000
87$55,172$45,500
90$57,371$47,000
93$59,597$48,500
96$61,851$50,000
99$64,134$51,500
102$66,445$53,000
105$68,786$54,500
108$71,155$56,000
111$73,555$57,500
114$75,984$59,000
117$78,444$60,500
120$80,935$62,000
Month 120
Deposited
$62,000
Interest earned
$18,935
%

When to use

  • The question is how something changes over time: savings growth, a loan payoff, a support backlog, stock levels.
  • The user wants to try what-ifs, such as a bigger deposit, more staff or an extra payment.

When not to use

  • The answer is a single number. Use a Calculator.
  • The figures already exist as data. Show them in a Chart instead of simulating them.
  • You're unsure how the system behaves from one step to the next. Explain the method in text.

How the model runs

The agent declares inputs (each one gets a slider), state (values carried from step to step) and one step formula per state that gives its next value:

{
  "inputs": [{ "id": "deposit", "label": "Monthly deposit", "min": 0, "max": 2000, "step": 50, "default": 500, "unit": "currency" }],
  "state": [{ "id": "balance", "label": "Balance", "initial": "initial" }],
  "step": [{ "id": "balance", "formula": "balance * (1 + rate / 100 / 12) + deposit" }],
  "steps": 120,
  "stepUnit": "month"
}
StepWhat happens
0Each state takes its initial value. Outputs are derived from the state
1, 2, …Each state's step formula runs on the previous step's state and outputs, the inputs and t (the step being computed). Outputs are derived again
Every stepThe first step where a milestone's when holds is marked. The run ends after the first step where stopWhen holds

Formulas use the same safe language as the Calculator: operators + - * / % ^ and comparisons, plus min, max, round, floor, ceil, abs, sqrt, pow, clamp and if. There's no eval, so a model can't run code through a formula. The schema rejects formulas that don't parse or that use names the model doesn't declare. Runs are capped at 1,000 steps.

If a step produces a value that isn't a finite number (for example, a division by zero), the run stops there and the component says "The model produced an invalid value at month 34" instead of drawing a wrong line.

Interaction

  • Answer first: the first milestone (or where the run ends, for stopWhen) is shown large above the chart, e.g. "Backlog cleared: Day 6". Without milestones or stopWhen, the first plotted value at the cursor is the headline. Further milestones are listed under it.
  • Inputs: below the chart, each with a slider, a number field and a reset button. Every change re-runs the model straight away.
  • Baseline: once the user changes an input, the run with the default inputs stays on the chart as a dashed line, and the answer shows the baseline's value on one quiet line, e.g. "Baseline: not reached". At the last step, the values under the chart show their change against it. Set baseline: false to turn it off.
  • Scrubber: Play moves a cursor across the run, and the step slider inspects any step. The values under the chart follow the cursor. With reduced motion, Play jumps to the last step.
  • Milestones: the first step each one holds is marked on the chart and stated above it.
  • description is read to screen readers only. Put assumptions the user must see in the reply.

How the agent should use it

  • Start from what the user said. Every input defaults to their numbers, and the default run is the baseline.
  • Give each state exactly one step formula, and use only declared IDs and t.
  • Use stopWhen for runs that end, like a paid-off loan, and milestones for moments worth marking.
  • Plot one to three series that share a unit.
  • Present results as estimates from the stated assumptions, not advice.
  • Set apply: false when the simulation is the whole answer. Otherwise the "Use these numbers" button (or your applyLabel) sends the chosen inputs back.

Installation

npx shadcn@latest add https://your-site.com/r/simulation.json

This adds the component, its schema and simulation-engine.ts, which holds the formula language and the model runner.

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { simulationTool } from "@/components/genui/simulation.schema"

const tools = {
  [simulationTool.name]: tool({
    description: simulationTool.description,
    inputSchema: simulationTool.inputSchema,
  }),
}
components/message.tsx
import { Simulation } from "@/components/genui/simulation"

function ToolPart({ part, addToolResult }) {
  if (part.type !== "tool-run_simulation") return null

  return (
    <Simulation
      {...part.input}
      status={part.state === "input-streaming" ? "streaming" : undefined}
      result={part.state === "output-available" ? part.output : undefined}
      onResult={(result) =>
        addToolResult({
          tool: "run_simulation",
          toolCallId: part.toolCallId,
          output: result,
        })
      }
    />
  )
}

With apply: false, the simulation is the whole answer, so give the tool an execute that returns immediately. Otherwise the agent receives the inputs and the final value of every state and output:

{
  action: "apply",
  inputs: { agents: 8, incoming: 180 },
  summary: { backlog: 0, queueDays: 0 },
}

With LangGraph

Push it from a graph node with ui.push({ name: "run_simulation", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.

Restoring from history

When a conversation is reloaded, pass the stored result back as result. The component restores the inputs that were sent, read-only, and never calls onResult again. The chart and scrubber stay usable. With the AI SDK, that's the tool call's output:

<Simulation
  {...part.input}
  result={part.state === "output-available" ? part.output : undefined}
/>

Schema

PropTypeNotes
titlestringThe question
descriptionstring?Screen readers only, e.g. where the defaults come from
currencystringISO 4217 code for currency values. Default USD
inputsInput[]1–6 sliders
stateState[]1–6 values carried between steps
step{ id, formula }[]One formula per state ID
stepsnumberThe horizon, 1–1,000
stepUnitstringday, week, month, year or a custom singular label
outputsOutput[]?Up to 6 values derived each step
stopWhenstring?Ends the run once true
plotstring[]1–3 state or output IDs to chart
milestones{ label, when }[]?Up to 4. The first step each holds is marked
baselinebooleanDashed default-input run. Default true
applybooleanShow the apply button. Default true
applyLabelstring?Default "Use these numbers"

Input

FieldTypeNotes
idstringName used in formulas
labelstring
min, max, stepnumberSlider range
defaultnumberAlso the baseline value
unitstring?currency, percent, number or a suffix such as tickets
decimalsnumber?0–4. Defaults to the step's precision

State and Output

FieldTypeNotes
idstringName used in formulas. t is reserved
labelstring
initial (state) / formula (output)stringSee "How the model runs"
unit, decimalsDisplay

Host props: status (streaming, ready, applied), onResult, result and className.

Source

On this page