Skip to main content

Overview

The Channels API lets your agent initiate messages on the channels it’s connected to — WhatsApp, SMS, email, web chat, and more — from anywhere your code runs: a tool, a scheduled job, a webhook, or a trigger. It’s the outbound half of a two-way conversation: inbound messages wake your agent, and Channels.* sends messages back out.
import { Channels } from 'lua-cli';

// Send a free-form message on any connected channel
await Channels.send({
  channel: 'whatsapp',
  to: { userId: 'user_123' },
  text: 'Your order has shipped! 📦'
});

// Send an email
await Channels.email.send({
  to: { email: '[email protected]' },
  subject: 'Order confirmation',
  text: 'Thanks for your order!'
});

// Re-open a closed WhatsApp window with an approved template
await Channels.whatsapp.sendTemplate({
  to: { phoneNumber: '+14155552671' },
  templateName: 'order_update',
  languageCode: 'en_US',
  messageContext: 'Told the customer their order shipped'
});
Every Channels.* send is recorded to the recipient’s conversation thread with your agent. When the user replies, your agent picks up with full context — the outbound message is already part of the conversation it remembers. See Proactive Messaging for the full model.

Channels.send

Free-form text on any connected channel

Channels.email.send

Rich email — subject, HTML, cc/bcc, attachments

Channels.whatsapp.sendTemplate

Approved templates to start or re-open a conversation

Where you can call it

Channels.* works in any execute context. Recipient resolution differs slightly by context:
ContextAvailable?Notes
ToolsHas conversational context — you can target the current user with their userId, or any user.
JobsNo conversational context — target an explicit userId, phoneNumber, or email.
WebhooksNo conversational context — target an explicit recipient.
TriggersSame as webhooks — target an explicit recipient.
Pre/Post-processorsAvailable, but most sending happens in tools and jobs.
Pair Channels.send with a scheduled job for time-based outreach (reminders, follow-ups, digests) — see the Proactive Send recipe.

Channels.send()

Send a free-form text message on a connected channel.
Channels.send(input: ChannelSendInput): Promise<ChannelSendOutput>
channel
string
required
The channel to send on. One of: 'whatsapp', 'sms', 'email', 'webchat', 'teams', 'instagram', 'messenger'.
to
object
required
The recipient. Provide exactly one of the fields below.
to.userId
string
A Lua user ID. Works on every channel — the recipient’s channel address is resolved from their conversation history with your agent.
to.phoneNumber
string
A phone number in E.164 format (e.g. +14155552671). Valid for whatsapp and sms only — lets you reach a number with no prior conversation (cold start).
to.email
string
An email address. Valid for the email channel only (cold start). For richer email, prefer Channels.email.send.
text
string
required
The message text. Supports the same response formatting components (::: blocks) as inline replies, where the channel renders them.
options
object
Optional send options — see Options.
Returns: ChannelSendOutput Examples:
// In a tool — message the user you're talking to, on a specific channel
await Channels.send({
  channel: 'whatsapp',
  to: { userId: user._luaProfile.userId },
  text: 'Here is the summary you asked for.'
});

Channels.email.send()

Send a rich email — subject, plain-text and/or HTML body, cc/bcc, and attachments.
Channels.email.send(input: EmailSendInput): Promise<ChannelSendOutput>
to
object
required
The recipient. Provide exactly one of to.userId or to.email.
to.userId
string
A Lua user ID — the email address is resolved from the user’s conversation history.
to.email
string
A literal email address (cold start).
subject
string
The email subject line.
text
string
Plain-text body. Provide text, html, or both — at least one is required.
html
string
HTML body.
cc
string[]
Carbon-copy recipients.
bcc
string[]
Blind-carbon-copy recipients.
attachments
EmailAttachmentInput[]
Files to attach. Each is { filename, contentType, url } — the file is fetched from the public url at send time. Combined attachment size is capped at 28 MB.
options
object
Optional send options — see Options.
Returns: ChannelSendOutput (with messageId set to the provider message ID where available). Example:
await Channels.email.send({
  to: { email: '[email protected]' },
  subject: 'Your invoice',
  html: '<h1>Invoice #12345</h1><p>Total: $99.00</p>',
  cc: ['[email protected]'],
  attachments: [
    {
      filename: 'invoice-12345.pdf',
      contentType: 'application/pdf',
      url: 'https://files.example.com/invoice-12345.pdf'
    }
  ]
});

Channels.whatsapp.sendTemplate()

Send a pre-approved WhatsApp template. Use this to start a conversation, or to reach a user whose 24-hour messaging window has closed (where free-form sends aren’t allowed).
Channels.whatsapp.sendTemplate(input: WhatsAppTemplateSendInput): Promise<ChannelSendOutput>
to
object
required
The recipient — exactly one of to.userId or to.phoneNumber (E.164).
templateName
string
required
The name of an approved template on the agent’s WhatsApp channel. List available templates with Templates.whatsapp.list.
languageCode
string
The template language, e.g. 'en_US'.
components
object[]
Meta template component objects supplying the header / body / button parameter values — e.g. { type: 'BODY', parameters: [{ type: 'text', text: 'Tuesday at 3pm' }] }. The component type is 'HEADER', 'BODY', or 'BUTTON'; the parameter type is 'text', 'image', 'video', 'document', or 'coupon_code'.
messageContext
string
A plain-text summary of what the template said. This is the text recorded to the conversation thread so your agent remembers the outreach when the user replies. Recommended whenever the template body isn’t self-explanatory.
options
object
Optional send options — see Options.
Returns: ChannelSendOutput Example:
await Channels.whatsapp.sendTemplate({
  to: { phoneNumber: '+14155552671' },
  templateName: 'appointment_reminder',
  languageCode: 'en_US',
  components: [
    { type: 'BODY', parameters: [{ type: 'text', text: 'Tuesday at 3pm' }] }
  ],
  messageContext: 'Reminded the customer about their Tuesday 3pm appointment'
});
Channels.whatsapp.sendTemplate is the canonical way to send a WhatsApp template as part of a conversation — it records the send to the recipient’s thread and respects the recipient resolution above. The lower-level Templates.whatsapp.send (batch send by channel ID and phone numbers) remains available for bulk/campaign sends.

Options

All three methods accept an optional options object:
options.channelIdentifier
string
Pin the send to a specific channel configuration (for agents with more than one channel of the same type). The configuration must belong to your agent.
options.whatsapp.onClosedWindow
'queue' | 'fail'
default:"'queue'"
What to do when a WhatsApp free-form send hits a closed 24-hour window:
  • 'queue' (default) — queue the message and deliver it after the user re-engages (the API returns queued: true).
  • 'fail' — reject the send so you can fall back to Channels.whatsapp.sendTemplate.

ChannelSendOutput

Every send method resolves to the same shape:
interface ChannelSendOutput {
  delivered: boolean;     // the channel accepted the message for delivery
  persisted: boolean;     // the message was recorded to the agent's conversation thread
  queued?: boolean;       // WhatsApp only: deferred until the recipient re-engages
  userId?: string;        // the Lua user the message was recorded against
  identifier?: string;    // the channel-native recipient (phone / email / etc.)
  messageId?: string;     // provider message ID, where available
  warning?: string;       // set when delivered but not persisted
}
delivered
boolean
The channel accepted the message. Independent of persisted.
persisted
boolean
The message was recorded to the recipient’s conversation thread with your agent. A delivered-but-unpersisted send returns delivered: true, persisted: false plus a warning — it does not throw.
queued
boolean
WhatsApp only. true means the 24-hour window was closed and the message is queued — delivered stays false until the recipient re-engages, at which point the queued text is delivered and recorded. See Proactive Messaging.

Error handling

Channels.* throws when a send is rejected (invalid recipient, unconfigured channel, provider rejection, or onClosedWindow: 'fail' on a closed window). Wrap calls in try/catch where a failure should be handled gracefully. A successful call may still report partial success — delivered: true with persisted: false (plus a warning) means the message went out but couldn’t be recorded to the conversation thread. This is not an error and won’t throw; check the flag if recording matters to your flow.
try {
  const result = await Channels.send({
    channel: 'whatsapp',
    to: { userId: 'user_123' },
    text: 'Quick update for you!'
  });

  if (result.queued) {
    // Window closed — message will deliver when the user replies
    console.log('Queued; will deliver on re-engagement');
  } else if (!result.persisted) {
    console.warn('Delivered but not recorded:', result.warning);
  }
} catch (err) {
  // Rejected — e.g. unconfigured channel, invalid recipient, closed-window 'fail'
  console.error('Send failed:', err.message);
}
WhatsApp free-form sending is time-limited. You can only send free-form WhatsApp messages within 24 hours of the recipient’s last inbound message. Outside that window, use Channels.whatsapp.sendTemplate with an approved template — or rely on the default onClosedWindow: 'queue' behavior. See Proactive Messaging and Channel Capabilities.

Channels & recipients at a glance

ChannelCold start (no prior conversation)Warm only (userId)
whatsapp✅ via phoneNumber (template required if window closed)
sms✅ via phoneNumber
email✅ via email
webchat
teams
instagram
messenger
See Channel Capabilities for per-channel limits, sender resolution, and compliance notes.

TypeScript types

type ChannelSendChannel =
  | 'whatsapp' | 'sms' | 'email' | 'webchat'
  | 'teams' | 'instagram' | 'messenger';

interface ChannelSendTarget {
  userId?: string;       // any channel
  phoneNumber?: string;  // whatsapp / sms (cold start)
  email?: string;        // email (cold start)
}

interface ChannelSendOptions {
  channelIdentifier?: string;
  whatsapp?: { onClosedWindow?: 'queue' | 'fail' };
}

interface ChannelSendInput {
  channel: ChannelSendChannel;
  to: ChannelSendTarget;
  text: string;
  options?: ChannelSendOptions;
}

interface WhatsAppTemplateSendInput {
  to: { userId?: string; phoneNumber?: string };
  templateName: string;
  languageCode?: string;
  components?: Array<Record<string, unknown>>;
  messageContext?: string;
  options?: ChannelSendOptions;
}

interface EmailAttachmentInput {
  filename: string;
  contentType: string;
  url: string;
}

interface EmailSendInput {
  to: { userId?: string; email?: string };
  subject?: string;
  text?: string;
  html?: string;
  cc?: string[];
  bcc?: string[];
  attachments?: EmailAttachmentInput[];
  options?: ChannelSendOptions;
}

interface ChannelSendOutput {
  delivered: boolean;
  persisted: boolean;
  queued?: boolean;
  userId?: string;
  identifier?: string;
  messageId?: string;
  warning?: string;
}

Next steps

Proactive Messaging

The full model: Channels.send vs user.send() vs templates, and per-channel windows

Channel Capabilities

Per-channel limits, sender resolution, and compliance

Proactive Send Recipe

Schedule outreach with defineJob + Channels.send

Templates API

List and batch-send approved WhatsApp templates