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

# Skills and tools

> How tools become callable by the model, why context and description decide when they run, and how to share tool code

A tool is one function the model can call; a skill is a named group of tools with a shared prompt `context`. Together they are how an [agent](/concepts/agents) does things Lua doesn't ship: the model reads the skill's context and each tool's description, decides which tool to call and with what arguments, and Lua runs your `execute` function.

## How a tool reaches the model

A tool is a class that implements `LuaTool` from `'lua-cli'` with four members: `name` (letters, digits, hyphens, underscores), `description`, a zod `inputSchema`, and `execute`. The model's arguments are validated against `inputSchema` before `execute` runs, so the input you receive is typed and complete. Whatever `execute` returns is serialized to JSON and handed back to the model.

A skill wraps tools with a `name`, a `description`, a `context`, and the `tools` array. The skill is the unit Lua versions and pushes (`lua push skill --name orders`); tools are never pushed on their own.

```ts src/skills/orders.skill.ts theme={null}
import { LuaSkill } from 'lua-cli';
import GetOrderStatusTool from './tools/GetOrderStatusTool';

export default new LuaSkill({
  name: 'orders',
  description: 'Order status lookups',
  context:
    'You can look up order status. Ask for the order number if it is missing; ' +
    'never guess one. Quote the carrier and the delivery estimate in the reply.',
  tools: [new GetOrderStatusTool()],
});
```

`context` is injected into the prompt whenever the skill is active. Together with each tool's `description`, it is how the model decides when to call your tools, so both are prompt text, not documentation. `context` carries the rules that span tools (when to ask first, what never to do, what to say back); `description` says what one tool does and when it applies. Write both for the model that reads them, and name the tools you refer to.

```text Do theme={null}
Look up the shipping status of one order by order number.
Use when the customer asks where an order is or when it arrives.
```

```text Don't theme={null}
Order tool.
```

Two optional gates decide whether the model sees a tool at all. A tool's `condition` runs before each turn; when it returns `false` or throws, that tool is left out of the prompt but the skill's name and context stay, so the model can still say it can't do that. A skill's `condition` hides every tool and omits the context, so an end user who fails the check talks to an agent that has never heard of it. Both are `async` functions that return a boolean and usually read the current end user with `User.get()`, which is `null` when no end user is known.

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

export default class ExportOrdersTool implements LuaTool {
  name = 'export_orders';
  description = 'Export the last 90 days of orders as a file.';
  inputSchema = z.object({ format: z.enum(['csv', 'json']) });

  // Evaluated before every turn; false leaves the tool out of the prompt.
  condition = async () => {
    const user = await User.get();
    return user?.data?.plan === 'enterprise';
  };

  async execute(input: z.infer<typeof this.inputSchema>) {
    return { url: `https://example.com/exports/orders.${input.format}` };
  }
}
```

A `context` can also take the `{ base, voice, text }` object form described on the [persona](/concepts/persona) page.

## Sharing tool logic across agents

When several agents call the same backend, put the shared parts in a base class and let each agent declare a thin subclass that overrides only what differs. The compiler walks the full `extends` chain, so a leaf that extends a base that implements `LuaTool` is detected as a tool, and field initializers on the leaf are ordinary JavaScript inheritance: `execute` reads `this.searchPath` and sees the leaf's value.

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

export abstract class SearchDocsTool implements LuaTool {
  name = 'search_docs';
  description = 'Search the product documentation.';
  inputSchema = z.object({ query: z.string() });
  searchPath = '/default/search';

  async execute(input: z.infer<typeof this.inputSchema>) {
    const res = await fetch(`https://api.example.com${this.searchPath}?q=${encodeURIComponent(input.query)}`);
    return await res.json();
  }
}

export class AcmeSearchTool extends SearchDocsTool {
  name = 'search_acme_docs';
  searchPath = '/acme/search';
}
```

Don't pass configuration through constructor arguments (`new AcmeSearchTool('/acme/search')`). The compiler instantiates each tool class once per project with no arguments and reuses that artifact at every reference, so the argument is dropped; `lua compile` warns with `lua/constructor-args-dropped`. A field override is the supported way to vary a tool per agent.

Test the leaf tool on its own before a model is involved. `lua test skill` runs one tool's `execute` with the input you pass; `--name` is the tool name, not the skill name, and `condition` is not evaluated.

```bash theme={null}
lua test --ci skill --name search_acme_docs --input '{"query":"refund policy"}'
```

## Skills and MCP servers

A skill runs your TypeScript inside Lua; an [MCP server](/concepts/mcp-servers) exposes tools that live elsewhere and are called over HTTP. Write a skill when the logic is yours (your API, your rules, your data). Attach an MCP server when a vendor already offers one, and connect an [integration](/concepts/integrations) when the vendor is in Lua's catalog, because the integration provisions its MCP server for you.

## When to use a skill

* An end user's message should cause your code to run: a lookup, a booking, a calculation. Give it a tool.
* Several tools share rules or a domain (orders, bookings, billing): group them in one skill so the rules live in one `context`.
* The skill must be invisible to some end users: put a `condition` on the skill, not on each tool.
* Nothing in the conversation starts the work (a payment event, a nightly report): that isn't a tool. Use a [webhook](/concepts/webhooks), a [trigger](/concepts/triggers), or a [job](/concepts/jobs).
* Every message needs the same transform (redaction, a disclaimer): use a [processor](/concepts/processors), not a tool.

One skill or several is a question of ownership and release cadence, not size. A skill is versioned and deployed on its own, so tools that change together belong together, and tools that different people own belong apart. Splitting for code organization alone gains nothing: the model sees every active skill's context in one prompt either way.

## Limits

* Tool `name` must match `^[a-zA-Z0-9_-]+$`, or adding it to a skill throws `Invalid tool name`; `lua compile` warns when it isn't lowercase with hyphens or underscores.
* A skill needs a non-empty `name` and a `context`; an object-form `context` needs at least one of `base`, `voice`, `text`.
* Tools are compiled only when their skill is registered on the `LuaAgent` in your entry file; an unregistered file is never bundled.
* A tool's `condition` is evaluated on every turn with a 30-second budget. Keep it to one cheap read.

## Next steps

<Columns cols={2}>
  <Card title="Add a tool to a skill" href="/build/add-a-tool">Write, test, and release your first tool.</Card>
  <Card title="Write skill context" href="/build/write-skill-context">Context that makes the model pick the right tool.</Card>
  <Card title="LuaSkill reference" href="/reference/sdk/luaskill">Every field, method, and error.</Card>
  <Card title="LuaTool reference" href="/reference/sdk/luatool">The interface, the voice flags, and `condition`.</Card>
</Columns>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.