Skip to main content
After this guide, an end user who asks for a person gets one: the agent flags the end user, stops answering, forwards each message to your team by email, and resumes when a teammate clears the flag. For a decision rather than a conversation, use a workflow approval. Verified against lua-cli 3.33.0. Before you begin
  • A project created with lua init and signed in with lua auth configure (Install and sign in).
  • An email channel on the agent (Email).
  • Your team’s inbox stored with lua env production -k HANDOFF_EMAIL -v [email protected], and with lua env sandbox, which writes .env for lua test.
1

Add a tool that asks for a person

The model calls this tool when the end user asks for a person. It flags the User record, shared across channels and environments, and emails your team the user ID and webhook URL that hand the conversation back.
src/skills/tools/RequestHumanTool.ts
2

Block the model while the flag is set

A preprocessor runs before the model on every message and receives the end user first. priority: 1 puts it before any other preprocessor; block ends the turn with response as the reply.
src/preprocessors/HandoffGate.ts
Channels.send has no Slack target and user.send() never picks Slack. To notify a Slack workspace, connect Slack as an integration and post through it; otherwise notify by email, WhatsApp, or SMS (Notify a teammate another way).
3

Add a way back

A webhook clears the flag when your team is done; it runs outside any conversation, so it looks the end user up by ID. user.send() delivers the closing message on the channel the end user last wrote from and mirrors it to the web widget; deployed, it resolves true whatever happened, so confirm delivery in lua logs. secret makes the platform reject calls without a valid x-lua-signature header; the compiler reads it, so use a literal or a const.
src/webhooks/ResumeHandoffWebhook.ts
4

Register everything on the agent

The skill’s context tells the model when to call the tool; the compiler bundles only what LuaAgent references.
src/index.ts
5

Run the handoff loop locally

lua test runs each primitive on your machine as you, the signed-in developer. Set your own flag with the tool; it emails the HANDOFF_EMAIL in .env.
Output
The gate now blocks you.
Output
Your user ID is auth.userId in lua status --json --ci and metadata.userId on skill log entries (lua logs --type skill --limit 1 --json --ci). The webhook clears the flag; the local run skips the signature check, so this is also how you clear your own flag if the signed request fails. Leave message out locally: user.send() under lua test throws Failed to send message without a direct-channel conversation, after unset has already cleared the flag.
Output
Rerun the preprocessor test: Action: PROCEED.
6

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.
The gate applies to every conversation from the promote on. A flag set during testing persists until the webhook clears it: User data is shared between sandbox and production.
7

Verify

Ask for a person, then send a second message.
The first reply comes from the model after it calls request_human; the handoff email arrives at HANDOFF_EMAIL. The second reply is the gate’s text (lua logs --type preprocessor --limit 5 shows the blocked turn). Then call the webhook at the URL from the email, signing the exact bytes you send.
The closing message arrives in the chat, and the next Hello? is answered by the model again.

Options you may need

Notify a teammate another way

Channels.send({ channel: 'whatsapp', to: { phoneNumber }, text }) or channel: 'sms' reaches a number that has never written to the agent; store it in an environment variable. Team.findMember('Dana') returns organization members matching a name with the channel handles they have shared.

If it isn’t working

Cause The tool was never called, so the flag was never set. Fix Make the skill context explicit about when to call request_human; lua logs --type skill --limit 5 shows whether it ran.
Cause HANDOFF_EMAIL is unset in the environment the chat runs in, or the agent has no email channel: No email channel configuration found. Ensure the agent has an email channel linked. Fix Check lua env production --list (or sandbox) and link an email channel.
Cause User.get(userId) found nobody: the body did not carry the Lua user ID. Fix Use the ID from the handoff email, or look the end user up with User.get({ email }) or User.get({ phone }).

Next steps

About processors

Block, proceed, priority, and failure handling.

User reference

get, update, unset, send, and the profile fields.

Handle a webhook

Signing, idempotent handling, and the URL.

Send proactive messages

Outbound channels and addressing.