Session events
Share events between files, observe startup, and manage global subscriptions.
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):
| Pattern | First 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.
