Skip to main content
Guests message this agent on WhatsApp to open a door. The unlock_door tool identifies the guest by the phone number WhatsApp verified, looks for an active booking in Data, applies a per-guest rate limit, pulses a relay through a small HTTP API on a Raspberry Pi, and writes an audit entry. A staff-only tool registers bookings. It is five files, four TypeScript and one Python, and every TypeScript file compiles against lua-cli 3.33.0; run it with the steps on Running any example. Verified against lua-cli 3.33.0.

The conversation

  1. A guest writes “open the front door”. The model calls unlock_door with door: "front"; the tool reads the guest’s phone numbers from the profile, finds a booking that is active now and includes that door, checks that the guest has fewer than three unlocks in the last minute, calls the Pi, and logs the unlock.
  2. A guest with no booking gets a refusal from the tool, and the persona tells them to contact the front desk. The model can’t bypass the check, because the check lives inside the only tool that unlocks.
  3. At check-in, a staff member calls register_guest. The tool’s condition hides it from everyone whose profile isn’t marked role: 'staff'.

Primitives and channels

  • Skill and tools: door-access, with unlock_door and register_guest.
  • Runtime objects: User.get and _luaProfile.mobileNumbers, Data.get, Data.create, and env for DOOR_CONTROLLER_URL, DOOR_CONTROLLER_KEY, and UNLOCK_MS.
  • Channels: WhatsApp. The verified phone number is the whole identity model, so on a channel without one the tool refuses.
  • Hardware: a Raspberry Pi with a relay module on BCM pins 17 (front) and 27 (garage), driving a fail-secure electric strike from its own 12 V supply.
Never power the lock from the Pi. Use a relay module with optical isolation, a separate 12 V supply for the strike, and a flyback diode across the coil.

The code

The Pi accepts only the door names it knows and caps the pulse length; the agent never sends a pin number.
edge_api.py
The unlock tool takes identity from the profile, checks the booking and the rate limit, calls the Pi, and writes the audit entry; the door names are an enum, so the model can only name doors that exist.
src/skills/tools/UnlockDoorTool.ts
The staff tool is hidden by its condition unless the profile says role: 'staff'; a hidden tool is not in the model’s tool list at all.
src/skills/tools/RegisterGuestTool.ts
The skill’s context tells the model that a refusal is final.
src/skills/door-access.skill.ts
The persona keeps replies short and never describes the hardware.
src/index.ts

First run

On the Pi (Raspberry Pi OS), install the dependencies and start the API with a secret of your own.
From your laptop on the same network, pulse the front door once.
In the agent project, set the variables and run the tool. Under lua test, User.get() resolves to your own developer profile; with no channel-verified phone number on it, the tool refuses before it touches the Pi, which is what it does on any channel without a verified number.
Output
For the real thing, connect a WhatsApp number (WhatsApp), point DOOR_CONTROLLER_URL at an address the platform can reach (see Ways to make it yours), and set the variables with lua env production. Release it with lua push all --ci --force, lua version create --ci -m "<message>", and lua version promote <n> (Release an agent to production explains what each command changes), register your own number with register_guest from a staff conversation, and send “open the front door” from that phone.

Ways to make it yours

  • Deployed code runs on the platform, so DOOR_CONTROLLER_URL must be an HTTPS address the platform can reach, such as a reverse proxy or tunnel in front of the Pi; raspberrypi.local works only for lua test on your own network. To avoid an inbound port, connect the Pi as a device instead, so it dials out and its commands become tools.
  • Mark staff by writing role: 'staff' to their profile from a tool or the user data REST endpoints; condition hides register_guest from everyone else.
  • Create bookings from your property system with a webhook instead of register_guest, writing the same fields to guests.
  • Tune the limits: three unlocks per minute in unlock_door, a two-second throttle and a 10-second cap in edge_api.py, and UNLOCK_MS per building.

Next steps

Identify users

What the profile holds per channel and how to add fields such as role.

Connect your first device

Let hardware dial out to the platform instead of exposing an HTTP port.

Store and search data

Filters, pagination, and indexes for collections such as guests.