# Knowledge

**You will:** Load documents, wait until they are searchable and give them to the agents that need them.

**You need:**
- A key with knowledge:write (knowledge:read to read)
- The webhook events knowledge_document.ready and knowledge_document.failed, if you want to be told

Knowledge is the facts an agent looks up instead of remembering: prices, hours, policies, product sheets, procedures. You organize it in **collections**. A collection holds **documents**, and an agent searches only the collections you grant it. Prices and policies belong here, not in the prompt, because you can change a document without touching the agent.

The examples use this client:

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

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

## Create a collection

A collection is a name and an optional description. Group documents by the topic the agent needs together, such as "Prices" or "Warranty".

```bash title="cURL"
curl https://api.lelemon.cl/v2/knowledge-collections \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: faq-collection-1" \
  --json '{ "name": "Clinic FAQ", "description": "Prices, hours and policies of the clinic." }'
```

```ts title="Node.js"
const collection = await lelemon.knowledgeCollections.create({
  name: 'Clinic FAQ',
  description: 'Prices, hours and policies of the clinic.',
});
```

```json title="MCP"
{
  "tool": "knowledgeCollections.create",
  "arguments": { "body": { "name": "Clinic FAQ" } }
}
```


Expected: `201` with `documentCount: 0`. The collection is this object:

**KnowledgeCollection**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | enum | yes | `knowledge_collection` |
| `id` | string | yes |  |
| `name` | string | yes |  |
| `description` | string or null | yes |  |
| `documentCount` | integer | yes | Documents in any status. |
| `createdAt` | string | yes |  |
| `updatedAt` | string | yes |  |

## Add a document

A document is text. Send it as JSON in `text`, or upload a file as multipart in `file`: PDF, DOCX, TXT or MD, up to 15 MB. Send either one, not both. `name` is the name you see; for a file it defaults to the file name.

```bash title="cURL"
curl https://api.lelemon.cl/v2/knowledge-collections/$COLLECTION_ID/documents \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: prices-doc-1" \
  --json '{
    "name": "prices-and-hours",
    "text": "# Clínica Dental Andes\n\nHours: Monday to Friday, 9:00 to 18:00.\n\n## Prices (CLP)\n- Check-up: 25.000\n- Cleaning: 35.000\n- Whitening: 120.000"
  }'

curl https://api.lelemon.cl/v2/knowledge-collections/$COLLECTION_ID/documents \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: warranty-pdf-1" \
  -F "name=warranty-policy" \
  -F "file=@warranty-policy.pdf"
```

```ts title="Node.js"
const document = await lelemon.knowledgeCollections.documents.create(collection.id, {
  name: 'prices-and-hours',
  text: [
    '# Clínica Dental Andes',
    '',
    'Hours: Monday to Friday, 9:00 to 18:00.',
    '',
    '## Prices (CLP)',
    '- Check-up: 25.000',
    '- Cleaning: 35.000',
    '- Whitening: 120.000',
  ].join('\n'),
});
console.log(document.status);
```


Sending the same `name` again replaces the document and processes it again, so a nightly sync can repeat the same call. Expected: `201` with `status: "processing"`.

**KnowledgeDocument**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | enum | yes | `knowledge_document` |
| `id` | string | yes |  |
| `collectionId` | string | yes |  |
| `name` | string | yes |  |
| `mimeType` | string | yes |  |
| `sizeBytes` | integer | yes |  |
| `status` | enum | yes | `processing`, `ready`, `failed` |
| `chunkCount` | integer | yes | Searchable passages. |
| `error` | KnowledgeDocumentError or null | yes | Set when status is failed. |
| `createdAt` | string | yes |  |
| `updatedAt` | string | yes |  |

## Follow the ingestion

A document starts as `processing`. Lelemon extracts its text, cuts it into passages and indexes them. The agent finds a document only when its status is `ready`.

| `status` | Meaning |
| --- | --- |
| `processing` | Being read and indexed |
| `ready` | Searchable. `chunkCount` counts the passages |
| `failed` | Not searchable. `error.code` says why |

| `error.code` | Cause and fix |
| --- | --- |
| `text_extraction_failed` | The file could not be read. Check that it opens and is not protected, then upload it again |
| `no_text_found` | The file has no text, for example a scanned image. Upload a version with text |
| `embedding_failed` | The text could not be indexed. Upload it again |
| `processing_failed` | The processing stopped. Upload it again |
| `enqueue_failed` | The job could not be queued. Upload it again |

You are told in two ways. Subscribe a [webhook endpoint](/webhooks/receive) to `knowledge_document.ready` and `knowledge_document.failed`, or read the document until the status changes:

```bash title="cURL"
curl https://api.lelemon.cl/v2/knowledge-collections/$COLLECTION_ID/documents/$DOCUMENT_ID \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

```ts title="Node.js"
let current = document;
while (current.status === 'processing') {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  current = await lelemon.knowledgeCollections.documents.retrieve(collection.id, document.id);
}
console.log(current.status, current.chunkCount, current.error);
```


Reading one document also returns its `content`, the text the agent searches, once it is `ready`. Lists do not carry the content.

## Give a collection to an agent

An agent uses 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, and the agent uses the change from its next answer.

```bash title="cURL"
curl -X PUT https://api.lelemon.cl/v2/agents/$AGENT_ID/knowledge-collections/$COLLECTION_ID \
  -H "Authorization: Bearer $LELEMON_API_KEY"

curl -X DELETE https://api.lelemon.cl/v2/agents/$AGENT_ID/knowledge-collections/$COLLECTION_ID \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

```ts title="Node.js"
await lelemon.agents.knowledgeCollections.grant('agt_...', collection.id);
await lelemon.agents.knowledgeCollections.revoke('agt_...', collection.id);
```

```json title="MCP"
{
  "tool": "agents.knowledgeCollections.grant",
  "arguments": { "id": "agt_...", "collectionId": "kbc_..." }
}
```


Expected: the grant answers `granted: true`. Granting needs `knowledge:write`. One collection can feed many agents, and one agent can hold many collections.

## List and read

`GET /v2/knowledge-collections` lists collections newest first. `GET /v2/knowledge-collections/{id}/documents` lists the documents newest first, without their content, and filters by `status`. Lists follow the [pagination rules](/basics/lists).

```bash title="cURL"
curl "https://api.lelemon.cl/v2/knowledge-collections/$COLLECTION_ID/documents?status=failed" \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

```ts title="Node.js"
for await (const item of lelemon.knowledgeCollections.documents.list(collection.id, { query: { status: 'failed' } })) {
  console.log(item.name, item.error?.code);
}
```


## Search the knowledge

`POST /v2/knowledge-collections/search` runs the search your agents use, by meaning and by exact words, and returns the passages with the collection and document each one comes from. Use it to check what an agent would find, or to let a tool such as [Claude](/guides/team-assistant) look up an answer. It needs `knowledge:read`, changes nothing and is safe to repeat.

```bash title="cURL"
curl https://api.lelemon.cl/v2/knowledge-collections/search \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "query": "How much is a cleaning?", "collectionIds": ["kbc_2LpX0x8bVw1TqS9mYp3RkL"], "limit": 3 }'
```

```ts title="Node.js"
const result = await lelemon.knowledgeCollections.search({
  query: 'How much is a cleaning?',
  collectionIds: [collection.id],
  limit: 3,
});
console.log(result.data.map((passage) => passage.text));
```


Without `collectionIds` it searches every collection of the organization, except the ones the internal assistant of the dashboard reads. `limit` is up to 10 and defaults to 5. `score` is the closeness in meaning, from 0 to 1, and is `null` for a passage found only by its exact words. An empty `data` means nothing answers it.

## Update

Rename a collection or change its description with `PATCH /v2/knowledge-collections/{id}`. `PATCH` a document to rename it or replace its text; new text processes the document again.

```ts
await lelemon.knowledgeCollections.update(collection.id, { description: 'Prices and policies, updated monthly.' });
await lelemon.knowledgeCollections.documents.update(collection.id, document.id, {
  text: 'Cleaning: 38.000 CLP. Check-up: 25.000 CLP.',
});
```

## Delete

`DELETE /v2/knowledge-collections/{id}/documents/{docId}` answers `204` and the agent stops finding the document right away. `DELETE /v2/knowledge-collections/{id}` deletes the collection with its documents and removes it from every agent that had it. Documents kept by a connected source, and the collection that source feeds, answer `409 resource_in_use`.

```ts
await lelemon.knowledgeCollections.documents.delete(collection.id, document.id);
await lelemon.knowledgeCollections.delete(collection.id);
```

## Errors you will meet

| Error | Cause |
| --- | --- |
| `400 invalid_request` | The body has neither `text` nor `file`, or both; `param` is `text` |
| `403 scope_missing` | The key lacks `knowledge:write` or `knowledge:read` |
| `404 not_found` | The collection, document or agent does not exist in this mode |
| `503 service_unavailable` | The search cannot run right now; retry in a moment |
| `409 resource_in_use` | A connected source keeps the document or feeds the collection |
| `413 payload_too_large` | The file is larger than 15 MB |
| `422 unsupported_file_type` | The file type is not PDF, DOCX, TXT or MD |

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

## Good practice

- One topic per document, with headings. The agent finds passages, so a document that mixes prices and warranty gives worse answers than two documents.
- Write facts the way a customer asks about them: "Cleaning costs 35.000 CLP" finds better than a table without a title.
- Use a stable `name` per document and re-send it when the source changes.
- Subscribe to `knowledge_document.failed` so a broken upload does not go unnoticed.
- Keep what the agent must never say out of the collection: the agent can quote anything it finds.

## What's next

- [Agents](/guides/agents) to give the collection to an agent and write the prompt that uses it.
- [Receive events](/webhooks/receive) to be told when a document is ready.
