> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waniwani.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Instrumentation

> Where each funnel event belongs in a flow, and how to auto-instrument it with the instrument-tracking skill.

<Note>
  **Platform feature.** Requires `WANIWANI_API_KEY`. Works whether your MCP server is self-hosted or on Managed Hosting. [About the Platform](/sdk/platform/overview).
</Note>

Instrumentation is the mapping between your flow's nodes and the Waniwani event taxonomy: which node emits which event, with which metadata. Get the mapping right and the dashboard shows a funnel that explains where users drop off and which conversations turn into revenue. This page gives you the placement rules, and a skill that applies them to your codebase automatically.

## The taxonomy

The event set is closed and typed. Model every funnel stage with one of these; there is no custom event name.

| Stage         | Event             | Emit via                                                   | Notes                                                    |
| ------------- | ----------------- | ---------------------------------------------------------- | -------------------------------------------------------- |
| Landing       | `page.viewed`     | chat widget, automatic                                     | Attributed to an anonymous `visitorId`.                  |
| Activity      | `tool.called`     | `withWaniwani(server)`, automatic                          | One per tool invocation, with timing and status.         |
| Identity      | `user.identified` | `waniwani?.identify(userId, properties?)`                  | Attach a stable external id as soon as one is known.     |
| Qualification | `lead_qualified`  | `waniwani?.track.leadQualified({ ... })`                   | The person met your qualification bar.                   |
| Step          | `price_shown`     | `waniwani?.track.priceShown({ amount, currency })`         | You showed one price.                                    |
| Step          | `prices_compared` | `waniwani?.track.pricesCompared({ options })`              | You showed two or more options side by side.             |
| Step          | `option_selected` | `waniwani?.track.optionSelected({ id, amount, currency })` | The user picked one of those options.                    |
| Conversion    | `converted`       | `waniwani?.track.converted({ amount, currency })`          | The user became paying, possibly later and off-platform. |

Inside a flow node, `waniwani` comes from the handler context and already carries the session identity. Full type shapes are in the [Event schema](/sdk/reference/event-schema).

## Placing `lead_qualified`

`lead_qualified` is your code declaring that a person met your qualification bar: they finished the qualifying questions, requested a demo, or matched your target profile. Three rules:

1. **It fires at the qualification bar, not at flow entry.** Starting a flow is activity (`tool.called` covers it). The event belongs in the node where qualification completes, which is often the node that pushes the lead to your CRM.
2. **It fires once per flow run.**
3. **Fill every property you can.** The properties are the join keys the dashboard uses to dedupe leads and connect them to your system.

| Property     | What to pass                                                                                                                                  |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `externalId` | Your own lead id, for example the record id your CRM or lead API returns. The strongest dedup key, so place the event after that push exists. |
| `email`      | The lead's email, from whichever state field holds it.                                                                                        |
| `name`       | The lead's name, if collected.                                                                                                                |
| `source`     | Acquisition source. `"mcp_chat"` is a good default for a flow.                                                                                |

```ts theme={null}
.addNode({
  id: "push_lead",
  label: "Push to CRM",
  run: async ({ state, waniwani }) => {
    const lead = await crm.createLead({
      email: state.email,
      role: state.role,
      useCase: state.useCase,
    });
    waniwani?.track.leadQualified({
      externalId: lead.id,
      email: state.email,
      source: "mcp_chat",
    });
    return { leadId: lead.id };
  },
})
```

<Note>
  Sharing an email mid-conversation is `identify(userId, { email })`, not a qualified lead. `identify` attaches identity; `lead_qualified` declares that your bar was met. Most flows emit both, at different nodes.
</Note>

## Placement rules for the rest

* **`identify` as early as possible.** The first node where an email or user id is in state calls `waniwani?.identify(state.email)`. This is what lets an off-platform `converted` find its way back to the session.
* **Price events go where the numbers are.** The node that computes a single price emits `priceShown`; the node that presents multiple plans emits `pricesCompared`; the node that runs after the user picked (the selection is now in state) emits `optionSelected`.
* **`converted` only on real conversion.** A confirmed booking or completed purchase inside the flow. If the sale closes later on your own site, emit it from your backend with the same `externalUserId` you identified during the flow. See [Identify](/sdk/tracking/identify).
* **Emit from node handlers, never from `validate` callbacks.** Validation can run several times per answer and would duplicate events.
* **Always guard with `waniwani?.`.** The scoped client is `undefined` when the server is not wrapped with `withWaniwani(server)`, and tracking throws without an API key. Optional chaining keeps keyless runs working.
* **Never pass `sessionId` manually inside a flow.** The scoped client carries it.

## A fully instrumented flow

```
START
  -> welcome            (interrupt: email)            identify(state.email)
  -> qualify            (interrupt: role, team size)
  -> push_lead          (action, CRM returns id)      lead_qualified { externalId, email, source }
  -> compute_quote      (action)                      price_shown { amount, currency }
  -> show_plans         (widget: 3 plans)             prices_compared { options }
  -> confirm_plan       (action)                      option_selected { id, amount, currency }
  -> book               (action)                      converted { amount, currency }
  -> END
```

Not every flow has every stage. A lead-gen flow can end at `lead_qualified`; a support flow may only ever `identify`. Forcing events onto nodes that do not represent that stage corrupts the funnel.

## Auto-instrument with the skill

The `instrument-tracking` agent skill applies everything on this page to your codebase: it inventories your flows, maps every node to the taxonomy, inserts guarded tracking calls with metadata read from your flow state, wraps the server with `withWaniwani` if needed, and runs your typecheck.

<CodeGroup>
  ```bash npx theme={null}
  npx skills add Waniwani-AI/sdk -s instrument-tracking
  ```

  ```bash bunx theme={null}
  bunx skills add Waniwani-AI/sdk -s instrument-tracking
  ```
</CodeGroup>

Then ask your agent, in natural language:

* *"Instrument tracking across my flows"*
* *"Track qualified leads in my demo flow"*
* *"Add conversion tracking to the booking flow"*

The skill also runs well as a subagent at the end of a scaffolding session, so a freshly generated flow ships instrumented from its first deploy. It reports the final node-to-event mapping as a table, so you can review every placement before committing.

## Checklist

* Exactly one `leadQualified` per flow run, placed at the qualification bar, with `externalId` when your CRM returns one.
* `identify` runs at the earliest node where a stable id exists.
* Every tracking call is guarded with `waniwani?.` and none passes `sessionId`.
* `withWaniwani(server)` wraps the server.
* Off-platform conversions send `converted` from your backend with the identified `externalUserId`.

## Next

<CardGroup cols={2}>
  <Card title="Event schema" icon="table" href="/sdk/reference/event-schema" />

  <Card title="Identify" icon="user" href="/sdk/tracking/identify" />

  <Card title="Sessions" icon="link" href="/sdk/tracking/sessions" />

  <Card title="Agent skills" icon="wand-magic-sparkles" href="/sdk/guides/skills" />
</CardGroup>
