open-wa logoopen-wa
DocsGuides

PluginInput Breakdown

Understand what your plugin receives: events, logger, config, sessionId, and client.

PluginInput Breakdown

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(), and off()
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 messages
  • warn(message, meta?): Warnings
  • error(message, meta?): Errors
  • debug(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.

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.

On this page