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

# defineDevice and defineDeviceTrigger

> Server-side declaration of a device, its commands as agent tools, and the triggers it can fire

`defineDevice` declares a [device](/concepts/devices) the agent can control: each command becomes a tool the model can call, and each trigger is a handler that runs when the device reports an event. `defineDeviceTrigger` declares a standalone device trigger that is pushed and versioned on its own and that any device on the agent can fire. Register the results on [`LuaAgent`](/reference/sdk/luaagent) under `devices` and `deviceTriggers`. The device-side client is documented under [Devices](/devices/node-client).

*Verified against lua-cli 3.33.0.*

```ts theme={null}
import { defineDevice, defineDeviceTrigger } from 'lua-cli';
```

## Quick example

A printer with two commands and one built-in trigger:

```ts src/devices/LabelPrinter.ts theme={null}
import { defineDevice, Agents } from 'lua-cli';
import { z } from 'zod';

// Device-trigger handlers receive no environment variables, so the
// agent ID is a file constant (it isn't a secret).
const AGENT_ID = 'agent_abc123';

export const labelPrinter = defineDevice({
  name: 'label-printer',
  description: 'Zebra label printer in the packing area',
  group: 'printers',
  commands: {
    print: {
      description: 'Print a shipping label for a confirmed order',
      inputSchema: z.object({ orderId: z.string(), copies: z.number().int().min(1).default(1) }),
      timeoutMs: 30000,
      retry: { maxAttempts: 3, backoffMs: 1000 },
    },
    status: {
      description: 'Report paper and ribbon levels',
      timeoutMs: 5000,
    },
  },
  triggers: {
    paper_low: {
      description: 'Fires when the paper level drops below 15 percent',
      payloadSchema: z.object({ level: z.number() }),
      async execute(payload, { device }) {
        await Agents.invoke(AGENT_ID, {
          prompt: `Printer ${device.name} reports paper at ${payload.level}%. Tell the packing lead.`,
        });
      },
    },
  },
});
```

## Functions

### defineDevice()

Returns a `LuaDevice`; identical to `new LuaDevice(config)`.

```ts theme={null}
defineDevice(config: LuaDeviceConfig): LuaDevice
```

<ParamField path="name" type="string" required>
  Device identifier. Must equal the name the device connects with. `lua compile` warns unless it matches `^[a-z][a-z0-9-]*$`. Hyphens become underscores in the tool names the model sees.
</ParamField>

<ParamField path="description" type="string" default="''">
  Shown in `lua devices list`.
</ParamField>

<ParamField path="group" type="string">
  Group name, for example `printers`. Enables the fan-out tools described under Tools the model receives and the `lua devices list --group <name>` filter.
</ParamField>

<ParamField path="commands" type="Record<string, DeviceCommandConfig>" default="{}">
  Commands the agent can send, keyed by command name.
</ParamField>

<ParamField path="triggers" type="Record<string, DeviceTriggerConfig>" default="{}">
  Events this device can fire, keyed by the exact trigger name the device sends.
</ParamField>

**`DeviceCommandConfig`**

<ParamField path="description" type="string" required>
  What the command does. This is the tool description the model reads.
</ParamField>

<ParamField path="inputSchema" type="ZodType">
  Input schema; becomes the tool's input schema. Omit for a command without arguments.
</ParamField>

<ParamField path="timeoutMs" type="number" default={30000}>
  How long the platform waits for the device's reply.
</ParamField>

<ParamField path="retry" type="{ maxAttempts: number; backoffMs: number }">
  Retries after a timeout, a server error, or a network error, waiting `backoffMs × attempt` between attempts. An offline device or a rate limit is not retried. Default: one attempt.
</ParamField>

**`DeviceTriggerConfig`**

<ParamField path="description" type="string" required>
  When the trigger fires. Shown in listings.
</ParamField>

<ParamField path="payloadSchema" type="ZodType">
  Schema for the payload the device sends.
</ParamField>

<ParamField path="execute" type="(payload, context) => Promise<any>">
  Handler that runs on the platform when the device fires the trigger. `context` is `{ device: { name }, trigger: { name, triggerId } }`; the `agent` member in the type is not populated at runtime, so hand the event to the agent with [`Agents.invoke`](/reference/sdk/agents) or return `{ startWorkflow }`. The handler receives no environment variables, so `env()` is empty for every key; keep identifiers such as the agent ID in a constant. A trigger without `execute` is recorded but does nothing.
</ParamField>

### defineDeviceTrigger()

Returns a `LuaDeviceTrigger`; identical to `new LuaDeviceTrigger(config)`. A standalone trigger is compiled and pushed as its own primitive (`lua push device-trigger`) and matches an event from any device on the agent. A pushed `defineDevice` declaration reaches the runtime on the agent's next turn whether or not you publish it; `--auto-deploy` only records it as the active version. A standalone `defineDeviceTrigger` runs only after its pushed version is published, with `--auto-deploy` or the push prompt.

```ts theme={null}
defineDeviceTrigger(config: LuaDeviceTriggerConfig): LuaDeviceTrigger
```

<ParamField path="name" type="string" required>
  Trigger identifier. Must be exactly the string the device sends as the trigger name; no case or separator mapping is applied. `lua compile` warns unless it matches `^[a-z][a-z0-9_-]*$`.
</ParamField>

<ParamField path="description" type="string" default="''">
  When the trigger fires. Shown in listings.
</ParamField>

<ParamField path="payloadSchema" type="ZodType">
  Schema for the payload the device sends.
</ParamField>

<ParamField path="execute" type="(payload, context) => Promise<any>" required>
  Handler as for a built-in trigger; `context.device.name` names the device that fired it. The type declares `context` as `{ agent, device: { name } }`, but the runtime passes `{ device: { name }, trigger: { name, triggerId } }` and never populates `agent`. `lua compile` fails with `Device trigger must have an execute function` when it is missing.
</ParamField>

A standalone trigger that starts a [workflow](/concepts/workflows) run instead of an agent turn:

```ts src/devices/JamDetected.ts theme={null}
import { defineDeviceTrigger } from 'lua-cli';
import { z } from 'zod';

export const jamDetected = defineDeviceTrigger({
  name: 'jam-detected',
  description: 'Any printer reports a paper jam',
  payloadSchema: z.object({ code: z.string() }),
  async execute(payload, { device }) {
    return {
      startWorkflow: {
        name: 'printer-incident',
        input: { device: device.name, code: payload.code },
        idempotencyKey: `jam:${device.name}:${payload.code}`,
      },
    };
  },
});
```

## Tools the model receives

Every command of a device declared with `defineDevice` is offered to the model as a tool while the device is registered, online or not; a call to an offline device returns `DEVICE_OFFLINE`. A self-describing device's commands are offered only while it is online. You do not write a `LuaTool` per command.

| Tool name                         | Input                       | Returns                                                                       |
| --------------------------------- | --------------------------- | ----------------------------------------------------------------------------- |
| `device__<device>__<command>`     | The command's `inputSchema` | The device's reply, or `{ success: false, error, message }`                   |
| `device__<device>__is_online`     | none                        | `{ status: 'online' \| 'offline' \| 'registered' \| 'disabled' }`             |
| `device__<group>__<command>__all` | The command's `inputSchema` | `{ total, succeeded, failed, results: [{ device, success, data?, error? }] }` |

Hyphens in the device and group names are replaced by underscores. The tool description is `[Device: <device>] <description>. If the device is offline, this will return an error.` An agent exposes at most 128 device tools; further commands are dropped with a warning in the logs. Fan-out tools exist only for devices declared with `defineDevice`. A fan-out call addresses every registered device whose connection reported the group, online or not; an offline member fails and is counted in `failed`. Failed calls return `error` values `DEVICE_OFFLINE`, `TOO_MANY_REQUESTS`, `TIMEOUT`, `DEVICE_ERROR`, or `MAX_RETRIES`.

## Trigger dispatch

* The platform matches the trigger name the device sends by exact string equality: first against the `triggers` of the device that sent it, then against standalone device triggers on the agent. An unmatched name is logged and dropped.
* `execute` receives the payload and `{ device: { name }, trigger: { name, triggerId } }` and has up to 10 minutes to return, the platform's default cap for event handlers.
* Events are delivered at least once from a durable queue: a handler that throws or times out is run again, up to 3 attempts 60 seconds apart, with the same `triggerId`. Make handlers idempotent.
* A return value of `{ startWorkflow: { name, input?, idempotencyKey?, correlationKey?, tags?, initialState?, notify?, replyTo?, onBehalfOf? } }` starts a run of the named workflow on the agent; the same contract as a [`LuaTrigger`](/reference/sdk/luatrigger) transform. Any other return value is logged.
* A thrown error is logged and the attempt is retried as above; nothing is sent to the end user.

## Classes

`LuaDevice` and `LuaDeviceTrigger` are exported for `new LuaDevice(config)` and `new LuaDeviceTrigger(config)`; they have no methods.

<ResponseField name="LuaDevice" type="class">
  <Expandable title="properties">
    <ResponseField name="name" type="string">The configured name.</ResponseField>
    <ResponseField name="description" type="string">The configured description, or `''`.</ResponseField>
    <ResponseField name="group" type="string | undefined">The configured group.</ResponseField>
    <ResponseField name="commands" type="Record<string, DeviceCommandConfig>">The configured commands, or `{}`.</ResponseField>
    <ResponseField name="triggers" type="Record<string, DeviceTriggerConfig>">The configured triggers, or `{}`.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="LuaDeviceTrigger" type="class">
  <Expandable title="properties">
    <ResponseField name="name" type="string">The configured name.</ResponseField>
    <ResponseField name="description" type="string">The configured description, or `''`.</ResponseField>
    <ResponseField name="payloadSchema" type="ZodType | undefined">The configured schema.</ResponseField>
    <ResponseField name="execute" type="function">The configured handler.</ResponseField>
    <ResponseField name="config" type="LuaDeviceTriggerConfig">The config object passed to the constructor.</ResponseField>
  </Expandable>
</ResponseField>

## CLI commands

| Task                              | Command                                                                         |
| --------------------------------- | ------------------------------------------------------------------------------- |
| Push a device definition          | `lua push device --name <name> --auto-deploy`                                   |
| Push a standalone trigger         | `lua push device-trigger --name <name> --auto-deploy`                           |
| List devices and connection state | `lua devices list [--group <group>]`, `lua devices status --device-name <name>` |
| Turn a device on or off           | `lua devices enable --device-name <name>`, `… disable …`                        |
| Send a command by hand            | `lua devices test --device-name <name> --payload '{"orderId":"123456"}'`        |

`lua deploy` has no device types, and devices and device triggers aren't part of an agent version. `--auto-deploy` publishes the pushed version in the same command; without it the push asks whether to publish, and `--force` answers no. A pushed `defineDevice` declaration reaches the runtime on the agent's next turn whether or not you publish it; `--auto-deploy` only records it as the active version. A standalone `defineDeviceTrigger` runs only after its pushed version is published, with `--auto-deploy` or the push prompt.

## Types

All of these are exported from `'lua-cli'`.

<ResponseField name="LuaDeviceConfig" type="interface">
  `{ name; description?; group?; commands?; triggers? }` as documented under `defineDevice()`.
</ResponseField>

<ResponseField name="DeviceCommandConfig" type="interface">
  `{ description; inputSchema?; retry?; timeoutMs? }`.
</ResponseField>

<ResponseField name="DeviceTriggerConfig" type="interface">
  `{ description; payloadSchema?; execute? }`.
</ResponseField>

<ResponseField name="LuaDeviceTriggerConfig" type="interface">
  `{ name; description?; payloadSchema?; execute }`.
</ResponseField>

## See also

* [Devices](/concepts/devices) — transports, commands as tools, offline behavior
* [Commands and tools](/devices/commands-and-tools) — how a device describes its commands
* [Device triggers](/devices/triggers) — firing a trigger from the device side
* [`lua devices`](/reference/cli/devices) — the device management command
* [`Agents`](/reference/sdk/agents) — invoking the agent from a trigger handler
