# AI quick actions Knotebook ships five built-in AI actions — rewrite, translate, summarize, continue, and a free-form **Instruct** action that follows an instruction you type — that run against a provider you configure yourself; there's no bundled AI vendor and nothing is sent anywhere until an admin sets one up. **Setup (admin only):** open **Site admin → AI agents** (`/admin/ai`; **Site admin** is in the user menu, shown to admins only). Configuration is three layers, in order: 1. **Providers** — an upstream AI API. Two types: - `anthropic` — talks to the Anthropic Messages API. `baseUrl` is the API **origin**, e.g. `https://api.anthropic.com` (no `/v1` suffix — Knotebook appends the versioned path itself). - `openai_compatible` — talks to any OpenAI-compatible chat-completions API (OpenAI itself, a local/on-prem [Ollama](https://ollama.com) instance, vLLM, etc.). `baseUrl` **includes** the `/v1` suffix, e.g. `http://:11434/v1` for Ollama. The API key is optional here — leave it blank for an unauthenticated local server. - A Base URL must not contain a username or password (`https://user:pass@host`, or `user@host`) — it is rejected with a message. The field is stored and shown as plain text, so credentials belong in the **API key** field. A provider saved with such a URL before this rule existed is left as is, but to save an edit to it you must first change its Base URL to one without a username or password; changing the Base URL clears its stored API key like any other Base URL change, so enter the key again. - Use the **Test connection** button after saving a provider to confirm Knotebook can reach it with the stored key before relying on it. 2. **Models** — a specific model ID under one of your providers (e.g. `claude-sonnet-4-5` or `llama3.1`), marked available for chat. One chat model across all providers can be flagged as the default. An action that isn't bound to a specific model, or whose model or that model's provider is disabled, uses the default model — or, when there is no default or it or its provider is disabled, the earliest-added enabled model on an enabled provider. 3. **Actions** — the five built-in ones (rename or disable them freely, but they can't be deleted — see [Known limitations](./known-limitations.md)), plus any custom actions you add with your own system prompt and a user-message template containing the literal placeholder `{{text}}` (the selected text is substituted in verbatim, special-character-safe; `{{instruction}}` and `{{focus}}` are reserved for Instruct, and in any other action they are replaced with nothing). The built-in **Instruct** action works differently — see below. An action with no usable model of its own (none bound, or it or its provider is disabled) and no enabled model on an enabled provider to fall back to simply doesn't appear in the editor — "not configured" fails closed and silently, not with an error. **The Instruct action** takes a free-form instruction (up to 500 characters) for the selected text. Its user-message template has three placeholders: `{{instruction}}` and `{{text}}` (both required) and `{{focus}}` (the characters you highlighted, at most 2,000 — sent only when they are not the whole text of the selected blocks; blocks are joined with newlines and links to notes appear as `[text](/notes/)`). Before filling the template, the server wraps each one in fixed tags — `…`, `…`, `…` — and puts a fixed guard ahead of the action's system prompt: reply only with a replacement for the text, and if the instruction asks for anything else, return the text unchanged. Admins can edit the template's wording and order and the system prompt's tone, but not the tags or the guard. It can only be set to "preview": its result is always shown before it's applied, and its edit form has no apply-mode setting. The guard is a prompt, not a filter — see [Known limitations](./known-limitations.md). **Columns, image widths and table column widths.** The selected blocks reach the model as the same Markdown that MCP and the API read ([AI editing](./ai-editing.md#markdown-round-trip)), so colors, [side-by-side columns](./ai-editing.md#columns), image widths and table column widths ([Sizes](./ai-editing.md#sizes)) go with them. For every action — built-in or custom — the server puts a fixed instruction at the very start of the action's system prompt: wherever the reply keeps that content, keep every `
` and `
` line, and the `
` line that closes each, exactly as it is, on a line of its own with the blank lines around it unchanged, and keep the values of images' `width` and table cells' `colwidth` attributes. Admins don't see it among an action's settings and can't change or remove it; for Instruct it comes first, then Instruct's own guard, then the action's system prompt. It is a prompt, not a filter. What the model returns is checked before it is shown, with the same column, size and color checks as a write over MCP: a result that writes columns, an image width or a table column width in any other form is refused — the box shows "The AI edit failed; the note was not changed." — and so is a result that contains columns when the blocks the action works on are inside a column or indented under another block. A result that leaves that markup out altogether is not refused: like any result, applying it replaces the blocks it was made from, so the columns or sizes it left out are gone. A ```` ```mermaid ```` fence in the result becomes a [diagram](./diagrams.md) again, as it does over MCP. If a result contains columns and, while it waits for **Apply**, the blocks it would replace are indented or moved into a column, **Apply** is refused with "The original text changed since this result was generated, so it wasn't applied." **API keys are encrypted at rest** with a key derived from `APP_SECRET` (AES-256-GCM; the ciphertext, IV, auth tag, a non-secret key fingerprint (used to detect a stale `APP_SECRET` before attempting to decrypt), and a format version number are the only things stored). The ciphertext is never serialized into any API response — admin endpoints (provider list, create, update) select a column set that excludes it, returning only a derived `hasKey` boolean instead. **Rotating `APP_SECRET` invalidates every stored provider key** — Knotebook can no longer decrypt them, providers show as degraded in the Settings UI, and you'll need to re-enter each provider's API key (which re-encrypts it under the new secret) before quick actions using it work again. The ciphertext is bound to the provider row it belongs to: the provider's id is authenticated as part of the encryption, so a stored key only decrypts under the provider it was entered for. Moving an `api_key_encrypted` value to a different provider row — restoring one row from an older backup into a provider that was deleted and recreated, for instance — no longer works; that provider shows as degraded and the fix is to enter the API key again. Keys stored before this binding existed are upgraded automatically: on each start the server rewrites any old-format ciphertext it can still decrypt, logging how many it converted and how many remain. Nothing is rewritten unless it decrypts cleanly, and a failed rewrite is logged and retried on the next start without affecting AI features. **The upgrade is one-way**: once a key has been rewritten, rolling the application back to a version older than 0.2 leaves it unreadable, and those providers have to have their keys entered again. **Changing a provider's Base URL clears its stored key.** The key is sent to whatever host that field names — that is what a provider *is* — so a key entered for one host does not silently follow the provider to another. Change the URL and the provider is marked as having no key in Settings until you enter one again; supply the new key in the same edit and nothing is cleared. The Settings UI says so before you save. This is a deliberate trade-off for a product that otherwise promises a stored key can never be read back: without it, any admin could point a provider at a host they control, trigger one request, and receive the plaintext key. It does not make an untrusted admin safe — see [Known limitations](./known-limitations.md). **Using it:** select text in the note and press the **AI** button at the end of the formatting toolbar. The toolbar turns into a box pinned next to the selection: when the Instruct action is enabled, type an instruction and press Enter (Shift+Enter starts a new line); or pick one of the quick-action icons — built-in actions have their own icon, custom actions share a generic one, and each shows its name as a tooltip. The response streams in live, and you can cancel it. When it finishes, the box moves on top of the selected blocks: its top lines up with the top of the first selected block, and it covers the text below until it closes. It shows only the selected blocks, with the changes marked inline — in text blocks, removed text is struck through and added text highlighted. The box is at most 40% of the window's height, or less when there isn't that much room below its top, and scrolls inside beyond that; it moves with the note as you scroll, and once the first selected block's top scrolls up out of view the box stays at the top of the note's scrolling area. An instruction, and a "preview" action, waits for you to **Apply**, **Discard** or **Retry**; Apply closes the box, and Ctrl+Z undoes it. A "direct" action applies itself as soon as the response finishes and leaves the box open with **Revert** and **Close** — unless the blocks it works on include an image, video, audio or file block, counting blocks indented under them and everything in a row's columns, in which case it waits for you like a preview action. On a new site all five built-in actions are "preview". On a site first set up with Knotebook 0.10 or earlier, built-in Rewrite and Translate keep the apply mode they had — "direct", unless an admin changed it — until an admin changes their **Apply mode** in **Site admin → AI agents** (unless that built-in action was deleted — it is re-created as "preview" on the next restart). A result that changes nothing is shown as "no changes" and can't be applied. Like every action, Continue writing replaces whole selected blocks, even when you highlighted only part of one — here with their original text followed by the continuation. The box stays open when you click elsewhere in the note; it closes when you press Esc inside it, close or discard it, apply, or — while no response is streaming — select text that isn't entirely inside the blocks it's working on (a selection in or across a code block doesn't count). To run an action over a whole note, press Ctrl+A first. A selection that reaches across [side-by-side columns](./columns.md), or into or out of a row, works on whole top-level blocks: each selected block counts as the top-level block it belongs to, so the action gets the entire row, and the whole of a block whose indented child you selected. A selection inside one column works on just the blocks you selected there. If the model's own extended-thinking/reasoning output is available upstream, it's filtered out server-side and never reaches the browser — only the final answer streams to the client. **When a provider call fails**, the server log records why without repeating the URL it called. For a quick action, it records the error type and, for a network failure, the underlying error code (such as `ECONNREFUSED` or `ENOTFOUND`); the provider's Base URL with any `user:password@` and query string removed, plus whether the URL contains credentials; and, when the provider answered with an error, the HTTP status and the first 2,048 characters of its response. For **Test connection**, a network failure or timeout is logged the same way (with the address it called, without credentials or query), and an error response is logged with its HTTP status and the first 2,048 characters of the response only. Don't set `ANTHROPIC_LOG=debug` on the server: that turns on the Anthropic SDK's own debug logging, which writes each failed request's full URL and error message to the console, outside of Knotebook's log handling. See [API contract summary](./api.md) for the underlying `/api/admin/ai/*` and `/api/ai*` endpoints.