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

# Expose device commands as tools

> Declare commands from the device or with defineDevice, read the results the model gets back, and test a command by hand

After this guide, each command your device accepts is a tool the model calls while the device is online, and you know what the model sees when a call fails. Declare commands on the device when the device owns its capabilities; declare them with [`defineDevice`](/reference/sdk/device-definition) when you want timeouts, retries, and group fan-out under version control in the agent project.

*Verified against lua-cli 3.33.0.*

**Before you begin**

* A [device credential](/devices/credentials) with the `commands` operation.
* The Node or Python client installed, or `lua-device` from `npm install -g @lua-ai-global/device-client` for the `defineDevice` path.

<Steps>
  <Step title="Declare the commands">
    A command has a `name` matching `^[a-z][a-z0-9_]{0,63}$`, a `description` the model reads as the tool description, and an optional JSON Schema `inputSchema`.

    <Tabs>
      <Tab title="From the device">
        The list is sent at connect time; nothing is pushed. Change it and restart the device.

        ```ts device.ts theme={null}
        import { DeviceClient } from '@lua-ai-global/device-client';

        const device = new DeviceClient({
          agentId: process.env.LUA_AGENT_ID!,
          deviceCredential: process.env.LUA_DEVICE_CREDENTIAL!,
          deviceName: 'label-printer',
          commands: [
            {
              name: 'print_label',
              description: 'Print a shipping label for an order',
              inputSchema: {
                type: 'object',
                properties: {
                  orderId: { type: 'string', description: 'Order to print the label for' },
                  copies: { type: 'integer', minimum: 1, maximum: 5, default: 1 },
                },
                required: ['orderId'],
              },
              timeoutMs: 20000,
            },
            { name: 'paper_level', description: 'Report the paper level as a percentage' },
          ],
        });
        ```
      </Tab>

      <Tab title="With defineDevice">
        The declaration lives in the agent project and is versioned with `lua push device`. The device connects with the same `name` and sends no command list, for example with `lua-device connect`.

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

        export const labelPrinter = defineDevice({
          name: 'label-printer',
          description: 'Thermal label printer at the packing bench',
          commands: {
            print_label: {
              description: 'Print a shipping label for an order',
              inputSchema: z.object({
                orderId: z.string().describe('Order to print the label for'),
                copies: z.number().int().min(1).max(5).default(1),
              }),
              timeoutMs: 20000,
              retry: { maxAttempts: 2, backoffMs: 1000 },
            },
            paper_level: { description: 'Report the paper level as a percentage' },
          },
        });
        ```

        Register it on the agent with `devices: [labelPrinter]`, then `lua push device --name label-printer --auto-deploy`. 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.
      </Tab>
    </Tabs>
  </Step>

  <Step title="Write the handlers">
    A handler receives the payload the model produced and returns the JSON the model gets back. A thrown error becomes `{ success: false, error: '<message>' }`.

    ```ts device.ts theme={null}
    device.onCommand('print_label', async (payload) => {
      const copies: number = payload.copies ?? 1;
      // Drive the printer here.
      return { printed: copies, orderId: payload.orderId };
    });

    device.onCommand('paper_level', async () => ({ percent: 63 }));

    device.connect().catch((err: Error) => console.error(err.message));
    ```
  </Step>

  <Step title="Check the tools the model has">
    While the device is online the agent has `device__label_printer__print_label`, `device__label_printer__paper_level`, and `device__label_printer__is_online`; hyphens in the device name become underscores. Each command tool is described as `[Device: label-printer] <description>. If the device is offline, this will return an error.` When a self-describing device goes offline its command tools are removed on the next turn; a device declared with `defineDevice` keeps its tools, and calls return `DEVICE_OFFLINE`. Either way `is_online` stops answering `{ status: 'online' }`.
  </Step>

  <Step title="Read the results">
    A successful call returns the device's reply as `{ commandId, success: true, data }`. Failures come back as `{ success: false, error, message }` with one of these `error` values.

    | `error`             | Cause                                                       | Retried           |
    | ------------------- | ----------------------------------------------------------- | ----------------- |
    | `DEVICE_OFFLINE`    | Device not connected                                        | No                |
    | `TOO_MANY_REQUESTS` | 100 commands already in flight for the agent                | No                |
    | `TIMEOUT`           | No response within `timeoutMs` (30,000 ms default) plus 2 s | Only with `retry` |
    | `DEVICE_ERROR`      | Transport or platform failure                               | Only with `retry` |
    | `MAX_RETRIES`       | Every attempt failed                                        | —                 |

    A handler that throws returns `success: false` with the error message and is not retried.
  </Step>

  <Step title="Verify">
    Send one command by hand and read the reply and latency; the command name is prompted for.

    ```bash theme={null}
    lua devices test --device-name label-printer --payload '{"orderId":"ord_123456"}' --timeout 20000
    ```

    `lua devices test` runs the command regardless of which declaration path you used. Under `--ci` it exits `1` at the prompt.
  </Step>
</Steps>

## Options you may need

### Retries and timeouts

`timeoutMs` sets how long the platform waits per attempt. `retry: { maxAttempts, backoffMs }` re-sends after a timeout or transport failure, waiting `backoffMs × attempt` between attempts; an offline or rate-limited device is never retried. Both fields work in the device's command list and in `defineDevice`.

### Group fan-out

Give several `defineDevice` declarations the same `group` and the agent also gets `device__<group>__<command>__all`, which sends the command to every registered device in the group and returns `{ total, succeeded, failed, results }`; an offline member counts as failed. Membership is the `group` each device reports when it connects, so pass the same name to the client. Fan-out tools exist only for devices declared with `defineDevice`. To try a group without hardware, run the `lua-device` runner with a handlers file once per member. `lua devices test` sends one command to one connected device and can't call the `__all` tool, and `lua test` has no device type, so exercise fan-out with `lua chat -m`.

### Both declarations for one name

If a device connects with its own command list and a `defineDevice` declaration with the same name is pushed, the device's list wins and the declaration's commands are ignored for that device.

## If it isn't working

<Accordion title="A command is missing from the agent's tools">
  Its name doesn't match `^[a-z][a-z0-9_]{0,63}$`, its schema is over 4 KB, the device declared more than 128 commands, or the agent already has 128 device tools. Rename or trim, then reconnect.
</Accordion>

<Accordion title="Every call returns DEVICE_OFFLINE">
  Run `lua devices status --device-name <name> --ci`. `offline` or `registered` means the client isn't connected; `disabled` means someone ran `lua devices disable`, so run `lua devices enable --device-name <name>`.
</Accordion>

<Accordion title="Calls return TOO_MANY_REQUESTS">
  The agent has 100 commands waiting on replies. Handlers that never return hold a slot until the timeout; return or throw promptly.
</Accordion>

## Next steps

<Columns cols={2}>
  <Card title="Handle device triggers" href="/devices/triggers">The other direction: events from the device.</Card>
  <Card title="defineDevice reference" href="/reference/sdk/device-definition">Every field of a command and trigger declaration.</Card>
  <Card title="lua devices" href="/reference/cli/devices">List, status, enable, disable, test.</Card>
</Columns>
