PluginInput Breakdown
Understand what your plugin receives: events, logger, config, sessionId, and client.
The host passes a PluginInput object to your plugin's init() function. It contains the event interface, logger, plugin configuration, session identifier, lifetime signal, and client proxy.
interface PluginInput<TConfig = unknown> {
events: PluginEventEmitter;
logger: PluginLogger;
config: TConfig;
sessionId: string;
signal: AbortSignal;
client: PluginClient;
runtime: PluginRuntime;
services: PluginServices;
}Each field is described below.
events: Filtered Event Emitter
The events field provides on(), once(), and off(). The host gateway checks event metadata and ignores events marked internal or sensitive. It does not create a sandbox around plugin code.
What you can do
- Subscribe to allowed events with
on(),once(), andoff()
events.on('message.received', (payload) => {
logger.info('Message received');
});What you cannot do
- Emit through this object: It has no
emit()method. - Subscribe to metadata marked internal or sensitive: The gateway ignores these requests and logs them. This is based on host event metadata, not name prefixes; unknown names pass to the underlying emitter.
The loader dynamically imports plugins into the host process. They retain that process's Node.js and operating-system permissions, so review plugin source and dependencies before loading them. See the plugin security model for deployment isolation options.
logger: Scoped Logger
The logger provides structured logging with automatic plugin name prefixing.
Methods
logger.info('Plugin initialized', { version: '1.0.0' });
logger.warn('API key is missing, using defaults');
logger.error('Failed to connect', { error: 'ECONNREFUSED' });
logger.debug('Processing message', { messageId: 'abc123' });info(message, meta?): Informational messageswarn(message, meta?): Warningserror(message, meta?): Errorsdebug(message, meta?): Debug output (only visible when debug logging is enabled)
Auto-Prefixing
All log output is automatically prefixed with your plugin name:
[greeting-bot] Plugin initialized { version: '1.0.0' }Log Persistence
Plugin logs flow through the host's logging system. Whether they are persisted depends on the host's logging configuration. By default, logs are written to stdout/stderr.
config: Validated Configuration
The config field contains your plugin's configuration from wa.config.js or wa.config.ts. When you provide a configSchema, the host validates and parses the value before calling init(); without a schema, it passes the configured value through (or {} when it is absent).
Access Pattern
Your plugin's config is accessed via the pluginConfig key in the global config file:
// wa.config.js or wa.config.ts
export default {
pluginConfig: {
'my-plugin': {
apiUrl: 'https://api.example.com',
apiKey: process.env.MY_PLUGIN_API_KEY,
},
},
};// src/my-plugin.ts
init: async ({ config }) => {
// config is { apiUrl: string; apiKey: string }
// Already validated against your configSchema
console.log(config.apiUrl);
}Type Inference
When you provide a configSchema, the config type is automatically inferred:
const configSchema = z.object({
apiUrl: z.string().url(),
apiKey: z.string().min(1),
retries: z.number().int().positive().default(3),
});
// config is typed as { apiUrl: string; apiKey: string; retries: number }
init: async ({ config }) => { ... }sessionId: Current Session
A string identifier for the current session.
init: async ({ sessionId }) => {
logger.info('Plugin loaded for session', { sessionId });
}Use this for:
- Logging and debugging
- Scoping data to a specific session
- Differentiating behavior across multiple sessions
signal: Plugin lifetime
The host aborts this signal when it starts disposing the plugin. Pass it to cancellable work or check signal.aborted before starting another operation.
init: async ({ signal, logger }) => {
if (signal.aborted) {
logger.debug('Plugin is already stopping');
}
return {};
}runtime: Host Client methods
runtime.invoke() calls the attached high-level Client facade on the Easy API
host. Use it for prepared media, decryption, and calling with streams; these
operations need host services that a raw browser method cannot provide.
const audio = await runtime.invoke<string>('decryptMedia', [message]);
const call = await runtime.invoke<Call | null>('getActiveCall');The facade must be attached before invocation, so start runtime work from
client.ready or a later event rather than init(). Calls return hydrated local
objects and can accept Web streams; don't JSON-serialize those values. This
bridge has the same process trust as the installed plugin.
client: WhatsApp Method Proxy
The client is a host-managed proxy for calling methods exposed by window.WAPI in the connected runtime. Its ask() method forwards method names and arguments; the SDK does not apply a safe-method allowlist. The proxy does not restrict Node.js imports or replace process isolation.
init: async ({ client }) => {
await client.sendText('1234567890@c.us', 'Hello!');
const number = await client.getHostNumber();
}See the PluginClient reference for the complete list of available methods.
Related
- Hooks reference: What your plugin can return
- PluginClient reference: Available client methods
- Plugin security model: What plugins can and cannot do
services: Shared host services
Cooperating plugins can use services.getOrCreate(name, factory) for one
instance per session host. The factory receives a host lifetime signal and
returns { value, dispose }. Concurrent acquisitions share the same promise; a
failed creation is removed so a later acquisition can try again. The host aborts
these services when shutting down, disposes plugins, then disposes the shared
services. Do not bind a shared service to the first acquiring plugin's signal.
Jev and Voxtral use this for their common conversation state and send journal.
Was this helpful?
Your answer includes the page path and docs version.
