Skip to main content
After this guide, your tools know who they are talking to, remember what they learn, and can reach the same person from code that runs outside a conversation. User.get() is the current end user inside a tool; in a webhook, trigger, or job nobody is talking, so you pass an id (execution contexts). For records that aren’t about one person, use Data. Verified against lua-cli 3.33.0. Before you begin
  • A skill on the agent to hold the tools (Add a tool to a skill).
  • An idea of what you’ll store per person: an account id, preferences, or progress through a flow.
1

Read the current end user

User.get() is typed UserDataInstance | null, so narrow it before use. The instance has two parts: _luaProfile, the platform’s read-only identity, and your own fields, which live on the record directly.
src/skills/tools/GetProfileTool.ts
The profile is the same record whichever channel the message came from: a sender whose phone number or email is already on a profile resolves to it, so what you store follows the person across channels. _luaProfile can’t be written; assignments to it are ignored.
2

Store what you learn

user.patch sets and removes top-level fields in one request; user.update merges fields; direct assignments stay local until user.save(). The record is per agent and shared by the sandbox and production.
src/skills/tools/LinkAccountTool.ts
Store the _luaProfile.userId in your own system when you first meet a person; it is the key that finds them again from outside a conversation.
3

Reach the end user outside a conversation

A webhook, trigger, or job has no current end user: a bare User.get() fails there in deployed code and resolves to you under lua test. Pass the id you stored, or look the person up by email or phone; null means nobody matched.
src/webhooks/AccountSuspendedWebhook.ts
user.send posts into the person’s conversation on the channel they last used: WhatsApp, Messenger, Instagram, a Teams personal chat, MessageBird, or SMS, and the web widget always receives a live copy; a person whose last channel was Slack, Front, iMessage, or RCS can’t be reached this way. In a deployed agent the call resolves true whether or not the message was delivered, and under lua test it throws on failure, so never branch on its return value; confirm delivery in lua logs. To pick a channel, reach someone with no prior conversation, or get a deliveryId for Channels.getStatus, use Channels.send.
4

Keep each account's data separate

Data has no tenant concept, so put the account id on every entry you write and filter by it on every read. Take the id from the end user’s record, never from a tool argument: an argument is whatever the model was told; the record is what your code verified.
src/skills/tools/ListAccountTicketsTool.ts
To hide a whole skill until an account is linked, put the same accountId check in a skill condition (Write skill context).
5

Register the skill and the webhook

Tools are compiled only from a skill on LuaAgent.skills, and the webhook only from LuaAgent.webhooks.
src/skills/accounts.skill.ts
The agent file below is the quickstart’s; if yours differs, add only the highlighted lines to your own LuaAgent.
src/index.ts
6

Verify

lua test runs as you, the signed-in developer, so the write lands on your own record for this agent and get_profile returns your profile.
Output
Run lua test --ci skill --name get_profile --input '{}' next: accountId reads acct_abc123, and emails lists the address you signed in with. Then run the webhook with an address nobody has.
Output
With your own userId from get_profile instead, user.send throws Failed to send message unless you have a conversation on a channel, and the run prints { status: 'error', error: 'Failed to send message' }.
7

Release

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). Run `lua version promote v<n>` to deploy.; <n> comes from that line, promote accepts <n> or v<n> and asks no confirmation, and in a script n=$(lua version list --limit 1 --json --ci | jq -r '.[0].version') reads it.

Options you may need

Look an end user up by phone

User.get({ phone: '+15551234567' }) accepts the number with or without the leading +. When both email and phone are given, email wins. To read the person’s turns with this agent, call User.getChatHistory() (User reference).

If it isn’t working

There is no current end user in those contexts. Pass the id you stored, User.get(userId), or look the person up with { email } or { phone } and handle null.
Neither the phone number nor the email was on the profile yet, so the channel identity resolved to a new one. Ask for a verified contact detail, or store your own key in both places and look up by it.
update() only merges. Use patch({ unset: ['field'] }) or unset('field'), which remove top-level fields; a field set to null is stored as null.

Next steps

User reference

get, lookups, patch, send, Inbox, and every type.

Execution contexts

What the current user resolves to in each place your code runs.

Send proactive messages

Reach a person on a chosen channel, and the WhatsApp 24-hour window.

Store and search data

Records that aren’t about one person.