Every change Wellknown observed on this MCP server, newest first, with what it was before and what it became. Tool-surface changes carry the definition diff. Nothing here is edited after the fact.
Changed the definition of "napkin_deck_compose", "search_docs" and "search_workspace"
⟨90 unchanged words⟩ "minLength":1,"type":"string"},"previews":{"description":"Default true: the result includes a picture of every slide made. Pass false to skip the pictures — the call returns sooner, which helps with long decks; look at slides afterwards with napkin_slide_view.","type":"boolean"},"size":{"description":"The size of every ⟨531 unchanged words⟩
⟨27 unchanged words⟩ Workbench, Caliper, Prism, Ledger, Napkin,zv1the assistant), an API behavior, or a policy. ⟨9 unchanged words⟩
⟨65 unchanged words⟩ sheet","diagram","interface","change","field","study","metric","folder"],"type":"string"},"minItems" ⟨60 unchanged words⟩
Changed the definition of "search_workspace"
Findsentities across everyanythingtoolinbythenameworkspace in one call — uploaded files (searching the text inside them), folders, Workbench flows, shims, knowledge bases and tasks, Compass pages and changes, Caliper datasets, evals, rubrics, reviews, specs and sources(apps sending agent traces), Ledger entries, Napkin sketches, decks, docs, sheets, diagrams anddecks.interfaces. The same search the person uses in Home.
Changed the definition of "comments_create", "comments_list", "comments_resolve" and 65 more
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Comment on an entity"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Read an entity's comment threads"}
Changed the definition of "napkin_interfaces_create", "napkin_interfaces_get", "napkin_interfaces_view" and 1 more
⟨119 unchanged words⟩ more files, served from its own origin.TherePut styles in a <style> block. LIBRARIES: there isNOno build step andNOnolibraryCDN, and a <script src> to any other site is blocked. A fixed shelf of libraries is served from the interface's own origin instead; load them with exactly these tags (the versions are pinned, and any other /lib/ path is not found):noPREACTReact+ HTM, for a page with several screens or a lot of state — components and hooks with no
Changed the definition of "napkin_interfaces_write"
⟨53 unchanged words⟩ for a new file or a deliberate rewrite. ONE CALL HOLDS ABOUT 15,000 CHARACTERS of file: a longer `body` is cut off mid-write and fails. Write a big file in parts instead — the first part with `body`, then each further part with `body` plus `append: true`, in order — and split an app across files (index.html, style.css, app.js) so each stays readable. `deleteFile` removes one (index.html can't be removed). ⟨1088 unchanged words⟩
{"$schema":"http://json-schema.org/draft-07/schema#","properties":{"append":{"description":"With `body`: add it to the end of `path` (creating the file if needed) instead of replacing it. How to write a file too long for one call: first part with `body`, the rest with `append: true`, in order.","type":"boolean"},"archived":{"type":"boolean"},"body" ⟨16 unchanged words⟩ a file that already has something in it. With `append: true`, added to the end of the file instead.
Added "napkin_interfaces_grant_data", "napkin_tables_add_columns", "napkin_tables_add_records" and 6 more; changed the definition of "napkin_interfaces_create", "napkin_interfaces_get" and "napkin_interfaces_write" (59 tools before, 68 now)
⟨682 unchanged words⟩ or "shim" alongside its id and name. DATA: an interface can be granted a sheet TABLE or a Ledger METRIC with napkin_interfaces_grant_data. ctx.tables lists each table with its `fields` (name, type, options) and `access` ("read", "add" or "edit"); ctx.metrics lists each metric with its unit and `access` ("read" or "record"). Records travel keyed by field NAME, choices by option NAME. zw.tables.query(table, { where: [{ field, op, value }], sort: [{ field, dir }], limit }) resolves { records: [{ id, fields }], total }; zw.tables.add(table, { Field: value }) or a list adds records; zw.tables.update(table, id, fields) and zw.tables.remove(table, ids) need "edit". zw.metrics.read(metric, { from, to }) resolves { metric, readings: [{ value, at, note }] }; zw.metrics.record(metric, value, { note }) needs "record". Name a table or metric by its id or its name. Through a share link a guest can read and add, never change or delete — so an intake form works when shared, a triage board needs a signed-in viewer. A granted Workbench TASK (ctx.tasks; each says whether it `takesInput`) starts with zw.tasks.start(task, input), which resolves { runId, status } at once — the run carries on after the page closes — and zw.tasks.get(task, runId) reads { status, output } for runs this page started. Running a shim on every keystroke is fine ⟨289 unchanged words⟩
Changed the definition of "napkin_diagrams_create" and "napkin_diagrams_update"
⟨83 unchanged words⟩ :#fee2e2` line. Only the flowchart subset is supported. Each label is a short name on one line, written plainly: no `\\n`, no backslashes before quotes. A step's label is its name everywhere in Compass.","maxLength":200000,"type":"string"} ⟨37 unchanged words⟩
⟨119 unchanged words⟩ :#fee2e2` line. Only the flowchart subset is supported. Each label is a short name on one line, written plainly: no `\\n`, no backslashes before quotes. A step's label is its name everywhere in Compass.","maxLength":200000,"type":"string"} ⟨73 unchanged words⟩
Changed the definition of "napkin_interfaces_create", "napkin_interfaces_get" and "napkin_interfaces_write"
⟨484 unchanged words⟩ the assistant text out of a chat result. VARIABLES: a flow grant lists `variables` — the values the flow reads besides the conversation, usually the {{key}} tokens in its prompt (who the user is, their plan, the page they are on). Send them on the same chat run: {kind:"chat", messages:[…], values:{<name>: value}}, using each variable's exact `name` and its `type` (string, number, boolean, array, object). A variable left out reaches the prompt empty. Read them from napkin_interfaces_get while you write the page, or at run time from ctx.flows[i].variables. A SHIM is not a flow and takes ⟨399 unchanged words⟩
⟨425 unchanged words⟩ the assistant text out of a chat result. VARIABLES: a flow grant lists `variables` — the values the flow reads besides the conversation, usually the {{key}} tokens in its prompt (who the user is, their plan, the page they are on). Send them on the same chat run: {kind:"chat", messages:[…], values:{<name>: value}}, using each variable's exact `name` and its `type` (string, number, boolean, array, object). A variable left out reaches the prompt empty. Read them from napkin_interfaces_get while you write the page, or at run time from ctx.flows[i].variables.
Certificate recorded, valid to 2026-12-19
Authorization not required
Unknown → Live
First tool surface recorded: 59 tools (server version 0.0.0)
https://api.zerowidth.ai/mcp/x/napkin (mcp_streamable_http) — from mcp_registry, with the record
Showing the latest 13 events. The API returns up to 500 and filters by kind: ?kind=tool_surface_changed
{"$schema":"http://json-schema.org/draft-07/schema#","properties":{"folderId":{"description":"Only search inside this folder and the folders under it (ids from workspace_folders_browse or a folder hit).","maxLength":100,"minLength":1,"type":"string"},"kinds":{"description":"Restrict to these kinds (flowfile,pagefolder,datasetflow,evalpage,entrydoc,boarddataset, …). Omit to search everything.","items": ⟨11 unchanged words⟩ ,"rubric","review","spec","source","doc","sheet","diagram","interface","change","folder"],"type":"string"},"minItems":1 ⟨10 unchanged words⟩ :"integer"},"q":{"description":"Case-insensitiveWords,substringanymatchedorder,againstcase-insensitive.names/titles.Fewer words find more.","maxLength":200,"minLength":1,"type ⟨33 unchanged words⟩
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Resolve or reopen a thread"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Browse the workspace's tags"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Read the tags on entities"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Set an entity's tags"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Fetch ZeroWidth doc by slug"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"List ZeroWidth docs pages"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Create a Napkin board"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Read a Napkin board's shapes"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"List Napkin sketches"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Rename, describe, archive, or share a Napkin board"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"View a Napkin sketch (rendered image)"}
{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":false,"title":"Add pictures to a brand kit"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Apply the brand to a piece"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Check writing against the brand"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Draft a brand kit"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Look at the brand's examples"}
{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":false,"title":"Get a brand's font files"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Read the brand"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"List brand kits"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Look at a brand's pictures"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Build a designed deck"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Export a deck as images"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Read a deck's outline (markdown)"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Set a deck's theme"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Write a whole deck from a markdown outline"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Create a Napkin diagram"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Read a Napkin diagram"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"List Napkin diagrams"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Edit a Napkin diagram"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Create a Napkin doc"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Read a Napkin doc"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"List Napkin docs"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Edit a Napkin doc"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Draw on a Napkin board"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Create a Napkin interface"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Read a Napkin interface"}
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false,"title":"Change what a Napkin interface can reach"}
{"destructiveHint":true,"openWorldHint":true,"readOnlyHint":false,"title":"Change what data a Napkin interface can use"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"List Napkin interfaces"}
{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":false,"title":"Save a version of a Napkin interface"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Look at a Napkin interface"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Edit a Napkin interface's files"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"List slide layouts"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Pin a chart onto a sheet"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Create a Napkin sheet"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"List Napkin sheets"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Run SQL against a sheet"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"A sheet's SQL schema"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Write cells into a sheet tab"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Rename, describe, archive, or share a Napkin sheet"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"View a sheet chart"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Add a slide to a deck"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Fill a slide's empty text areas"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Move or change a deck slide"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"View one deck slide (rendered image)"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Add columns to a sheet table"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Add records to a sheet table"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Add a view to a sheet table"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Make a table of records"}
{"destructiveHint":true,"openWorldHint":false,"readOnlyHint":false,"title":"Delete records from a sheet table"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Describe a sheet table"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Find records in a sheet table"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Change records in a sheet table"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Search ZeroWidth docs"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true,"title":"Search the whole workspace"}
{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":false,"title":"Save a picture from this chat to your files"}
⟨12 unchanged words⟩ {"description":"Which worked example to startfrom.from`chat`— a whole working page, written well, that you then shape to what was asked. `blank`: a title and the client library. Start here unless one of the others is exactly the shape you want. `chat`: a conversation against oneflow;flow,`form`streamed,iskeeping the history client-side so the flow sees the whole thread. `form`: labelled fields sent as a formrun;run,`decision`withisthe answer underneath. `decision`: text inand, a shim's answerout.out,`blank`with its confidence and the action it recommends. `app`: an app with several screens (Preact, htm and Lucide from thedefaultshelf):istabs along the bottom on atitlephone and theclienttoplibraryon—atakewide screen, the screen kept in the address, a home screen and a streamed chat. For a member app or a tool with a few views. `dashboard`: cards over the granted data (Chart.js from the shelf): each Ledger metric's latest reading and a line of its history, each table's count, a bar of how it splits on its first choice column, and its latest records. `intake`: a form built from a granted table's columns that adds a record to it, with a done state and 'add another'. Works through a share link. `board`: a granted table's records in lanes by its first choice column, moved a lane at a time by someone who may edit. Needs a signed-in viewer to move cards. Take `blank` (the default) when none of the others is the shape you want, rather than deleting one you didn't need. The data starters (dashboard, intake, board) read what's granted with napkin_interfaces_grant_data, so grant the table or metric too.","enum":["blank","chat","form","decision","app","dashboard","intake","board"],"type":"string"},"workspace": ⟨29 unchanged words⟩
⟨60 unchanged words⟩ more files, served from its own origin.TherePut styles in a <style> block. LIBRARIES: there isNOno build step andNOnolibraryCDN, and a <script src> to any other site is blocked. A fixed shelf of libraries is served from the interface's own origin instead; load them with exactly these tags (the versions are pinned, and any other /lib/ path is not found):noPREACTReact+ HTM, for a page with several screens or a lot of state — components and hooks with noTailwindbuild: <script src="/lib/preact@10.29.8/preact.min.umd.js"></script> <script src="/lib/preact@10.29.8/hooks.umd.js"></script> <script src="/lib/htm@3.1.1/htm.umd.js"></script> then var html = htm.bind(preact.h); var useState = preactHooks.useState; function App(){ var s = useState(0); return html`<button onClick=${function(){ s[1](s[0] + 1) }}>${s[0]}</button>` } preact.render(html`<${App}/>`,nodocument.getElementById("app")).CDN.TemplatesAare tagged strings (html`…`), not JSX. CHART.JS, for a chart: <scriptsrc>src="/lib/chart.js@4.5.1/chart.umd.min.js"></script>tothenanywherenewbutChart(canvasElement,this{origintype:is"bar"blocked| "line" | "doughnut" | …,sodata: { labels: [...], datasets: [{ label, data: [...] }] }, options: {} }). Color the datasets from the brand variables, read with getComputedStyle(document.documentElement).getPropertyValue("--brand-accent"). LUCIDE, for icons: <script src="/lib/lucide@1.49.0/lucide.min.js"></script>. In plain markup, writevanilla<iJSdata-lucide="calendar"></i> andputcallstyleslucide.createIcons()inonce it's on the page (again after adding more). In a<style>Preactblock.page DON'T — createIcons swaps elements Preact owns; draw the icon from its shapes instead: lucide.icons.Calendar is a list of [tag, attributes], so render <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> with preact.h(tag, attributes) for each (the app starter has an Icon component that does this). Names are lucide.dev's: kebab-case in markup, PascalCase in lucide.icons. A one-screen page needs none of these: plain HTML, CSS and JavaScript is still the default. React, Tailwind and every other library are not available. index.html is the entry point and must exist. ⟨34 unchanged words⟩ --brand-accent), var(--brand-accent-2), var(--brand-color-<colour name>) (napkin_interfaces_view lists the exact names this brand defines), var(--brand-font-heading), var(--brand-font-body), var( ⟨225 unchanged words⟩ src="/zerowidth.js"></script>. Then zw.ready() resolves with the context { viewer, grants, flows, tables, metrics, tasks }; ctx.flows lists the flow grants, each with its `id`, `name` and `variables`. zw.flows.run(flowIdflow.id, input) runs one. Call it inside zw.ready().then(...), and handle the rejection — it rejects only when the page isn't open in ZeroWidth, so show "Open this from ZeroWidth" there instead of aflowdead button. A run can take up to two minutes; show it working (stream it, below), and show theinterfaceerror'swasmessagegranted.when one fails — err.message is a sentence written for the person using the page. Input is {kind:"chat", messages:[{ ⟨20 unchanged words⟩ the assistant text out of a chat result. SOURCES: when the flow searches a knowledge base, zw.sources(result) lists the documents the run actually read, as [{ name, documentId }], once each. Show those under the answer for "where did this come from" — never ask the model to write a sources line and parse it out of the text. The flow needs the knowledge base attached BEFORE it is published (workbench_flows_attach_knowledge_base, then workbench_flows_publish, then re-grant the new version here); the interface runs the published version it was granted. FEEDBACK: to let someone rate an answer or flag a wrong one, call zw.feedback.send(result, { rating: "up" | "down", correction, comment }) with the result zw.flows.run returned. It goes to Caliper, into the flow's feedback dataset, with the question and the answer taken from the run itself; `correction` is what the answer should have been. Needs no extra grant and no table — never make a table to log flagged answers. It returns a promise: say it was sent only once it resolves, and show the error's message if it rejects. Tell the user flagged answers are in Caliper, in the dataset named after the flow, under Datasets. VARIABLES: a flow grant lists `variables` — the ⟨322 unchanged words⟩ delete — so an intake form works whenshared, a triage board needs a signed-in viewer. A granted Workbench TASK (ctx.tasks; each says whether it `takesInput`) starts with zw.tasks.start(task, input), which resolves { runId, status } at once — the run carries on after the page closes — and zw.tasks.get(task, runId) reads { status, output } for runs this page started. Running a shim on every keystroke is fine — it costs nothing and there is no rate limit. Debounce ~150ms and COALESCE: remember the latest text and run it when the current call finishes, so the answer matches what is on screen. Clear any in-flight guard on failure as well as success, or one call that doesn't come back wedges the interface. `d.action` is "act" | "suggest" | "refuse" — those three strings, nothing else. Branch on it rather than on a confidence threshold you invent; "refuse" means the shim doesn't recognise the input well enough to answer, so say so rather than showing a low-confidence guess. If you run a requestAnimationFrame loop, remove CSS transitions from any property it writes — the two fight and the property looks frozen. A flow answers in markdown: render it with el.replaceChildren(zw.markdown(text)). It builds DOM nodes, so model output is never treated as markup. STREAM chat answers: zw.flows.run(id, input, { onEvent: fn }) delivers the text token by token, and a flow that takes several seconds reads as broken without it. Use zw.textDelta(event) for each chunk — it returns null for anything that isn't text, so pass it every event — and accumulate. The promise still settles at the end with the whole result; take the final text from there. onEvent also sees node_start / node_complete / node_error if you want to name the step. Don't name a top-level variable `history`, `name`, `status`, `length`, `origin` or `top`: those are already window properties, so `var history = []` leaves you with the browser's History object and `history.push` fails. Prefix it, or keep it inside a function. Style it plainly and legibly: a system font stack, generous spacing, one column unless there's a reason. It runs on phones too.s
See what an interface LOOKS like and DOES right now: thewholepage,drawnRUNS from its current filesat desktop widthunder the same rules as the live page (no web fonts, no remote pictures), and you get a picture plus anything that went wrong (script errors, failed promises, blocked requests, console errors). Use it after every write and before ⟨9 unchanged words⟩ you what you wrote, not what it renders. `size: "phone"` looks at it 390px wide — use it for anything meant for a phone, like a mobile app mockup. `steps` drive the page before and between pictures, so you can check every screen and that buttons work: `{click: "Plan"}` (by visible text or label, or a CSS selector like "#nav-plan"), `{type: {into: "textarea", text: "hi"}}`, `{press: "Enter"}`, `{wait: 500}`, and `{shot: "Plan screen"}` for a picture. With no steps you get one picture of the first screen. Check each tab of an app in ONE call: shot, click a tab, shot, click the next, shot. Flows don't run here: a flow call gets a placeholder reply, so you can see the chat send and answer without spending anything; tables and metrics read empty. Look for text that's too small or runs ⟨17 unchanged words⟩
⟨4 unchanged words⟩ {"interfaceId":{"type":"string"},"size":{"description":"The window to look through. Default desktop (1280 wide); phone is 390 wide.","enum":["desktop","phone"],"type":"string"},"steps":{"description":"Done in order, with a picture at each `shot` (at most 6). Default: one shot of the first screen.","items":{"anyOf":[{"properties":{"click":{"maxLength":200,"minLength":1,"type":"string"}},"required":["click"],"type":"object"},{"properties":{"type":{"properties":{"into":{"maxLength":200,"minLength":1,"type":"string"},"text":{"maxLength":2000,"type":"string"}},"required":["into","text"],"type":"object"}},"required":["type"],"type":"object"},{"properties":{"press":{"maxLength":20,"minLength":1,"type":"string"}},"required":["press"],"type":"object"},{"properties":{"wait":{"maximum":3000,"minimum":0,"type":"integer"}},"required":["wait"],"type":"object"},{"properties":{"shot":{"maxLength":80,"minLength":1,"type":"string"}},"required":["shot"],"type":"object"}]},"maxItems":20,"type":"array"},"workspace":{"description":"Workspace slug. Personal tokens ⟨26 unchanged words⟩
⟨154 unchanged words⟩ more files, served from its own origin.TherePut styles in a <style> block. LIBRARIES: there isNOno build step andNOnolibraryCDN, and a <script src> to any other site is blocked. A fixed shelf of libraries is served from the interface's own origin instead; load them with exactly these tags (the versions are pinned, and any other /lib/ path is not found):noPREACTReact+ HTM, for a page with several screens or a lot of state — components and hooks with noTailwindbuild: <script src="/lib/preact@10.29.8/preact.min.umd.js"></script> <script src="/lib/preact@10.29.8/hooks.umd.js"></script> <script src="/lib/htm@3.1.1/htm.umd.js"></script> then var html = htm.bind(preact.h); var useState = preactHooks.useState; function App(){ var s = useState(0); return html`<button onClick=${function(){ s[1](s[0] + 1) }}>${s[0]}</button>` } preact.render(html`<${App}/>`,nodocument.getElementById("app")).CDN.TemplatesAare tagged strings (html`…`), not JSX. CHART.JS, for a chart: <scriptsrc>src="/lib/chart.js@4.5.1/chart.umd.min.js"></script>tothenanywherenewbutChart(canvasElement,this{origintype:is"bar"blocked| "line" | "doughnut" | …,sodata: { labels: [...], datasets: [{ label, data: [...] }] }, options: {} }). Color the datasets from the brand variables, read with getComputedStyle(document.documentElement).getPropertyValue("--brand-accent"). LUCIDE, for icons: <script src="/lib/lucide@1.49.0/lucide.min.js"></script>. In plain markup, writevanilla<iJSdata-lucide="calendar"></i> andputcallstyleslucide.createIcons()inonce it's on the page (again after adding more). In a<style>Preactblock.page DON'T — createIcons swaps elements Preact owns; draw the icon from its shapes instead: lucide.icons.Calendar is a list of [tag, attributes], so render <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> with preact.h(tag, attributes) for each (the app starter has an Icon component that does this). Names are lucide.dev's: kebab-case in markup, PascalCase in lucide.icons. A one-screen page needs none of these: plain HTML, CSS and JavaScript is still the default. React, Tailwind and every other library are not available. index.html is the entry point and must exist. ⟨34 unchanged words⟩ --brand-accent), var(--brand-accent-2), var(--brand-color-<colour name>) (napkin_interfaces_view lists the exact names this brand defines), var(--brand-font-heading), var(--brand-font-body), var( ⟨225 unchanged words⟩ src="/zerowidth.js"></script>. Then zw.ready() resolves with the context { viewer, grants, flows, tables, metrics, tasks }; ctx.flows lists the flow grants, each with its `id`, `name` and `variables`. zw.flows.run(flowIdflow.id, input) runs one. Call it inside zw.ready().then(...), and handle the rejection — it rejects only when the page isn't open in ZeroWidth, so show "Open this from ZeroWidth" there instead of aflowdead button. A run can take up to two minutes; show it working (stream it, below), and show theinterfaceerror'swasmessagegranted.when one fails — err.message is a sentence written for the person using the page. Input is {kind:"chat", messages:[{ ⟨20 unchanged words⟩ the assistant text out of a chat result. SOURCES: when the flow searches a knowledge base, zw.sources(result) lists the documents the run actually read, as [{ name, documentId }], once each. Show those under the answer for "where did this come from" — never ask the model to write a sources line and parse it out of the text. The flow needs the knowledge base attached BEFORE it is published (workbench_flows_attach_knowledge_base, then workbench_flows_publish, then re-grant the new version here); the interface runs the published version it was granted. FEEDBACK: to let someone rate an answer or flag a wrong one, call zw.feedback.send(result, { rating: "up" | "down", correction, comment }) with the result zw.flows.run returned. It goes to Caliper, into the flow's feedback dataset, with the question and the answer taken from the run itself; `correction` is what the answer should have been. Needs no extra grant and no table — never make a table to log flagged answers. It returns a promise: say it was sent only once it resolves, and show the error's message if it rejects. Tell the user flagged answers are in Caliper, in the dataset named after the flow, under Datasets. VARIABLES: a flow grant lists `variables` — the ⟨240 unchanged words⟩ choices by option NAME. zw.tables.query(table, {where: [{ field, op, value }], sort: [{ field, dir }], limit }) resolves { records: [{ id, fields }], total }; zw.tables.add(table, { Field: value }) or a list adds records; zw.tables.update(table, id, fields) and zw.tables.remove(table, ids) need "edit". zw.metrics.read(metric, { from, to }) resolves { metric, readings: [{ value, at, note }] }; zw.metrics.record(metric, value, { note }) needs "record". Name a table or metric by its id or its name. Through a share link a guest can read and add, never change or delete — so an intake form works when shared, a triage board needs a signed-in viewer. A granted Workbench TASK (ctx.tasks; each says whether it `takesInput`) starts with zw.tasks.start(task, input), which resolves { runId, status } at once — the run carries on after the page closes — and zw.tasks.get(task, runId) reads { status, output } for runs this page started. Running a shim on every keystroke is fine — it costs nothing and there is no rate limit. Debounce ~150ms and COALESCE: remember the latest text and run it when the current call finishes, so the answer matches what is on screen. Clear any in-flight guard on failure as well as success, or one call that doesn't come back wedges the interface. `d.action` is "act" | "suggest" | "refuse" — those three strings, nothing else. Branch on it rather than on a confidence threshold you invent; "refuse" means the shim doesn't recognise the input well enough to answer, so say so rather than showing a low-confidence guess. If you run a requestAnimationFrame loop, remove CSS transitions from any property it writes — the two fight and the property looks frozen. A flow answers in markdown: render it with el.replaceChildren(zw.markdown(text)). It builds DOM nodes, so model output is never treated as markup. STREAM chat answers: zw.flows.run(id, input, { onEvent: fn }) delivers the text token by token, and a flow that takes several seconds reads as broken without it. Use zw.textDelta(event) for each chunk — it returns null for anything that isn't text, so pass it every event — and accumulate. The promise still settles at the end with the whole result; take the final text from there. onEvent also sees node_start / node_complete / node_error if you want to name the step. Don't name a top-level variable `history`, `name`, `status`, `length`, `origin` or `top`: those are already window properties, so `var history = []` leaves you with the browser's History object and `history.push` fails. Prefix it, or keep it inside a function. Style it plainly and legibly: a system font stack, generous spacing, one column unless there's a reason. It runs on phones too.whe
⟨623 unchanged words⟩ or "shim" alongside its id and name. DATA: an interface can be granted a sheet TABLE or a Ledger METRIC with napkin_interfaces_grant_data. ctx.tables lists each table with its `fields` (name, type, options) and `access` ("read", "add" or "edit"); ctx.metrics lists each metric with its unit and `access` ("read" or "record"). Records travel keyed by field NAME, choices by option NAME. zw.tables.query(table, { where: [{ field, op, value }], sort: [{ field, dir }], limit }) resolves { records: [{ id, fields }], total }; zw.tables.add(table, { Field: value }) or a list adds records; zw.tables.update(table, id, fields) and zw.tables.remove(table, ids) need "edit". zw.metrics.read(metric, { from, to }) resolves { metric, readings: [{ value, at, note }] }; zw.metrics.record(metric, value, { note }) needs "record". Name a table or metric by its id or its name. Through a share link a guest can read and add, never change or delete — so an intake form works when shared, a triage board needs a signed-in viewer. A granted Workbench TASK (ctx.tasks; each says whether it `takesInput`) starts with zw.tasks.start(task, input), which resolves { runId, status } at once — the run carries on after the page closes — and zw.tasks.get(task, runId) reads { status, output } for runs this page started. Running a shim on every keystroke is fine ⟨289 unchanged words⟩
⟨661 unchanged words⟩ or "shim" alongside its id and name. DATA: an interface can be granted a sheet TABLE or a Ledger METRIC with napkin_interfaces_grant_data. ctx.tables lists each table with its `fields` (name, type, options) and `access` ("read", "add" or "edit"); ctx.metrics lists each metric with its unit and `access` ("read" or "record"). Records travel keyed by field NAME, choices by option NAME. zw.tables.query(table, { where: [{ field, op, value }], sort: [{ field, dir }], limit }) resolves { records: [{ id, fields }], total }; zw.tables.add(table, { Field: value }) or a list adds records; zw.tables.update(table, id, fields) and zw.tables.remove(table, ids) need "edit". zw.metrics.read(metric, { from, to }) resolves { metric, readings: [{ value, at, note }] }; zw.metrics.record(metric, value, { note }) needs "record". Name a table or metric by its id or its name. Through a share link a guest can read and add, never change or delete — so an intake form works when shared, a triage board needs a signed-in viewer. A granted Workbench TASK (ctx.tasks; each says whether it `takesInput`) starts with zw.tasks.start(task, input), which resolves { runId, status } at once — the run carries on after the page closes — and zw.tasks.get(task, runId) reads { status, output } for runs this page started. Running a shim on every keystroke is fine ⟨289 unchanged words⟩
Change what data a Napkin interface can use — Set the sheet tables, Ledger metrics and Workbench Tasks an interface may use (ADR 0085, 0086), replacing the whole list. Tables: access read, add (read and add records) or edit (also change and delete); optionally limit to some columns by name. Metrics: read or record. Tasks: the page may start them. The page then calls zw.tables.query / add / update / remove, zw.metrics.read / record and zw.tasks.start / get. Anyone who can open the interface, including people it's shared with, gets this access (a shared link can only read and add), so ask the user before widening it.
Add columns to a sheet table — Adds columns to the end of a table. Existing records leave them empty.
Add records to a sheet table — Adds records, each a map of field name → value. Choices by option name (a new name becomes a new option); dates as YYYY-MM-DD; people by user id; checkboxes true/false. Every problem is reported at once and nothing is written until all records are valid.
Add a view to a sheet table — Adds a saved view everyone sees: a grid (with where / sort), a board (lanes from a select or person column — groupBy), or a form people fill in to add records (formFields, formTitle, formIntro). A form's share link is made from the sheet's form view.
Make a table of records — Makes a table tab — typed columns, one record per row, with grid, board and form views — in an existing sheet, or in a new sheet when sheetId is omitted. Use a table (not napkin_sheets_set_cells) for lists people keep: requests, vendors, bugs, contacts. The first column names each record. Column types: text, long_text, number, date, checkbox, select, multi_select, person, url, email.
Delete records from a sheet table — Deletes records by id. People can undo this in the sheet only while it's open, so confirm with the person first.
Describe a sheet table — A table tab's columns (name, type, choices), its views, and how many records it holds. Read this before writing records so field names and choices match.
Find records in a sheet table — Records from a table tab, keyed by field name, with optional where / sort / search / limit (max 500). For counts, sums and joins, napkin_sheets_query runs SQL over tables too (each table is an SQL table with an id column).
Change records in a sheet table — Changes some fields of records by id (from napkin_tables_query). Fields left out keep their values.
⟨463 unchanged words⟩ the assistant text out of a chat result. VARIABLES: a flow grant lists `variables` — the values the flow reads besides the conversation, usually the {{key}} tokens in its prompt (who the user is, their plan, the page they are on). Send them on the same chat run: {kind:"chat", messages:[…], values:{<name>: value}}, using each variable's exact `name` and its `type` (string, number, boolean, array, object). A variable left out reaches the prompt empty. Read them from napkin_interfaces_get while you write the page, or at run time from ctx.flows[i].variables. A SHIM is not a flow and takes ⟨399 unchanged words⟩