> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Proactive Messaging

> Reach out first — how agent-initiated messages work across channels

## Channels are two-way

Your agent doesn't only reply — it can **initiate**. A scheduled job can send a reminder, a webhook can confirm a payment, a tool can follow up hours later. Outbound messages flow through the [Channels API](/api/channels), and every one is recorded to the recipient's conversation thread so your agent stays coherent across the whole exchange.

```typescript theme={null}
import { Channels } from 'lua-cli';

await Channels.send({
  channel: 'whatsapp',
  to: { userId: 'user_123' },
  text: "Your appointment is confirmed for tomorrow at 3pm."
});
```

## How continuity works

There's no durable "wait for reply" machinery to manage. The model is simple:

1. Your agent **sends** a message (from a tool, job, webhook, or trigger). The message is recorded to the recipient's thread.
2. Hours or days later, the user **replies** on that channel.
3. The reply wakes your agent with the **same thread loaded** — it sees the message it sent and continues naturally.

<Note>
  A user is identified by their Lua `userId`, and one user ↔ agent pair shares one conversation thread. Send on WhatsApp, get a reply on WhatsApp — the agent has the full history either way. You don't manage sessions or state machines for this; the thread is the memory.
</Note>

## Three ways to send

Pick the one that matches what you have and where you want the message to go.

<CardGroup cols={1}>
  <Card title="Channels.send(...) — pick the channel explicitly" icon="paper-plane">
    Choose exactly which channel and recipient. Works from any context, can reach **cold** recipients (a phone number or email with no prior conversation), and records to the thread. This is the general-purpose proactive send.
  </Card>

  <Card title="user.send(...) — reach the user you already have" icon="user">
    If you've already loaded a [`User`](/api/user) instance, `user.send([...])` delivers to that user's active conversation. Simplest when you just want to message the user you're working with, on the channel they're already on.
  </Card>

  <Card title="Templates.whatsapp.send(...) — bulk template send" icon="whatsapp" iconType="brands">
    Send an approved WhatsApp template to one or many phone numbers by channel ID. Best for campaigns and batch notifications. See the [Templates API](/api/templates).
  </Card>
</CardGroup>

### Which should I use?

| You want to…                                                      | Use                                                                               |
| ----------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Message the current user, on the channel they're on               | [`user.send([...])`](/api/user#send)                                              |
| Choose a specific channel (e.g. always WhatsApp) for a known user | [`Channels.send`](/api/channels#channels-send) with `to.userId`                   |
| Reach a phone/email with no prior conversation                    | [`Channels.send`](/api/channels#channels-send) with `to.phoneNumber` / `to.email` |
| Start or re-open a WhatsApp conversation (window closed)          | [`Channels.whatsapp.sendTemplate`](/api/channels#channels-whatsapp-sendtemplate)  |
| Blast an approved template to many numbers                        | [`Templates.whatsapp.send`](/api/templates#send)                                  |
| React to a specific WhatsApp message with an emoji                | [`Channels.whatsapp.sendReaction`](/api/channels#channels-whatsapp-sendreaction)  |

## The WhatsApp 24-hour window

WhatsApp only allows **free-form** business messages within **24 hours** of the user's last inbound message. This is a Meta rule, not a Lua one — and it shapes how proactive WhatsApp sends behave.

* **Window open** (user messaged within 24h) → `Channels.send({ channel: 'whatsapp', ... })` delivers immediately (`delivered: true`).
* **Window closed** → free-form isn't allowed. Two options:
  * **Send an approved template** with [`Channels.whatsapp.sendTemplate`](/api/channels#channels-whatsapp-sendtemplate). Templates are allowed any time and are how you start or re-open a conversation.
  * **Let `Channels.send` queue it** (the default). The message is held, the recipient is shown a short opt-in prompt, and the queued text is delivered once they re-engage. The call returns `queued: true` (not yet `delivered`).

```typescript theme={null}
// Default: queue if the window is closed
const result = await Channels.send({
  channel: 'whatsapp',
  to: { userId: 'user_123' },
  text: 'Following up on your request.'
});

if (result.queued) {
  // Held until the user re-engages; not delivered yet
}

// Or fail fast and fall back to a template yourself
try {
  await Channels.send({
    channel: 'whatsapp',
    to: { userId: 'user_123' },
    text: 'Following up on your request.',
    options: { whatsapp: { onClosedWindow: 'fail' } }
  });
} catch {
  await Channels.whatsapp.sendTemplate({
    to: { userId: 'user_123' },
    templateName: 'follow_up',
    languageCode: 'en_US',
    messageContext: 'Followed up on the customer’s open request'
  });
}
```

<Warning>
  You **cannot** send a free-form WhatsApp message to someone who has never messaged your agent, or whose window has closed. The honest cold-start path is an approved template. Plan template content for any outreach that might land outside the window.
</Warning>

<Note>
  **US (+1) recipients:** the opt-in prompt used by the queue path is a marketing-category template, which Meta may not deliver to US numbers. For reliable US outreach outside the window, send an approved utility template directly with `Channels.whatsapp.sendTemplate`.
</Note>

## Per-channel constraints

Windows and policies differ by channel:

| Channel                           | Proactive send                    | Constraint                                                                     |
| --------------------------------- | --------------------------------- | ------------------------------------------------------------------------------ |
| **WhatsApp**                      | Free-form (in-window) or template | 24-hour window; templates required outside it                                  |
| **SMS**                           | Free-form any time                | No window, but subject to carrier opt-out (STOP/HELP/START) and regional rules |
| **Email**                         | Free-form any time                | No window                                                                      |
| **Web chat**                      | Free-form to a known user         | Delivered to the user's web widget                                             |
| **Teams / Instagram / Messenger** | Free-form to a known user         | **Warm-only** — the user must have an existing conversation with your agent    |

See [Channel Capabilities](/channels/channel-capabilities) for the full matrix, sender resolution, and compliance details.

## Scheduled outbound

There's no separate "campaign" primitive — scheduled outreach is just a [job](/api/luajob) that calls `Channels.send`:

```typescript theme={null}
import { LuaJob, Channels } from 'lua-cli';

const reminders = new LuaJob({
  name: 'appointment-reminders',
  description: 'Send next-day appointment reminders every morning',
  schedule: {
    type: 'cron',
    expression: '0 9 * * *' // every day at 9 AM
  },
  execute: async () => {
    const due = await getTomorrowsAppointments();
    for (const appt of due) {
      await Channels.send({
        channel: 'whatsapp',
        to: { userId: appt.userId },
        text: `Reminder: your appointment is tomorrow at ${appt.time}.`
      });
    }
  }
});

export default reminders;
```

See the [Proactive Send recipe](/examples/proactive-send) for a complete walkthrough.

## Components in proactive messages

Proactive messages support the same [response formatting components](/formatting/introduction) (the `:::` blocks) as inline replies — lists, links, images, actions — on channels that render them. The web chat widget renders the full component set; messaging channels render what their platform supports (text, media), and fall back to text otherwise.

To react to a specific message rather than send new text, call [`Channels.whatsapp.sendReaction`](/api/channels#channels-whatsapp-sendreaction) directly with the target message's ID — useful from a job or webhook, outside the context of a normal reply.

## Next steps

<CardGroup cols={2}>
  <Card title="Channels API" icon="paper-plane" href="/api/channels">
    Full reference for send, sendTemplate, sendReaction, and email.send
  </Card>

  <Card title="Channel Capabilities" icon="table-list" href="/channels/channel-capabilities">
    Per-channel limits and sender resolution
  </Card>

  <Card title="Proactive Send Recipe" icon="clock" href="/examples/proactive-send">
    Schedule outreach with defineJob + Channels.send
  </Card>

  <Card title="Jobs API" icon="calendar" href="/api/jobs">
    Schedule recurring and one-off work
  </Card>
</CardGroup>
