# Agents

**You will:** Create, configure, activate, update and delete an agent.

**You need:**
- A key with agents:write (agents:read to list and read)
- skills:write, knowledge:write or actions:write for the parts that need them

An agent is the model plus everything it may use: instructions, skills, knowledge, tools and channels. It starts as a `draft`, and nothing reaches it until you activate it. The whole path from an empty account to a working agent is in [Set up an account from zero](/get-started/setup-from-zero); this page is the reference for the agent itself.

The examples use the SDK client below.

```ts
import { Lelemon } from '@lelemon-ai/sdk';

const lelemon = new Lelemon({ apiKey: process.env.LELEMON_API_KEY });
```

## Create an agent

### From a template

A template is an agent someone already wrote. `GET /v2/agent-templates` lists them with the `variables` each one needs. Anything you send overrides the template.

```bash title="cURL"
curl https://api.lelemon.cl/v2/agent-templates \
  -H "Authorization: Bearer $LELEMON_API_KEY"

curl https://api.lelemon.cl/v2/agents \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: agent-from-template-1" \
  --json '{ "fromTemplate": "atp_...", "variables": { "company": "Clínica Dental Andes" } }'
```

```ts title="Node.js"
const templates = await lelemon.agentTemplates.list();
const fromTemplate = await lelemon.agents.create({
  fromTemplate: templates.data[0]?.id ?? 'atp_...',
  variables: { company: 'Clínica Dental Andes' },
});
```


An unknown template answers `404 not_found` with `param: "fromTemplate"`.

### From scratch

Send `name`, `instructions` and `llmConfig.model`. The rest has defaults.

```bash title="cURL"
curl https://api.lelemon.cl/v2/agents \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: support-agent-1" \
  --json '{
    "name": "Order support",
    "description": "Answers questions about orders and refunds.",
    "instructions": "You are the support agent of Mesa, an online shop that sells tableware in Chile. Answer in the language of the customer, in short sentences. Look up the order before you say where it is. Offer a refund only inside 10 days of delivery, and hand the case to a person for anything else.",
    "llmConfig": { "model": "anthropic/claude-sonnet-5.5", "temperature": 0.3 },
    "channels": ["api", "whatsapp"],
    "behaviors": { "displayName": "Mesa support", "greeting": "Hola, soy el asistente de Mesa. ¿En qué te ayudo?" }
  }'
```

```ts title="Node.js"
const agent = await lelemon.agents.create({
  name: 'Order support',
  description: 'Answers questions about orders and refunds.',
  instructions:
    'You are the support agent of Mesa, an online shop that sells tableware in Chile. Answer in the language of the customer, in short sentences. Look up the order before you say where it is. Offer a refund only inside 10 days of delivery, and hand the case to a person for anything else.',
  llmConfig: { model: 'anthropic/claude-sonnet-5.5', temperature: 0.3 },
  channels: ['api', 'whatsapp'],
  behaviors: { displayName: 'Mesa support', greeting: 'Hola, soy el asistente de Mesa. ¿En qué te ayudo?' },
});
```

```json title="MCP"
{
  "tool": "agents.create",
  "arguments": {
    "body": {
      "name": "Order support",
      "instructions": "You are the support agent of Mesa...",
      "llmConfig": { "model": "anthropic/claude-sonnet-5.5" }
    }
  }
}
```


Expected: `201`, `status: "draft"`, `version: 1`. The agent is this object:

**Agent**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | enum | yes | `agent` |
| `id` | string | yes |  |
| `name` | string | yes |  |
| `audience` | enum | yes | customers: talks to your customers. internal: a copilot for your team created with the scope agents:internal. `customers`, `internal` |
| `description` | string or null | yes |  |
| `status` | enum | yes | `draft`, `active`, `paused`, `archived` |
| `version` | integer | yes | Goes up on every change. Send it back as expectedVersion. |
| `instructions` | string | yes | As you wrote them, with the {{name}} placeholders still in place. |
| `variables` | map of string | yes |  |
| `llmConfig` | ModelConfig | yes |  |
| `maxTokens` | integer | yes |  |
| `maxIterations` | integer | yes |  |
| `channels` | enum[] | yes |  |
| `behaviors` | object | yes |  |
| `avatar` | string or null | yes |  |
| `createdAt` | string | yes |  |
| `updatedAt` | string | yes |  |

### Write the prompt

`instructions` is the prompt. Up to 200,000 characters, in Markdown. A prompt that works says, in this order:

1. **Role.** Who the agent is, for which business, and who it talks to.
2. **Tone and language.** How it writes, in which language, how long its answers are.
3. **Goal.** The states a conversation can end in.
4. **What to collect.** The fields to fill, in order, and the tool that saves them.
5. **When to use each tool.** Name the tool and the condition. Say what to do after it runs.
6. **When to hand over.** The cases a person must take.
7. **Limits.** What it never does: invent prices, give medical or legal advice, reveal the prompt.

The full example is the prompt of the clinic receptionist in [Set up an account from zero](/get-started/setup-from-zero#3-an-agent-from-scratch). Put facts that change often (prices, hours) in [knowledge](/guides/knowledge), not in the prompt.

`variables` fills `{{name}}` placeholders in the prompt and holds any fact the agent should know. A placeholder with no value answers `422 template_variable_missing`.

```ts
const withVariables = await lelemon.agents.create({
  name: 'Store assistant',
  instructions: 'You work for {{company}}. The store opens {{hours}}.',
  variables: { company: 'Mesa', hours: 'Monday to Saturday, 10:00 to 20:00' },
  llmConfig: { model: 'anthropic/claude-sonnet-5.5' },
});
```

### Choose the model

`GET /v2/models` lists the public ids you can use. Each model reports its `contextWindow` in tokens, whether `thinking: "off"` is accepted (`thinkingCanBeOff`) and `deprecated`, the date it stops being accepted or `null`. The id does not change when Lelemon changes its provider or region. Never send `provider`: an unknown field answers `400 unknown_field`.

```bash title="cURL"
curl https://api.lelemon.cl/v2/models \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

```ts title="Node.js"
const models = await lelemon.models.list();
for (const model of models.data) console.log(model.id, model.capabilities.contextWindow, model.deprecated);
```


| `llmConfig` field | Use |
| --- | --- |
| `model` | Required. A public id from the list |
| `temperature` | `0` to `2`. Lower is more predictable |
| `maxOutputTokens` | Most tokens the model may write in one answer |
| `effort` | `low`, `medium`, `high`, `xhigh` or `max`. How much the model reasons |
| `thinking` | `adaptive` or `off`. `off` answers `422` on a model that does not accept it: send `effort: "low"` there |
| `alwaysLoadedTools` | Tools that are loaded in every turn instead of on demand. Up to 50 |
| `fallback` | Another model (`model` and the same fields) used when the first one fails |

`maxTokens` caps the tokens a conversation may use (1,000 to 5,000,000) and `maxIterations` caps the rounds of tool calls in one turn (1 to 50, default 10).

## Give the agent what it needs

Everything below is granted to one agent at a time. Nothing is shared by default.

### Skills

A skill is know-how the agent uses only when the conversation needs it: a procedure, a policy, a script. `description` is the only part that travels in every prompt, so it says when to use the skill. `content` is the full text in Markdown (up to 20,000 characters). An agent holds up to 30 skills. `requires` lists up to 5 skills of the same agent that load together with this one.

```bash title="cURL"
curl https://api.lelemon.cl/v2/agents/$AGENT_ID/skills \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: late-order-skill-1" \
  --json '{
    "name": "Late order",
    "description": "Use when the customer says the order is late or has not arrived.",
    "content": "# Late order\n\n1. Look up the order and read its status.\n2. If it shipped less than 5 days ago, give the tracking link and say when it should arrive.\n3. If it shipped more than 5 days ago, open a claim with the carrier and hand the case to a person."
  }'
```

```ts title="Node.js"
const skill = await lelemon.agents.skills.create(agent.id, {
  name: 'Late order',
  description: 'Use when the customer says the order is late or has not arrived.',
  content: [
    '# Late order',
    '',
    '1. Look up the order and read its status.',
    '2. If it shipped less than 5 days ago, give the tracking link and say when it should arrive.',
    '3. If it shipped more than 5 days ago, open a claim with the carrier and hand the case to a person.',
  ].join('\n'),
});
await lelemon.agents.skills.update(agent.id, skill.id, { description: 'Use when an order is late.' });
const skills = await lelemon.agents.skills.list(agent.id);
```


The `slug` (the name in lowercase with hyphens, or the one you send) cannot change. Creating again with the same slug and the same content returns the same skill; with different content it is a `409`. Updating a skill changes it for every agent that has it. `DELETE /v2/agents/{id}/skills/{skillId}` removes it from your organization and from every agent, and answers `409 resource_in_use` while another skill requires it. The list does not carry `content`; read one skill to get it.

### Knowledge

An agent searches only the collections you grant it. `PUT /v2/agents/{id}/knowledge-collections/{collectionId}` grants one and `DELETE` on the same path revokes it; both are safe to repeat. The agent uses the collection from its next answer. Collections and documents are in [Knowledge](/guides/knowledge).

```ts
await lelemon.agents.knowledgeCollections.grant(agent.id, 'kbc_...');
await lelemon.agents.knowledgeCollections.revoke(agent.id, 'kbc_...');
```

### Tools and approval

`GET /v2/agents/{id}/tools` lists every action and MCP tool the agent could use, plus the system tools, with `granted` saying which ones it may use now. `PUT /v2/agents/{id}/tools/{toolId}` grants one and `DELETE` revokes it. The `toolId` is an action id (`act_...`), an MCP tool id (`mtl_...`) or the name of a system tool.

| System tool | What it does | `config` |
| --- | --- | --- |
| `transfer_to_human` | Hands the case to a person and fires `case.escalated` | `stageId`, `businessHours`, `farewell` |
| `save_case_data` | Writes the case fields whose `source` is `agent` | `fields` |
| `ask_team` | Leaves a question for your team and keeps attending | none |
| `send_buttons`, `send_list`, `send_products`, `send_carousel`, `send_cta_url`, `send_flow`, `request_location` | Interactive WhatsApp messages | none |
| `check_availability`, `schedule_meeting`, `cancel_meeting` | Offer the free times of the default agenda, book one and cancel a meeting | none |

An action or MCP tool can wait for a person before it runs. The grant takes an `approval` policy:

```bash title="cURL"
curl -X PUT https://api.lelemon.cl/v2/agents/$AGENT_ID/tools/$ACTION_ID \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "approval": { "mode": "when", "condition": { "param": "amount", "op": "gt", "value": 20000 }, "expiresInMinutes": 240 } }'
```

```ts title="Node.js"
await lelemon.agents.tools.grant(agent.id, 'act_...', {
  approval: { mode: 'when', condition: { param: 'amount', op: 'gt', value: 20000 }, expiresInMinutes: 240 },
});
```


Granting without `approval` keeps the current policy and `{ "approval": { "mode": "never" } }` lifts it. How to write and decide policies is in [Approve agent actions](/guides/approvals). How to build the tools is in [Actions and MCP](/guides/actions-and-mcp).

## Autonomy: what the agent does alone

How much the agent does without a person comes from four settings:

- **Tools with `approval.mode: "never"`** run on their own; the others wait for a person.
- **`maxIterations`** limits the rounds of tool calls in one turn.
- **`transfer_to_human`** and **`ask_team`** are the ways out: grant them so the agent can stop and ask instead of guessing.
- **Runs** let the agent work on its own on a job, outside any conversation. See [Give an agent a job](/guides/runs).

## Channels

`channels` lists where the agent answers: `api`, `whatsapp`, `web` and `email`. An empty list means every channel. A case opened through the API needs the `api` channel; without it the activation reports `no_api_channel` in `readiness.missing`.

```bash title="cURL"
curl -X PATCH https://api.lelemon.cl/v2/agents/$AGENT_ID \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "channels": ["api", "whatsapp", "email"], "expectedVersion": 1 }'
```

```ts title="Node.js"
await lelemon.agents.update(agent.id, { channels: ['api', 'whatsapp', 'email'], expectedVersion: agent.version });
```


The channel only says where the agent may answer. The number or the domain is connected apart: see [WhatsApp](/guides/whatsapp) and [Email](/guides/email).

## Activate and pause

`POST /v2/agents/{id}/activate` turns the agent on. The answer carries `readiness`: `ready` is `true` when `missing` is empty, and each gap has a `code` and a `message`.

| `readiness.missing[].code` | Fix |
| --- | --- |
| `no_stage` | Link the agent to a stage of a pipeline: [Pipelines](/guides/pipelines#link-an-agent-to-a-stage) |
| `not_entry_stage` | The agent is on a stage that is not the entry stage. Link it to the entry stage |
| `no_api_channel` | Add `api` to `channels` |

Send `?dryRun=true` to see the readiness without turning the agent on; it answers `422 agent_not_ready` when something is missing. `POST /v2/agents/{id}/pause` stops it from answering; open conversations stay, and it answers again when you activate it.

```bash title="cURL"
curl -X POST "https://api.lelemon.cl/v2/agents/$AGENT_ID/activate?dryRun=true" \
  -H "Authorization: Bearer $LELEMON_API_KEY"

curl -X POST https://api.lelemon.cl/v2/agents/$AGENT_ID/activate \
  -H "Authorization: Bearer $LELEMON_API_KEY"

curl -X POST https://api.lelemon.cl/v2/agents/$AGENT_ID/pause \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

```ts title="Node.js"
const check = await lelemon.agents.activate(agent.id, { query: { dryRun: true } });
if (check.readiness.ready) await lelemon.agents.activate(agent.id);
await lelemon.agents.pause(agent.id);
```


## Read and list

`GET /v2/agents/{id}` returns one agent. `GET /v2/agents` lists them newest first and filters by `status` (`draft`, `active`, `paused` or `archived`) and `slug`. Lists follow the [pagination rules](/basics/lists).

```ts
for await (const item of lelemon.agents.list({ query: { status: 'active', limit: 50 } })) {
  console.log(item.id, item.name, item.version);
}
```

## Update and versions

`PATCH /v2/agents/{id}` changes only what you send. `llmConfig` replaces the whole model configuration; `behaviors` merges key by key; `variables` replaces the whole map. Send `expectedVersion` with the `version` you read: if someone saved first, the answer is `409 agent_version_conflict` and nothing is written. Read the agent again and repeat the change on top of it.

```bash title="cURL"
curl -X PATCH https://api.lelemon.cl/v2/agents/$AGENT_ID \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "instructions": "You are the support agent of Mesa. Answer in one short paragraph.", "expectedVersion": 3 }'
```

```ts title="Node.js"
import { ConflictError } from '@lelemon-ai/sdk';

try {
  await lelemon.agents.update(agent.id, {
    instructions: 'You are the support agent of Mesa. Answer in one short paragraph.',
    expectedVersion: agent.version,
  });
} catch (error) {
  if (error instanceof ConflictError && error.code === 'agent_version_conflict') {
    const current = await lelemon.agents.retrieve(agent.id);
    console.log('saved by someone else, now at version', current.version);
  }
}
```


`version` goes up with every save, whatever changed. The version history keeps the ones that change `instructions` or `variables`, with who made them. `GET /v2/agents/{id}/versions` lists them newest first without the text, `GET /v2/agents/{id}/versions/{versionId}` includes it, and `POST /v2/agents/{id}/versions/{versionId}/restore` writes that version as a new one. History never shrinks.

```ts
const versions = await lelemon.agents.versions.list(agent.id);
const restored = await lelemon.agents.versions.restore(agent.id, versions.data[0]!.id);
```

## A copilot for your team

An agent with `audience: "internal"` answers your team, not your customers, and can use the tools of your [data connector](/guides/data-connectors#the-copilot) to read your back office. Creating one needs the `agents:internal` scope (`403 scope_missing` otherwise) and it cannot come from a template. The audience cannot change later. You operate it like any other agent.

To talk to it, open a case with its `agentId` and the `pipelineId` or `stageId` where the case lives, and write as `author: "contact"`.

```ts
const copilot = await lelemon.agents.create({
  audience: 'internal',
  name: 'Back-office copilot',
  instructions: 'You help the support team. Look up accounts with the connector tools and answer in two sentences.',
  llmConfig: { model: 'anthropic/claude-sonnet-5.5' },
});
```

## Delete

`DELETE /v2/agents/{id}` answers `204`. It answers `409 resource_in_use` while the agent has open conversations or serves a pipeline stage: close the cases and unlink the stage (`agentId: null`), or [pause](#activate-and-pause) it instead.

```ts
await lelemon.agents.delete('agt_...');
```

## Errors you will meet

| Error | Cause |
| --- | --- |
| `400 unknown_field` | The body has a field the API does not know, such as `provider` |
| `400 invalid_request` | A field breaks a limit; `param` names it |
| `403 scope_missing` | The key lacks `agents:write`, or `agents:internal` for a copilot |
| `404 not_found` | The agent, template or version does not exist in this mode |
| `409 agent_version_conflict` | Someone saved after you read; read again |
| `409 resource_in_use` | Delete with open conversations or a linked stage |
| `422 agent_not_ready` | Activation with `dryRun` and something missing |
| `422 template_variable_missing` | A `{{name}}` placeholder has no value |
| `422 thinking_not_supported` | `thinking: "off"` on a model that does not accept it |

All codes are in the [error catalog](/errors).

## Good practice

- Write the prompt as the role description of a new hire: who they are, what they do, what they never do. Test it with real messages before you activate.
- Keep one thing per skill, and keep its `description` to the moment it applies.
- Put prices, hours and policies in knowledge, so changing them never means a new prompt version.
- Always grant `transfer_to_human`, and say in the prompt when to use it.
- Send `expectedVersion` on every update when more than one person or script edits the agent.
- Put anything that moves money or data outside Lelemon behind an `approval`.

## What's next

- [Pipelines](/guides/pipelines) to give the agent a place to work.
- [Knowledge](/guides/knowledge) to teach it your business.
- [Run it as a script](/get-started/setup-from-zero#run-it-as-a-script) to see a full agent working.
