Add knowledge to Last9 AI
Create knowledge topics, upload app runbooks, opt topics into incident triage, and bind remote MCP tools with auto or approve policies.
Give Last9 AI your app context: runbooks, service notes, and investigation procedures. Knowledge topics can also bind tools from remote MCP servers so incident workflows call the right tools with the approval policy you choose.
Prerequisites
- Last9 AI enabled for your organization. See Enable Last9 AI.
- An Admin user (writes need an administrator).
- An API refresh token with write scope to create or update, and delete scope to remove topics or documents. See Getting started with API.
- Your organization slug (from the dashboard URL:
app.last9.io/v2/organizations/<org_slug>/...). - Optional: a remote MCP server already registered if you will bind tools. See Remote MCP servers.
GET requests are available to organization users with an allowed read scope. Create and PATCH require an administrator and write scope; DELETE requires an administrator and delete scope. Both the user’s role and the token’s scope are checked. Agent tokens cannot access these management endpoints, including GET.
Create a topic and upload runbooks
Worked example: topic payments-incident-playbook bound to a remote MCP server named payments. Register that server first if you need tool bindings.
-
Mint an access token
curl -sS -X POST 'https://app.last9.io/api/v4/oauth/access_token' \-H 'Content-Type: application/json' \-d '{"refresh_token":"<refresh_token>"}'export LAST9_TOKEN='<access_token>'export LAST9_ORG='<org_slug>'export LAST9_AI="https://app.last9.io/api/v4/organizations/${LAST9_ORG}/ai" -
Create an incident knowledge topic
curl -sS -X POST "${LAST9_AI}/knowledge/topics" \-H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \-H 'Content-Type: application/json' \-d '{"id": "payments-incident-playbook","name": "Payments incident playbook","description": "How to triage payments and ledger incidents","overview": "For SEV1/2 payments incidents: call get_incident on the payments MCP, then Last9 traces and logs for the same service and window, then add_comment. Prefer payments-incident-playbook__* tools over raw mcp__payments__* names.","use": ["incident_triage"],"mcp": [{"server": "payments", "tools": ["get_incident", "add_comment"], "approval": "auto"},{"server": "payments", "tools": ["close_incident"], "approval": "approve"}]}'Field Meaning use: ["incident_triage"]Topic is eligible on investigation / incident turns. mcp[].serverMust already exist in GET /mcp-servers.mcp[].toolsAllowlist of tool names, or ["*"]for every tool on that server.mcp[].approvalautoruns without a UI click;approve(default) requires human confirmation.overviewShort procedure. Put the when and order of tools here. useis a closed list. Unknown values return400. You can create a topic withoutmcpand add bindings later with PATCH. Expected create response:201withactivation.status: "pending". -
Upload app knowledge documents
JSON body:
curl -sS -X POST "${LAST9_AI}/knowledge/topics/payments-incident-playbook/documents" \-H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \-H 'Content-Type: application/json' \-d '{"id": "sev1-runbook","title": "SEV1 payments runbook","content": "## SEV1\n1. Confirm affected merchant and region.\n2. Call get_incident.\n3. Correlate with Last9 error rate and traces.\n4. add_comment with findings.\n5. close_incident only after human approval."}'To upload a Markdown, plain-text, or Word file instead, see Attach a local text, Markdown, or Word file. The API stores the extracted text; uploads do not install executable tools.
-
Activate with a chat turn
After create or PATCH,
activation.statusispending. Open AI Assistant (or start an investigation) and send a turn. Then check:curl -sS "${LAST9_AI}/config/status" \-H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}"Expect
active. -
Verify in an investigation
Ask about a payments incident and instruct the assistant to follow the playbook. Bound tools appear as
{topic_id}__{tool}— for examplepayments-incident-playbook__get_incident— with the approval you configured.Raw names
mcp__payments__get_incidentremain available for API-managed servers but still default to approve, even when the topic binding saysauto. Prefer the topic-prefixed tools in the overview so auto-approved paths are used.
Attach a local text, Markdown, or Word file
An attachment here is a document saved under a knowledge topic, available to later AI turns. It is not a one-message chat attachment. Create the topic first, then POST one document at a time to /knowledge/topics/{topic_id}/documents.
Document fields
| Field | JSON create | Multipart create | PATCH behavior |
|---|---|---|---|
id | Required string | Required text form field | Immutable; use the document ID in the URL, never the PATCH body. Same 100-character kebab-case rules and reserved values as topic IDs. Must be unique within the topic. |
title | Required string | Required text form field | Optional non-whitespace string. Omitting it preserves the title. It is not inferred from the uploaded filename on create. |
content | Required non-whitespace string | Not allowed | JSON PATCH can replace the entire text. This is the actual text or Markdown, not Base64 or a file path. |
file | Not allowed | Required single file part | Required for multipart PATCH; replaces the entire document text. Do not also send content. |
JSON PATCH accepts title, content, or both, with at least one field. Multipart PATCH accepts file and an optional title. Use JSON for a title-only update. Unknown or repeated fields and multiple files are rejected. source_filename and source_media_type are response metadata, not request fields.
Upload a Markdown file
Save your runbook as sev1-runbook.md in UTF-8, then run:
curl --fail-with-body -sS -X POST \ "${LAST9_AI}/knowledge/topics/payments-incident-playbook/documents" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \ --form-string 'id=sev1-runbook-file' \ --form-string 'title=SEV1 payments runbook (file)' \ -F 'file=@./sev1-runbook.md;type=text/markdown'@ tells curl to read the local file. file is the exact required form field name. --form-string sends the ID and title literally. Do not manually set the request’s Content-Type: curl generates multipart/form-data with the required boundary. The type= suffix sets the MIME type of the file part.
Upload a plain-text file
curl --fail-with-body -sS -X POST \ "${LAST9_AI}/knowledge/topics/payments-incident-playbook/documents" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \ --form-string 'id=service-context' \ --form-string 'title=Payments service context' \ -F 'file=@./service-context.txt;type=text/plain'Both text types require valid UTF-8. Windows (CRLF) and classic Mac (CR) line endings are normalized to LF during upload. Empty and whitespace-only files are rejected. The server chooses the extractor from the file part’s MIME type, not its filename extension: an .md filename alone is insufficient if the client sends application/octet-stream.
Upload a Word document
curl --fail-with-body -sS -X POST \ "${LAST9_AI}/knowledge/topics/payments-incident-playbook/documents" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \ --form-string 'id=operations-manual' \ --form-string 'title=Payments operations manual' \ -F 'file=@./operations-manual.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document'DOCX uploads extract text from the main document body, joining paragraphs with newlines and preserving text tabs and line breaks. This does not preserve Word styling, images, headers, footers, comments, or the original binary file. Text in table paragraphs is extracted as text, not reconstructed as a formatted table. Encrypted or malformed DOCX files are rejected. PDF, legacy .doc, and other MIME types are unsupported; export them to UTF-8 text, Markdown, or DOCX first. Changing an unsupported file’s extension or MIME type does not convert its contents.
Send a file’s text through JSON instead
Use jq --rawfile to read and JSON-escape UTF-8 text, including newlines, quotes, and backslashes. This example requires jq:
jq -n --arg id 'escalation-guide' --arg title 'Payments escalation guide' \ --rawfile content ./escalation-guide.md \ '{id: $id, title: $title, content: $content}' | curl --fail-with-body -sS -X POST \ "${LAST9_AI}/knowledge/topics/payments-incident-playbook/documents" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \ -H 'Content-Type: application/json' \ --data-binary @-In inline JSON, encode a newline as \n, a double quote as \", and a backslash as \\. Do not Base64-encode content: the API stores that as literal text. JSON uploads preserve the supplied text and line endings, do not run the file extractor, and do not set source filename/MIME metadata. To load a topic overview from a local file, use the same jq --rawfile overview ./overview.md technique with the topic’s required id, name, and description fields; topics themselves do not accept multipart uploads.
Upload and storage limits
| Limit | Maximum |
|---|---|
| Topics | 100 in the organization’s knowledge collection |
| Attached documents | 50 per topic; overview is separate |
| JSON request body | 1 MiB (1,048,576 bytes), including JSON encoding overhead |
| Multipart request body | 10 MiB + 64 KiB (10,551,296 bytes), including boundaries and form fields |
| Uploaded file | 10 MiB (10,485,760 bytes) |
| Text file or extracted text | 512 KiB (524,288 UTF-8 bytes) |
| Stored overview or document record | 512 KiB, including saved metadata; leave room below the text limit |
| Total stored knowledge | 16 MiB (16,777,216 bytes), including overview/document metadata |
| Multipart metadata field | 16 KiB per text part; create allows two text fields and one file |
| DOCX extraction | 5 seconds; at most 256 ZIP members and 16 MiB total uncompressed content |
These are byte limits, not character counts. Non-ASCII text can use multiple UTF-8 bytes per character, and JSON escaping can expand the request body. A file that passes the upload limit can still exceed the extracted-text or stored-record limit. Split large runbooks into smaller documents.
Topic request fields
Create with POST /knowledge/topics. The JSON object must contain all four required fields below. Unknown fields, duplicate JSON keys, null, and incorrect types return 400 invalid_request. After you create a topic, attach longer runbooks and notes to it as documents.
| Field | Type | Required on create? | Meaning and validation |
|---|---|---|---|
id | String | Yes | Stable identifier used in URLs and topic-prefixed tool names. Maximum 100 characters; lowercase kebab-case matching [a-z0-9]+(?:-[a-z0-9]+)*. Digits are allowed at the start. overview and references are reserved. Cannot be changed by PATCH. |
name | String | Yes | Human-readable topic name. Must contain non-whitespace text. |
description | String | Yes | Short explanation of what the topic covers. An empty string is allowed, but omitting the field is not. |
overview | String | Yes | Plain text or Markdown instructions for using the topic. Must contain non-whitespace text. Send the text itself, not a filename, URL, Base64 value, or document object. |
use | Array of strings | No; defaults to [] | Purposes for which the topic is eligible. The only supported value is "incident_triage". Duplicate and unknown values are rejected. This is not a freeform tag list. |
mcp | Array of objects | No; defaults to [] | Tool bindings described below. Maximum 32 entries. Omit when adding knowledge without remote tools. |
On PATCH /knowledge/topics/{topic_id}, name, description, overview, use, and mcp are optional individually, but send at least one field and an If-Match header. Omitted fields stay unchanged. Arrays replace the whole existing array: "use": [] opts out of investigations and "mcp": [] clears tool bindings. null does not clear a field. The same string validation applies on PATCH.
MCP binding fields
Each entry in mcp accepts exactly these fields:
| Field | Type | Required? | Meaning and validation |
|---|---|---|---|
server | String | Yes | Existing server name from GET /mcp-servers. Must match [a-z][a-z0-9_-]{0,99} and cannot be test. Register an API-managed server before referencing it. |
tools | Array of strings | Yes | Nonempty allowlist, maximum 64 entries per binding. Use actual tool names returned by the MCP server, without the mcp__server__ or topic prefix. Each name matches [a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}; duplicates within an allowlist are rejected. ["*"] selects all tools and cannot be combined with other names. |
approval | String | No; defaults to "approve" | "approve" requires human confirmation. "auto" runs the topic-bound tool without a confirmation click. Applies to topic-prefixed calls, not raw MCP calls. |
Binding validation checks the server exists and the allowlist has valid syntax; it does not prove that every requested tool is advertised by that server. Verify the resulting tools after activation. Avoid binding the same tool name from multiple servers into one topic: topic-prefixed names do not include the server name and can collide. Separate those bindings into different topics.
Minimal topic without MCP tools
After you export LAST9_AI and LAST9_TOKEN as above, this is a complete create request:
curl --fail-with-body -sS -X POST "${LAST9_AI}/knowledge/topics" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \ -H 'Content-Type: application/json' \ --data '{"id":"service-notes","name":"Service notes","description":"","overview":"Use these notes when answering questions about our services."}'This topic is eligible for regular chat after activation but is not opted into incident triage. Add "use": ["incident_triage"] when it should participate in investigations. Eligibility does not guarantee the model reads the topic on every turn.
Response fields
Create returns 201; PATCH and DELETE return 200. Successful writes have this envelope. This example is an illustrative document-upload response:
{ "resource": { "id": "sev1-runbook-file", "title": "SEV1 payments runbook (file)", "content": "## SEV1\nConfirm the affected service and region.", "updated_at": "2026-09-25T10:30:00Z", "source_filename": "sev1-runbook.md", "source_media_type": "text/markdown" }, "saved": true, "activation": { "status": "pending" }}resource is the saved topic or document, or {"id":"..."} after deletion. saved: true means persistence succeeded; it does not mean the AI is using the change yet. activation.status: "pending" means the next activation attempt must load it. Create and PATCH also return the saved resource’s ETag response header.
GET returns the resource directly, without the write envelope. GET /knowledge and GET /knowledge/topics both return {"topics": [...]}; the document collection returns {"documents": [...]}. These lists contain full resources, including document text, and have no pagination parameters.
| Topic response field | Meaning |
|---|---|
id, name, description | Saved topic identity and labels. |
overview | A document object, not the string used in create/PATCH. Its id is "overview", its title is "Overview", and overview.content contains the procedure. Update this through the topic’s overview request field, not the documents endpoint. |
documents | Array of attached document objects; excludes the overview. |
use | Saved purpose array, including "incident_triage" when opted in. |
mcp | Array of normalized bindings containing server, tools, and approval, including the default "approve" when omitted in the request. |
| Document response field | Meaning |
|---|---|
id | Document identifier, unique within its topic. |
title | Saved display title. |
content | Saved plain text/Markdown or extracted DOCX text. |
updated_at | Last modification timestamp in UTC, formatted as YYYY-MM-DDTHH:MM:SSZ. This is not an ETag or version to use with If-Match. |
source_filename | Present for file uploads. Display filename with path components/control characters removed and capped at 255 characters; does not determine the document ID. |
source_media_type | Present for file uploads; normalized accepted MIME type, without parameters. |
A title-only JSON PATCH preserves upload metadata. A multipart replacement updates it. A JSON PATCH containing content removes prior upload metadata because the replacement is now supplied as text. Reads return extracted text; there is no original-file download endpoint.
List and read
curl -sS "${LAST9_AI}/knowledge" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}"
curl -sS "${LAST9_AI}/knowledge/topics" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}"
curl -sS -D - "${LAST9_AI}/knowledge/topics/payments-incident-playbook" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" -o /tmp/topic.json
curl -sS "${LAST9_AI}/knowledge/topics/payments-incident-playbook/documents" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}"
curl -sS -D - "${LAST9_AI}/knowledge/topics/payments-incident-playbook/documents/sev1-runbook" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}"Keep the quoted ETag from the topic GET for PATCH or DELETE.
Update or remove
curl -sS -X PATCH "${LAST9_AI}/knowledge/topics/payments-incident-playbook" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \ -H 'Content-Type: application/json' \ -H 'If-Match: "<etag_from_get>"' \ -d '{"overview":"Updated procedure…"}'Clear MCP bindings or opt out of triage:
curl -sS -X PATCH "${LAST9_AI}/knowledge/topics/payments-incident-playbook" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \ -H 'Content-Type: application/json' \ -H 'If-Match: "<etag>"' \ -d '{"mcp": [], "use": []}'Missing If-Match returns 428; a stale value returns 412. Delete topics or documents with DELETE and the same precondition header (needs delete scope).
Replace an existing document from a file
GET the document itself to obtain its ETag. A topic ETag and a document ETag are different; do not substitute one for the other. This Bash example preserves the quotes returned in the header:
DOCUMENT_URL="${LAST9_AI}/knowledge/topics/payments-incident-playbook/documents/sev1-runbook"HEADERS_FILE=$(mktemp)curl --fail-with-body -sS -D "${HEADERS_FILE}" "${DOCUMENT_URL}" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}"DOCUMENT_ETAG=$(awk 'tolower($1) == "etag:" {sub(/\r$/, "", $2); print $2}' "${HEADERS_FILE}")test -n "${DOCUMENT_ETAG}" &&curl --fail-with-body -sS -X PATCH "${DOCUMENT_URL}" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \ -H "If-Match: ${DOCUMENT_ETAG}" \ -F 'file=@./sev1-runbook.md;type=text/markdown'The replacement preserves the document ID and title. Add --form-string 'title=Updated runbook' to change the title too. Re-read the document or use the PATCH response’s new ETag before another edit or deletion. For topic PATCH/DELETE, obtain an ETag from the topic GET: adding, changing, or deleting any attached document also changes that topic ETag. An If-Match value must exactly match the current quoted ETag; *, weak ETags, or a list of ETags are not substitutes.
Delete one document by getting its current ETag, then sending:
curl --fail-with-body -sS -X DELETE "${DOCUMENT_URL}" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \ -H "If-Match: ${DOCUMENT_ETAG}"DELETE requests have no body. Deleting a topic removes its overview and attached documents. There is no API revision history or rollback endpoint; keep source runbooks in your own version control. After a stale ETag error, re-read, review intervening changes, and submit the intended edit with the fresh ETag.
Activation status
Knowledge and remote MCP changes share GET /config/status. Saving changes marks configuration pending; a subsequent AI turn attempts to activate the current saved configuration. A turn already running keeps its existing configuration. Reading status or running an MCP diagnostic does not activate a change.
| Response field | Meaning |
|---|---|
status | "pending" after a save, "active" after successful activation, or "error" after an activation failure. |
last_attempt_at | ISO 8601 UTC time of the latest activation attempt, or null before any attempt. |
request_id | Request correlation value associated with the latest mutation, or null when unavailable. Not a configuration revision or activation job ID. |
error_code | Present after activation failure; currently "configuration_unavailable". Absent after successful activation or a new save. |
An activation error blocks new turns until the saved configuration can activate. Check any remote MCP dependencies, correct the topic or server, and try another turn. active confirms configuration activation, not that the model has already read every document or called every bound tool. Verify with a chat or investigation that uses the relevant procedure.
Tool names and approval
| Call shape | Typical approval |
|---|---|
{topic_id}__{tool} from a topic mcp binding | As configured (auto or approve) |
mcp__{server}__{tool} for an API-managed server | approve (human must confirm) |
Deployment-managed Last9 tools (mcp__last9__…) | Per Last9 product policy |
Investigation turns only load knowledge topics whose use includes incident_triage.
Checklist
- Admin has AI enabled and an API refresh token with write and delete scopes.
- Remote MCP (if needed) is registered and healthy. See Remote MCP servers.
- Topic exists with
use: ["incident_triage"], a concreteoverview, andmcpbindings for the tools the runbook needs. - At least one document holds the detailed runbook (Markdown preferred).
- After one AI Assistant turn,
GET /config/statusisactive. - A test investigation calls a topic-prefixed tool;
autotools run without a click,approvetools wait for confirmation.
Related
- Remote MCP servers — register and test remote MCP endpoints
- AI Assistant — run a verification chat after activation
- Investigate an incident with AI — investigation method
- Getting started with API — refresh and access tokens
Troubleshooting
Management errors use a JSON body such as {"error":"invalid_request"}.
| Symptom or error | Fix |
|---|---|
400 invalid_request | Check required fields, exact types, unknown or duplicate keys, the kebab-case id format, use values, nonempty content, and binding rules. An mcp binding must reference a server that already exists. Topic description is mandatory even when empty. For multipart, send exactly one file and no content field. |
403 on DELETE | Token has write but not delete scope. Create a refresh token that includes delete, or ask an Admin. |
404 not_found | Create the parent topic first; verify the topic/document IDs and organization. |
409 resource_conflict | An ID already exists or the resource is protected. Use PATCH for an existing editable resource or choose a new ID. |
412 / 428 on PATCH or DELETE | Send a fresh If-Match from the latest GET. |
413 request_too_large, upload_too_large, text_too_large, or knowledge_limit_exceeded | Reduce the request/file/text or remove unneeded knowledge. Stored metadata counts toward limits. A knowledge-limit error may include an ETag for deleting the oversized resource. |
413 too_many_members, zip_expansion_too_large, or extraction_timeout | Simplify the DOCX or export its text to Markdown/plain text. |
415 unsupported_media_type | PDF and legacy .doc are not supported; export them to Markdown, plain text, or .docx. For a supported file, set the file part MIME type explicitly. A .md extension with application/octet-stream still fails. |
422 invalid_text_encoding, empty_upload, or no_text | Save as UTF-8 and ensure the file has readable, non-whitespace text. |
422 invalid_docx, encrypted_docx, invalid_xml, invalid_xml_encoding, or xml_declaration | Re-export an unencrypted DOCX or upload UTF-8 text. |
422 invalid_filename or unreadable_upload | Supply a valid filename and a readable file part. |
503 persistence_uncertain | The write may have succeeded. GET the resource and compare it with the intended state before retrying; fetch its current ETag. |
503 persistence_unavailable or upstream_unavailable | Read back current state and retry later; contact support if persistent. |
| Model never uses the topic | Missing incident_triage, empty overview, or you only registered MCP without a topic. Add incident_triage and an overview that names the tools. |
| Auto approval does not apply | You called mcp__{server}__{tool} instead of {topic_id}__{tool}. Prefer topic-prefixed names in the overview. |
Please get in touch with us on Discord or Email if you have any questions.