Write and test App source locally, build it in Knowi, and connect it to Knowi data and services.
Work with source and build locally
You can use your own editor, Git workflow, and development environment. An AI provider is not required to create or build an App from a repository.
- Connect a repository under New app > Advanced settings > Repository. See repository setup.
- Choose Create from repository. The repository must contain the files described below.
-
Clone the repository and make your changes locally. Use Node.js 22 and the checked-in lockfile:
npm ci --ignore-scripts --no-audit --no-fund npm run build
-
Test the App, commit your source, and push to the branch connected to the App. App settings > Repository shows that branch.
- Select Build in Knowi. When the version is Ready, preview it. Publish separately when it is ready for users.
The source revision is recorded with each version. A local build does not change the live App. Calls to Knowi data and services need a Knowi request identity; use preview to test those integrations. Preview does not receive live secrets or the live managed database.
To test changes to a live App separately, or to install the App on another Knowi installation, see Copy and Move Apps.
Supported stack
Use HTML, CSS, JavaScript, or a frontend framework with an npm build script. The backend is a Node.js 22 HTTP service; Express, Fastify, Nest, and pinned npm packages are supported. Listen on process.env.PORT at 0.0.0.0. Use relative browser asset and API paths because previews can have a path prefix.
Build dependencies for the target Linux platform, particularly packages with native modules. The default is linux/amd64; an ARM target requires linux/arm64 and release images that support it. A macOS or Windows dependency directory is not a deployable App. The Knowi builder creates the release for its configured platform; do not commit node_modules.
Repository contract
| File | Purpose |
|---|---|
app.json |
Service entry, data bindings, and optional features. |
package.json |
Dependencies and an npm build script. |
package-lock.json |
Locked dependencies. |
| Frontend and server source | Your App code and assets. |
db.json |
Optional additive schema for managed PostgreSQL. |
Example app.json:
{
"v": 2,
"service": "dist/server.mjs",
"bindings": [
{"key": "orders", "type": "data", "id": 1234},
{"key": "overview", "type": "dashboard", "id": 5678},
{"key": "crm", "type": "http", "connection": "crm"}
],
"reportPages": [{"path": "/", "title": "Overview"}],
"capabilities": ["data.query", "dashboard.embed", "http.request", "export.pdf"]
}
service is a relative .js, .mjs, or .cjs file that exists after the build. bindings is required; use [] if none are needed. Use actual authorized dataset and dashboard IDs, obtained from Knowi or its search tools.
A binding's stable key is the name your code calls. It is not a credential. Supported types are:
-
dataanddashboard: an exact Knowi assetid. -
http,workspace, andaction: an existing Appconnectionname. Its destination, policy, and credentials stay outside Git. -
ai: onlykeyandtype, for example{"key":"assistant","type":"ai"}.
Declare at most 64 bindings. Keys and connection names use 1-64 letters, digits, periods, underscores, or hyphens, beginning with a letter or digit. Keep keys stable when moving an App; the target maps those keys to its own assets and connections.
The optional capabilities array declares required features for portable-package checks. List the features the App uses, including PDF export and reports, which do not have their own binding. Supported names are data.query, dashboard.embed, workspace.embed, http.request, ai.generate, action.execute, report.send, report.schedule, export.pdf, state.get, state.set, and database. Declaring a capability does not grant permission to use it.
reportPages declares 1-64 reportable pages with distinct absolute paths and titles up to 120 characters. Paths cannot contain query strings, fragments, traversal, or reserved /__knowi routes. Pass filters in a report request's query, not in this declaration. An older manifest without reportPages exposes / as Home. Rebuild and publish to change the list.
Add "db":"db.json" for a managed database schema. Add branding for sign-in branding. Knowi validates the manifest at build, checks the editor's asset access for preview, and checks the publisher's access at publication.
Use the knowi_app MCP tool
An MCP client can write and build source without invoking Knowi's AI generator. Connect it to Knowi and call setup first. Every action uses the caller's Knowi identity, role, App sharing, and repository access.
| Action | Inputs and result |
|---|---|
setup |
Lists authorized repositories and remaining setup requirements. |
create |
name, slug, and an authorized source. Creates the App shell. With one writable repository, source can be omitted. |
read |
appId. Returns source files, parentCommit, the source contract, and shared change notes. |
change |
appId, exact parentCommit, files, and commit message. Commits the file changes and queues a build. Omitted files stay unchanged; null deletes a file. Read again after a conflict. |
build |
appId. Builds the current repository commit without generating source. |
status |
appId, optional jobId. Poll until done; a Ready result supplies versionId. Does not start preview. |
preview |
appId and Ready versionId. Starts preview and returns a short-lived URL when the user requests it. |
publish |
appId, Ready versionId, and confirm:true. Requires the user's request to publish and publish permission. |
secret |
appId and secret:{name,value}. To remove, use secret:{name,remove:true} with confirm:true. Changes apply at the next publish. |
export |
appId, optional versionId (defaults to the published version, or latest Ready version if none is published). Returns signed JSON for accounts in the same installation. Add portable:true to get an authenticated download path for a complete ZIP for another installation. |
When setup permits it, create accepts repository:"knowi_managed" to create a private repository. For SSH, use repository:"ssh" and remoteUrl; authorize the returned deploy key with write access, then create with the returned source label. create and change also accept the supported access and database settings when authorized.
Submit the complete desired bindings in app.json with each source change. Resolve datasets and dashboards before writing IDs. Optional intent and handoff notes are limited to 2,000 characters each and shared with App collaborators. Keep private conversations and secrets out of them. Commits shows changes made through any client; Refresh loads external changes.
Example portable export:
{"action":"export","appId":123,"portable":true}
Download the returned path while signed in to the source installation. Upload that file in the target Apps list. MCP does not upload packages, create development copies, or promote them; use the Apps UI for those workflows. OAuth and manually generated MCP tokens use the same permissions; there is no separate Apps token scope.
Runtime access and isolation
App server code calls Knowi through a request-specific gateway token. Send {capability, input} to process.env.KNOWI_GATEWAY_URL + '/v1/capabilities', forwarding only the current request's X-Knowi-Capability-Token. Never expose, retain, or log that token. Complete the call before sending the App's response; the token cannot authorize a later background job. The entire JSON request, including the capability envelope, must fit within 16 KiB.
The examples use this Node.js server helper:
async function invoke(req, capability, input) {
const response = await fetch(process.env.KNOWI_GATEWAY_URL + '/v1/capabilities', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Knowi-Capability-Token': req.headers['x-knowi-capability-token']
},
body: JSON.stringify({capability, input})
});
const result = await response.json();
if (!response.ok) {
const error = new Error(result.error || 'Knowi request failed');
error.status = response.status;
error.result = result;
throw error;
}
return result;
}
Handle errors in your route and show a useful message. Preserve error.result.requestId for write-action support, and never log request headers.
A Knowi sign-in request carries the trusted X-Knowi-App-User header: base64url-encoded JSON containing id, name, and email. Public and App-managed visitors have no Knowi user identity. Their selected datasets use an App grant checked against the publisher's current access and content filters. Dashboard, workspace, report, PDF, and saved write-action capabilities require Knowi sign-in.
App manages access does not create authentication. Implement your own login and server-managed sessions. Deployed files are read-only, resources are bounded, and outbound connections follow the installation's policy. Code receives neither the user's Knowi browser session nor datasource credentials.
Data and connections
| Capability | Behavior |
|---|---|
data.query |
Reads a bound dataset with governed fields, filters, sorting, and paging. |
dashboard.embed |
Returns a secure URL for a bound dashboard. Default lifetime: 30 minutes, bounded by the App session. Nested dashboard embedding is unavailable while the App itself is embedded. |
workspace.embed |
Returns an Agentic BI workspace URL over the connection's allowed datasets. Lifetime: 2-30 minutes, default 10. |
http.request |
Calls an approved connection URL and method. Knowi adds the stored credential. Only public destinations are allowed; redirects are not followed. Request and response bodies are each limited to 8 KB. |
state.get / state.set
|
Small App-scoped JSON values: 8 KB per key, 128 keys, 256 KB total. Use managed PostgreSQL for larger storage. |
const page = await invoke(req, "data.query", {
binding: "orders",
fields: ["Order Date", "Region", "Revenue"],
filters: [{field: "Region", operator: "eq", value: "West"}],
sort: [{field: "Order Date", direction: "desc"}],
limit: 1000,
offset: 0
});
Filters support eq, neq, gt, gte, lt, lte, isNull, and isNotNull. Field names must exist in the dataset. A direct dataset's declared runtime tokens can use eq with the exact token name. Knowi adds the effective viewer's content filters; App filters can only narrow those results.
The response contains columns, row arrays in rows, and truncated. If nextOffset is present, use it to continue. The default limit is 1,000 rows, with a maximum of 200,000; a large result can reach the response-size limit first.
Writes and other side effects must run while handling a same-origin POST, PUT, PATCH, or DELETE, never as a side effect of a GET. Ordinary external HTTP calls and managed PostgreSQL are separate runtime paths; use http.request when you need a Knowi-managed connection and credential.
Managed database schema
Use a pinned PostgreSQL client and the App's restricted connection. KNOWI_DATABASE_CONFIG identifies its JSON configuration file in the live runtime. A db.json schema must be additive; Knowi applies it before publication. A schema failure leaves the current version live. Preview does not connect to the live database, and rollback changes code without rolling data back. Package transfer does not copy database records.
Secrets
Save secrets in App settings > Secrets. Server code reads them as environment variables, such as process.env.CRM_API_KEY, after the next publish. Preview receives no secrets. Replacing or removing a secret also takes effect at publication. Keep values out of prompts, source, browser code, logs, and shared change notes.
Runtime actions
AI text inside an App
Declare an ai binding and call:
const result = await invoke(req, "ai.generate", {
prompt: "Summarize these incidents: " + JSON.stringify(incidents),
system: "Use only these records. Keep the summary brief."
});
The result contains text. Query authorized data first and provide the needed context; this call does not run the full Knowi agent. Render the result safely. The provider comes from the granting user's Apps configuration under AI Settings. Credit controls and a 60-second timeout apply; keep combined prompt and system text under 12 KB. Preview returns dryRun:true without calling the provider.
Report pages and PDF rendering
Declare reportable pages in app.json. For reliable rendering, set window.knowiReportReady = false before loading page data, then true when rendering finishes. A flag that never becomes true fails the render. Without it, rendering waits for network idle. Use @media print to hide navigation and buttons.
To download a declared page as PDF, call export.pdf with page and optional query. It returns {name,pdf}, where pdf is base64-encoded; decode it on your server and return a PDF download. The request requires Knowi sign-in and App access. Preview returns dryRun:true. PDF export is unavailable during report rendering or View as user, and PDFs are limited to 11 MiB.
Send or schedule a report from an App
Call from a user-initiated, same-origin POST. The user must be signed in to Knowi, have App access, and have report creation and email permissions. Core derives the App and user; requests cannot choose appId, userId, or reportAs.
const result = await invoke(req, "report.send", {
page: "/",
query: "region=west",
sendTo: "leads@example.com",
subject: "Latest orders",
body: "The current review is attached."
});
For a recurring report, use report.schedule with the same fields plus a name and schedule:
const result = await invoke(req, "report.schedule", {
name: "Weekly orders",
page: "/",
sendTo: "leads@example.com",
schedule: {
frequencyType: "weekly", frequency: 1,
dayOfWeek: "MONDAY", startTime: "08:00",
timezone: "America/Los_Angeles"
}
});
sendTo is a comma-separated email list. Both calls accept optional page (default /), query, from, replyTo, cc, bcc, subject, body, and name. Omit from to use the configured sender; any supplied sender must be permitted for the user or account. The attachment is one App PDF.
Schedules support daily, weekly, monthly, hours, and minutes, with a positive integer frequency (default 1). startTime is HH:mm; timezone defaults to the user's. Weekly schedules require an uppercase weekday; monthly schedules require dayOfMonth from 1 to 28. The signed-in user owns the schedule in Reports.
A send returns {"status":"sent"}; a saved schedule returns {"status":"scheduled","reportId":123}. Preview returns {"dryRun":true,"status":"preview"} and does neither. Do not send on page load or automatically retry a timeout: delivery may already have occurred. Calls during PDF rendering are refused.
Alerts evaluate their dataset condition, not the attached App page. An App PDF attachment does not add per-record delivery tracking. Alert data attachments are CSV; anomaly alerts must be configured through the Alerts UI.
Run a saved write action from an App
Configure the write action first. Invoke its action binding from a same-origin mutating request:
const result = await invoke(req, "action.execute", {
binding: "approve-order",
input: {orderId: 101, status: "approved", expectedVersion: 3}
});
Use plain parameter names (orderId for $c9_orderId$) and declared JSON types. Unknown, missing, or out-of-range inputs are rejected. Core supplies authenticated user values; the request cannot choose SQL, datasource, or identity. A write runs in one transaction and must change exactly one row.
For example, save this SQL for approve-order:
UPDATE dbo.customer_orders SET status = $c9_status$, version = version + 1 WHERE id = $c9_orderId$ AND tenant_id = $c9_tenant_id$ AND version = $c9_expectedVersion$
Configure tenant_id as a customer, group, or user filter in Knowi. When the action is saved, that name becomes a trusted user value. Each caller must have one enabled Equals value; a missing or ambiguous value blocks the action, without falling back to form input. The App supplies only orderId, status, and expectedVersion. Limit status to the approved values. The version test detects concurrent edits and returns a conflict instead of overwriting another user's change.
Tokens bind values, not SQL fragments; leave them unquoted and never use them for table or column names. Supported statements are one INSERT ... VALUES, UPDATE ... WHERE, or DELETE FROM ... WHERE on SQL Server. Use a dedicated database login limited to the required tables. Deletes need the same trusted row filters and a user confirmation. The one-row limit counts directly affected rows; triggers and cascading foreign keys can affect related records.
| Outcome | Meaning |
|---|---|
succeeded |
One row changed. Includes requestId and affectedRows:1. |
preview |
No write; includes dryRun:true. |
rejected (400/403/429) |
Input, access, destination, or rate-limit rejection. Nothing was written. |
conflict (409) |
No row matched. Reload the record before retrying. |
failed (502) |
The statement failed or matched multiple rows. Rolled back. |
unknown (502) |
The database did not confirm the commit. Read the record before retrying. |
Errors include requestId, outcome, and error. Show the outcome and keep the request ID for support. Never run writes on page load or retry an uncertain outcome automatically. Limits are 120 calls per minute per App, 4 concurrent calls, a 30-second statement timeout, and 32 parameters. Text values are limited to 2,048 characters; a parameter can have up to 100 allowed values. Writes require Knowi sign-in and are refused during report rendering and View as user.
Sign-in branding configuration
The optional branding object in app.json styles the App's hosted Knowi sign-in and MFA pages:
"branding": {
"name": "Orders",
"logo": "web/assets/logo.svg",
"colors": {"background":"#0b1b2b","button":"#2f6df6","buttonText":"#ffffff"}
}
name is at most 80 characters. logo points to a repository SVG, PNG, JPEG, or WebP up to 64 KB. Colors accept 3- or 6-digit hex values for background, surface, text, muted, border, button, and buttonText. Unknown keys, invalid colors, and invalid logos fail the build. Branding is versioned and applies only to Knowi sign-in Apps.
Image validation and asset paths
Brand assets uploaded through Knowi are committed to web/assets/<name> and served at the /assets/<name> URL shown after upload. Use that URL in browser code; branding.logo uses the repository path, such as web/assets/logo.svg. Attached design references guide generation but are not committed. See Manage Apps for upload limits.
File extensions must match the image bytes. PNGs are limited to 4,096 pixels per side and 12 megapixels. SVGs cannot contain scripts, event handlers, javascript: URLs, or foreignObject; links are restricted to fragments or embedded image data. Used brand assets stay in Git; upload again only to add or replace one.
Embedding setup and code
Enable App settings > Embedding and list the allowed sites before embedding. This requires edit and publish permissions. Use up to eight exact origins, such as https://portal.example.com; no paths, queries, wildcards, or ports. HTTPS is assumed if omitted; plain HTTP is accepted only for localhost. App manages access cannot be embedded.
Embedding a Knowi sign-in App
Your server obtains a fresh viewer token from the Knowi single sign-on API. Pass it to the page:
<script src="https://www.knowi.com/minify/knowi-api.min.js"></script>
<div id="orders-app" style="height: 100vh"></div>
<script>
Knowi.render('#orders-app', {
type: 'appSso',
url: 'https://orders.apps.knowi.com/',
token: '<sso_session_token>',
title: 'Orders',
onSessionExpired: function () { /* obtain a fresh token and render again */ },
onError: function (reason) { console.error('App embed failed', reason); }
});
</script>
Use the script URL supplied by your Knowi installation. url must be the App's HTTPS origin, without path, query, or credentials. No App ID is needed. The token is posted, never placed in the URL, and App code does not receive it. The viewer must be a Knowi user in the App's account with App access; their permissions and content filters apply.
Mint a fresh token for each page load. Render again with a new token to change users; call Knowi.logout(container) on portal logout. Sessions last at most one hour, or until the original token expires if sooner. App sign-in does not consume the token; use short lifetimes. An expired token, or one already consumed by dashboard sign-in under single-use-token enforcement, is refused.
Other options are onReady(container) (frame loaded, not necessarily App data ready), onMessage(data,container) (messages from that App frame), and timeoutMs (default 20000, used with onError). Error reasons are storage-blocked, embed-rejected, and timeout. Do not loop on storage-blocked: open the App separately or use an App custom domain under the portal's own domain.
Embedding a Public App
<iframe src="https://orders.apps.knowi.com/" title="Orders" width="100%" height="800"></iframe>
No token is needed. Knowi.render also supports type:'appPublic'. Give each App its own container with an explicit height.
What to expect
Embedding does not grant access, and a site outside the allowed list is refused. For browser limits and the custom-domain workaround, see Embed an App in your site.
Custom event code and limits
Published Apps record page views, capability calls, and HTTP errors. Custom events cover actions such as browser-only route changes:
await invoke(req, 'events.track', {
name: 'results.exported', properties: {format: 'csv', resultCount: 12}
});
From the browser:
navigator.sendBeacon('/__knowi/events', new Blob([JSON.stringify({
name: 'filter.changed', properties: {region: 'west'}
})], {type: 'application/json'}));
Custom events require a same-origin POST, or a server action handling another same-origin mutating method. The gateway supplies identity. Names start with a letter and use up to 64 letters, numbers, periods, underscores, or hyphens; knowi. is reserved. Properties allow 16 scalar values, strings up to 512 characters, and 4 KB total. Exclude personal data, secrets, tokens, cookies, and query strings.
{accepted:true} means queued, not durably stored; {accepted:false} means the queue is full. Preview, authenticated report rendering, and View as user return {dryRun:true}. Batches can be lost during restart or overload, so events are usage analytics, not an audit log.
After the first live batch, App Events - <App name> appears as a Knowi dataset in the owner's account. Use normal sharing and content filters for it. Public and App-managed visitors have no Knowi user ID. Do not expose raw activity or user IDs through a Public App. If the dataset is deleted or access removed, collection stops. Sign-in records shown in Activity are stored separately.