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.25.0: flows list their fields on the first question

Apply this entire section automatically: npx skills add Waniwani-AI/sdk -s migrate-waniwani-sdk-0.24-to-0.25, then invoke it from your project on 0.24.x. It also takes a project on 0.23.x straight to 0.25. Skip the 0.23-to-0.24 skill in that case.
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 first interrupt 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, so tsc 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 interrupt or widget response carries fields, and its nextStep gains a line telling the model to send any listed value the user states. That line comes first, after the intro line when there is one. See Response fields added by the engine. Snapshot tests of that response change.
  • nextStep on interrupts. continue now 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. The stateUpdates description reads Values for the flow's fields, keyed by the field names its responses give. Snapshot tests of the listing change.
  • createFlowTestHarness. start(intent, known) sends known as stateUpdates on start, then answers each question whose field known still holds. A value in known for 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. withWaniwani in 0.25.0 still reads it and moves it off the wire. It stays deprecated, so read the graph from flow.flowGraph and 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 new start. Its next call follows the new write rule. No action.
New in 0.25:
  • neverPrefilled(schema) from @waniwani/sdk/mcp leaves a field out of fields and drops any value for it until the run has asked it. See Fields only a question can fill.
  • ctx.sessionId in 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.
Node handlers and their return values stay the same. So do the interrupt and showWidget signatures, validators, redacted(), intro and widget data.

Migration

  1. Bump with bun add @waniwani/sdk@^0.25.0.
  2. Collect the call sites:
  3. 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.
  4. Apply rewrites 1 to 3.
  5. Run bun run typecheck && bun test, with a test per flow that sends the rewritten fields on start and 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 in state 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:
A value can also stay in 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. Set hideAtBottom: false (or data-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/sdk as its source, and V2EventEnvelope.source is optional. Code that reads it as a string needs a fallback. tool.called from an MCP client the SDK does not recognise takes the name the client advertises, such as Cursor.

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. With WaniwaniChat, 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

Apply this entire section automatically: npx skills add Waniwani-AI/sdk -s migrate-waniwani-sdk-0.23-to-0.24, then invoke it from your project on 0.23.x.
In 0.23 a flow tool listed its whole Zod state schema under stateUpdates 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.
0.25.0 replaces the asked-only rule this section works around. It sends every state field to the model once per conversation, in fields, and writes any declared field whose value passes its schema. Going from 0.23 straight to 0.25, apply rewrite 5, the tools/list assertions in rewrite 4 and the advice on the flow description and resubmission in Ship it from this section, then the 0.25.0 section. Skip rewrites 1 to 3 and the other test changes in rewrite 4, and skip the 0.23-to-0.24 skill: the 0.24-to-0.25 skill covers a 0.23 project. Code that already has those rewrites keeps working on 0.25. On 0.23 a call merged every key as sent. 0.25 drops a key the state does not declare and a value that fails its field’s schema, so declare in state every key a widget or the model sends.

What breaks

Visible to tsc: Invisible to tsc: A key that is not a state field is dropped. Other behavior changes:
  • tools/list. A flow’s stateUpdates is an untyped record, the same for every flow. The definition _meta still carries the flow graph and the redacted-fields list for withWaniwani, wrapped in an object whose toJSON returns undefined, so a serializing transport never sends them. Snapshot tests of the listing change.
  • Hand-set _meta._flowGraph. withWaniwani still reads it and moves it off the wire. It is deprecated, and 0.25.0 still reads it.
  • createFlowTestHarness. start(intent, known) no longer sends known as stateUpdates. It starts empty, then answers each question whose field known holds with a continue, until the flow asks something known has no value for. A value in known for a field no question asks is ignored.
  • nextStep text 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.
Node handlers and their return values stay the same (a value a node returns always writes state). So do the interrupt and showWidget signatures, validators, redacted(), intro, the state schema itself, and widget data.

Migration

  1. Bump with bun add @waniwani/sdk@^0.24.0.
  2. Collect the call sites:
  3. For each key in createFlow({ state }), note what writes it: an interrupt question, a widget (showWidget({ field }) or a z.object group it names), a node return value, or the conversation (the description or .describe() tells the model to send it, a question’s context tells the model to put a value in it, a widget sends it back, or a test sends it in stateUpdates). 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 through stateUpdates.
  4. Apply rewrites 1 to 5 to every target.
  5. 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 whose context 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’s continue 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.
Update the widget’s own code, or the instruction its tool response gives the model, so it sends the group key. A widget with a 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.
A raw handler call that sends 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.
When you pass your own _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 silent continue. 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

Apply this entire section automatically: npx skills add Waniwani-AI/sdk -s migrate-waniwani-sdk-0.22-to-0.23, then invoke it from your project on 0.22.x.
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:
The shape follows OpenAI’s plugin guidelines, which allow a brief, task-specific user intent field and rule out broad contextual fields and accumulated conversation context.

What breaks

Visible to tsc: 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 an intent string instead of a telemetry object. Snapshot tests of tool schemas change.
  • Tool call arguments. A test that calls a wrapped tool with telemetry: { intent } should pass intent: "..." instead. Schema validation strips a leftover telemetry key, except on a tool whose schema is .strict(), which rejects the call.
  • Tools with their own intent argument are left alone and record nothing. In 0.22 they got a telemetry argument beside it.
  • Tools with their own telemetry argument gain the intent string. Their own telemetry still reaches the handler unchanged.
The 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

  1. Bump with bun add @waniwani/sdk@^0.23.0.
  2. Collect the call sites:
  3. Apply the two rewrites below.
  4. 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? }.
Apps built with @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.
Assertions on the recorded event keep working, since the value is still at 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 pass captureIntent: 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.
Through 0.20.x, .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:
A tool result is the ordinary place for control flow, and it is not classified the way a description is. The assistant also reads less per turn: one branch instead of the whole protocol on every call.

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 nextStep key 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.
If you assert on 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

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: RevenueLeadInput → RevenueLeadQualifiedInput, LeadProperties → LeadQualifiedProperties.
  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).

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

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