> ## 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.

# Identify users

> Read the current end user, store what you learn on their record, reach them from a webhook or job, and keep each account's data separate

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()`](/reference/sdk/user) is the current end user inside a tool; in a webhook, trigger, or job nobody is talking, so you pass an id ([execution contexts](/concepts/execution-contexts)). For records that aren't about one person, use [`Data`](/build/store-and-search-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](/build/add-a-tool)).
* An idea of what you'll store per person: an account id, preferences, or progress through a flow.

<Steps>
  <Step title="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.

    ```ts src/skills/tools/GetProfileTool.ts theme={null}
    import { LuaTool, User } from 'lua-cli';
    import { z } from 'zod';

    export default class GetProfileTool implements LuaTool {
      name = 'get_profile';
      description = "Read the current customer's name, contact details, and linked account.";
      inputSchema = z.object({});

      async execute() {
        const user = await User.get();
        if (!user) throw new Error('No end user in this context');
        return {
          userId: user._luaProfile.userId,
          name: user._luaProfile.fullName,
          emails: user._luaProfile.emailAddresses,
          phones: user._luaProfile.mobileNumbers,
          accountId: user.accountId ?? null,
        };
      }
    }
    ```

    The profile is the same record whichever [channel](/concepts/channels) 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.
  </Step>

  <Step title="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.

    ```ts src/skills/tools/LinkAccountTool.ts theme={null}
    import { LuaTool, User } from 'lua-cli';
    import { z } from 'zod';

    export default class LinkAccountTool implements LuaTool {
      name = 'link_account';
      description = 'Link the customer to their Acme account once they have given a valid account number.';
      inputSchema = z.object({
        accountId: z.string().regex(/^acct_[a-z0-9]+$/).describe('Account number, for example acct_abc123'),
      });

      async execute(input: z.infer<typeof this.inputSchema>) {
        const user = await User.get();
        if (!user) throw new Error('No end user in this context');
        await user.patch({
          set: { accountId: input.accountId, linkedAt: new Date().toISOString() },
          unset: ['pendingAccountId'],
        });
        return { linked: true, accountId: input.accountId };
      }
    }
    ```

    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.
  </Step>

  <Step title="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.

    ```ts src/webhooks/AccountSuspendedWebhook.ts theme={null}
    import { LuaWebhook, User } from 'lua-cli';
    import { z } from 'zod';

    export default new LuaWebhook({
      name: 'account-suspended',
      description: 'Tells a customer that their account was suspended',
      bodySchema: z.object({
        userId: z.string().optional(),
        email: z.string().email().optional(),
        reason: z.string(),
      }),
      async execute({ body }) {
        const user = body.userId
          ? await User.get(body.userId)
          : body.email
            ? await User.get({ email: body.email })
            : null;
        if (!user) return { found: false };
        await user.send([{ type: 'text', text: `Your account was suspended: ${body.reason}` }]);
        return { found: true, userId: user._luaProfile.userId };
      },
    });
    ```

    `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`](/build/send-proactive-messages).
  </Step>

  <Step title="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.

    ```ts src/skills/tools/ListAccountTicketsTool.ts theme={null}
    import { LuaTool, User, Data } from 'lua-cli';
    import { z } from 'zod';

    export default class ListAccountTicketsTool implements LuaTool {
      name = 'list_account_tickets';
      description = "List the open support tickets on the customer's linked account.";
      inputSchema = z.object({});

      async execute() {
        const user = await User.get();
        if (!user?.accountId) return { error: 'Ask the customer to link their account first.' };
        const page = await Data.get(
          'tickets',
          { accountId: user.accountId, status: { $ne: 'closed' } },
          1,
          50
        );
        return page.data.map((entry) => ({ id: entry.id, ...entry.data }));
      }
    }
    ```

    To hide a whole skill until an account is linked, put the same `accountId` check in a skill `condition` ([Write skill context](/build/write-skill-context)).
  </Step>

  <Step title="Register the skill and the webhook">
    Tools are compiled only from a skill on `LuaAgent.skills`, and the webhook only from `LuaAgent.webhooks`.

    ```ts src/skills/accounts.skill.ts theme={null}
    import { LuaSkill } from 'lua-cli';
    import GetProfileTool from './tools/GetProfileTool';
    import LinkAccountTool from './tools/LinkAccountTool';
    import ListAccountTicketsTool from './tools/ListAccountTicketsTool';

    export default new LuaSkill({
      name: 'accounts',
      description: 'Account linking and per-account ticket lookups',
      context:
        'Call link_account once the customer gives an account number of the form acct_…; ' +
        'call list_account_tickets only after the account is linked. Never ask for a password.',
      tools: [new GetProfileTool(), new LinkAccountTool(), new ListAccountTicketsTool()],
    });
    ```

    The agent file below is the quickstart's; if yours differs, add only the highlighted lines to your own `LuaAgent`.

    ```ts src/index.ts highlight={3-4,9-10} theme={null}
    import { LuaAgent } from 'lua-cli';
    import weatherSkill from './skills/weather.skill';
    import accountsSkill from './skills/accounts.skill';
    import accountSuspended from './webhooks/AccountSuspendedWebhook';

    const agent = new LuaAgent({
      name: 'docs-quickstart',
      persona: 'You are a weather assistant. Answer in one or two sentences.',
      skills: [weatherSkill, accountsSkill],
      webhooks: [accountSuspended],
    });
    ```
  </Step>

  <Step title="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.

    ```bash theme={null}
    lua test --ci skill --name link_account --input '{"accountId":"acct_abc123"}'
    ```

    ```text Output theme={null}
    …
    🚀 Executing tool...
    ✅ Tool execution successful!

    Tool returned: Object — fields: linked, accountId
    Output:
    { linked: true, accountId: 'acct_abc123' }
    ```

    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.

    ```bash theme={null}
    lua test --ci webhook --name account-suspended --input '{"body":{"email":"nobody@example.com","reason":"Unpaid invoice"}}'
    ```

    ```text Output theme={null}
    …
    🚀 Executing webhook...
    Query: {}
    Headers: {}
    Body: {
      "email": "nobody@example.com",
      "reason": "Unpaid invoice"
    }

    ✅ Webhook execution successful!

    Webhook returned: Object — fields: found
    Output:
    { found: false }
    ```

    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' }`.
  </Step>

  <Step title="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](/ship/releasing)).

    ```bash theme={null}
    lua push all --ci --force
    lua version create --ci -m "Add account linking"
    lua version promote <n>
    ```

    `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.
  </Step>
</Steps>

## 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](/reference/sdk/user#getchathistory)).

## If it isn't working

<AccordionGroup>
  <Accordion title="User.get() fails in a webhook or job">
    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`.
  </Accordion>

  <Accordion title="The record is empty on a channel where the user already talked to the agent">
    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.
  </Accordion>

  <Accordion title="An unset field is still there">
    `update()` only merges. Use `patch({ unset: ['field'] })` or `unset('field')`, which remove top-level fields; a field set to `null` is stored as `null`.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="User reference" href="/reference/sdk/user">get, lookups, patch, send, Inbox, and every type.</Card>
  <Card title="Execution contexts" href="/concepts/execution-contexts">What the current user resolves to in each place your code runs.</Card>
  <Card title="Send proactive messages" href="/build/send-proactive-messages">Reach a person on a chosen channel, and the WhatsApp 24-hour window.</Card>
  <Card title="Store and search data" href="/build/store-and-search-data">Records that aren't about one person.</Card>
</Columns>
