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

# Write skill context

> Write the context and tool descriptions that make the model call the right tool, check the effect in the sandbox, and release the change

After this guide, the model calls your tools when it should, with the arguments you need, and not otherwise. A [skill](/concepts/skills-and-tools)'s `context` is injected into the prompt whenever the skill is active, and each tool's `description` is what the model reads to pick a tool: both are prompt text. Use this guide when a tool is ignored, called with missing arguments, or called when it shouldn't be; for the agent's overall tone or identity, change the [persona](/concepts/persona) instead.

*Verified against lua-cli 3.33.0.*

**Before you begin**

* A skill with at least one tool registered on the agent ([Add a tool to a skill](/build/add-a-tool)).
* Signed in, so `lua chat` and `lua logs` can reach the agent ([Install and sign in](/get-started/install)).

<Steps>
  <Step title="Write the rules that span the tools">
    `context` carries what applies across the skill: when to ask before calling, what never to do, and what to say back. Name tools by their `name`; the model matches on it. The skill's own `description` is listing text for `lua skills view` and the admin dashboard, not prompt text.

    ```text Don't theme={null}
    You manage support tickets for Acme Helpdesk.
    ```

    ```text Do theme={null}
    You manage support tickets for Acme Helpdesk. Before calling create_ticket, ask for a
    one-line summary and the customer's email if you don't have them; never invent an email.
    Call lookup_tickets when the customer asks about an existing ticket or their history.
    Call escalate_ticket only when the customer asks for a person or the issue can't be
    solved with a known workaround. Always quote the ticket id back to the customer.
    ```

    The first version leaves the model to infer everything from tool names, so it guesses emails and escalates on the first complaint. The second says what each tool is for and what must be true before it runs.
  </Step>

  <Step title="Describe each tool for the model">
    `description` says what one tool does and when it applies, in one or two sentences. Field descriptions in `inputSchema` tell the model how to fill each argument, and constraints such as `.email()` or an enum reject values it made up.

    ```text Don't theme={null}
    Ticket lookup.
    ```

    ```text Do theme={null}
    Look up all support tickets for a customer by email. Use when the customer asks about
    the status of their ticket(s) or their support history.
    ```

    Rules that span tools belong in `context`; what one tool does belongs in its `description`.
  </Step>

  <Step title="Test on a fresh thread">
    `lua chat` compiles the project and runs the conversation with your local skills, so a context change needs no push ([About environments](/concepts/environments)). `-t` starts a fresh thread each time; without it the previous answer shapes the next one.

    ```bash theme={null}
    lua chat -m "My router keeps dropping Wi-Fi, can you open a ticket?" -t
    lua chat -m "Where is my ticket from yesterday?" -t
    ```

    With the rewritten context, the first message ends in a question for your email rather than a ticket, and the second calls `lookup_tickets` without asking. The first sandbox run after adding a skill registers it and answers without the tools (`Skipping skill tickets - no skillId found in lua.skill.yaml`); run it again.
  </Step>

  <Step title="Verify">
    Read the skill's log: `--name` is the skill's name, and each entry names the tool the model called and the arguments it chose.

    ```bash theme={null}
    lua logs --type skill --name tickets --limit 3 --ci
    ```

    ```text Output theme={null}
    📊 Skill Logs

    ────────────────────────────────────────────────────────────────────────────────
    Page 1 of 9 (26 total logs)

    🔍 [12/09/2026, 13:58:53] DEBUG
       Skill Name: tickets
       Skill ID:   0c7927ec-ffc5-40ad-b76e-63cba81e1390
       Tool Name: lookup_tickets
       Execute function completed in 29 ms
       ------------------------------------------------------------------------------
    🔍 [12/09/2026, 13:58:53] DEBUG
       Skill Name: tickets
       Skill ID:   0c7927ec-ffc5-40ad-b76e-63cba81e1390
       Tool Name: lookup_tickets
       Tool result []
    …
    ```

    A `Calling tool with input {…}` line shows the arguments; if one is invented, describe that field in `inputSchema` and say in `context` to ask for it.
  </Step>

  <Step title="Release">
    `lua push` uploads a version and changes nothing for end users; `lua version create` snapshots the agent; `lua version promote <n>` makes that snapshot live and is also the rollback path ([Release an agent to production](/ship/releasing)).

    ```bash theme={null}
    lua push all --ci --force
    lua version create --ci -m "Rewrite tickets context"
    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.
  </Step>
</Steps>

## Options you may need

### Give voice and text different context

`context` also takes `{ base, voice, text }`: `base` applies everywhere and `voice` or `text` is added for that kind of conversation; at least one must be set. The [persona](/concepts/persona) uses the same object form.

### Hide the skill when it doesn't apply

A skill `condition` runs before every turn with a 30-second budget, so keep it to one cheap read. When it returns `false` or throws, the tools and the context are left out of the prompt: the end user talks to an agent that has never heard of the skill. Your file differs; add only the highlighted lines to your own `LuaSkill`.

```ts src/skills/tickets.skill.ts highlight={1,8-11} theme={null}
import { LuaSkill, User } from 'lua-cli';
import LookupTicketsTool from './tools/LookupTicketsTool';

export default new LuaSkill({
  name: 'tickets',
  description: 'Support tickets: create, look up, and escalate',
  context: 'Call lookup_tickets when the customer asks about an existing ticket or their history.',
  condition: async () => {
    const user = await User.get();
    return Boolean(user?.accountId);
  },
  tools: [new LookupTicketsTool()],
});
```

`accountId` is a field your own tools stored on the end user's record ([Identify users](/build/identify-users)). To gate one tool instead, put the `condition` on the tool class: the skill's context stays in the prompt, so the model can still say what it can't do for this user.

### Teach a formatting component

A [`:::` formatting block](/channels/formatting/overview) is a prompt rule like any other. Put the instruction in the skill's `context` when the component belongs to that skill's replies (a `list-item` per product in a catalog skill, a `payment` block after `create_checkout`), and in the [persona](/concepts/persona) when it should apply to every reply. Blocks render differently per channel, so name the channel when a block renders only there: "On WhatsApp, send the verification flow."

## If it isn't working

<AccordionGroup>
  <Accordion title="The tool never shows in lua logs">
    Either the skill's `condition` returned `false` or threw, which hides the skill, or the context never says when the tool applies. Check what `User.get()` returns for the end user you test with, then name the tool in `context`.
  </Accordion>

  <Accordion title="The model calls the tool with an invented argument">
    The field has no `.describe()` and nothing in `context` says to ask for it. Add both, and add a constraint (`.email()`, `.regex()`, an enum) so a wrong value is rejected before `execute` runs.
  </Accordion>

  <Accordion title="Two sandbox messages give different tool choices">
    Without `-t` both messages share your default thread, so the first answer anchors the second. Run each probe with `-t`, or `lua chat clear` between them.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="Skills and tools" href="/concepts/skills-and-tools">How context and descriptions reach the model, and when to split skills.</Card>
  <Card title="Test an agent before you release" href="/ship/testing">Local runs, sandbox chat, thread isolation.</Card>
  <Card title="LuaSkill reference" href="/reference/sdk/luaskill">Every field, the context object form, and errors.</Card>
</Columns>
