Skip to main content
This agent answers questions from help articles stored in Data, opens a Zendesk ticket when the articles don’t help, tells the customer on their own channel when Zendesk marks the ticket solved, and reminds customers with open tickets every morning. It is six files, 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. The customer asks a question. The model calls search_knowledge_base, which runs a semantic search over the help_articles collection and returns the closest articles with a score.
  2. If nothing relevant comes back, the model offers a ticket and calls create_ticket. The tool takes the requester’s email from the profile the channel verified, creates the ticket through Zendesk’s REST API, and stores a copy in support_tickets.
  3. When a support agent solves the ticket, Zendesk calls the zendesk-ticket-events webhook, which finds the customer by email and sends a message on the channel they last used.
  4. Every day at 10:00, the ticket-follow-up job messages every customer whose stored ticket is still open.

Primitives and channels

  • Skill and tools: support, with search_knowledge_base and create_ticket.
  • Webhook: zendesk-ticket-events, authenticated with the bearer token you set on the Zendesk webhook.
  • Job: ticket-follow-up, cron 0 10 * * * in Europe/London, two attempts.
  • Runtime objects: Data.search, Data.create, Data.get, User.get, user.send, and env for ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, ZENDESK_API_TOKEN, and ZENDESK_WEBHOOK_TOKEN.
  • Channels: any inbound channel. user.send delivers where the customer last wrote when that is WhatsApp, Messenger, Instagram, Teams, SMS, or MessageBird, and resolves true whether or not it delivered, so confirm with lua logs; for WhatsApp, Send proactive messages explains the 24-hour window.

The code

The search tool returns the top five articles above a similarity of 0.7.
src/skills/tools/SearchKnowledgeBaseTool.ts
The ticket tool prefers the email address the channel verified, and only falls back to one the model collected when the profile has none.
src/skills/tools/CreateTicketTool.ts
The skill’s context tells the model to search before it offers a ticket.
src/skills/support.skill.ts
The webhook checks the bearer token you configure on the Zendesk webhook, validates the body from its payload template, and messages the customer when the event is ticket.solved.
src/webhooks/ZendeskWebhook.ts
The job reads the stored tickets and messages each customer; it runs outside any conversation, so it looks every user up by email.
src/jobs/TicketFollowUpJob.ts
The agent registers all three; anything not listed here is not compiled.
src/index.ts

First run

Set the Zendesk values for local runs, then run the search tool. With an empty help_articles collection it returns no articles, which is the branch that leads to a ticket.
Output
Run the webhook with the bearer token and a solved-ticket body, then the job once; User.get returns null for an email no end user has, so neither sends anything.
For the full loop with the model, send lua chat --ci -e sandbox -m "How do I reset my password?" -t; it uploads the .env values with the sandbox version, so the same four variables apply. Then 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.

Ways to make it yours

  • Fill help_articles with Data.create('help_articles', article, searchText), where searchText is the title and body joined; Store and search data shows the pattern. To skip the search tool entirely, upload documents in the admin dashboard and use the knowledge feature.
  • In Zendesk, create a webhook that posts the bodySchema JSON from its payload template, with bearer-token authentication set to the value of ZENDESK_WEBHOOK_TOKEN; Handle a webhook explains where the URL comes from and when to use a Lua secret instead.
  • Swap Zendesk for another ticketing system by changing create_ticket and the webhook’s bodySchema; the skill, job, and persona stay the same.
  • Escalation rules such as the $500 threshold live in the persona, so an operator can change them without a release.

Next steps

Handle a webhook

Secret, signature check, and idempotent handling for events from your systems.

Schedule a job

Cron, interval, and one-time schedules, retries, and how to run a job by hand.

Store and search data

Collections, filters, and semantic search with Data.