# Events
URL: /tap/docs/store/events

Emit and subscribe to typed events.

> For AI agents: a documentation index is available at [llms.txt](/llms.txt). Use `.md` for canonical markdown pages; `.mdx` is kept as a backwards-compatible alias on supported URL paths.

State tells you what things look like right now. Events tell you what just happened. Use events for side effects that respond to discrete actions: showing a toast, scrolling to a new message, firing analytics.

## Declaring events

Add an `events` field to your scope in `ScopeRegistry`. Event names follow the format `"scopeName.eventName"`:

```
import "@assistant-ui/store";

declare module "@assistant-ui/store" {
  interface ScopeRegistry {
    counter: {
      methods: {
        getState: () => { count: number };
        increment: () => void;
      };
      events: {
        "counter.incremented": { newCount: number };
        "counter.reset": undefined;
      };
    };
  }
}
```

Each event has a typed payload. Use `undefined` for events that carry no data.

## Emitting events

Call `useAssistantEmit()` inside a resource to get an emit function:

```
import { resource } from "@assistant-ui/tap";
import { useState, useMemo } from "react";
import { useAssistantEmit } from "@assistant-ui/store";
import type { ClientOutput } from "@assistant-ui/store";

const useCounterResource = (): ClientOutput<"counter"> => {
  const [count, setCount] = useState(0);
  const emit = useAssistantEmit();

  const state = useMemo(() => ({ count }), [count]);

  return {
    getState: () => state,
    increment: () => {
      const newCount = count + 1;
      setCount(newCount);
      emit("counter.incremented", { newCount });
    },
  };
};

const CounterResource = resource(useCounterResource);
```

Events are delivered via microtask: they fire after the current state update settles, not mid-update. This means listeners always see consistent state.

## Subscribing to events

Use `useAuiEvent` in any component inside an `AuiProvider`:

```
import { useAuiEvent } from "@assistant-ui/store";

const CounterToast = () => {
  useAuiEvent("counter.incremented", ({ newCount }) => {
    toast(`Count is now ${newCount}`);
  });

  return null;
};
```

The callback receives the typed payload. It's stable internally (via `useEffectEvent`), so you don't need to memoize it.

## Event scoping

When you pass a string like `"counter.incremented"` to `useAuiEvent`, it listens for events from the `counter` scope **in your current context**. This is scope filtering: the listener only fires when the event comes from the specific scope instance you're inside.

This matters when there are multiple instances of the same scope. Consider a list of counters, each with its own `counter` scope:

```
App
  └ CounterItem (counter scope, id: "a")
  └ CounterItem (counter scope, id: "b")
  └ CounterItem (counter scope, id: "c")
```

A `useAuiEvent("counter.incremented", ...)` inside `CounterItem "a"` only fires for that counter, not for "b" or "c". Each listener is scoped to its own instance.

### Listening to child events

If you try to listen to an event from a scope that isn't available in your component's context, Store throws an error. For example, if your component sits above the `counter` scope, `useAuiEvent("counter.incremented", ...)` will throw because there is no `counter` scope to filter against.

To listen to events from child scopes, set `scope` to your own scope (the parent):

```
// inside a component that has counterList but not counter
useAuiEvent(
  { scope: "counterList", event: "counter.incremented" },
  ({ newCount }) => {
    console.log("some counter changed:", newCount);
  },
);
```

This fires for `counter.incremented` events from any counter instance below the `counterList` scope. The `scope` field tells Store which level to filter at: any event emitted by a descendant of that scope instance will match.

### Listening to sibling events

There's no direct way to listen to events from a sibling scope. Instead, listen at a mutual parent (or `"*"`) and filter by metadata in the event payload:

```
// inside counter "a", listening for increments from any sibling
useAuiEvent(
  { scope: "counterList", event: "counter.incremented" },
  ({ counterId, newCount }) => {
    if (counterId === "b") {
      console.log("counter b incremented:", newCount);
    }
  },
);
```

Include whatever metadata you need for filtering in the event payload; Store doesn't provide sibling identity automatically.

### Wildcard scope

Use `"*"` as the scope to listen to an event from any instance, regardless of your position in the tree:

```
useAuiEvent(
  { scope: "*", event: "counter.incremented" },
  ({ newCount }) => {
    console.log("any counter incremented:", newCount);
  },
);
```

### Wildcard event

Use `"*"` as the event to listen to every event. The payload is wrapped with the event name:

```
useAuiEvent("*", ({ event, payload }) => {
  console.log(event, payload);
});
```

This is useful for debugging and logging.

## Choosing a scoping pattern

| You want to...                            | Selector                                                                |
| ----------------------------------------- | ----------------------------------------------------------------------- |
| Listen to events from your own scope      | `"scope.event"`                                                         |
| Listen to events from any child below you | `{ scope: "yourScope", event: "child.event" }`                          |
| Listen to events from a sibling           | `{ scope: "mutualParent", event: "sibling.event" }` + filter by payload |
| Listen to events from anywhere            | `{ scope: "*", event: "scope.event" }`                                  |
| Listen to all events (debugging)          | `"*"`                                                                   |