Action is a powerful abstraction for handling user actions in your app. It allows you to define user actions with a brilliant interface inspired by tRPC.

The interfaces within Lusat are built on top of Action and provide global and contextual hotkeys, command palettes, search, and AI copilot / voice assistant capabilities.

import { z } from 'zod'
import { app, action } from 'lusat'

export const todoApp = app({
  name: 'Todos',
  actions: {
    // Define an action:
    listAll: action()
      .describe('List all todos')
      .handle(() => {/* ... */}),
    // Optionally define an input schema:
    create: action()
      .describe('Create a todo')
        text: z.string().max(280),
      .handle(({ text }) => {/* ... */}),


The action() function creates an Action builder, tRPC-style. It can be given a name as an optional argument, or infer its name from a parent app. It supports the following methods:

  • describe(): Sets a natural-language description of the action, which is used both in visual UI and for AI copilot reasoning.

  • metadata(): Sets a metadata object for the action. This can be used to store arbitrary data about the action, which can be used for things like icons and labels in menus, logging, etc.

  • input(): Sets an input schema for the action. This provides typesafety in your code, validates input, helps with AI copilot reasoning, and can be used to generate dynamic UI. If there is no input schema, the action will be nullary, meaning it takes no arguments.

  • output(): Sets an output schema for the action. This provides typesafety in your code, validates output, helps with AI copilot reasoning, and can be used to generate dynamic UI.

  • middleware(): Adds a middleware function to the action. Middleware functions are called in order, and can be used to modify the input or output of the action, or to perform side effects. If a middleware throws, the action will not be called. Nullary action middelwares don't pass any data, but still block the action if they throw.

  • handle(): Sets the handler function for the action. This is the function that will be called when the action is triggered. It is passed the validated input (if any), and will run the output validator after it returns.

Zod schemas

You must use zod for your input and output schemas. Contact us if you'd like Lusat to support other schema libraries. Schemas are used for parsing, validation, AI copilot reasoning, and dynamic UI generation.

You can use the describe() method on any shape in a schema to add natural-language information about the shape. You can also supply custom error messages to your schema validators. This can be helpful for AI copilot reasoning and allows dynamic UI to show better labels and error messages.

See example below. Also, beware, lengthy descriptions will eat into your AI copilot memory and increase costs.

import { z } from 'zod'
import { action } from 'lusat'

const myAction = action("Update profile")
  .describe("Update your profile")
    name: z.string().max(70, {
      message: "Your name must be less than 70 characters",
    }).describe("Your display name"),
    bio: z.string().max(280, {
      message: "Your bio must be less than 280 characters",
    }).describe("Your bio"),
    email: z.string().email({
      message: "Please enter a valid email address",
    }).describe("Your email address"),
  .handle(({ name, bio, email }) => {
    // ...

We recommend that you start with as little extra information as possible in your schemas, and add more as you need it. Adding natural language descriptions and error messages takes time, creates more work to update as your underlying code changes, and can reduce the available memory in your AI reasoning.

It can however be a great way to increase the accuracy and reliability of your AI copilot, and a useful place to define metadata for your action that can be used across your interfaces in dynamic UI, logging, error dialogs, and more.


Apps are the top-level abstraction in Lusat. They are used to group actions together, and provide a way to access them from anywhere in your app. They also provide a way to define global hotkeys and commands, and to trigger the command palette.

Apps are created with the app() function, which gives you a lightweight and typesafe way to define a Lusat App object.

They contain metadata, just like actions, and the following:

  • actions: An object containing all of the actions in the app. This is used to index the actions within an app and is leveraged by AI reasoning and dynamic UI like command palettes.

  • models: A record of zod schemas. This is used during AI reasoning to help the copilot understand the data in your app and can dramatically optimize the compute cost, available memory, and speed of your copilot.

  • examples: A set of example prompts and workflows that can be used to train the AI copilot. This is used to help the copilot understand the context of your app and can improve the quality of its reasoning.

You can use the app, models, and actions functions to define an app either all in one place or in separate files. These functions are lightweight object wrappers for typesafety. See the example below.

import { z } from 'zod'
import { app } from 'lusat'
import { musicModels } from './models'
import { musicActions } from './actions'

// Define the app:
export const musicApp = app({
  name: 'Music Player',
  models: musicModels,
  actions: musicActions,
  examples: [
      prompt: 'Play a song',
      workflow: [
          action: 'play',
          input: 'shall we'

By isolating action and model definitions before you aggregate them within Lusat apps, you can keep a large codebase organized and maintainable.

You can also export those models and actions individually and use them in multiple Lusat apps, load them dynamically, or even use them in other contexts outside of Lusat, like sharing the zod schemas for your models across your codebase and calling your actions directly from your UI code.

The possibilities are endless! Jokes aside, we created this toolset to augment your existing code and give you the flexibility to use it however you want. Please use it in whatever way makes sense for your app.

Join our newsletter to stay up to date on our progress!