Skip to content
MessageBuilder
Documentation menu
Build & send lifecycle

Build & send lifecycle

From builder state to a WAMessage, native flow nodes, and AIRich edit synchronization.

Concepts
v4.8/Baileys runtime
On this page

Every builder follows the same flow. Understanding it helps when debugging payloads or building a custom delivery path.

setter/adder  →  internal state  →  build()/toCard()  →  WAMessage  →  relayMessage()
                                        │
                                        ├─ resolve media
                                        ├─ await nested promises
                                        └─ assemble Baileys structure

build()

build(jid, options) assembles a WAMessage without sending it. Media is resolved, promises are awaited, and the final structure is created with generateWAMessageFromContent().

const msg = await new Button(sock)
  .setBody('Hello')
  .addReply('OK', 'ok')
  .build(jid, { messageId: 'custom-id' });

console.log(msg.key.id);

Use build() to:

  • inspect the payload before sending;
  • store a draft;
  • deliver through your own mechanism;
  • construct an edit message (AIRich.buildEdit).

send()

send() calls build() and then relayMessage(). For Button, ButtonV2, and Carousel, the library adds the following XMPP node:

biz
└── interactive (type=native_flow, v=1)
    └── native_flow (v=9, name=mixed)

This node is required for buttons to render. If you send a build() result manually, add the same node.

await sock.relayMessage(jid, msg.message, {
  messageId: msg.key.id,
  additionalNodes: [
    {
      tag: 'biz',
      attrs: {},
      content: [
        {
          tag: 'interactive',
          attrs: { type: 'native_flow', v: '1' },
          content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }],
        },
      ],
    },
  ],
});

The AIRich flow

AIRich sends in two stages:

  1. The main message contains botForwardedMessage.message.richResponseMessage, including the base64 unifiedResponse and fallback submessages.
  2. When includesUnifiedResponse and bypassDownload are enabled, send() calls sendEdit() with the same payload.

The second stage makes clients synchronize the rich payload immediately instead of waiting for an additional download. _lastMessageKey is updated, so calling sendEdit() without arguments edits the most recent message.

const rich = new AIRich(sock).addText('Hello from AIRich');
await rich.send(jid);

// Edit the last message
rich.addText('Replacement text', { replace: 'old-block' });
await rich.sendEdit();

Editing messages

buildEdit() wraps the payload in a type 14 protocolMessage:

const edit = await rich.buildEdit(jid, messageId, {
  msg: originalMessage.message,
});

sendEdit(jid, id) builds and relays the edit. When jid or id is omitted, the library uses the last message it sent.

Message IDs

By default, IDs are generated with generateMessageIDV2(). You can provide your own through messageId:

await button.send(jid, { messageId: 'message-' + Date.now() });

Never reuse the same ID for two different messages. When editing, use the target message’s ID, not a new one.

Common errors

Symptom Cause
Socket is required The builder was created without a socket.
interactiveMessage not found loadFrom() received the wrong message.
Buttons do not render The native flow node is missing from a custom delivery path.
Message id is required sendEdit() was called without an ID and there is no recent message.
ITEM_NOT_FOUND The target ID for replace, insertAt, or delete does not exist.

Production recommendations

  • Store msg.key after send() for edits or auditing.
  • Wrap sends in try/catch and log err.code for AIRich errors.
  • Do not reuse the same builder instance for multiple recipients.
  • Test on real devices; rendering can differ between WhatsApp versions.