In-product Help
Declare app-owned Markdown once for the shared Help UI, full-page Help, Assistant, and MCP.
Declare an application's product guidance once. Cloud can then expose the same Markdown through the shared Layout, full-page Help, Assistant search and reads, and the authenticated Cloud MCP server. These Cloud surfaces show an application's Help only to signed-in users who may see that application; see Who can read Help.
The single declaration keeps human and agent guidance aligned even when the application is developed and released outside the Cloud repository. It is a public application contract; no built-in package or repository integration is required.
Help is for static product guidance: tasks, concepts, reference material, and troubleshooting. Use developer documentation for application APIs. Keep live, permission-sensitive data in an authorized Query or application route.
| The application owns | Cloud owns |
|---|---|
| Markdown content and article order | Validation and bounded registration |
| Stable article IDs and useful metadata | Layout Help and full-page Help |
| Whether the content is safe for everyone who sees the app | Who can read Help, search, reads, and agent discovery |
| Specialized embedded presentation, when needed | Publication lifecycle and derived routes |
Open Help beside the app
On signed-in Layout pages, Help, ? (Shift+/), and the cloud.help
command open Core's Help provider in the shared panel slot.
Ctrl+J, or Cmd+J on Apple devices, toggles the slot. The rail and phone header
Help buttons toggle Help: they close it when it is already open, or switch
from another provider. The Help command opens or focuses it without closing it.
Without a matching slot, Help opens as a dialog. This includes anonymous
pages, Layout panel={false}, and pages from an application built against an
older @k2b/cloud. On a phone, below 48 rem, that dialog is the shared bottom
sheet at full height.
Inside Help, / focuses search unless the reader is editing another field.
Escape in a nonempty search field clears it first. Otherwise Escape closes the
panel and returns focus to the opener. Articles of the current app open inside
Help. Open as page opens the current article or start in the app window.
Panel article navigation leaves the iframe address unchanged. Cross-app Help
links also open in the app window.
Without page context, Help shows Core's own start. An unknown or inaccessible app shows a neutral unavailable note without its name or article titles.
Keep Help in one module
When an application owns Markdown Help, put the declaration in
src/help/index.ts and keep every Markdown source below src/help/:
src/help/
├── index.ts
└── documents/
├── inventory-start.help.md
└── inventory-access.help.mdSmall collections may place Markdown files directly beside index.ts. A
larger collection may group them under documents/. Both follow the same
boundary: the declaration and its content stay in src/help/.
Use one declaration module to avoid ambiguous imports. Use src/help/index.ts
for file-backed Help, or src/help.ts when it owns no Markdown files.
Cloud does not scan the filesystem. Import every article and list it explicitly so ownership, review order, and bundle contents remain visible.
Add localized articles
Keep all languages in the same Help declaration. Use one folder per canonical locale when an application ships translations:
src/help/
├── index.ts
└── documents/
├── en/
│ ├── inventory-start.help.md
│ └── inventory-access.help.md
└── de/
└── inventory-start.help.mdThe base locale is complete and owns article IDs and navigation metadata.
A localized folder may contain only the translated articles available today.
It owns their title, description, and Markdown body. Keep the same id; do not
create language-specific IDs or duplicate Help registrations. Localized
frontmatter may omit icon, order, kind, needs, next, and related;
omitted fields inherit the base value. If repeated, they must match exactly,
including the order of the related list.
Cloud resolves each article through the exact requested locale, its BCP 47
ancestors, then the base locale. For example, de-CH can use a de-CH article,
fall back to de for another article, and finally use en for an untranslated
article.
Write an article
Each article is a Markdown asset with YAML frontmatter:
Follow Write app help for article structure, step and sentence rules, and the Cloud glossary for product terms. Product language and tone covers the voice of all product text and translation equivalence.
---
id: inventory-start
title: Start with Inventory
kind: start
icon: ti ti-package
description: Create and update inventory items.
order: 10
---
# Start with Inventory
**First steps**
## Create an item
Open Inventory and choose **New item**{icon="plus"}.| Field | Required | Contract |
|---|---|---|
id |
Yes | Lowercase kebab case; unique in the Help declaration |
title |
Yes | Non-empty article title |
order |
No | Integer; defaults to 100 |
icon |
No | Tabler icon classes for the article |
description |
No | Short search and overview text |
kind |
No | start, task, reference, or troubleshoot; defaults to task |
needs |
No | One glossary access level: view, edit, or manage; never an app-specific permission |
next |
No | Same-app document ID for the next task; cannot point to itself |
related |
No | Ordered list of <id> or <app>/<id> references; defaults to []; no duplicates |
The body must not be empty. Articles are sorted by order, then by title.
Level-two headings need no icon metadata. An optional trailing
{icon="plus"} is removed from the title and ignored by the reader.
Heading IDs keep the same slugging rules and must be unique.
Name keys, labels, and menu paths
These forms belong to Help Markdown. Ordinary content Markdown keeps its existing syntax.
| Source | Reader |
|---|---|
:kbd[mod+k] |
Key caps for the reader's platform: Ctrl and K, or ⌘ and K on Mac |
**Hide**{icon="eye-off"} |
A label chip with the control's icon; inline Markdown stays supported |
**Space settings → Sharing** |
Two label chips joined by ›, with → for screen readers |
**Space settings → Sharing**{icon="settings"} |
A menu path with the icon on its first chip |
Icon names use lowercase letters, digits, and hyphens. Plain bold text stays bold. A shortcut must format to at least one key; invalid shortcuts fail startup. Code spans and fenced examples remain literal.
Link related guidance
Use [Share](help:inventory-sharing#invite) for a same-app article and
[Reply](help:mail/mail-invitations#reply) for another app. The reader resolves
each target in its current published corpus and language, using the same app
visibility rule as article reads. Resolved links use
/help/apps/<app>/<id>#section. Unknown or invisible targets render only the
link text and expose no target address. next and related use this same
resolution; unresolved entries are omitted. Write help: links inline: a reference
definition, autolink, image, or HTML attribute with a help: target fails
startup. The scheme must be lowercase help:; spellings such as HELP: fail
startup. Help syntax that remains unconverted in agent text also fails startup.
Use guided blocks
Help supports these guided blocks:
| Block | Use |
|---|---|
steps |
Ordered task |
reference |
Compact facts or controls |
compare |
Alternatives or differences |
details Question |
Collapsed reasons or special cases; a title is required |
Start a guided block after a blank line, at the start of the document, or after another block. An opener directly after a text line fails startup. The block contains normal Markdown:
:::steps
1. Enter a name.
2. Set the initial quantity.
3. Choose **Create**.
:::Keep the main task short. Put reasons and special cases in details:
:::details What happens to existing items?
Inventory keeps the items you already created.
:::steps
1. Open **Items**.
2. Choose **Export**{icon="download"}.
:::
:::Details start collapsed. Their full body remains searchable and is given in
full to agents. They can contain steps, reference, and compare blocks and
ordinary Markdown, but no other details. A callout inside details fails
startup: a warning or note belongs where the reader sees it. An opener directly
after a text line, without a title, without a closing :::, or with details
inside fails startup.
A paragraph containing only bold text becomes an eyebrow. Use it as a short label, not another heading.
Use callouts
Callouts support note, info, success, warning, and danger:
:::warning Before deleting
Deleting an item cannot be undone.
:::The callout body is ordinary Markdown: emphasis, links, lists, tables, and
code render as they do elsewhere on the page. Close the callout with ::: on
its own line; a ::: inside code or a list does not close it. Callouts inside
lists, quotes, or another callout stay text.
A callout shows its type's icon and a light tint. The icon and title use the tone colour; body text stays neutral. The icon is decorative. The optional plain-text title names the callout. Without a title, screen readers hear the type name in the Help document's language. Sighted readers see the icon and tint only, so add a title when the type matters.
Callouts are one syntax and one look across Cloud: MarkdownView from
@k2b/ui, the shared markdown.render() and markdown.renderSync() helpers
from @k2b/cloud/shared (pass { locale } to name untitled callouts in the
reader's language), Notebooks, and Markdown PDF exports. See
Markdown content for the exact rules.
A Cloud renderer with its own sanitize-html pass spreads
markdownCalloutSanitizeOptions() from @k2b/cloud/shared into its options,
creating fresh options for each sanitization, so the callout icon survives
and every other SVG is still discarded.
By default these helpers render links like MarkdownView: a relative link is
a reference pill with a type icon, and a web or mail link is quiet text with a
thin accent underline (see
Markdown content). A raw
anchor inside a link's text stays text. A Notebooks note:// or attach://
link renders as its text, because only Notebooks resolves it.
Pass { links: "plain" } when the HTML is read outside Cloud, for example in
email. Each link then renders as an ordinary anchor around its text instead.
Plain output also renders a code block, fenced or indented and including a
Mermaid fence, as its source in a pre, without the language label or diagram.
Email drops the screen-reader styling, so an
untitled callout shows its type name there as text. HTML that Cloud renders
without one reader's locale, such as notification batches, announcements, and
messages written in Mail, names untitled callouts in English; give those
callouts a title in the readers' language.
The Help renderer also:
- enables GitHub-flavored Markdown;
- turns source line breaks into visible line breaks;
- sanitizes rendered HTML;
- keeps internal links in the current tab and opens external links in a new tab;
- renders code without executable scripts.
Do not use Mermaid in Help articles. The Help reader does not start the Mermaid client renderer.
Define Help once
Import the articles in src/help/index.ts and pass only the documents to
defineHelp():
import { defineHelp } from "@k2b/cloud";
import access from "./documents/inventory-access.help.md" with {
type: "text",
};
import start from "./documents/inventory-start.help.md" with {
type: "text",
};
export const inventoryHelp = defineHelp({
documents: [start, access],
});For localized Help, map explicitly imported sources by locale:
import accessEn from "./documents/en/inventory-access.help.md" with { type: "text" };
import startEn from "./documents/en/inventory-start.help.md" with { type: "text" };
import startDe from "./documents/de/inventory-start.help.md" with { type: "text" };
export const inventoryHelp = defineHelp({
baseLocale: "en",
documents: {
en: [startEn, accessEn],
de: [startDe],
},
});The declaration has no route, base path, role, router, or Layout configuration. Cloud already knows the owning application's ID and base path when it starts.
defineHelp() validates the article shape and creates an immutable source
declaration. Startup compiles the complete collection, rejects duplicate IDs,
and calculates its manifest hash before the application advertises Help.
Same-app IDs in next, related, and help: links must exist in the base
locale. This includes references prefixed with the owning app ID. Validation
names the locale, document, and bad reference; code examples are ignored.
Cross-app references resolve for the reader at read time and do not prevent
startup. Unknown extra frontmatter fields are stripped. Help reserves kind,
needs, next, and related: an article that uses one of these keys for
something else fails startup. Third-party articles with only id, title,
icon, description, and order keep working.
One Markdown article is limited to 128 KiB. Search text is retained for every language. Invalid articles fail startup before the application advertises Help.
Core creates the shared Help schema in Postgres during setup. Applications publish their complete collection in a transaction before advertising readiness. An unchanged content hash reuses the stored collection; a changed hash publishes a separate version. The app registry carries only the current hash and route metadata. Article metadata and Markdown live in Postgres.
Register Help when the app starts
Pass the declaration to app.start() next to other executable app-owned
surfaces such as capabilities:
import { defineApp } from "@k2b/cloud";
import { Hono } from "hono";
import { inventoryHelp } from "./help";
const app = defineApp({
id: "inventory",
name: "Inventory",
description: "Track inventory items.",
icon: "ti ti-package",
basePath: "/app/inventory",
baseUrl: "http://app-inventory:3000",
routes: ["/app/inventory"],
});
const router = new Hono().get("/app/inventory", (c) =>
c.html("<h1>Inventory</h1>"),
);
export default await app.start({
help: inventoryHelp,
fetch: router.fetch,
});Do not mount a Help API router, render a Layout.HelpDocuments registrar, or
add standalone Help page routes. Those are consumers of the registration, not
additional declarations.
The existing app heartbeat renews the published collection before renewing app registration. If the collection disappears, that heartbeat republishes it from the application declaration. Core must recreate a missing schema before this can succeed. Persistent registration failures use the existing app-lease failure policy and restart the process.
Core removes at most one expired collection per minute. A collection becomes eligible after six minutes without renewal: two app-lease lifetimes. Stopping one replica does not delete Help used by another. Stored collections from unavailable applications are not exposed as current Help.
Use the automatically derived surfaces
For the example above, Cloud derives these product routes:
| Surface | Derived route or behavior |
|---|---|
| Layout Help | Registers the current app's manifest automatically |
| Help overview | /app/inventory/help |
| Article deep link | /app/inventory/help/:documentId |
| Search data | /api/help/v1/inventory/search?q=... |
| Article data | /api/help/v1/inventory/documents/:documentId |
| Agents | Use search_help and read_help against the same live corpus |
The full-page routes come from the application's basePath. Applications do
not repeat that path in their Help declaration. Core owns search and article
transport and the shared reader; the derived application routes forward to
that reader while the application remains the content owner.
The panel Help head shows the app tile and app name. Search stays in place when an article opens. The ⋯ menu offers Copy as Markdown and Open as page.
The start lists guidance in this order:
- For this view: up to three articles named by the current page.
- What do you want to do?:
taskarticles as tiles, sorted byorder(default100), then title. At most eight become tiles; the remaining tasks follow as a list below them. - Get started, Look up, and Solve problems: articles with
kind: start,kind: reference, andkind: troubleshoot, respectively. Each group uses the same sorting; empty groups are absent. Missingkindmeanstask. - Keyboard shortcuts: Everywhere, In <App>, and With a selection. Commands without a shortcut and empty groups are absent.
The shell's Search, Help, and panel shortcuts appear on the server under
Everywhere, in that order. The panel then follows the host page's live
shortcuts; it does not use its own provider document's command registry.
Commands scoped to a selection go under With a selection; other cloud.* commands go under Everywhere, and other
commands go under In <App>. Key caps follow the reader's keyboard platform.
Name two or three relevant articles on each main page with Layout's help
prop:
<Layout c={c} title="Inventory" help={["inventory-start", "inventory-access"]}>
<InventoryOverview />
</Layout>Layout sends these IDs through the panel context, including the first iframe URL, so For this view is present in the server frame. Core treats the context as a selector and filters it again against the reader's manifest. Live context changes update that section; another app or null clears it.
These are IDs from the current app's Help declaration. Cloud keeps their declared order, removes duplicates, ignores IDs outside the reader's authorized manifest, and keeps at most three. Without a manifest, no page articles are shown. These articles also keep their place in the task tiles or kind groups, so the app's start stays consistent across pages.
An app without Help articles shows a short note above the shortcuts. When Cloud could not read the app's Help metadata for the page, Help says so and offers Reload.
Full-page Help renders in the owning application's frame. The rail marks that app, its canvas and accent apply, and the title row leads back to the app. Third-party applications use the same frame.
Links to another article or section of the same app's Help open inside the
reader without reloading: /app/<id>/help/<documentId>,
/help/apps/<id>/<documentId>, or #section. On the full page, the address
updates and the browser's Back button returns to the previous article.
Returning to the Help start keeps the typed search, and an address with an
unknown article shows the Help start. The phone menu or a dialog that closes does not move the
open article. Other links navigate normally.
An article starts with its head: the app path, title, and first paragraph as a
short answer on the app's own canvas gradient (the same colours as the app
background, without a border or shadow), and the article's icon in the flat
shared icon tile. A narrow reader puts the tile before the title. One chip row
shows “You need next; article order
sorts the task tiles, remaining tasks, and kind groups on the Help start.
SSR loads the current application's localized article metadata from Postgres.
The browser receives this manifest. When the address names an article, on a
full page or through the panel's ?topic=, the server renders that article in
the first frame; other articles load on demand. An agent uses bounded search
and read operations; Cloud does not create one permanently loaded tool for
every article.
Every automatic surface uses the same request locale. Article responses and AI/MCP matches report each article's actual content locale. Browser article caches distinguish locales, so regional fallback cannot mix content between requests. AI and MCP clients receive final localized titles, descriptions, and Markdown; they never receive application message keys.
For a user-backed direct chat on a tool-capable model, AI Core resolves
search_help and read_help dynamically through the shared Help service, as
the chat's user. This
does not require capability discovery to be enabled. Applications register
their Help declaration only; they do not define AI tools or provider settings.
Tool discovery does not load article bodies. A temporary Help read failure is
isolated from ordinary chat and app capabilities; a later tool call can retry.
Every reader selects the hash advertised by the current app registration. Missing articles or versions are not served from an older collection. Database failures remain errors rather than appearing as successful empty searches. The application heartbeat can restore a missing collection.
Search product guidance
UI, Assistant, and MCP use the same PostgreSQL search and locale selection.
Search accepts up to 200 characters and returns at most 25 results. Use concise
terms in the request language. English and German articles use their respective
PostgreSQL language configurations; other languages use simple tokenization.
Exact article IDs and titles rank first. Native full-text search weights titles
above descriptions and body text. Whitespace-separated terms must all match;
quoted phrases, OR, and exclusions follow PostgreSQL web-search syntax.
Cloud chooses one language variant per article before searching. A de-CH
request can therefore return a German article and an untranslated English
article, each reporting its actual content locale. It does not search a hidden
English translation and then open a different German article.
When the operator has enabled pg_textsearch and all three Help search indexes
are valid, Cloud uses BM25 to rank matching articles after exact-title/ID
priority. Without it, native full-text search remains available. Both paths
use the same matching and language rules; ordering can differ. The Help UI
preserves the returned document order and uses local metadata matches while
a search is pending or unavailable. See
Deployment requirements
for installation and verification. Once the indexes exist, every Postgres
server that runs the database must keep pg_textsearch preloaded.
read_help and cloud__help__read return at most 7,000 characters, selecting
relevant sections when a query is supplied. An exact level-two heading selects
that section. Full article reads in the UI and MCP resources still return the
complete article.
Agent text and Copy as Markdown
read_help, cloud__help__read, MCP resource reads, and the Help API's
markdown field return portable agent text rather than the Markdown source.
“Copy as Markdown” uses that same field. Search returns metadata only.
| Source | Agent text |
|---|---|
:kbd[mod+k] in English |
Ctrl+K (⌘K on Mac) |
:kbd[mod+k] in German |
Strg+K (⌘K auf dem Mac) |
:kbd[enter] |
Enter: a key without a platform modifier reads by its name |
**Hide**{icon="eye-off"} |
**Hide** |
## Hide {icon="eye-off"} |
## Hide |
:::details Question … ::: |
**Question**, a blank line, and the full body |
[text](help:mail/mail-invitations#reply) |
[text](/help/apps/mail/mail-invitations#reply) when visible; otherwise text |
Other locales use English shortcut explanations. Code spans and fenced code stay verbatim. Steps, reference, compare, and callout directives remain as written. The 7,000-character tool-read limit applies after conversion; MCP resources and the Help API return the complete converted article.
Who can read Help
Help is for signed-in people. Layout Help, full-page Help, the Help API, the Assistant, and MCP all read through one shared reader that applies the same rule, so no surface shows more than another:
- Without sign-in, the Help API answers
401and full-page Help redirects to sign-in. No titles, descriptions, or articles are returned. OAuth clients also need thereadoradminscope, as for MCP. - The reader uses the signed-in user, or the user an API credential acts for. A service account without a user sees no Help.
- An application's Help follows its declared visibility.
nav.requiresRoleslimits Help exactly like the navigation;guestadmits guests. - An application reached only through the admin area, with
adminHrefand no ownnav, shows its Help to administrators. - Without either declaration, every signed-in user can read the Help,
including guests. If guests may not use the application, declare
requiresRoles: ["user"]; that hides both its link and its Help from them.
For anyone else, the application's Help does not exist: search returns no results, and reads and full-page Help answer like a missing article. The application needs no extra configuration; the declaration it already has for the navigation decides.
This is visibility, not authorization. The application's routes still decide who may use the application, and links from Help to protected pages still authorize normally.
The rule covers the Help that Cloud serves. When an application renders
Markdown from its own declaration, for example with getMarkdown(), that text
is part of the application's page and follows the page's route policy. API
Docs does this on purpose: its public page shows its getting-started article to
everyone. Render an article this way only when it may be public.
Keep the content safe to expose
The Help declaration has no per-article role or authorization callback. Every article of an application is visible to everyone who may see that application, so write each article for that whole audience. Registered Help is static product guidance, not a resource authorization boundary.
Do not include:
- secrets, tokens, internal hostnames, or credentials;
- user, tenant, or resource data;
- role-restricted operational state;
- instructions whose disclosure itself requires a permission check.
Put dynamic or permission-sensitive context in a Query such as gql.context.
The Query must authorize every request through the current access subject.
Reuse the declaration for specialized readers
An application may need a focused embedded reader, such as the Grids GQL reference. That consumer may select documents from the same Help declaration. It must not create another collection, registry entry, API router, or copied manifest.
Use the automatic Layout and full-page surfaces for ordinary application Help. Add a specialized consumer only when its surrounding workflow needs a distinct presentation.
Core's provider document uses LayoutHelpPanel with LayoutHelpPanelProps
from @k2b/cloud/ssr. Render it in a server page, such as inside PanelLayout,
never inside an island. It renders the start and initial article as server HTML
and mounts small islands for interaction. Search, menus, Markdown copy, and
the client article reader load on demand.
It receives the authorized documents, app identity and appearance, filtered
view IDs, keyboard platform, and optional topic from the server. It reads no
host shell data and follows context and shortcuts through the panel API.
Application authors normally use the automatic registration above.
Pass the result of preloadLayoutHelpArticle(c, appId, documentId) from
@k2b/cloud/ssr/help as initialArticle alongside a manifest-validated
initialTopic to Layout.HelpPage or LayoutHelpPanel. This renders the
article in the first frame. If the helper returns null, omit the prop to load
on demand.
Layout.HelpPage with embedded renders no Help head: the host's own window
names the reader and chooses its article. Its start keeps only the search.
For server-rendered embedded readers, preloadLayoutHelp(c, appId) from
@k2b/cloud/ssr/help loads the published, request-localized metadata. Reuse its
documents and derived URLs; do not read article lists from the app registry.
It returns null without a signed-in user or when the user may not see the
application, so an embedded reader on a public page receives no Help.
Verify Help
defineHelp() validates document frontmatter and duplicate IDs when the module
loads; app.start() compiles the bounded corpus and fails instead of publishing
an invalid registration. Keep a small application-owned test that imports the
declaration so those checks run in CI.
Before shipping, also verify:
- the application package typecheck;
- application startup with the complete Help declaration;
- Layout Help in normal and focus modes;
- the overview and one article deep link;
- search and article reads;
- one agent Help search and read;
- any specialized embedded reader;
- recovery after the published collection disappears.
Cloud repository maintainers additionally run the repository-wide Help corpus checks for built-in applications. Third-party application CI does not depend on those private source paths.
For a focused reference read, pass an exact level-two heading as the read_help
query. The response selects that section, including its examples, within the
existing read limit. Other queries retain ranked topic matching.