Incoming messages and conversation agents

I filter incoming events by both the client session and destination chat before running a reply workflow. These examples use a synthetic group JID; replace it with the chat that should activate your automation.

Extract text and filter one chat

Text can appear in message.conversation or message.extendedTextMessage.text. This example handles both and ignores events that do not contain text:

- alias: WhatsApp group ping
  triggers:
    - trigger: event
      event_type: new_whatsapp_message
      event_data:
        clientId: default
        key:
          remoteJid: 120363000000000000@g.us
  actions:
    - variables:
        message_text: >-
          {% set message = trigger.event.data.get('message', {}) %}
          {{ message.get('conversation') or
             message.get('extendedTextMessage', {}).get('text', '') }}
    - condition: template
      value_template: "{{ message_text | string | trim == '!ping' }}"
    - action: whatsapp.send_message
      data:
        clientId: "{{ trigger.event.data.clientId }}"
        to: "{{ trigger.event.data.key.remoteJid }}"
        body:
          text: pong
  mode: queued

Use the event's key.remoteJid as the reply target. In a group, the sender can also appear in key.participant; preserve the entire key for reactions and read markers. Keep reply triggers on new_whatsapp_message so outgoing whatsapp_message_sent events do not trigger another reply.

Show the sender and group name

Every new_whatsapp_message event carries pushName, the display name the sender set on their own WhatsApp profile. It is present for direct chats and for each member of a group. It is not your saved contact name, and a few protocol messages omit it, so keep a fallback.

The group itself is only identified by key.remoteJid. Use whatsapp.get_group_info to turn that JID into the group name when the automation needs it:

- alias: Forward WhatsApp group messages with names
  triggers:
    - trigger: event
      event_type: new_whatsapp_message
      event_data:
        clientId: default
        key:
          remoteJid: 120363000000000000@g.us
  actions:
    - variables:
        message_text: >-
          {% set message = trigger.event.data.get('message', {}) %}
          {{ message.get('conversation') or
             message.get('extendedTextMessage', {}).get('text', '') }}
        sender: "{{ trigger.event.data.get('pushName', 'Group member') }}"
    - condition: template
      value_template: "{{ message_text | string | trim != '' }}"
    - action: whatsapp.get_group_info
      data:
        clientId: "{{ trigger.event.data.clientId }}"
        to: "{{ trigger.event.data.key.remoteJid }}"
      response_variable: group
    - action: notify.mobile_app_phone
      data:
        title: "{{ group.subject }}"
        message: "{{ sender }}: {{ message_text }}"
  mode: queued

whatsapp.get_group_info queries WhatsApp on every call. For a busy group, store the name in a helper instead of looking it up for each message.

Reply with a conversation agent

This requires a configured Home Assistant conversation agent. Replace conversation.example_agent with that agent's entity ID. I check its response before sending any reply and stop typing even when the agent or send action reports an error.

- alias: WhatsApp conversation reply
  triggers:
    - trigger: event
      event_type: new_whatsapp_message
      event_data:
        clientId: default
        key:
          remoteJid: 120363000000000000@g.us
  actions:
    - variables:
        agent_result: {}
        incoming_text: >-
          {% set message = trigger.event.data.get('message', {}) %}
          {{ message.get('conversation') or
             message.get('extendedTextMessage', {}).get('text', '') }}
    - condition: template
      value_template: "{{ incoming_text | string | trim != '' }}"
    - action: whatsapp.send_presence_update
      data:
        clientId: "{{ trigger.event.data.clientId }}"
        to: "{{ trigger.event.data.key.remoteJid }}"
        type: composing
      continue_on_error: true
    - action: conversation.process
      data:
        agent_id: conversation.example_agent
        text: "{{ incoming_text | string }}"
      response_variable: agent_result
      continue_on_error: true
    - variables:
        reply_text: >-
          {% set response = agent_result.get('response', {})
             if agent_result is mapping else {} %}
          {% if response is mapping
             and response.get('response_type', 'error') != 'error' %}
            {{ response.get('speech', {}).get('plain', {}).get('speech', '')
               | string | trim }}
          {% else %}
            {{ '' }}
          {% endif %}
    - if:
        - condition: template
          value_template: >-
            {{ reply_text | string | trim != ''
               and reply_text | string | trim | lower != 'no response' }}
      then:
        - action: whatsapp.send_message
          data:
            clientId: "{{ trigger.event.data.clientId }}"
            to: "{{ trigger.event.data.key.remoteJid }}"
            body: '{{ {"text": reply_text | string | trim} }}'
          continue_on_error: true
    - action: whatsapp.send_presence_update
      data:
        clientId: "{{ trigger.event.data.clientId }}"
        to: "{{ trigger.event.data.key.remoteJid }}"
        type: paused
      continue_on_error: true
  mode: queued
  max: 10

no response is an optional convention for an agent instructed to remain silent; it is not a special WhatsApp response. This example sends one incoming text to the agent. It does not add chat history or configure the agent's tools. For a longer request, use the bounded typing helper. For context across messages, see the complete conversation history recipe. For outgoing alerts, see AI-written notifications.

Recognize incoming media

I branch on the event's detected type and inspect the matching message object:

Event type Message field Use in a workflow
conversation message.conversation Plain text.
extendedTextMessage message.extendedTextMessage.text Text with additional metadata.
imageMessage message.imageMessage Image processing; the object may include a caption.
audioMessage message.audioMessage Audio or voice-message processing.
documentMessage message.documentMessage Document processing.
videoMessage message.videoMessage Video processing.

For example, this automation acknowledges an image with a reaction:

- alias: Acknowledge a WhatsApp image
  triggers:
    - trigger: event
      event_type: new_whatsapp_message
      event_data:
        clientId: default
        type: imageMessage
        key:
          remoteJid: 120363000000000000@g.us
  actions:
    - action: whatsapp.send_message
      data:
        clientId: "{{ trigger.event.data.clientId }}"
        to: "{{ trigger.event.data.key.remoteJid }}"
        body:
          react:
            text: "\u2705"
            key: "{{ trigger.event.data.key }}"
  mode: queued

Message payloads can also contain wrappers or protocol fields. These examples cover the listed shapes and do not implement a general Baileys message decoder.

Incoming attachments can be saved automatically with the incoming media feature. It enriches this same event with a decrypted local file and an authenticated download link before your automation runs.

Workflows that need separate helpers

These workflows require another integration or a custom helper:

Workflow Additional requirement
Voice message to text, then an agent reply Enable incoming media downloads and pass the saved audio to a transcription integration.
Image or document analysis Enable incoming media downloads and pass the saved file to an OCR or analysis integration.
Agent reply as a voice message A text-to-speech service that produces a downloadable audio file, followed by body.audio with ptt: true.
Download or transform a video, then send clips A separate video service returning ready-to-send URLs.
Generate a sticker, then send it A custom generator returning a prepared WebP URL.
Keep per-chat history for an agent Agent-managed conversation IDs or separate storage, such as the File and Shell Command recipe.

The original URL inside message.imageMessage, message.audioMessage, or another media message still refers to encrypted WhatsApp data. Use the added media.local_path or authenticated media.url when media.status is ready. See sending helper results for the outgoing side of the workflow.