Skip to main content
After this guide, your agent sends the first message: a reminder from a job, a receipt from a tool. Use Channels.send when you choose the channel and the recipient; use user.send() when you hold a User and want the channel they last wrote from. Sends to a person are recorded in their conversation. Verified against lua-cli 3.33.0. Before you begin
  • The channel linked to the agent (Channels overview).
  • For WhatsApp outside the 24-hour window, a WhatsApp message template created and approved in Meta’s WhatsApp Manager (WhatsApp).
1

Pick the channel and the address

Channels.send accepts eight channel IDs; userId works everywhere, a phone number or email address only where first contact is allowed.Front, RCS, and iMessage are inbound only; MessageBird is reachable through user.send() alone. text may carry ::: formatting blocks (Rendering by channel).
2

Send from a job

A job has no current user, so it addresses each recipient explicitly; the appointments entries come from your booking tool, which stores the Lua user ID with Data.create('appointments', { userId: user._luaProfile.userId, date, time }) (Store and search data, Identify users). On WhatsApp, onClosedWindow: 'fail' makes the send throw when the 24-hour window has closed, and the catch falls back to an approved WhatsApp message template; messageContext is recorded as what it said.
src/jobs/AppointmentRemindersJob.ts
Leave onClosedWindow unset and the platform queues the text: it sends an opt-in prompt, returns queued: true, and delivers your text when the recipient replies. Meta blocks that prompt for US (+1) numbers (the result carries a warning), so send an approved WhatsApp message template to those recipients.
3

Send from a tool

Inside a conversation, User.get() returns the end user. Channels.email.send takes a subject and one of text, html, or richBody. user.send() delivers on the channel the end user last wrote from, when that is WhatsApp, Messenger, Instagram, Teams, MessageBird, or SMS, and mirrors the text to the web widget; deployed, it resolves true whatever happened, so confirm delivery with Channels.getStatus or lua logs (under lua test it throws instead).
src/skills/tools/SendReceiptTool.ts
4

Test locally

lua test runs the job or tool on your machine; the sends are real. With nothing due, the job returns its empty result.
Output
lua test skill runs send_receipt as you, so the receipt goes to your own user record; until you have emailed the agent, the run ends with email send requires a recipient email, or a userId with an existing email conversation.
5

Release it

lua push uploads a version and changes nothing for end users; lua version create snapshots the agent; lua version promote <n> makes that snapshot live and is also the rollback path (Release an agent to production).
lua version create prints ✓ Created v<n> (staged); in a script, n=$(lua version list --limit 1 --json --ci | jq -r '.[0].version') then lua version promote "$n". lua push all exits 0 even when a primitive fails; check its output for component(s) failed to push.
6

Verify

Trigger the job once and read the run.
lua jobs trigger runs the job against production data and messages every recipient it finds.
Every send returns a deliveryId and a status: queued or accepted at return, then sent, delivered, read, failed, or expired as receipts arrive. Channels.getStatus(deliveryId) reads one record; Channels.listDeliveries({ status: 'failed', since, limit }) lists failures with category, vendor code, and remediation link (Check deliveries).

Options you may need

Thread an email reply

Pass the inbound email’s Message-ID as inReplyTo and the thread’s IDs as references; in a tool handling an email turn, it is Lua.request.webhook.payload.messageId.

Post in Slack

Slack sends go through the agent’s own Slack bot. During a Slack channel-thread turn, Lua.request.conversation.externalId is that thread’s key (T…:A…:C…:threadTs); a tool can store it with Data.create('report-threads', { conversationId }), and a job posts into the thread later. to: { email } DMs the workspace member with that address.
src/jobs/NightlyReportJob.ts
The bot must be a member of the channel. Threads in one channel share that channel’s limit of about one post per second, so space the posts out, or catch the 503 and retry after its retryAfterSeconds. A post in a thread where the bot has already been mentioned is recorded there, so the next mention knows what the agent posted. Every address form, the rules on which bot posts, and the errors are in Post from code.

Check deliveries

A webhook reads delivery records from outside a conversation.
src/webhooks/DeliveryCheckWebhook.ts

If it isn’t working

Cause The recipient’s WhatsApp window is closed; the text waits for their reply to the opt-in prompt. Fix For time-sensitive messages, set onClosedWindow: 'fail' and send an approved WhatsApp message template.
Cause The message was delivered but not recorded, or it went to a Teams or Slack conversationId the agent has no shared conversation for yet: a Teams chat it has not been added to, or a Slack thread or group DM where nobody has mentioned the bot. A conversation send is recorded only where that shared conversation exists. Fix Nothing is thrown; log the warning if continuity matters. On Slack, the bot’s earlier posts are read in when someone first mentions it in that thread.
Cause Those channels deliver only to an end user who has already written, by userId. Fix Pass to: { userId }; phoneNumber works only on WhatsApp and SMS, and email only on email and Slack.
Cause The agent’s Slack bot is not a member of that channel. Fix Invite it with /invite @your-bot in the channel, then send again.

Next steps

About channels

Inbound and outbound vocabularies.

Channels reference

send, whatsapp.sendTemplate, email.send, getStatus, and listDeliveries.

WhatsApp

Link the channel and get a message template approved.

Schedule a recurring job

The job that carries your outbound messages.