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