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

# Add a tool to a skill

> Write a LuaTool class with a Zod input schema, register it in a skill, test it locally and in the sandbox, and release it

After this guide, the model calls a tool you wrote whenever an end user's request needs it. A [tool](/concepts/skills-and-tools) runs only when the model calls it during a conversation; for work that starts outside one, [handle a webhook](/build/handle-a-webhook) or [create a trigger](/build/create-a-trigger) instead.

*Verified against lua-cli 3.33.0.*

**Before you begin**

* A project created with `lua init` and signed in with `lua auth configure` ([Build and release your first agent](/get-started/quickstart)).
* `zod` in `package.json`; the project scaffold adds it.

<Steps>
  <Step title="Write the tool">
    Create a class that implements `LuaTool`. `name` is what you test and log by, `description` is prompt text the model reads to decide when to call the tool, and `inputSchema` is the Zod object the model's arguments are parsed with before `execute` runs.

    ```ts src/skills/tools/CalculateRefundTool.ts theme={null}
    import { LuaTool } from 'lua-cli';
    import { z } from 'zod';

    export default class CalculateRefundTool implements LuaTool {
      name = 'calculate_refund';
      description =
        'Calculate the refund for a returned order under the 30-day returns policy. ' +
        'Use when a customer asks whether they can return an order or how much they get back.';
      inputSchema = z.object({
        orderTotal: z.number().positive().describe('Order total in the currency the customer paid in'),
        daysSincePurchase: z.number().int().min(0).describe('Whole days between the purchase and today'),
        opened: z.boolean().describe('True when the customer has opened the packaging'),
      });

      async execute(input: z.infer<typeof this.inputSchema>) {
        if (input.daysSincePurchase > 30) {
          return { eligible: false, refund: 0, reason: 'The return window is 30 days from purchase.' };
        }
        const restockingFee = input.opened ? 0.15 : 0;
        const refund = Math.round(input.orderTotal * (1 - restockingFee) * 100) / 100;
        return { eligible: true, refund, restockingFee };
      }
    }
    ```

    Return plain data, not prose: the value is serialized to JSON and handed to the model, which writes the reply. Each `.describe()` reaches the model too; use it to say what a field means and how to fill it.
  </Step>

  <Step title="Register it in a skill">
    A tool is compiled only when a skill on the `LuaAgent` references it. The skill's `context` tells the model when and how to use the tool ([Write skill context](/build/write-skill-context)).

    <Tabs>
      <Tab title="New skill">
        ```ts src/skills/returns.skill.ts theme={null}
        import { LuaSkill } from 'lua-cli';
        import CalculateRefundTool from './tools/CalculateRefundTool';

        export default new LuaSkill({
          name: 'returns',
          description: 'Returns and refunds under the 30-day policy',
          context:
            'You handle returns for Acme orders. Before calling calculate_refund, make sure you have ' +
            'the order total, the purchase date, and whether the packaging was opened; ask for ' +
            'whatever is missing. Quote the refund amount and any restocking fee in the reply. ' +
            'Never promise a refund for an order older than 30 days.',
          tools: [new CalculateRefundTool()],
        });
        ```

        Register the new skill on the agent. The file below is the quickstart's; if yours differs, add only the highlighted lines to your own `LuaAgent`.

        ```ts src/index.ts highlight={3,8} theme={null}
        import { LuaAgent } from 'lua-cli';
        import weatherSkill from './skills/weather.skill';
        import returnsSkill from './skills/returns.skill';

        const agent = new LuaAgent({
          name: 'docs-quickstart',
          persona: 'You are a weather assistant. Answer in one or two sentences.',
          skills: [weatherSkill, returnsSkill],
        });
        ```
      </Tab>

      <Tab title="Existing skill">
        Add an instance to `tools` and extend the `context`. The file below is the skill from [Build a support agent](/get-started/build-an-agent); if yours differs, add only the highlighted lines to your own `LuaSkill`.

        ```ts src/skills/support.skill.ts highlight={4,8,13-15} theme={null}
        import { LuaSkill } from 'lua-cli';
        import LookupOrderTool from './tools/LookupOrderTool';
        import CreateTicketTool from './tools/CreateTicketTool';
        import CalculateRefundTool from './tools/CalculateRefundTool';

        export default new LuaSkill({
          name: 'support',
          description: 'Order status lookups, support tickets, and refunds',
          context: `Use lookup_order when the user asks where an order is or what its status is.
        Ask for the order number if the user hasn't given one.
        If lookup_order returns found: false, say the order isn't in the system yet and offer to open a ticket.
        Use create_ticket when the user reports a problem with an order. Confirm the ticket id back to them.
        Before calling calculate_refund, make sure you have the order total, the purchase date, and whether
        the packaging was opened; ask for whatever is missing. Quote the refund and any restocking fee.`,
          tools: [new LookupOrderTool(), new CreateTicketTool(), new CalculateRefundTool()],
        });
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Compile">
    `lua compile` bundles every primitive the agent references into `dist-v2/` and reports what it found.

    ```bash theme={null}
    lua compile --ci
    ```

    ```text Output theme={null}
    …
    ✅ Compiled 5 primitives (1 agent, 2 skills, 2 tools) in 495ms
    ✨ Tip: run `lua test` to verify your tools work locally before pushing.
    ```

    The counts are the quickstart project plus the new skill; in any project the tool count goes up by one, and a class no registered skill references is left out without a message. A `name` outside lowercase letters, digits, hyphens, and underscores gets the warning `Tool name should be lowercase with hyphens or underscores` and still compiles.
  </Step>

  <Step title="Run it locally">
    `lua test skill` runs `execute` with the JSON you pass. `--name` takes the tool's `name`, not the skill's.

    ```bash theme={null}
    lua test --ci skill --name calculate_refund --input '{"orderTotal":120,"daysSincePurchase":12,"opened":true}'
    ```

    ```text Output theme={null}
    …
    ✅ Selected tool: calculate_refund
    Input: {
      "orderTotal": 120,
      "daysSincePurchase": 12,
      "opened": true
    }

    🚀 Executing tool...
    ✅ Tool execution successful!

    Tool returned: Object — fields: eligible, refund, restockingFee
    Output:
    { eligible: true, refund: 102, restockingFee: 0.15 }
    ```

    No model is involved and the input isn't parsed against `inputSchema`, so the output is exactly what `execute` returned. A tool that throws still exits 0 and prints `{ status: 'error', error: '<message>' }`.
  </Step>

  <Step title="Try it in the sandbox">
    `lua chat` compiles the project, uploads your skills as sandbox versions, and runs a conversation with them; nothing is pushed and end users see no change ([About environments](/concepts/environments)). The first sandbox run after adding a skill registers it, prints `Skipping skill returns - no skillId found in lua.skill.yaml`, and answers without the tool, so run the command twice. `-t` starts a fresh thread so earlier messages don't shape the answer.

    ```bash theme={null}
    lua chat -m "Can I return the 120 order I bought 12 days ago? I opened the box." -t
    ```

    The reply quotes 102 and the 15% restocking fee.
  </Step>

  <Step title="Release">
    `lua push` uploads a skill version and changes nothing for end users. `lua version create` snapshots every primitive into an [agent version](/concepts/releases-and-versions), and `lua version promote` makes that snapshot live for everyone on their next message; it is also the rollback path.

    ```bash theme={null}
    lua push all --ci --force
    lua version create --ci -m "Add calculate_refund"
    lua version promote <n>
    ```

    `lua version create` prints ``✓ Created v<n> (staged). Run `lua version promote v<n>` to deploy.``; `<n>` comes from that line, `promote` accepts `<n>` or `v<n>` and asks no confirmation, and in a script `n=$(lua version list --limit 1 --json --ci | jq -r '.[0].version')` reads it. `lua deploy skill --name <skill> --set-version <v>` serves that version from the agent's next turn, but the next `lua version promote` resets every skill to the version pinned in the promoted agent version, so a skill deployed without a new agent version is reverted by the next promote; for an immediate rollback of one skill, deploy the earlier version. The full flow, review, and rollback are in [Release an agent to production](/ship/releasing).
  </Step>

  <Step title="Verify">
    List the production skills. Each is printed as `📦 <name>` with its `Skill ID`; `returns` shows `Deployed ⭐` and a `Deployed:` line with the time of the promote.

    ```bash theme={null}
    lua skills view --ci
    ```

    Then send the sandbox message to production and read the call in the skill's log (`--name` is the skill).

    ```bash theme={null}
    lua chat -e production -m "Can I return the 120 order I bought 12 days ago? I opened the box." -t
    lua logs --type skill --name returns --limit 5 --ci
    ```
  </Step>
</Steps>

## If it isn't working

<AccordionGroup>
  <Accordion title="not_found: Tool &#x22;calculate_refund&#x22; not found">
    `--name` takes the tool's `name`, not the skill's or the class's, and the tool must be in a skill listed in `LuaAgent.skills`. Run `lua compile --ci` and check the tool count.
  </Accordion>

  <Accordion title="The model answers without calling the tool">
    On the first sandbox run after adding a skill this is expected; run it again. Otherwise the skill's `context` or the tool's `description` doesn't say when the tool applies; see [Write skill context](/build/write-skill-context). Use `-t` so an earlier answer doesn't anchor the next one.
  </Accordion>

  <Accordion title="Production still runs the old code after lua deploy skill">
    A later `lua version promote` reset the skill to the version pinned in that agent version, or `--set-version latest` picked the most recently created version rather than the one you meant. Run `lua version create --ci -m "Add calculate_refund"` and `lua version promote <n>` after the push so the change survives the next promote.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="Write skill context" href="/build/write-skill-context">Make the model call the right tool with the right arguments.</Card>
  <Card title="Call your API" href="/build/call-your-api">Read a key from the environment and call an HTTP API from execute.</Card>
  <Card title="LuaTool reference" href="/reference/sdk/luatool">Every member, the voice flags, and condition.</Card>
  <Card title="lua test reference" href="/reference/cli/test">Types, input shapes, and JSON output.</Card>
</Columns>
