Skip to main content
This page tracks API changes that require code updates, organized by the version that introduced them. Deprecations are safe to ignore short-term — the old shape keeps working until the removal version listed in the deprecation notice.

Deprecations at a glance

TypeScript will strike through deprecated signatures in your IDE. Hover for the replacement.

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.
Migrate automatically. Every version hop with breaking changes ships a self-contained migration skill named migrate-waniwani-sdk-<from>-to-<to>. Add the one for your hop and an agent applies the whole section for you:

0.20.0: the legacy tier is deleted

Apply this entire section automatically: npx skills add Waniwani-AI/sdk -s migrate-waniwani-sdk-0.19-to-0.20, then invoke it from your project on 0.19.x.
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:
The shape is unchanged and structural ({ 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 }.
Mechanical parts of the rewrite:
  • config.id becomes registerTool’s first argument; the rest of config becomes the second, minus id, resource, invoking, invoked and autoInjectResultText.
  • { text } becomes { content: [{ type: "text" as const, text }] }.
  • { text, data } becomes the same plus structuredContent: data.
  • The handler’s second parameter changes from { extra, waniwani } to MCP’s extra. Read extra._meta directly and get the scoped client with extractScopedClient(extra), still exported from @waniwani/sdk/mcp.
  • Add annotations.title. createTool accepted a top-level title, and Claude’s Connectors Directory requires one inside annotations.
For a tool that pauses, branches or holds state across turns, port it to 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.
Keep a backend of your own only if you were using 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: useWaniwani from @waniwani/sdk/mcp/react, or @waniwani/sdk/mcp/react/skybridge when 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

Apply this entire section automatically: npx skills add Waniwani-AI/sdk -s migrate-waniwani-sdk-0.17-to-0.18, then invoke it from your project on 0.17.x.
The generic funnel events quote.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 QuoteSucceededProperties and PurchaseCompletedProperties
  • The legacy input fields quoteAmount, quoteCurrency, purchaseAmount, purchaseCurrency on LegacyTrackEvent

Breaking: quote.* and purchase.completed no longer typecheck

Before

After

Migration

  1. track({ event: "quote.succeeded", properties: { amount, currency } }) (or legacy eventType: "quote.succeeded" with quoteAmount / quoteCurrency) becomes track.priceShown({ amount, currency }). Keep any identity fields (sessionId, externalUserId, meta) on the call.
  2. track({ event: "purchase.completed", properties: { amount, currency } }) (or legacy purchaseAmount / purchaseCurrency) becomes track.converted({ amount, currency }).
  3. track({ event: "quote.requested" }) and track({ event: "quote.failed" }): delete the call. The funnel start is already covered by the auto-captured tool.called and session.started.
  4. Replace type imports: QuoteSucceededProperties with PriceShownProperties, PurchaseCompletedProperties with ConvertedProperties.
No @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

Apply this entire section automatically: npx skills add Waniwani-AI/sdk -s migrate-waniwani-sdk-0.16-to-0.17, then invoke it from your project on 0.16.x.
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 for useWaniwani. It reads skybridge’s useToolInfo().responseMetadata and feeds it to the core hook, so skybridge widgets call useWaniwani() bare. skybridge is an optional peer dependency.
  • toolResponseMetadata option on useWaniwani (from @waniwani/sdk/mcp/react) — pass the host’s tool-response _meta on 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.
Why: on an MCP-Apps host the tool-response _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

  1. Grep for imports of useWaniwani from @waniwani/sdk/mcp/react.
  2. If the project uses skybridge (skybridge dependency / imports of skybridge/web), change the import to @waniwani/sdk/mcp/react/skybridge and leave the call bare. Any options passed (source, token, sessionId, metadata) are forwarded, so keep them.
  3. Otherwise, pass toolResponseMetadata (the host’s tool-response _meta) to the call, or leave an existing explicit { endpoint, source } call as-is.
No @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.
Apply this entire section automatically: npx skills add Waniwani-AI/sdk -s migrate-waniwani-sdk-0.15-to-0.16, then invoke it from your project on 0.15.x.
New in this release:
  • useWaniwani().track is the typed TrackFn: track({ event, properties }) plus the flat revenue helpers (track.priceShown(), track.converted(), …). One widget_render event is emitted automatically on init. The hook works without the legacy WidgetProvider.
  • WaniWani.chat.track / WaniWani.chat.identify on the <script> embed, and track / identify on the WaniwaniChat ref handle. Session attaches live once the chat server assigns it; the anonymous visitorId covers events before that.
  • createFrontendClient from @waniwani/sdk: the browser tracking primitive both surfaces wrap.
  • context.waniwani.sessionId on the scoped client: read the resolved session id and store it with your own records to attribute off-platform events later.
  • visitorId as a first-class correlation field on TrackInput and the V2 envelope.
  • EVENT_TYPES runtime constant, and extractScopedClient exported 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)

  1. Delete any capture option passed to useWaniwani().
  2. Replace wani.step(...) and wani.conversion(...) with the typed revenue helper that matches the funnel stage (optionSelected, converted, …).
  3. 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.
  4. Remove any type imports of AutoCaptureToggles or WidgetTrackFn; track is the same TrackFn type the server client uses.
  5. 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:
Widgets on older SDK versions paired with a 0.16 server (or the reverse) will not resolve tracking config; upgrade both sides together.

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

All new properties are optional; a bare track.leadQualified({ source }) is valid.

Migration (codemod)

  1. Replace every .track.lead( call with .track.leadQualified(.
  2. Replace event: "lead" with event: "lead_qualified" in generic track() calls (and eventType: "lead" in the legacy shape).
  3. Replace type imports: RevenueLeadInputRevenueLeadQualifiedInput, LeadPropertiesLeadQualifiedProperties.
  4. Where the call site has them available, enrich the event: pass your lead record id as externalId, plus email and name.
  5. Run bun run typecheck && bun test.

Compatibility

  • track.lead() exists as a @deprecated alias since 0.15.1: it emits a lead_qualified event 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. The EventType union 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. Declaring to 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.
  1. Find every two-argument .addConditionalEdge( call.
  2. Read the condition body and collect every node name (string literal or END) it can return.
  3. Insert that list, deduplicated, as the new second argument — leaving the condition as the third.
  4. Run bun run typecheck. The compiler now flags any return value missing from to, so any over- or under-declared list surfaces immediately. Fix by aligning to with what the condition actually returns.

Verifying

If typecheck reports 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/mcp is 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.ai in 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 types
  • KvStore, MemoryKvStore, WaniwaniKvStore
  • withWaniwani, WithWaniwaniOptions
  • createTrackingRoute, TrackingRouteOptions
  • ScopedWaniWaniClient
  • McpServer, ZodRawShapeCompat (shared MCP types)
And on @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 an import … from "<path>" statement — semantics are preserved end-to-end.
If you split imports across files, the only ambiguous case is step 1 vs step 3 (Express) when both come from the same root. Mechanical rule: the source path determines the destination, not the symbol name. The same symbol does not appear in two unrelated source paths within this matrix.

Verifying a one-shot migration

After substituting, the codebase should still typecheck and run unchanged. Run:
If you see errors of the form 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:
The first command should produce hits (those are the new paths). The remaining commands should produce either zero hits, or the same lines as before the migration (meaning the back-compat shim is doing its job and you can defer the rewrite).

ChatCardChatEmbed

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

  • id replaces the first positional argument.
  • run replaces the second positional argument (the handler).
  • label and hideFromFunnel move from the third options argument onto the same config object.
  • label is now optional. When omitted, it defaults to id. The positional form required label whenever you passed an options object.

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 new AddNodeConfig<TState, TName> type is exported from @waniwani/sdk/mcp if you need to type a config object outside the call site.