Skip to content
Last9
Book demo

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.

  1. 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"
  2. 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"}
    ]
    }'
    FieldMeaning
    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[].approvalauto runs without a UI click; approve (default) requires human confirmation.
    overviewShort procedure. Put the when and order of tools here.

    use is a closed list. Unknown values return 400. You can create a topic without mcp and add bindings later with PATCH. Expected create response: 201 with activation.status: "pending".

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

  4. Activate with a chat turn

    After create or PATCH, activation.status is pending. 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.

  5. 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 example payments-incident-playbook__get_incident — with the approval you configured.

    Raw names mcp__payments__get_incident remain available for API-managed servers but still default to approve, even when the topic binding says auto. 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

FieldJSON createMultipart createPATCH behavior
idRequired stringRequired text form fieldImmutable; 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.
titleRequired stringRequired text form fieldOptional non-whitespace string. Omitting it preserves the title. It is not inferred from the uploaded filename on create.
contentRequired non-whitespace stringNot allowedJSON PATCH can replace the entire text. This is the actual text or Markdown, not Base64 or a file path.
fileNot allowedRequired single file partRequired 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

LimitMaximum
Topics100 in the organization’s knowledge collection
Attached documents50 per topic; overview is separate
JSON request body1 MiB (1,048,576 bytes), including JSON encoding overhead
Multipart request body10 MiB + 64 KiB (10,551,296 bytes), including boundaries and form fields
Uploaded file10 MiB (10,485,760 bytes)
Text file or extracted text512 KiB (524,288 UTF-8 bytes)
Stored overview or document record512 KiB, including saved metadata; leave room below the text limit
Total stored knowledge16 MiB (16,777,216 bytes), including overview/document metadata
Multipart metadata field16 KiB per text part; create allows two text fields and one file
DOCX extraction5 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.

FieldTypeRequired on create?Meaning and validation
idStringYesStable 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.
nameStringYesHuman-readable topic name. Must contain non-whitespace text.
descriptionStringYesShort explanation of what the topic covers. An empty string is allowed, but omitting the field is not.
overviewStringYesPlain 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.
useArray of stringsNo; 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.
mcpArray of objectsNo; 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:

FieldTypeRequired?Meaning and validation
serverStringYesExisting 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.
toolsArray of stringsYesNonempty 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.
approvalStringNo; 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 fieldMeaning
id, name, descriptionSaved topic identity and labels.
overviewA 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.
documentsArray of attached document objects; excludes the overview.
useSaved purpose array, including "incident_triage" when opted in.
mcpArray of normalized bindings containing server, tools, and approval, including the default "approve" when omitted in the request.
Document response fieldMeaning
idDocument identifier, unique within its topic.
titleSaved display title.
contentSaved plain text/Markdown or extracted DOCX text.
updated_atLast modification timestamp in UTC, formatted as YYYY-MM-DDTHH:MM:SSZ. This is not an ETag or version to use with If-Match.
source_filenamePresent for file uploads. Display filename with path components/control characters removed and capped at 255 characters; does not determine the document ID.
source_media_typePresent 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 fieldMeaning
status"pending" after a save, "active" after successful activation, or "error" after an activation failure.
last_attempt_atISO 8601 UTC time of the latest activation attempt, or null before any attempt.
request_idRequest correlation value associated with the latest mutation, or null when unavailable. Not a configuration revision or activation job ID.
error_codePresent 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 shapeTypical approval
{topic_id}__{tool} from a topic mcp bindingAs configured (auto or approve)
mcp__{server}__{tool} for an API-managed serverapprove (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

  1. Admin has AI enabled and an API refresh token with write and delete scopes.
  2. Remote MCP (if needed) is registered and healthy. See Remote MCP servers.
  3. Topic exists with use: ["incident_triage"], a concrete overview, and mcp bindings for the tools the runbook needs.
  4. At least one document holds the detailed runbook (Markdown preferred).
  5. After one AI Assistant turn, GET /config/status is active.
  6. A test investigation calls a topic-prefixed tool; auto tools run without a click, approve tools wait for confirmation.

Troubleshooting

Management errors use a JSON body such as {"error":"invalid_request"}.

Symptom or errorFix
400 invalid_requestCheck 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 DELETEToken has write but not delete scope. Create a refresh token that includes delete, or ask an Admin.
404 not_foundCreate the parent topic first; verify the topic/document IDs and organization.
409 resource_conflictAn 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 DELETESend a fresh If-Match from the latest GET.
413 request_too_large, upload_too_large, text_too_large, or knowledge_limit_exceededReduce 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_timeoutSimplify the DOCX or export its text to Markdown/plain text.
415 unsupported_media_typePDF 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_textSave as UTF-8 and ensure the file has readable, non-whitespace text.
422 invalid_docx, encrypted_docx, invalid_xml, invalid_xml_encoding, or xml_declarationRe-export an unencrypted DOCX or upload UTF-8 text.
422 invalid_filename or unreadable_uploadSupply a valid filename and a readable file part.
503 persistence_uncertainThe 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_unavailableRead back current state and retry later; contact support if persistent.
Model never uses the topicMissing 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 applyYou 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.