Deprecations at a glance
Breaking changes at a glance
When you bump
@waniwani/sdk across a minor version (0.x minors can break), scan this page for every breaking change between your old and new version and apply the documented migration. Each one below is a mechanical rewrite an agent or codemod can apply in a single pass, followed by bun run typecheck && bun test.0.25.0: flows list their fields on the first question
In 0.24 a call wrote only the fields the run had asked. The model saw no field name until its question came up, so it could not pass on what the user said in the opening message, and a field no question asks (a locale, or a classification the opener implies) could never be set from the conversation. In 0.25 the session’s firstinterrupt or widget response lists every state field under fields, with its type, enum values and description. It goes out once per conversation, like intro. tools/list still names no field. Any call, start included, writes the answers to asked fields as sent, plus any declared field whose value passes its Zod schema. The flow skips the questions those values fill. A question’s validator runs once every question in its node is filled. A value that fails its schema is dropped, and so is a key the state does not declare. The full rule is in What a call can write, and Answers in the user’s opening message shows it from the flow’s side.
Some fields leaned on the 0.24 gate and need a change now that it is gone. A consent the model sends on start counts as given. So does a widget pick, and the widget never shows. A node that computes a value only when the field is empty keeps whatever the model sent.
Projects with no createFlow call have nothing to migrate. Bump the dependency and run bun run typecheck && bun test.
What breaks
No flow type changes, sotsc catches none of this:
An answer to an asked field still merges as sent, with no schema check. A
validate on the question is the only check it gets.
Other behavior changes:
- First response. The session’s first
interruptorwidgetresponse carriesfields, and itsnextStepgains a line telling the model to send any listed value the user states. That line comes first, after theintroline when there is one. See Response fields added by the engine. Snapshot tests of that response change. nextStepon interrupts.continuenow carries the answers plus any other listed field the user has stated and the model has not sent yet. Snapshot tests of it change.tools/list. ThestateUpdatesdescription readsValues for the flow's fields, keyed by the field names its responses give.Snapshot tests of the listing change.createFlowTestHarness.start(intent, known)sendsknownasstateUpdatesonstart, then answers each question whose fieldknownstill holds. A value inknownfor a field no question asks is written when it passes its schema.- Hand-set
_meta._flowGraph. The 0.24.0 notes said 0.25.0 would stop reading it.withWaniwaniin 0.25.0 still reads it and moves it off the wire. It stays deprecated, so read the graph fromflow.flowGraphand delete the key (rewrite 5 in the 0.24.0 section). - Sessions in flight. A session created on 0.24 never receives
fields, even on a newstart. Its next call follows the new write rule. No action.
neverPrefilled(schema)from@waniwani/sdk/mcpleaves a field out offieldsand drops any value for it until the run has asked it. See Fields only a question can fill.ctx.sessionIdin every node is the session the flow’s record is stored under. Key your own store by it. See Values that stay on the server.
interrupt and showWidget signatures, validators, redacted(), intro and widget data.
Migration
- Bump with
bun add @waniwani/sdk@^0.25.0. - Collect the call sites:
- For each key in
createFlow({ state }), decide whether the model may send it before the flow asks. A consent, an attestation or a widget pick that must happen on screen is a target of rewrite 1. A value a node computes and keeps when it is already set (state.vehicleId ? {} : lookup(),state.agreedAt ?? Date.now()) is a target of rewrite 2. - Apply rewrites 1 to 3.
- Run
bun run typecheck && bun test, with a test per flow that sends the rewritten fields onstartand checks the flow still asks or computes them.
Rewrite 1: neverPrefilled() for answers that only count from their own question
Wrap the field’s schema in neverPrefilled(), at the end of the chain. It combines with redacted(). On a z.object group, wrap the group to close every member, or wrap one member to close only that member. A question or widget that asks a group by its key is skipped once the model sends any one member, so wrap that group too, or ask each member as its own field.
shareConsent is asked by an interrupt. confirmed is a widget’s field, so the summary card always renders before the flow moves on.
Rewrite 2: values the model must never set
Everything instate is listed in fields and writable from the conversation. Move a computed value the model must not choose, such as the vehicle id a plate resolves to, out of state and into a store keyed by ctx.sessionId:
state when its node writes it on every pass. Drop the guard (state.agreedAt ?? Date.now() becomes Date.now()) and the model’s value is overwritten before any node reads it. See Server-side values for your nodes for the store.
Rewrite 3: tests
harness.start(intent, known). known now goes out on start. A test that expected a value for a field no question asks to be ignored sees it written. Update the expectation or take the value out of known. The harness still answers a neverPrefilled() field from known when its question comes up. To check that a marked field stays closed, start without it in known, send it with continueWith alongside the answer to an earlier question, and expect the flow to ask it anyway.
reset before the field was asked. It restarts the flow when the value passes its schema. A test that expected status: "error" fails. A test that matches the error text of a reset with nothing to write needs the new message.
Snapshots of the first response, of nextStep or of the listing. Regenerate them with bun test --update-snapshots and check the diff only touches fields, nextStep and the stateUpdates description.
The questions you added for 0.24 (an interrupt that asks the model for a locale, a question split in two, a widget group field) keep working. A silent question such as the locale one can go once every node that reads its field has a fallback for an empty value (state.locale ?? "en"), since the model now sends the field from fields. Keep it when a node needs the value before the model has seen fields.
Other changes in 0.25.0
- The floating bar hides at the page footer by default. Every floating embed now slides the dock away once the page’s last
<footer>or[role="contentinfo"]scrolls into view, and brings it back when the visitor scrolls up. A page with no footer hides it within 120px of the end. SethideAtBottom: false(ordata-hide-at-bottom="false") to keep it pinned. A number still means a distance in px from the end of the page. - Tracking events can leave out
source. When nothing identifies the caller, an event no longer names@waniwani/sdkas its source, andV2EventEnvelope.sourceis optional. Code that reads it as astringneeds a fallback.tool.calledfrom an MCP client the SDK does not recognise takes the name the client advertises, such asCursor.
0.24.1: the chat tells the agent which page the visitor is on
No code changes. The embed script picks it up by itself. WithWaniwaniChat, bump the dependency.
The chat now sends the address of the page the visitor is on with every message, and the agent is told which page that is. See The page the visitor is on, including how to tell the agent what your pages are.
0.24.0: flows accept only the fields they ask for
In 0.23 a flow tool listed its whole Zod state schema understateUpdates in tools/list, and any call could write any state field. A model told to “always include locale on start” set it. So could text injected into the conversation, including fields the flow computes for itself, such as a vehicle id that decides what gets priced.
In 0.24 the listing names no field. Each interrupt or widget response names the fields it asks for, with their fieldSchema. The engine records the fields a run has asked (asked on the session record) and merges stateUpdates only for those. A question on a z.object group accepts every key inside it.
Projects with no createFlow call have nothing to migrate. Bump the dependency and run bun run typecheck && bun test.
A value sent for a field the run has not asked yet is dropped. When the flow later asks that question, the model answers it from earlier messages with one silent continue, and the user is not asked again.
This section covers 0.24.0 to 0.24.2.
What breaks
Visible totsc:
Invisible to
tsc:
A key that is not a state field is dropped.
Other behavior changes:
tools/list. A flow’sstateUpdatesis an untyped record, the same for every flow. The definition_metastill carries the flow graph and the redacted-fields list forwithWaniwani, wrapped in an object whosetoJSONreturnsundefined, so a serializing transport never sends them. Snapshot tests of the listing change.- Hand-set
_meta._flowGraph.withWaniwanistill reads it and moves it off the wire. It is deprecated, and 0.25.0 still reads it. createFlowTestHarness.start(intent, known)no longer sendsknownasstateUpdates. It starts empty, then answers each question whose fieldknownholds with acontinue, until the flow asks somethingknownhas no value for. A value inknownfor a field no question asks is ignored.nextSteptext on interrupts. It now tells the model to answer questions the user already settled. Snapshot tests of it change.- Sessions in flight. A record written by 0.23 has no
asked, so its next call merges everything once. No action.
interrupt and showWidget signatures, validators, redacted(), intro, the state schema itself, and widget data.
Migration
- Bump with
bun add @waniwani/sdk@^0.24.0. - Collect the call sites:
- For each key in
createFlow({ state }), note what writes it: an interrupt question, a widget (showWidget({ field })or az.objectgroup it names), a node return value, or the conversation (the description or.describe()tells the model to send it, a question’scontexttells the model to put a value in it, a widget sends it back, or a test sends it instateUpdates). A field the conversation writes that has no interrupt question and no widget field is a target. So is a field only a node return writes but a test seeds throughstateUpdates. - Apply rewrites 1 to 5 to every target.
- Run
bun run typecheck && bun test. A flow that compiles can still lose answers, so the tests need to drive each rewritten flow to the node that reads the moved field.
Rewrite 1: a field the conversation writes and no question asks
If a node already falls back to another source when the field is empty (state.locale ?? localeFrom(meta)), or the value is a function of other state fields, compute it in a node (1a). Add an action node right before the first node that reads it, make the field .optional(), and describe it as set by the flow. Anything else, such as the language the user writes in or a flag inferred from their intent, you ask (1b): add the field as a question in an interrupt placed before the first node that reads it, and say in the question’s context when the model can answer from the conversation.
In both cases, delete the sentence in description or .describe() that tells the model to send the field. A 1b question costs one silent model call when the user already gave the answer.
locale and transferAllowed are asked (1b). ageBand is computed (1a), and a call that sends it is ignored. Never ask a computed field such as a lookup id or a price, or the conversation can set it again.
Rewrite 2: one question that fills several fields
A question whosecontext tells the model to split the answer into a second field writes a field nothing asks. Add every field it fills as its own question in the same interrupt call. The model still asks them in one message.
Rewrite 3: a widget that sends back values
A widget’scontinue merges only the widget’s field. Give every widget that sends values a field. When it sends more than one value, group them in one z.object state field, name the group as the widget’s field, and copy the members out in an action node after the widget.
field is skipped when that field is already filled, so a widget that can show twice in one run needs the node after it to clear the group (upload: undefined).
Rewrite 4: tests
harness.start(intent, known) with a field no question asks. The value is ignored now. Put the inputs the node computes from in known and let the node run. Stub a lookup when the value comes from one.
stateUpdates on start and expects nodes to be skipped. Replace it with harness.start(intent, known), or send one continue per question.
tools/list assertions. Read the graph from flow.flowGraph. Over InMemoryTransport the client receives the server-only wrappers as objects, so round-trip through JSON before asserting what a client would see.
reset before the field was asked. On 0.24.0 to 0.24.2 it returns status: "error". Answer the question first, then call resetWith.
Snapshots of nextStep or of the listing. Regenerate them with bun test --update-snapshots and check the diff only touches those strings.
Rewrite 5: _meta._flowGraph
Read the graph from the compiled flow and stop setting the key by hand. flow.config._meta already carries the graph for withWaniwani.
_meta, spread flow.config._meta into it. A hand-set _flowGraph still works on 0.25.0. It is deprecated, so delete it.
Openers that extract everything
A first node that asks one open question and tells the model to fill other fields from the answer keeps working when later nodes ask those fields. Each later node costs one silentcontinue. Fields in that opener that no later node asks are targets of rewrite 1.
Ship it
The flow tool’s listing shrinks:stateUpdates names no field, description or enum value, and _meta sends no graph. A published app keeps working without resubmission, because the server keeps only the asked fields from whatever the old listing makes the model send. The ChatGPT app directory shows the tool metadata from your last submission until you submit a new version. Resubmit to show reviewers the smaller listing, which no longer lists personal fields such as ID numbers or birth dates.
Leave no “send X on start” sentence in the flow description. It is the consent card a ChatGPT user approves, and steering text there gets flagged.
0.23.0: one brief intent string replaces the telemetry object
withWaniwani adds one argument to every tool it wraps so the calling model can say why the user came. In 0.22 that argument was a nested telemetry object ({ intent, context }) the model was asked to fill after every user message. In 0.23 it is one optional string, sent only on the model’s first call to the app:
What breaks
Visible totsc:
There is no deprecated alias. In plain JavaScript a leftover
captureTelemetry is ignored and capture stays on, so search for it even when nothing fails to compile.
Invisible to tsc:
tools/list. Each wrapped tool lists anintentstring instead of atelemetryobject. Snapshot tests of tool schemas change.- Tool call arguments. A test that calls a wrapped tool with
telemetry: { intent }should passintent: "..."instead. Schema validation strips a leftovertelemetrykey, except on a tool whose schema is.strict(), which rejects the call. - Tools with their own
intentargument are left alone and record nothing. In 0.22 they got atelemetryargument beside it. - Tools with their own
telemetryargument gain theintentstring. Their owntelemetrystill reaches the handler unchanged.
tool.called event still records the captured value under properties.telemetry ({ intent } for plain tools, { intent, context } copied from a flow’s own arguments). Flow tools keep their intent and context arguments and their schema. { tools, omitPII } mean the same thing under captureIntent, and handlers never see the injected argument.
Migration
- Bump with
bun add @waniwani/sdk@^0.23.0. - Collect the call sites:
- Apply the two rewrites below.
- Run
bun run typecheck && bun test.
Rewrite 1: captureTelemetry becomes captureIntent
The option and its type are renamed. The value keeps its shape: true, false, or { tools?, omitPII? }.
@waniwani/kit set this under tracking in waniwani.config.ts. Rename it there once the app is on a kit release whose TrackingOptions names captureIntent.
Rewrite 2: tests that see the injected argument
Only tests that inspect a wrapped tool’s advertised schema or pass the injected argument themselves need this.properties.telemetry.intent. A properties.telemetry.context from a plain tool no longer appears. Only flows send context, so do not add a context argument back to a plain tool: what prompted the request belongs in the one intent sentence.
Ship it
The argument is optional, so a published app keeps working without resubmission. The ChatGPT app directory shows the tool metadata from your last submission until you submit a new version, so resubmit to move reviewers onto the 0.23 shape. The argument collects free text about the user’s goal. Mention it in the app’s privacy policy, or passcaptureIntent: false.
0.21.1: the flow protocol moves out of the tool description
No code changes. Bump the dependency and redeploy your MCP server.There is no 0.21.0 on npm. That version number was published and withdrawn in August 2026, and npm never allows a withdrawn version to be republished, so this release is 0.21.1. Nothing is missing between 0.20.5 and 0.21.1.
.compile() appended a ~3 000-character FLOW EXECUTION PROTOCOL block to the registered tool’s description: numbered steps, Do NOT invent state values, You MUST STILL call the display tool FIRST, STOP and WAIT for the user. It worked, and it is also the exact shape a prompt-injection classifier is built to catch. ChatGPT flags it on the connector approval card:
⚠️ Suspicious Instruction — The tool description attempts to prescribe conversation behavior, tool sequencing, required follow-up actions, and policy outcomes for the classifier.A user asked to approve a tool under that banner has no way to tell an honest protocol from an injected one, because both arrive in the same field. From 0.21.1 the description is your
createFlow({ description }) plus one sentence saying the tool runs a multi-step flow. The protocol rides the response instead, in a new nextStep key, scoped to the status that actually came back:
What this changes for you
Two things worth knowing:
- Your
createFlow({ description })is now the whole consent card. Read it once and make sure it says what the flow does in a sentence or two. See the flow tool contract. - The
nextStepkey is additive on the wire. A client that ignores it behaves as before, so there is no coordinated deploy between your server and anything reading its responses.
flow.config.description in your own tests, those assertions are the one thing that needs updating.
Ship it
For an already-published ChatGPT plugin, this is a tool-definition change: OpenAI’s continuous review rescans and swaps the new definition in once it passes automated checks. No resubmission, no new plugin version. The consent card is rendered from the live description, so the warning is gone for new connections as soon as your server is redeployed.0.20.0: the legacy tier is deleted
0.12.0 moved the MCP-widget-in-host stack and the chat-server BFF adapters to@waniwani/sdk/legacy* and kept the old paths re-exporting every symbol. 0.20.0 deletes all of it. There is no shim: these are module-resolution errors at build time, not deprecation warnings.
Entry points removed
@waniwani/sdk, @waniwani/sdk/mcp, @waniwani/sdk/mcp/react, @waniwani/sdk/mcp/react/skybridge, @waniwani/sdk/chat, @waniwani/sdk/kb and the two chat assets are untouched.
ChatCard and ChatCardProps are gone from @waniwani/sdk/chat.
RegisteredTool moved rather than disappeared
The OSS flow API’s showWidget accepts RegisteredTool | string, so the type outlived the tier that defined it:
{ id, title, description, register }), so any object you already pass to showWidget still satisfies it.
@modelcontextprotocol/ext-apps is no longer a peer dependency
Only the legacy widget clients imported it. If your app talks to a widget host directly, depend on it yourself; nothing in the SDK asks for it any more.
Rewrite 1 — createTool becomes registerTool
createTool returned an object with a register(server) method. Register on the server instead, and return MCP’s content shape from the handler rather than { text, data }.
config.idbecomesregisterTool’s first argument; the rest ofconfigbecomes the second, minusid,resource,invoking,invokedandautoInjectResultText.{ text }becomes{ content: [{ type: "text" as const, text }] }.{ text, data }becomes the same plusstructuredContent: data.- The handler’s second parameter changes from
{ extra, waniwani }to MCP’sextra. Readextra._metadirectly and get the scoped client withextractScopedClient(extra), still exported from@waniwani/sdk/mcp. - Add
annotations.title.createToolaccepted a top-leveltitle, and Claude’s Connectors Directory requires one insideannotations.
createFlow instead of registerTool. That is the surface the whole tier was deprecated in favour of.
Rewrite 2 — widget tools carry their own _meta
createTool derived widget metadata from config.resource. Write it out:
invoking and invoked default to "Loading..." and "Loaded". Add ui.autoHeight: true when the resource set autoHeight. createResource built mcpUri as ui://widgets/ext-apps/{id}.html; keep whatever URI you already serve.
Rewrite 3 — drop the chat BFF
toNextJsHandler, toExpressJsHandler and createApiHandler existed so the chat widget could proxy through your backend. WaniwaniChat talks to app.waniwani.ai directly, so the route goes away entirely.
beforeRequest to inject per-visitor context or a self-hosted model. In that case stay on ChatEmbed and point api at a route you write yourself; the SDK no longer ships the router.
Rewrite 4 — widget React hooks
WidgetProvider and the host bridge hooks (useToolOutput, useCallTool, useDisplayMode, useSendFollowUp, and the rest) have no drop-in replacement in the SDK. Widgets belong to the app framework now:
- Tracking from inside a widget:
useWaniwanifrom@waniwani/sdk/mcp/react, or@waniwani/sdk/mcp/react/skybridgewhen skybridge hosts the widget. Both survive 0.20. - Everything else (host handshake, display mode, tool calls, follow-ups): use the kit’s widget runtime.
0.18.0: quote and purchase events removed from the taxonomy
The generic funnel eventsquote.requested, quote.succeeded, quote.failed, and purchase.completed are removed from EVENT_TYPES and the TrackEvent union. They predate the typed revenue taxonomy (price_shown, prices_compared, option_selected, lead_qualified, converted), which models the same funnel stages with typed properties and flat track.* helpers. link.clicked is not affected and remains a first-class event.
Removed with them:
- The type exports
QuoteSucceededPropertiesandPurchaseCompletedProperties - The legacy input fields
quoteAmount,quoteCurrency,purchaseAmount,purchaseCurrencyonLegacyTrackEvent
Breaking: quote.* and purchase.completed no longer typecheck
Before
After
Migration
track({ event: "quote.succeeded", properties: { amount, currency } })(or legacyeventType: "quote.succeeded"withquoteAmount/quoteCurrency) becomestrack.priceShown({ amount, currency }). Keep any identity fields (sessionId,externalUserId,meta) on the call.track({ event: "purchase.completed", properties: { amount, currency } })(or legacypurchaseAmount/purchaseCurrency) becomestrack.converted({ amount, currency }).track({ event: "quote.requested" })andtrack({ event: "quote.failed" }): delete the call. The funnel start is already covered by the auto-capturedtool.calledandsession.started.- Replace type imports:
QuoteSucceededPropertieswithPriceShownProperties,PurchaseCompletedPropertieswithConvertedProperties.
@deprecated shim: the names are removed outright, so tsc surfaces every call site as an error. After applying, run bun run typecheck && bun test.
0.17.0: useWaniwani() is host-agnostic
useWaniwani() from @waniwani/sdk/mcp/react takes the tool-response _meta as data and never opens a host connection of its own. It resolves config (endpoint, source, widget token, session id) from explicit options or a toolResponseMetadata object you pass.
New in this release:
@waniwani/sdk/mcp/react/skybridge— a skybridge-host adapter foruseWaniwani. It reads skybridge’suseToolInfo().responseMetadataand feeds it to the core hook, so skybridge widgets calluseWaniwani()bare.skybridgeis an optional peer dependency.toolResponseMetadataoption onuseWaniwani(from@waniwani/sdk/mcp/react) — pass the host’s tool-response_metaon any host you already have it from.
Breaking: useWaniwani() no longer auto-discovers its config
Config resolution is now: explicit { endpoint, source }, or a toolResponseMetadata object you pass, and nothing else. The hook does not read a WidgetProvider context and does not connect to the widget host to find the config. A bare useWaniwani() that relied on that auto-discovery returns a no-op widget (no sessionId; track.* does nothing).
This break does not surface in
tsc. A bare useWaniwani() still typechecks and fails only at runtime, by tracking nothing. Find call sites by auditing imports of useWaniwani, not by chasing type errors._meta is delivered once, to whichever host bridge is connected and listening at that moment. In a skybridge widget that bridge is skybridge. A hook opening a second connection raced it and missed the one-shot on Claude. The core hook now takes the metadata as data; the skybridge adapter supplies it.
Before
After — skybridge-hosted widget (the common case)
Change the import; the call stays bare.After — any other host
Pass the metadata you already hold, or an explicit endpoint.Migration
- Grep for imports of
useWaniwanifrom@waniwani/sdk/mcp/react. - If the project uses skybridge (
skybridgedependency / imports ofskybridge/web), change the import to@waniwani/sdk/mcp/react/skybridgeand leave the call bare. Any options passed (source,token,sessionId,metadata) are forwarded, so keep them. - Otherwise, pass
toolResponseMetadata(the host’s tool-response_meta) to the call, or leave an existing explicit{ endpoint, source }call as-is.
@deprecated shim: the self-connect path was unreliable (a no-op on Claude) and the WidgetProvider-context read belonged to the retired legacy widget host, so both are removed outright. After applying, run bun run typecheck && bun test; because tsc cannot see this break, the completion check is that every bare useWaniwani() call site has moved to one of the forms above.
0.16.0: one tracking client on every surface
Tracking is a single client that exists on four surfaces: the server (waniwani()), tool handlers and flow nodes (the scoped client), MCP-app widgets (useWaniwani()), and chat host pages (chat.track). The browser surfaces send the same typed events through the same batching transport as the server, with session identity stamped automatically. See Events and Widgets & chat.
New in this release:
useWaniwani().trackis the typedTrackFn:track({ event, properties })plus the flat revenue helpers (track.priceShown(),track.converted(), …). Onewidget_renderevent is emitted automatically on init. The hook works without the legacyWidgetProvider.WaniWani.chat.track/WaniWani.chat.identifyon the<script>embed, andtrack/identifyon theWaniwaniChatref handle. Session attaches live once the chat server assigns it; the anonymousvisitorIdcovers events before that.createFrontendClientfrom@waniwani/sdk: the browser tracking primitive both surfaces wrap.context.waniwani.sessionIdon the scoped client: read the resolved session id and store it with your own records to attribute off-platform events later.visitorIdas a first-class correlation field onTrackInputand the V2 envelope.EVENT_TYPESruntime constant, andextractScopedClientexported from@waniwani/sdk/mcp.
Breaking: useWaniwani() surface
The widget hook returns { sessionId, track, identify, flush }. The string-based track(name, properties), step(name, meta), conversion(name, data), and the capture option are gone, along with DOM auto-capture and its event names.
Before
After
Migration (codemod)
- Delete any
captureoption passed touseWaniwani(). - Replace
wani.step(...)andwani.conversion(...)with the typed revenue helper that matches the funnel stage (optionSelected,converted, …). - Replace string calls
wani.track("<name>", props):- If
<name>is a typed event, use the object form:wani.track({ event: "<name>", properties: props }). - Otherwise, model the moment with the closest typed event; there are no custom event names in the typed surface.
- If
- Remove any type imports of
AutoCaptureTogglesorWidgetTrackFn;trackis the sameTrackFntype the server client uses. - Run
bun run typecheck && bun test.
Breaking: widget auto-capture event names removed from EventType
widget_click, widget_link_click, widget_error, widget_scroll, widget_form_field, and widget_form_submit are no longer part of the EventType union. widget_render remains and is emitted automatically. Dashboards keep historical events; nothing new arrives under the removed names. Code that referenced them in track() calls or type positions must switch to typed taxonomy events.
Breaking: widget config meta key is waniwani/widget
withWaniwani injects the widget tracking config into tool responses under _meta["waniwani/widget"], following the waniwani/* namespacing every other SDK meta key uses. useWaniwani() reads the new key. Only code that read _meta.waniwani directly needs updating:
Breaking: track.lead() removed
Deprecated since 0.15.1, removed on schedule. Replace .track.lead( with .track.leadQualified( (see the 0.15.0 entry below for the full codemod). The mapper no longer rewrites the event name "lead"; send "lead_qualified".
Breaking: createTrackingRoute accepts the V2 batch envelope
The proxy route parses the same V2 batch shape the frontend client sends ({ events: [{ id, name, correlation, properties, ... }] }). Point createFrontendClient or useWaniwani({ endpoint }) at your route and no further change is needed; hand-rolled clients posting the old snake_case widget payload (event_type, session_id) must switch to the V2 envelope documented in Events.
0.15.0: lead becomes lead_qualified
The lead event is named lead_qualified, its helper is track.leadQualified(), and its properties are LeadQualifiedProperties: externalId, email, and name alongside source. The typed input is RevenueLeadQualifiedInput (RevenueLeadInput and LeadProperties no longer exist).
lead_qualified means the person met your qualification bar (finished the qualifying questions, requested a demo, matched your target profile). It belongs at the node where qualification completes, not at flow entry; see Instrumentation for placement rules.
Why
lead under-specified what the event represented, so integrations fired it anywhere from flow entry to CRM push and funnels were not comparable across projects. The name now states the semantic, and the new properties (externalId as your CRM record id, plus email and name) give the dashboard real dedup and join keys instead of an anonymous count.
Before
After
track.leadQualified({ source }) is valid.
Migration (codemod)
- Replace every
.track.lead(call with.track.leadQualified(. - Replace
event: "lead"withevent: "lead_qualified"in generictrack()calls (andeventType: "lead"in the legacy shape). - Replace type imports:
RevenueLeadInput→RevenueLeadQualifiedInput,LeadProperties→LeadQualifiedProperties. - Where the call site has them available, enrich the event: pass your lead record id as
externalId, plusemailandname. - Run
bun run typecheck && bun test.
Compatibility
track.lead()exists as a@deprecatedalias since 0.15.1: it emits alead_qualifiedevent and will be removed in 0.16.0. On 0.15.0 exactly, the alias is absent and step 1 is required to compile.- The transport normalizes the event name
"lead"to"lead_qualified"at runtime (since 0.15.1), so already-compiled pre-0.15 callers keep producing valid events. TheEventTypeunion does not accept"lead", so TypeScript call sites must migrate.
Verifying
0.14.0: addConditionalEdge declares its branch targets
addConditionalEdge now takes the reachable nodes explicitly as the second argument: addConditionalEdge(from, to, condition). The condition’s return type is constrained to to, so a branch can never route to an undeclared node, and the flow graph used for funnel analytics and Mermaid diagrams is correct by construction.
Why
Before 0.14.0, graph introspection discovered a conditional edge’s targets by stringifying the condition function and scanning its source for node names. That heuristic was fragile: it broke under minification, missed dynamically-computed targets, and produced false positives when a node id appeared as a comparison value or in a comment — so funnel analytics could show the wrong branches. Declaringto removes the guesswork entirely.
Before
After
Migration (codemod)
For each.addConditionalEdge(from, condition) call, insert a second argument: an array of every node the condition can return (including END if it returns END). The simplest reliable source for that array is the set of literal node names returned by the condition body.
- Find every two-argument
.addConditionalEdge(call. - Read the condition body and collect every node name (string literal or
END) it can return. - Insert that list, deduplicated, as the new second argument — leaving the condition as the third.
- Run
bun run typecheck. The compiler now flags any return value missing fromto, so any over- or under-declared list surfaces immediately. Fix by aligningtowith what the condition actually returns.
Verifying
Type '"x"' is not assignable to type '"a" | "b"', the condition returns a node missing from to — add it. If it reports an unused-looking target, the condition never returns it — safe to leave (over-declaration is allowed) or trim.
0.12.0: Legacy reorganization
The MCP-widget-in-host stack (createTool / createResource / WidgetProvider and host bridge hooks) and the chat-server BFF adapters (toNextJsHandler, toExpressJsHandler, createApiHandler) were moved to dedicated @waniwani/sdk/legacy* entry points. New code should use createFlow from @waniwani/sdk/mcp and the direct-to-backend ChatEmbed from @waniwani/sdk/chat.
The old entry points still re-export every symbol unchanged — your imports keep working without edits. The legacy entry points exist so you can adopt the final import paths now and avoid the next major bump.
Why
- OSS-first framing.
@waniwani/sdk/mcpis now the OSS surface:createFlow,StateGraph,KvStore,MemoryKvStore. No API key required, no hosted dependency on the core path. - Smaller default bundle. Splitting the legacy surface off lets host-only React hooks and the chat-server router stay out of the OSS bundle once the re-exports are dropped.
- Sunset signal. The chat widget will talk directly to
app.waniwani.aiin a future release, removing the need for a self-hosted BFF. The Next.js / Express adapters become unnecessary.
Migration matrix
Every row is a mechanical rewrite — no behavior changes, only the import path. An LLM or codemod can apply all of these in one pass.useWaniwani, UseWaniwaniOptions, and WaniwaniWidget stay on @waniwani/sdk/mcp/react — that hook is non-legacy.
What stays the same
These remain on@waniwani/sdk/mcp and are the recommended surface for new code:
createFlow,StateGraph,START,END,redacted,createFlowTestHarness,AddNodeConfig, all flow-related typesKvStore,MemoryKvStore,WaniwaniKvStorewithWaniwani,WithWaniwaniOptionscreateTrackingRoute,TrackingRouteOptionsScopedWaniWaniClientMcpServer,ZodRawShapeCompat(shared MCP types)
@waniwani/sdk:
waniwani(),defineConfig,WaniWaniError, all tracking helpers
Codemod recipe (one-shot)
An agent updating a downstream repo can apply these substitutions in order. Each is a string-level rewrite of animport … from "<path>" statement — semantics are preserved end-to-end.
Verifying a one-shot migration
After substituting, the codebase should still typecheck and run unchanged. Run:Module '"@waniwani/sdk/mcp"' has no exported member 'X', it means that symbol is being moved entirely out of the old path in a later major (0.14.0). Until then the old import keeps working — the matrix above tells you the final destination.
Verifying a customer codebase has zero legacy imports
To audit a downstream repo for any remaining legacy imports:ChatCard → ChatEmbed
ChatCard is still exported but marked @deprecated. It wraps ChatEmbed with always-visible card chrome (header, border, fixed dimensions) and assumes the Waniwani-hosted backend.
Because
ChatCard assumed the hosted backend, most callers want WaniwaniChat, not ChatEmbed — it keeps the hosted wiring and adds dashboard configuration, visitor correlation, and tracking. Reach for ChatEmbed, as the rewrite below shows, only if you run the chat backend yourself.ChatEmbed does not fetch /config or /tool against the Waniwani host — you point api at your own endpoint and (optionally) supply mcp.resourceEndpoint if your backend serves widget resources.
evals/* removed
The src/evals/* subtree (chat eval runner, scorers, reporter) was removed entirely. No re-export shim. If you imported runChatEval, createScorer, or any sibling from @waniwani/sdk/evals, the build will fail on upgrade — there is no equivalent in this release. Eval tooling has moved to the Waniwani platform and is no longer shipped in-SDK.
0.12.0: addNode object form
The positional .addNode(id, run, options?) signature is deprecated in favor of an options-bag form. Metadata sits at the top of the call where the eye lands, the handler is a named field, and future options can be added without widening a positional signature.
Before
After
What changed
idreplaces the first positional argument.runreplaces the second positional argument (the handler).labelandhideFromFunnelmove from the thirdoptionsargument onto the same config object.labelis now optional. When omitted, it defaults toid. The positional form requiredlabelwhenever you passed anoptionsobject.
Compatibility
Both signatures are supported. The positional form is marked@deprecated and will be removed in 0.13.0. You can mix the two within the same flow during the transition. There is no functional difference between the two shapes.
Type export
A newAddNodeConfig<TState, TName> type is exported from @waniwani/sdk/mcp if you need to type a config object outside the call site.