open-wa logoopen-wa
DocsGuides

Session events

Share events between files, observe startup, and manage global subscriptions.

Session events

Use ev when several files need the same event helper, or when you need to observe authentication before create() returns. Every import shares the same emitter in the current process. Use client.events when you want the native events from one client.

The Effect hybrid and optional PubSub APIs described below are available from OpenWA 5.6.0.

Observe startup events

Register the listener before starting the session:

import { create, ev } from '@open-wa/wa-automate';

ev.on('client.ready', (_event, sessionId) => {
  console.log(`Session ${sessionId} is ready`);
});

const client = await create({ sessionId: 'support' });

client.ready follows authentication and runtime finalization. For Easy API, also wait for /health to report both connected: true and session.ready: true before sending. See readiness.

Native events keep their core payload as the first argument. The second argument identifies the session, and the third contains the event name:

ev.on('session.connection.disconnected', (event, sessionId) => {
  console.warn(`Session ${sessionId} disconnected`, event.details?.reason);
});

Share application events between files

In jobs.ts, register a listener:

import { ev } from '@open-wa/wa-automate';

ev.on('app.job.finished', ({ jobId }) => {
  console.log('Finished:', jobId);
});

In index.ts, load that module before emitting the event:

import './jobs';
import { ev } from '@open-wa/wa-automate';

ev.emit('app.job.finished', { jobId: 'daily-summary' });

Run bun run index.ts. It prints Finished: daily-summary. emit() invokes listeners immediately, in the same process. It does not store or replay events for modules that subscribe later. Use await ev.emitAsync(...) when your application needs to wait for its asynchronous listeners.

Use v4 wildcard subscriptions

* matches one segment of an event name; ** matches zero or more. Both trailing and leading patterns work, including qr.** and **.support.

Supported message and state listener aliases use <listener>.<sessionId>.<instanceId> and carry the familiar envelope:

ev.on('onAck.**', ({ sessionId, data: message }) => {
  console.log(sessionId, message.id, message.ack);
});

ev.on('onMessage.support.**', ({ data: message }) => {
  console.log('Incoming message:', message.id);
});

The envelope contains { ts, sessionId, id, event, data }. Its id identifies the event; data.id identifies the message. Aliases are provided for onMessage, onAnyMessage, onAck, onPollVote, onEvent, onEventResponse, onStateChanged, onLogout, onInteractiveResponse, and onFormResponse. An alias does not add a missing WhatsApp capability; consult the migration guide for unsupported listeners.

eventMode defaults to true. Set it to false in create() or createClient() to disable the automatic listener aliases. Native lifecycle events and your own application events remain available. A catch-all ** subscription sees both native events and their aliases, so choose one naming scheme when processing a message once.

Authentication and session data

Lifecycle aliases use <namespace>.<sessionId> and call listeners with (data, sessionId, namespace):

PatternFirst argument
qr.**The pairing string for your QR renderer.
linkCode.**The generated link code.
sessionData.**The encrypted compact session object after durable storage accepts a save.
sessionDataBase64.**The same compact session JSON encoded as base64.

The session object contains current v5's encrypted auth and data values. It does not recreate v4 token fields. Custom encryption keys are not included in these events. Keep pairing and session data inside a private application; never forward them to shared logs, analytics, or public endpoints.

For native event names, QR generation is launch.auth.qr.generated, with the value in event.details.qr. Link-code generation is launch.auth.linkCode.generated, with the value in event.details.linkCode. See link-code login and the event reference for those flows.

Remove a subscription

Keep the callback reference so you can remove just your listener:

const onFinished = ({ jobId }: { jobId: string }) => {
  console.log('Finished:', jobId);
};

ev.on('app.job.finished', onFinished);
ev.off('app.job.finished', onFinished);

once() removes its subscription after the first matching event. Stopping a client detaches that session's forwarding connection. Other sessions and listeners registered by other files keep working.

Use an Effect scope

If your application uses Effect, onGlobalEvent() owns a subscription for its current scope and removes it automatically when the scope closes:

import { Effect } from 'effect';
import { ev, onGlobalEvent } from '@open-wa/wa-automate';

await Effect.runPromise(Effect.scoped(Effect.gen(function* () {
  yield* onGlobalEvent('app.job.finished', ({ jobId }) => {
    console.log('Finished:', jobId);
  });

  ev.emit('app.job.finished', { jobId: 'daily-summary' });
})));

// The scoped listener has now been removed.

The helper uses the same synchronous emitter. It does not create a queue or fiber per message. Session forwarding is also owned by the runtime's Effect scope and is removed after shutdown or failed startup. See Effect scopes for the resource-lifetime model.

Await Effect listeners

Listeners can return a Promise or an Effect. emit() still calls ordinary listeners synchronously; Effect and Promise work belongs to the emitter's scope. emitAsync() waits for listener work and returns the results in listener order. Use yield* ev.emitEffect(name, ...args) inside Effect to wait without starting another root runtime. ev.onScoped() and ev.onAnyScoped() also interrupt in-flight listener work when their scope closes.

Observe through PubSub

ev.pubsub() exposes a native Effect PubSub for queued observers. ev.subscribe() provides a scoped subscription, and ev.stream() provides a Stream for filtering, batching, or sequential processing. Ordinary callbacks retain their synchronous behavior.

import { Effect, PubSub } from 'effect';
import { ev } from '@open-wa/wa-automate';

await Effect.runPromise(Effect.scoped(Effect.gen(function* () {
  const subscription = yield* ev.subscribe();
  ev.emit('app.job.finished', { jobId: 'daily-summary' });
  const { event, args } = yield* PubSub.take(subscription);
  console.log(event, args[0]);
})));

Queued packets contain { event, args }, preserving every callback argument, including session metadata. The default hub is created on first use, has no replay, and retains pending events for active subscribers without a memory cap. Choose an explicit finite policy on first access, for example ev.pubsub({ strategy: 'sliding', capacity: 256 }), if retaining the newest observations is preferable to keeping a backlog. Dropping and sliding can lose observations; pubsubStatus reports rejected emitter publications.

Emitter methods reach callbacks and queued observers. Publishing directly with PubSub.publish(ev.pubsub(), packet) reaches only queued observers. emitAsync() waits for callback results, so queued consumers control their own completion and retry policies. A subscription ends with its Effect scope; stopping one client detaches its forwarding without closing other sessions' global subscriptions.

For complete standalone examples and consumer cleanup, read Consume events with Effect. Exact methods, buffer options, and counters are in the HyperEmitter reference.

Was this helpful?

Your answer includes the page path and docs version.

On this page