Builder guide for MCP clients
The complete reference Claude Code follows when building on Kareenos: contracts, shapes, error codes, and the hosted web application flow.
On this page
- 0. The short version
- 1. Connect
- 2. The build order
- 3. Working with Live Workbook data
- 4. Widgets: the in-app UI
- 5. Actions: forms that reach mobile
- 6. Reports: printed, branded, shareable
- 7. K-Jobs: deterministic automation
- 8. Pages: the in-app end-user surface
- 9. Debugging playbook
- 10. Build and host a Kareenos Agentic Web-App
- 10.0 Who signs in: the app's user_mode
- 10.1 One project, two layers, built together
- 10.2 The handler is the door
- 10.3 Where your app's data lives: Live Workbooks only
- 10.4 Register the app
- 10.5 The backend: routes and handlers
- 10.6 The frontend: Vue 3 + Quasar
- 10.6b Migrating an app built before the config file
- 10.7 Build, zip, push, verify
- 10.8 Versions, rollback, redeploy
- 10.8b Publishing your Web-App to the marketplace
- 10.9 What the host does and does not do
- 10.10 Your source folder: what is committed, what stays local
- 10.11 Continue an imported Web-App from its source
- 10.12 Open-source your frontend
- 11. Before you declare any build done
- 12. The last phase: Agentic Operations
This guide is distributable. Hand it to anyone building on Kareenos through the MCP connector with Claude Code (or any MCP-capable AI agent). It describes only the public tool surface: the same contracts the tools themselves teach through their descriptions and corrective error messages.
The live channel always wins.
kareenos_context({topic})andget_skill(...)are assembled from your running deployment; when they disagree with this document, they are right. Every failed tool call returns amessagethat states the exact fix. Follow it, and never retry an unchanged call.Twin copies. The canonical file is
docs/platform-design/kareenos-mcp-builder-guide-for-clients.md.kareenos_backend/scripts/sync_builder_guide.jscopies it into the connector (kareenos_context({topic:"builder_guide"})) and into the public docs (Developers, Builder guide for MCP clients); the frontend build runs it automatically as its first step. Edit the canonical file only, and keep it free of em-dashes: the generator refuses one and the build stops.
0. The short version
Add the Kareenos MCP to Claude Code, then build. Kareenos is your backend, your agentic ecosystem and your host.
Two things get built on Kareenos, and these are their names:
| Name | What it is | Who uses it |
|---|---|---|
| Kareenos Agentic Web-App | A web application hosted by Kareenos at https://<app_name>.kareenos.com. You write the frontend (Vue 3 + Quasar v2) and its backend (K-API routes bound to handler K-Jobs) together. Its user_mode says who signs in: your own customers, who register in the app (third_party_tenants), or your own team with their Kareenos login (kareenos_users). |
Your customers, or your team, in a browser, at the app's own address. |
| Kareenos Agentic Business Project | The operation you run INSIDE Kareenos: Live Workbook sheets, K-Jobs, K-Agents, widgets, pages, dashboards and reports. | Your own team, in the Kareenos console. |
They are not alternatives. A Web-App keeps its data and its logic in a Business Project, so building a Web-App normally means building both. Sections 2 to 9 are the Business Project contracts; section 10 adds the Web-App on top. (A Kareenos Agentic Mobile-App is planned and cannot be built yet.)
One command gets you started: /kareenos:kareenos_start_app. It works out which of the
two you want from what you described, loads the right guidance and the full guide, then
builds. You can also just describe what you want: the connector's own instructions carry the
same flow. For a Web-App:
kareenos_context()thenkareenos_context({topic:"web_apps"})(the doctrine and the loop).- Settle who signs in (section 10.0), then
create_app({app_name, description, projectid, user_mode}): the app name becomeshttps://<app_name>.kareenos.com. - Per feature: a sheet, a handler K-Job, a K-API route, the screen that calls it,
test_k_api. quasar build, zip the output folder,shasum -a 256,request_app_build_upload, run the curl.get_app_hosting_status({probe:true}), open the URL, iterate.
Starting from an existing folder instead? A fork or a clone of someone else's frontend,
typically after you imported their project from the marketplace, still carries THEIR ids.
Say "link this project to my imported project" (or run /kareenos:kareenos_link_app): one
call to link_app_source connects the folder to your own copy. Section 10.11.
When the build works, the last phase begins. The Agentic Operations Phase hands what you
built to the in-app Super Agent: it studies the live operation, proposes the AI Agents that
could run it with your team, and builds and wires the ones you pick. Claude Code offers it
once at the end of a build; start it any time with /kareenos:kareenos_agentic_operations.
Section 12.
Three rules never bend: the frontend and its backend are built together; the handler K-Job is the only door from the app into the ecosystem; all application data lives in Live Workbook sheets and nowhere else. Section 10 explains each.
1. Connect
- In Kareenos, open Settings, Exposures and create an exposure in tools mode. Copy the key. It is shown once.
- Add the server to Claude Code (the URL must be absolute; restart the Claude Code session after adding):
claude mcp add --transport http kareenos https://<your-kareenos-domain>/mcp \
--header "Authorization: Bearer <your kmcp2_… key>"
- First call, always:
kareenos_contextwith no arguments (or run thekareenos_start_appprompt, which does this and more in one step). It returns the orientation guide plus the topic list (project_composition,data_model,jobs,agents_authoring,widgets,actions_reports,documents,limits_and_errors,web_apps,link_source,agentic_operations,builder_guide). It is free; read the topic for each area before you build in it. - Before a substantial build in any domain, load the platform's own playbook:
get_skill_list, thenget_skill("<name>")(for examplelive_workbook_design,widgets_author,k_job_design,pages_author,k_api_builder).
Every tool call is metered against your account's credits (writes cost more than reads);
kareenos_context is free. Results share one envelope:
{status: "success"|"failed", message, data, errors}. On "failed" nothing was saved.
2. The build order
A complete solution composes in this order. Each step's returned ids feed the next:
- Project (
add_k_project): the container. Widgets, actions and reports are project-scoped and take itsprojectid. - Data: Live Workbook sheets (
lw_create_sheet). Always passprojectidso the workbook is linked to the project in the same call. Widget and action data grants only validate against linked workbooks (an unlinked one fails every later widget save with a corrective message;link_component_to_projectrepairs it). - Automation: K-Jobs.
create_k_job(lands awaiting approval),dry_run_k_job(reads are real, side effects are captured and listed, nothing is applied), fix,approve_k_job,trigger_k_job_nowto smoke-test. - Agents (when needed: the ones the user asked for, such as the assistant a screen
calls; the operation's agent team comes in step 8):
add_k_agent,add_k_agent_version(always prepend the output ofget_k_agent_system_rulesto the prompt),assign_tool_to_k_agentper tool (zero assignments = zero tools), activate the version. - UI: dialog/editor widgets first, then the screens that open them, then any dashboard widget (it needs its processor job's id). Then actions and reports.
- Seed, verify, place: sample rows,
test_widget/test_widget_render/run_report, then Pages (add_page_section,add_page,add_widget_to_page). - Readiness:
get_solution_readiness({projectid})scans for gaps;get_project_componentsinventories what you built. - Hand off, the last phase: offer the Agentic Operations Phase once. On yes,
handoff_to_super_agentopens the in-app Super Agent on the project with your briefing, and it proposes, builds and wires the AI Agents that run the business (section 12).
Keep an id ledger. Every create returns ids (sheet ids, column col_keys, job ids,
widget ids) that later steps consume. Column keys especially are generated and cannot be
guessed. Finish every build with a handover document (create_document) listing the full
inventory and any remaining manual steps: your next session inherits only what you wrote
down. In a Web-App source folder the platform itself is the ledger: never write ids into
files you commit, ask for them with link_app_source({app_id, detail:"full"}) (section 10.10).
3. Working with Live Workbook data
The row shapes, which everything else depends on:
| Operation | Shape |
|---|---|
Insert (lw_add_rows / lw_write op:add) |
rows: [{"<col_key>": value, ...}], bare 6-hex column keys |
Update (lw_update_rows) |
rows: [{row_id, values: {"<col_key>": value}}] |
Delete (lw_delete_rows) |
row_ids: ["..."] |
Read (lw_query_rows / lw_read) |
rows come back keyed "<col_key>_value", plus _row_id and _created_at/_updated_at |
| Filters | [{col_key, op, value}] (array = AND) or {and:[...]} / {or:[...]}; ops: eq neq gt gte lt lte in not_in contains starts_with is_null not_null; _row_id and _created_at are filterable |
| Sort | `[{col_key, dir: "asc" |
Rules that save you an afternoon:
- Discover before writing:
lw_list, thenlw_get_sheetgives every column'scol_key, type and config. - Inserts return
data.row_idsin input order. Capture them for relation columns. - Relations are
refcolumns:value_type: "ref"stores the referenced row's_row_id;config.ref = {sheet_id, display_col_key, on_delete: "set_null"|"restrict"}. Writes are validated (REF_NOT_FOUND), the column is indexed automatically, reads return a resolved"<col_key>_label"beside the id, and deleting referenced rows honorson_delete(REF_RESTRICTrefuses with the referencing sheet named). Design every join on refs, never on free-typed names. required/is_uniqueare enforced by the database: a violating write returnsfield_errors [{col_key, code, message}]and saves nothing.- From widget code, always pass
return_mode: "rows"onlw_query_rows(the default mode switches large results to a cached handle that widget JavaScript cannot read), and page withpage_size(max 500). - Address / media / QR / barcode cells are JSON strings:
JSON.parsebefore use, and fill them from the producing tools' ready-made cell objects (geocode_addressgivesaddress_cell,put_filegivesmedia_cell,kwidget.uploadFilesgives one media cell per uploaded file,generate_codegivesqrcode_cell), never hand-assembled.
The heavy-application verbs:
| Verb | What it buys |
|---|---|
lw_write_batch |
ONE atomic transaction across up to 5 sheets / 200 rows: the header+lines save (header row id comes back in data.ops[0].row_ids); any failure means nothing was saved |
if_version on update patches |
optimistic concurrency: pass the _version you read; a stale row fails with VERSION_CONFLICT instead of silently overwriting |
lw_next_number |
race-free document numbers (INV-2026-0042); yearly reset is the sequence NAME; write the value into an is_unique column |
lw_distinct |
distinct values of one column over the whole sheet: filter dropdowns without loading rows |
include_children on lw_query_rows |
per-parent child counts/sums joined onto the page in one call (child column holds this sheet's row ids) |
4. Widgets: the in-app UI
A K-Widget is one self-contained HTML document. Input/form/table widgets use the hosted
Quasar runtime: author the bare form (one <div id="app"> template plus one inline
setup script ending .use(Quasar).mount('#app')); the host injects Vue 3, Quasar 2, the
brand theme and the icon set. Display-only tiles can stay plain HTML.
Validated at save (all-or-nothing, every rejection names the fix):
- Only the allow-listed
q-*components pass;q-dialog/q-menuand layout scaffolding are excluded. Real dialogs open in the HOST viakwidget.openWidget/kwidget.openAction, and confirms are inline two-step banners. - No
setInterval, nowindow.confirm/alert/prompt, no browser storage, no external resources or CDNs, no<input type="file">(uploads usekwidget.uploadFiles, below), no literal hex colors (use the injectedvar(--kn-*)design tokens; hex is allowed only as avar()fallback and inside chartcolors: [...]arrays), one primary button per action bar, kebab-case attributes, custom elements never self-closed,v-htmlnever bound to data. - CSS namespace rule: widget styles are document-global and the framework uses utility
class names (
col,row,column,block,fit,items-*,justify-*, and more) on its own internals. Defining any of those names in widget CSS breaks every button, chip and input (rejected asWIDGET_CLASS_SHADOWS_QUASAR). Prefix every widget class, for example.kb-col,.kb-card. Descendant selectors that merely target framework classes (.q-table th {...}) remain fine.
Capability grants (fail-closed, validated at save): everything the widget's own JavaScript calls must be granted:
capabilities: {
tools: ["tool.lw_query_rows", "tool.lw_add_rows"], // every kwidget.callTool name
lw_sheets: [{sheet_id: "...", mode: "read"|"write"|"readwrite"}], // AND per-sheet grants for row tools
jobs: ["<job_id>"], // kwidget.runJob targets
widgets: ["<widget_id>"], // kwidget.openWidget targets
actions: ["<action_id>"], agents: ["<agent_id>"]
}
Row tools need BOTH the tool entry and a matching lw_sheets entry; sheets must live in
workbooks linked to the widget's project. On update_widget, omitted (or null)
capabilities keeps the stored grants, {} revokes them all, and an object replaces them
wholesale, never merges.
The CRUD pattern: editor/form widgets are separate, unplaced widgets. A screen opens
one with kwidget.openWidget(id, {title, min_width, prefill: {row_id}}); the editor reads
kwidget.getContext().prefill, loads its row by _row_id, and finishes with
kwidget.close({saved: true, created: {label, value}}); the opener refreshes when
r.result.saved is set. All interactive wiring goes inside kwidget.ready(cb), gated by
kwidget.can(cap). Every write ends in a visible outcome: :loading on the button, an
inline success banner that NAMES the artifact (auto-dismissed with one setTimeout), the
specific error kept next to the form, and two-step banner confirms for destructive verbs.
Uploads, many files at once: a widget never hosts a file picker. A button calls
kwidget.uploadFiles(...) and the HOST opens an upload dialog: the user drops or picks
many files, and each file starts uploading at once with its own progress bar (video mp4,
mov or webm up to 200 MB, other files up to 25 MB). The promise resolves when the user
closes the dialog, with {files: [mediaCell], failed: [{file_name, size_bytes, error}], cancelled}. onProgress reports every file, and its done event carries that file's
media cell, so each row can land as soon as its file is stored. Write each cell as it is
into the sheet's media column, one row per file, and prefill what you can (the title from
file_name, today's date) so the user types nothing per file; details are edited later in
the grid. The upload itself needs no grant; the row writes keep their tools and
lw_sheets grants. Keep max_files at its default of 20 when you write a row per file:
that stays under the per-user limit of widget tool calls per minute.
kwidget.uploadFiles({
kinds: ["video"], multiple: true, title: "Upload videos",
onProgress: function (p) {
if (p.status !== "done") return
var row = {}
row[COL_TITLE] = p.file.file_name.replace(/\.[^.]+$/, "")
row[COL_FILE] = p.file // the media cell, as it is
kwidget.callTool("lw_add_rows", { sheet_id: SHEET_ID, rows: [row] })
.catch(function (e) { failedRows.value.push(p.file_name + ": " + e.message) })
}
}).then(function (r) {
if (r.cancelled) return
banner.value = r.files.length + " added" + (r.failed.length ? ", " + r.failed.length + " not uploaded" : "")
kwidget.refresh()
})
Use kwidget.openAction for a file only when it belongs to an authored action's answer.
Verify before declaring done: test_widget (data contract; processor-less widgets get
validation-only mode) and, where your deployment provides it, test_widget_render: a
headless render returning console errors, layout findings (page-level horizontal overflow,
oversized controls, blank render) and a screenshot URL. Any finding means not done;
"oversized controls" almost always means the CSS namespace rule above was broken.
5. Actions: forms that reach mobile
An action is a deliberately simple structured form that agents send by name and that
renders in the inbox on web and mobile. add_action takes an emit_template
({title, intro?, blocks: [...]}) of display blocks plus {kind: "fields"} input blocks
(field kinds include short_text, number, date, time, single_choice, media,
address, lookup).
- Lookup fields reference sheet rows:
lookup: {sheet_id, value_column: "_row_id", label_column: "<col_key>"}. The submitted value is the row id. on_submit(top level, up to 5 ops) writes the answers to sheets:[{op: "add_row", sheet_id, values_map: {"<col_key>": "$fieldId" | {literal: x}}}](alsoupdate_rows/update_cell/delete_rowswithrow_ids_from).- The action's own
capabilities.lw_sheetscovers its bindings (read for lookups, write foron_submit). The response schema is derived from the fields; never write it.
6. Reports: printed, branded, shareable
Reports have no processor: the runtime executes the report's declarative queries
directly, tenant-fenced (never filter by account yourself).
sheet_sqlqueries reference sheets ONLY as{{sheet:<id>}} alias, use the human column aliases fromget_report_sheet_schema(never raw column keys), pass values via$1..$nparams, and are one statement: no;, no comments, no writes. Rows also expose_row_id, so parent/child joins arechild.parent_fk = parent._row_id::text.- The HTML is one document of
<div class="report-page">A4 pages. Contract: listen formessage {type: "kreport:data", data}, render an initial state from an inline sample constant you embed yourself, callKReport.ready()when rendering settles, and disable chart animations. - Verify with
test_report, thenrun_report. A saved run renders live, is shareable, and can be frozen to PDF.
7. K-Jobs: deterministic automation
- Plain sandboxed JavaScript, no AI inside; capabilities come only from the declared
grants (
allowed_lw_sheets,allowed_tools, and the rest). The grants ARE the wiring. ctx.lw.read(sheetId)returns all rows in the read shape above;ctx.lw.write/updatetake the write shapes; write results carryrow_ids.- The loop: create,
dry_run_k_job, diff the captured effects against what you expected to write (decide the expected effects before running; that diff is your test),update_k_job_code, dry-run again, approve. - Make mutations idempotent: check an "already done" marker first and return the existing ids on re-run.
- A widget-processor job's
output_schemamust deep-equal the widget'sinput_schema. Author both from one canonical JSON. - Beyond
ctx.lw.read(full table, small sheets only), jobs havectx.lw.query(filters/sort/pagination/search),ctx.lw.aggregate,ctx.lw.distinct,ctx.lw.updateByFilter,ctx.lw.batch(the atomic multi-sheet write) andctx.lw.nextNumber. Reads run for real in dry runs; writes are captured as effects.
8. Pages: the in-app end-user surface
add_page_section, add_page, add_widget_to_page (col_span: 12 for full-row).
Place only screen widgets; editor/dialog widgets are "placed" by being granted in an
opener's capabilities.widgets or a drawer widget link. Open browser tabs need a reload
to pick up newly placed widgets.
9. Debugging playbook
Rule zero: the corrective message IS the fix. Common failures and what they mean:
| Symptom | Fix |
|---|---|
INTERACTIVE_NO_CAPABILITY |
the html calls a kwidget.* verb with no matching grant. Add it to capabilities (row tools need tools AND lw_sheets) |
WIDGET_UPLOAD_NOT_ALLOWED |
the html has a file input. Call kwidget.uploadFiles from a button instead (section 4) |
| widget save says the workbook is not linked to the project | pass projectid on lw_create_sheet, or call link_component_to_project |
WIDGET_CLASS_SHADOWS_QUASAR |
a widget CSS class redefines a framework utility. Prefix it (.kb-col) in style AND template |
HTML_SCRIPT_SYNTAX: Unexpected token '&' |
&& was HTML-escaped inside a <script> block. Scripts take raw && |
| a widget list shows only a few rows / stops loading | add return_mode: "rows" + page_size to lw_query_rows |
SCHEMA_MISMATCH |
processor output_schema differs from the widget input_schema. Copy one JSON to both |
REQUIRED_MISSING / UNIQUE_VIOLATION with field_errors |
fix the named columns; nothing was saved |
REF_NOT_FOUND |
a ref cell carries a row id that does not exist in the target sheet. Pick real ids |
REF_RESTRICT |
rows are still referenced by a restrict-mode ref. Delete or repoint the referencing rows first |
VERSION_CONFLICT |
your if_version is stale. Re-read the row and retry with its current _version |
test_widget_render reports oversized_controls |
the CSS namespace rule was broken. Find the unprefixed class |
NAME_INVALID / NAME_RESERVED / NAME_TAKEN |
the app name must be a free DNS label (2-40 lowercase letters, digits, hyphens; unique across the platform). Pick another |
NO_INDEX_HTML |
the zip must contain index.html at its root (or under one top-level folder). Zip the build OUTPUT folder, not its parent |
SHA_MISMATCH |
the uploaded file differs from the hash the ticket was minted for. Hash the exact file you upload, mint a new ticket |
TICKET_EXPIRED / TICKET_USED |
upload tickets live 30 minutes and work once. Mint a new one |
BUILD_IS_CURRENT |
you cannot delete the served version. Activate another one first |
push reports your bundle hardcodes an app key (warning) |
a kapp_ literal is in the bundle. Read /kareenos.config.json at boot (section 10.6); the marketplace will not carry a hardcoded build |
HANDLER_SQL_READWRITE (warning) |
a K-API handler carries read-write Data Space SQL. Application data belongs in sheets; keep SQL for compute caches only |
| the app still serves an old version | get_app_hosting_status({probe:true}): the edge re-pulls on the next page load; wait a few seconds, probe again, then redeploy_app |
PROJECT_LOCKED / INSUFFICIENT_KAGENT_BALANCE |
stop and tell the account owner. These need a human |
APP_USER_MODE_KAREENOS |
a handler called ctx.create_app_account or ctx.app_account_user_login on a kareenos_users app. Delete that handler: the built-in _login route signs users in |
a route name starting with _ is refused |
those names are the built-in routes of a kareenos_users app. Pick another name |
_login answers 401 INVALID_CREDENTIALS / INVALID_OTP / DEVICE_TOKEN_INVALID |
the user's Kareenos credentials, OTP code or stored device token are wrong or expired. Show the message; on DEVICE_TOKEN_INVALID drop the stored device token and ask for the password again |
_login answers 403 EMAIL_VERIFICATION_REQUIRED / ACCOUNT_NOT_ACTIVATED / NOT_A_MEMBER |
the user must finish verification or activation in the Kareenos console, or is not a member of this account. The app cannot register anyone |
401 on a business route with a kusr_ token |
the token is not valid for this app (another account, a rotated platform secret, a deactivated account). Drop it and show the sign-in screen |
What the user saw. A widget can fail inside the user's browser where no tool call of
yours ever shows it: a widget that does not start, a kwidget.* call that was refused, a
time-out. Kareenos reports each one to the project log stream. Read them with
get_project_logs({projectId, component_type: "widget"}): they are the rows whose message
starts with Browser:, naming the widget, the call, the code and the message the user saw.
When a user reports a broken widget, read these rows first, and ask the user to repeat the
action if there are none yet.
10. Build and host a Kareenos Agentic Web-App
Kareenos hosts your web application at https://<app_name>.kareenos.com and is its entire
backend. You build two layers and push one bundle.
10.0 Who signs in: the app's user_mode
Settle this before create_app. It shapes the data model and every handler, so it is the
one question worth one line: infer it from what the user said, state it as an assumption
with your recommendation, and go.
user_mode |
Who the users are | How they sign in | What you write | Identity in a handler |
|---|---|---|---|---|
third_party_tenants (default) |
Your OWN customers, members, drivers, partners. They are app accounts, never Kareenos users. | They register and sign in through routes you write (ctx.create_app_account, ctx.app_account_user_login); the app holds a kapps_ token. |
register, login, me, then the business routes. |
ctx.trigger.app_user = {app_account_userid, app_userid, app_account_id, is_owner}; per-user sheets keyed by app_userid. |
kareenos_users |
Your OWN team: the people who already sign in to the Kareenos console. | The same email and password (and Google sign-in, email OTP, trusted devices) as the console, through routes the platform serves under your base URL: _auth_config, _login, _google_login, _me, _logout. The app holds a kusr_ token, which is the user's real Kareenos session. |
The business routes only. No register or login handler. Route names starting with _ are reserved. |
ctx.trigger.kareenos_user = {userid, email, fullname, accountid, account_name, is_owner}; the handler runs AS that user; per-user sheets keyed by the Kareenos userid. |
Signals: "my customers", "people sign up", "a portal for clients", "a shop" mean
third_party_tenants. "For my team", "staff", "an internal tool", "the same login as
Kareenos", "extend our Kareenos pages" mean kareenos_users. Every app created before this
choice existed is third_party_tenants. update_app({user_mode}) switches a live app and
logs every existing session out.
Adding a person to a kareenos_users app is adding them to the Kareenos account (the Teams
page, or create_member / invite_members); the app itself never registers anyone.
10.1 One project, two layers, built together
Your application has exactly two layers: the frontend (a single-page app you write) and the backend Kareenos runs for you: K-API routes bound to handler K-Jobs. Build them together, feature by feature, never one ahead of the other:
- the sheet(s) the feature needs (
lw_create_sheetwithprojectid); - the handler K-Job (
create_k_job,trigger_type: "api_handler", minimal grants,dry_run_k_job,approve_k_job); - the route (
create_k_api({app_id, api_name, auth_mode, jobid}); - the screen that calls the route;
test_k_apito prove the route, then rebuild and push.
A screen without its route is a mock; a route nobody calls is dead code.
10.2 The handler is the door
The frontend never touches a sheet, an agent or a tool directly. Every read and write is:
screen -> POST https://<api-origin>/api/kapi/<app_key>/<route> -> handler K-Job -> ctx.*
The handler K-Job is the door to the whole ecosystem, and its grants are the keys:
| Inside the handler | Opened by | Use it for |
|---|---|---|
ctx.lw.* |
allowed_lw_sheets |
the app's data: query, write, batch, nextNumber, aggregate, distinct |
ctx.tools.call(name, args) |
allowed_tools |
builtin and partner tools: send an email or a WhatsApp message, geocode an address, write a Google Sheet, take a Stripe payment, generate a QR code |
ctx.agents.* |
allowed_agents_callable |
a K-Agent answers inside the API call (keep it short; the caller waits at most 30 s) |
ctx.intents.fire(name, payload) |
allowed_intents_published |
the event bus: publish order.created, let agents and other jobs react asynchronously, answer "accepted" |
ctx.jobs.call |
allowed_jobs_callable |
delegate to another job |
ctx.location.* |
allowed_location |
push a courier's position, read the last one, geofences, routes |
ctx.vision.* |
allowed_vision |
image and video capture sessions |
ctx.files, ctx.messages, ctx.notify, ctx.gis, ctx.codes, ctx.crypto, ctx.dedupe, ctx.py |
granted surfaces | media, messaging, push to the owner's team, routing, codes, hashing, idempotency, Python compute |
ctx.create_app_account, ctx.app_account_user_login |
third_party_tenants apps |
your users' accounts and sessions (refused on a kareenos_users app) |
ctx.trigger.app_user (third_party_tenants) or ctx.trigger.kareenos_user (kareenos_users)
is the only trusted identity on an authenticated route. Never trust a user id, email or
account id sent in the body. Anything that reasons or calls a model is
not a K-API handler: enqueue it (an intent, a job call) and answer "accepted".
10.3 Where your app's data lives: Live Workbooks only
Every entity of your app is a sheet in the app's project, keyed by the user key of its mode:
app_userid for third_party_tenants, the Kareenos userid for kareenos_users (or a ref
column to a users sheet). That is what makes the same data visible to the owner's widgets,
reports, dashboards and agents, and to the in-app Super Agent that can extend your
application later.
Never create tables outside the Live Workbook ecosystem:
- no Data Space
CREATE TABLEfor application data (ctx.sqltables are job-internal compute caches that nothing else on the platform can see;create_k_apiwarnsHANDLER_SQL_READWRITEwhen a handler carries read-write SQL); - no external databases;
- no browser storage used as a database (localStorage holds the session token and UI preferences only);
- no files used as a database (
ctx.filesis for media).
There is no browser-side sheet API by design. If the frontend needs data, it needs a route.
10.4 Register the app
create_app({app_name: "acme-shop", description: "Customer portal for Acme", projectid, user_mode: "third_party_tenants"})
create_app({app_name: "acme-ops", description: "Field app for Acme's own team", projectid, user_mode: "kareenos_users"})
app_nameis the subdomain: 2 to 40 lowercase letters, digits and hyphens, unique across the platform, not a reserved word (www,api,login,admin, and similar). The app serves athttps://acme-shop.kareenos.com.- You receive the app key (public; it is part of every K-API URL) and the app secret
(shown once; on a
third_party_tenantsapp it signs your users' session tokens on the server; it must never ship in the bundle, in a job, or in chat again. On akareenos_usersapp it signs nothing; keep it anyway). list_apps,update_app(description, rate limit,hosting_enabled,isactive,user_mode),rotate_app_secret(every user is logged out),delete_app(also deletes the app's users and builds).
10.5 The backend: routes and handlers
kareenos_users app. Sign-in is built in; you write the business routes only, all
authenticated:
// my_tasks_api (authenticated route) - the handler runs AS the signed-in Kareenos user
const me = ctx.trigger.kareenos_user; // {userid, email, fullname, accountid, account_name, is_owner}
const rows = await ctx.lw.query(TASKS_SHEET, { filters: [{ col_key: 'a1b2c3', op: 'eq', value: me.userid }] });
ctx.return({ tasks: rows.rows, owner: me.is_owner });
The SPA signs in through the built-in routes under kapi_base (see 10.6 for the client):
POST _auth_config {} -> {google_signin_enabled, google_client_id, nonce}
POST _login {email, password} -> {otp_required: true, email} an OTP was mailed (the console's own flow)
POST _login {email, password, device_token} -> {token, user, modules, teams} trusted device: no OTP
POST _login {email, otp_code, step: "verify_otp",
trust_device: true, device_info: {...}} -> {token, user, modules, teams, device_token, device_expires_at}
POST _google_login {credential} -> {token, user, ...} | 403 NOT_A_MEMBER
POST _me (Authorization: Bearer kusr_...) -> {user, modules, teams}
POST _logout (Authorization: Bearer kusr_...) -> {logged_out: true} stateless: drop the token
Store device_token next to the session token and send it with the password on the next
sign-in; the user then skips the OTP, exactly as in the console. modules and teams
mirror the console's login payload, so the screens can hide what the user may not do; the
runtime enforces it regardless.
third_party_tenants app. Nearly every app needs register, login and me, then
the business routes.
// register_api (public route)
const r = await ctx.create_app_account(ctx.trigger.app_key, ctx.input.app_userid, ctx.input.password);
if (r.status !== 'success') return ctx.fail(r.message, r.code); // DUPLICATE_USER -> 409
// extra profile fields go to the project's own sheet, keyed by app_userid
ctx.return({ registered: true });
// login_api (public route)
const r = await ctx.app_account_user_login(ctx.trigger.app_key, ctx.input.app_userid, ctx.input.password);
if (r.status !== 'success') return ctx.fail('Invalid app_userid or password', 'UNAUTHORIZED');
ctx.return({ token: r.data.token });
// me_api (authenticated route)
ctx.return({ user: ctx.trigger.app_user });
// orders_list_api (authenticated route) - always filter by the verified user
const rows = await ctx.lw.query(ORDERS_SHEET, { filters: [{ col_key: 'a1b2c3', op: 'eq', value: ctx.trigger.app_user.app_userid }] });
ctx.return({ orders: rows.rows });
Contract of a handler: name it <route>_api, trigger_type: "api_handler",
trigger_config: {}, declare input_schema (required params become a 400 INPUT_SCHEMA
for free), grant only what the route needs. ctx.return(value) is the HTTP response
(200 {type:"result", data: value}); ctx.fail(message, code) is a 4xx with that code
(UNAUTHORIZED/INVALID_CREDENTIALS 401, FORBIDDEN 403, NOT_FOUND 404,
DUPLICATE_USER/CONFLICT 409, anything else 400). Normalize the user identifier the same
way in register and login (lowercase an email, E.164 a phone); the platform stores it as
opaque text.
Bind and prove each route:
create_k_api({app_id, api_name: "login", auth_mode: "public", jobid}) // third_party_tenants only
create_k_api({app_id, api_name: "orders_list", auth_mode: "authenticated", jobid})
test_k_api({api_id, params: {...}, as_app_userid: "jane@example.com"}) // third_party_tenants: a registered user
test_k_api({api_id, params: {...}}) // kareenos_users: runs as you
test_k_api({api_id, params: {...}, as_userid: "colleague@acme.com"}) // kareenos_users: as a colleague
list_k_api_logs({app_id})
Never log ctx.input in a register or login handler (the console buffer would carry the
password); log field names and outcomes.
10.6 The frontend: Vue 3 + Quasar
The recommended stack is Vue 3 with Quasar v2 (Vite). Scaffold it once:
npm init quasar@latest # App with Quasar CLI, Vite, Vue 3, SPA, history router
Settings come from the host, never from the bundle. When Kareenos deploys a version it
writes /kareenos.config.json at the root of the site: the app key, the app name, the API
origin and the ready-made K-API base URL. Your code fetches that file at boot; nothing in the
bundle names an app key or an origin. The same zip therefore works under any app, which is
what lets the marketplace carry a Web-App and its build without touching it. The file:
{
"app_key": "kapp_...",
"app_name": "acme-shop",
"app_url": "https://acme-shop.kareenos.com",
"api_base": "https://<api-origin>",
"kapi_base": "https://<api-origin>/api/kapi/kapp_.../",
"hosting_domain": "kareenos.com",
"user_mode": "third_party_tenants",
"builtin_auth": null,
"version": "1.0.0",
"generated_at": "2026-09-12T10:00:00.000Z"
}
On a kareenos_users app user_mode is "kareenos_users" and builtin_auth is
{"auth_config": "_auth_config", "login": "_login", "google_login": "_google_login", "me": "_me", "logout": "_logout"}.
The client module below reads user_mode to decide which sign-in screen to show.
For quasar dev you need the same file locally: call get_app_web_config({app_id}) once
after create_app and write data.config to public/kareenos.config.json. It is byte for
byte what the host generates. quasar build copies it into dist/spa, the zip carries it,
and the host overwrites it with the generated file on every deploy. Never read these values
from anywhere else and never hardcode them: the push reports a warning when the bundle
contains an app key literal, and the marketplace will not carry such a build.
One API client module is the whole backend integration:
// src/services/kareenos.js
let config = null
const ready = fetch('/kareenos.config.json', { cache: 'no-cache' })
.then(r => { if (!r.ok) throw new Error('kareenos.config.json ' + r.status); return r.json() })
.then(c => { config = c; return c })
export function getConfig () { return ready } // resolves { app_key, kapi_base, user_mode, builtin_auth, ... }
export function getToken () { return localStorage.getItem('kapi_token') }
export function setToken (t) { t ? localStorage.setItem('kapi_token', t) : localStorage.removeItem('kapi_token') }
export function getDeviceToken () { return localStorage.getItem('kapi_device_token') } // kareenos_users: trusted device
export function setDeviceToken (t) { t ? localStorage.setItem('kapi_device_token', t) : localStorage.removeItem('kapi_device_token') }
export async function call (route, params = {}) {
const c = config || await ready
const headers = { 'Content-Type': 'application/json' }
const token = getToken()
if (token) headers.Authorization = `Bearer ${token}`
const res = await fetch(c.kapi_base + route, { method: 'POST', headers, body: JSON.stringify(params) })
const body = await res.json() // {type:'result', data} | {type:'error', value, text}
if (res.status === 401) { setToken(null); throw Object.assign(new Error(body.text || 'Please sign in'), { code: 'UNAUTHORIZED', unauthenticated: true }) }
if (body.type !== 'result') throw Object.assign(new Error(body.text || 'Request failed'), { code: body.value, status: res.status })
return body.data
}
// third_party_tenants usage
// await call('register', { app_userid: email, password })
// setToken((await call('login', { app_userid: email, password })).token)
// const { orders } = await call('orders_list')
// kareenos_users usage (the built-in routes; names come from config.builtin_auth)
// const c = await getConfig()
// const step1 = await call(c.builtin_auth.login, { email, password, device_token: getDeviceToken() || undefined })
// if (step1.otp_required) { // the console's email OTP: ask the user for the code
// const s = await call(c.builtin_auth.login, { email, otp_code, step: 'verify_otp', trust_device: true, device_info: { app: c.app_name } })
// setToken(s.token); setDeviceToken(s.device_token)
// } else { setToken(step1.token) } // trusted device: signed in at once
// const { user, modules } = await call(c.builtin_auth.me)
On a kareenos_users app a DEVICE_TOKEN_INVALID error means the stored device token
expired: call setDeviceToken(null) and ask for the password again. Never build a register
screen for such an app; members are added in the Kareenos console.
This works with quasar build as it is: no top-level await (the default build target does not
allow it) and no boot file is required, because the fetch starts when the module loads and every
call awaits it. If a screen needs a value before its first render (the app name in a header),
add a boot file src/boot/kareenos.js with export default async () => { await getConfig() }
and register it in quasar.config.js; the whole app then mounts after the config has loaded.
Rules: the app secret is never in the bundle; keep the token in localStorage and drop it on
401; use the history-mode router (the host falls back to index.html); CORS to the K-API
origin is already open, nothing to configure; no app key and no API origin anywhere under
src/: /kareenos.config.json is the only source of settings.
10.6b Migrating an app built before the config file
An app whose bundle still hardcodes its key keeps working, but the marketplace will not carry its build. To migrate it, nothing changes on the platform side:
get_app_web_config({app_id})and writedata.configtopublic/kareenos.config.json.- Replace the hardcoded
APP_KEY/API_ORIGIN/BASEinsrc/services/kareenos.jswith the module above. Thecall()surface is unchanged, so no screen needs editing. grep -rn "kapp_" src/must be empty (and no API origin undersrc/either).- Build, zip and push as usual (a new version, or the same one to replace it). The push
response must show
warnings: []; if it names a file with a key literal, fix it and push again. curl https://<app_name>.kareenos.com/kareenos.config.jsonshows the key; sign in through the app to confirm the calls go throughkapi_base. Publish only after that.
10.7 Build, zip, push, verify
quasar build # -> dist/spa
cd dist/spa && zip -r ../../acme-shop-1.0.0.zip . && cd ../.. # index.html at the zip root
shasum -a 256 acme-shop-1.0.0.zip # or: sha256sum acme-shop-1.0.0.zip
Then, over MCP:
request_app_build_upload({app_id, version: "1.0.0", sha256: "<the hash>"})
It returns a single-use upload ticket (30 minutes) and the exact curl command. Run that
curl from the folder that holds the zip; your MCP key never enters the shell. The push goes
live by default (pass activate: false to stage it). Then:
get_app_hosting_status({app_id, probe: true}) # in_sync: true -> open https://acme-shop.kareenos.com
__MACOSX folders and .DS_Store files inside the zip are ignored. The bundle must contain
index.html at its root or under one top-level folder; no symlinks, no ZIP64, no
encryption; at most 50 MB zipped, 300 MB unpacked, 20,000 files.
10.8 Versions, rollback, redeploy
list_app_builds({app_id}): every pushed version,is_currentmarks the served one.- Re-pushing the same version label replaces it; bump the label for a new release.
activate_app_build({app_id, version}): switch the served version. This is the rollback.redeploy_app({app_id}): force the edge to re-download and re-extract the current build.delete_app_build({app_id, build_id}): never the served one.- The owner sees the same versions in Settings, Apps & K-APIs, Versions, and can upload a zip by hand there.
The edge re-checks what to serve on page navigations, so a new version lands within seconds; assets never switch under a running session.
10.8b Publishing your Web-App to the marketplace
A project that contains a Web-App publishes it with the project: the app, its routes and
the current version travel as one unit. Whoever imports the listing chooses a new name for
the app (the name is its address), receives a new app key and secret, and gets the build
copied and live at the new address. Because the bundle reads /kareenos.config.json, the
same zip works for them without any change; a bundle that hardcodes an app key is excluded
from the listing. The importer's routes answer 503 until they approve the handler jobs
(one consent and one call with approve_app_handlers, section 10.11), and your users are
never copied. If you open-source the frontend, declare the repository on the app
(update_app({app_id, source_repo_url})): the listing and the import result then tell every
importer where the source lives and what to say to Claude Code (section 10.12). Later
revisions never overwrite a build the importer pushed themselves: your new build reaches
them as a staged version they may activate.
10.9 What the host does and does not do
It serves static single-page applications: index.html is never cached, hashed assets are
cached for a year, unknown paths without a file extension fall back to index.html, missing
files answer 404. It does not run server code, does not render on the server, does not
serve custom domains (v1), and does not serve /.well-known. It writes /kareenos.config.json
on every deploy and serves it uncached; a copy inside the zip is replaced. Secrets do not belong
in a bundle: the browser holds only the user's session token and reads the public settings from
that file.
10.10 Your source folder: what is committed, what stays local
A Web-App source folder is linked to exactly one app of one account. The link is two small files that are never committed; everything that is committed carries no account data. This is what lets anybody fork a frontend and point it at their own copy of the backend.
| File | Git | Content |
|---|---|---|
kareenos.source.json |
committed | The account-neutral manifest: kareenos_source_format, name, version, user_mode, routes, and origin (app_id, project_id, listing_id): the app this source was published from. origin is what resolves a fork to the importer's copy. It is opaque and useless outside your account, and less sensitive than the app key every visitor already sees. |
CLAUDE.md section |
committed | Tells every Claude Code session: ids for YOUR account come from .kareenos/link.json and link_app_source; ids anywhere else may be the publisher's; when the folder is not linked yet, call link_app_source first. |
.mcp.json |
committed | The connection without a secret: url ${KAREENOS_MCP_URL:-https://<your deployment>/mcp}, header Authorization: Bearer ${KAREENOS_MCP_KEY}. Every account brings its own key. |
.kareenos/link.json |
ignored | Which project and app of THIS account the folder is linked to, and where it was imported from. |
public/kareenos.config.json |
ignored | The development copy of the runtime settings (section 10.6). |
| Knowledge docs | committed | Contracts, gotchas, conventions. They name components, they never quote ids. |
Three files have similar names and different jobs. Do not confuse them:
| File | Job |
|---|---|
kareenos.json |
The manifest of a marketplace PACKAGE repository (the "Export to GitHub" of a listing). |
kareenos.source.json |
The manifest of a frontend SOURCE repository (this section). |
kareenos.config.json |
The runtime settings the host writes on every deploy. |
The platform is the id ledger. link_app_source({app_id, detail:"full"}) returns, at any
time and always current: every route with its handler job and approval state, the jobs,
the workbooks and sheets, the agents. Nothing of that belongs in a committed file.
10.11 Continue an imported Web-App from its source
You imported a project from the marketplace and you hold a fork or a clone of its
open-source frontend. Importing minted new ids for everything: the project, the app (a new
name, so a new address, and a new app key), every route, handler job, sheet, workbook and
agent. Inside your account the import already rewrote every reference. The one place the
publisher's ids survive is the folder. Until you link it, the ids in it belong to someone
else's account, and quasar dev would talk to the publisher's live app.
Connect Claude Code with a Tools-mode connection on the imported project (the default Agents mode carries no builder tools), open the folder and say:
link this project to my imported project
What happens, and what you can check:
- One call. Claude Code reads the ids it finds in the folder (
kareenos.source.jsonorigin, or a few ids from an olderLEDGER.md) and the route names the frontend calls, then callslink_app_source({origin_ids, route_names, origin_listing_id}). Your app is found through the id map your import recorded, never by name. With nothing to pass, the connection's default project decides. If several apps qualify you are asked once which one. - Two files.
data.linkgoes to.kareenos/link.json,data.web_configtopublic/kareenos.config.json(always overwritten). Both are git-ignored; in a fork the ignore lines go to.git/info/exclude, and a tracked config file getsgit update-index --skip-worktree public/kareenos.config.json. No tracked file is edited.git statusstays clean and merging the publisher's later work never conflicts on ids. - One summary.
data.checksis everything an import leaves open, recomputed from the live state of your account: routes that answer 503 (their handler is unapproved, disabled or missing), the jobs those handlers call, routes the source calls that your app does not have, Agent Config keys that are empty (values never travel with a listing: you provide them), draft agents, disabled integrations, grants that point nowhere, and whether a newer revision of the listing is available. - One consent.
approve_app_handlers({app_id})previews exactly the jobs your routes need: the handlers plus the jobs they call. You answer one yes or no; on yes the same tool is called withconfirm_job_countand approves that list, recorded in your name. Scheduled and event-driven jobs are deliberately not part of it, because they start running the moment they are approved: each is explained and approved on its own. - Proof.
test_k_apion one route answers 200. From there the loop of sections 10.5 to 10.8 applies unchanged. Push under a NEW version label, never the label of the build you imported: that row is your rollback copy of the publisher's frontend. On a localhost development origin sign in with email and one-time code.
Later, the publisher's work reaches you through two channels. Backend changes (routes, handlers, sheets) arrive as a marketplace update that you review and apply in the console (Projects, the update banner on the imported project, Review update). It never overwrites a build you pushed: the listing's build lands as a staged version. Frontend changes arrive through git: merge upstream, build, push. After either, say "link again": new handlers land unapproved and new routes may be missing. If the source calls routes your app lacks and no update is available, the source is ahead of what you imported: check out the tag of the imported build, or build the missing handlers.
| Code | Meaning | Fix |
|---|---|---|
linked:false, AMBIGUOUS_APP / AMBIGUOUS_PROJECT |
Several apps qualify. | Pick one; the call is repeated with app_id. |
NOT_IMPORTED |
The ids in the folder belong to no import of this account and the app lacks most routes the source calls. | Import the listing first (the message carries its address). |
NOT_FOUND on app_id |
The id came from the repository (it is the publisher's), or the link is stale (a restored project, another account). | Link again without app_id. |
NO_APP_IN_PROJECT / NO_APP_IN_ACCOUNT |
Nothing to link to. | Import the listing, or build the app (section 10.4). |
COUNT_CHANGED |
The list of jobs changed between the preview and your yes. | Review the new preview. |
10.12 Open-source your frontend
Run /kareenos:kareenos_open_source_app, or say "prepare this project for open source".
The result is a repository anybody can fork and link with one sentence:
link_app_source({app_id, detail:"full"})on your own app:data.manifestbecomeskareenos.source.json, the two link files are written, nothing else..gitignorelists.kareenos/,public/kareenos.config.json,dist,*.zip,.env*. A config file that was ever committed is removed from the index (git rm --cached public/kareenos.config.json), otherwise every fork develops against your live app.- Account data leaves the tracked files: id tables, the app key, the API host, the app address, log ids and sample row ids. The knowledge stays: contracts, gotchas, conventions, naming components and never ids.
CLAUDE.md,.mcp.jsonand a README section "Run this app on your own Kareenos account": import the listing, fork, connect Claude Code in Tools mode on the imported project, say "link this project to my imported project".- A scrub check over the tracked files (
git ls-files): no UUID, nokapp_,kmcp2_,kmcp_,ksec_orkaup_literal, no app address and no API host, with the single exception oforigininkareenos.source.json. - Versions line up:
package.jsonversion,kareenos.source.jsonversion, the pushed build label and the git tagv<label>; the commit id goes into the build notes. update_app({app_id, source_repo_url}), then publish a new revision of the listing.
The marketplace "Export to GitHub" of a listing is a different thing: it publishes the importable PACKAGE, and it replaces the whole tree of its target repository on every push. It must never point at your source repository; the platform refuses it when it does.
11. Before you declare any build done
- Workbooks linked to the project before any widget/action was authored.
- Every widget:
test_widgetclean, andtest_widget_renderclean where available. - Every widget CSS class prefixed; every
kwidget.*verb granted;return_mode:"rows"on every widget-side query. - After the user tried each interactive widget in the browser,
get_project_logs({projectId, component_type: "widget"})shows noBrowser:error. - Jobs dry-run-diffed before approval; mutations idempotent.
- Reports verified with a real run.
- Screen widgets on Pages; editors referenced by their openers.
- For a hosted app:
user_modesettled and stated beforecreate_app; every screen has its route, every route its handler and atest_k_apiproof; all app data in sheets, keyed by the mode's user key; the secret nowhere in the bundle; no app key or API origin hardcoded (the client module reads/kareenos.config.jsonand the push reported no warning);get_app_hosting_status({probe:true})reportsin_sync: true; the real sign-in (register and login, or the built-in_loginwith a Kareenos account) and business flows tested in the browser at the app URL. - A handover document with the full id ledger (app URL, app key, routes, sheet ids, job ids, the current version) and remaining manual steps.
- For a Web-App source folder: linked (
.kareenos/link.jsonandpublic/kareenos.config.jsonpresent and git-ignored),kareenos.source.jsoncommitted, no id, key or host in any tracked file (section 10.10). - The Agentic Operations Phase offered once (section 12): on yes, one
handoff_to_super_agentcall with a briefing that holds no secret, and the link opened for the user.
12. The last phase: Agentic Operations
A working app is not the end of the build. The people who run the business know their business, not the agentic vocabulary, so they rarely know which AI Agents to ask for. The Agentic Operations Phase closes that gap: the in-app Super Agent studies the live operation you built, proposes the AI Agents that could run it with the team, and builds and wires the ones the user picks.
What you build, and what the Super Agent builds. You build the app, the data and the automation, plus any agent that is part of an app feature the user asked for (for example the assistant a screen calls). The operation's agent team (the agents that watch for changes, message customers and suppliers, and escalate to people) is designed in this phase, grounded in live data. Do not invent it yourself.
When to offer it. Once, when the build works: for a Web-App, its routes answer 200, its
handlers are approved and hosting is in sync; for a Business Project,
get_solution_readiness is clean. One line, recommending yes, for example: "Your app is
live. The last step is the Agentic Operations Phase: the Kareenos Super Agent studies what
we built, proposes the AI Agents that can run it with your team, and builds the ones you
pick. Shall I open it in your browser?" If the user declines,
/kareenos:kareenos_agentic_operations starts it later.
The briefing. Write 800 to 24,000 characters of prose and bullet lists (no JSON), using
the names the components have in Kareenos. Draw on the conversation, the folder's notes
(PLAN.md, LEDGER.md, README) and, when useful, get_operation_map({projectid}).
Eight sections:
- The business: what it does, its customers, suppliers, team roles, volumes, languages, working hours.
- What was built: the Web-App (who uses it, its main screens), each workbook and sheet and what it holds, each automated tool (K-Job) and when it runs, any existing agents.
- How work flows today: the core loop, step by step, who does what.
- Where people still notice something and react by hand.
- Channels and contact fields: what exists and what the user wants.
- Decisions and deferred items.
- Open items: unapproved jobs, empty Agent Config keys, known gaps.
- What the user hopes AI Agents will do.
Never include keys, passwords, tokens, hashes or customers' personal data: describe fields,
not values. The public app key is fine. A secret-looking line refuses the call
(BRIEFING_HAS_SECRET) and nothing is stored.
The call. handoff_to_super_agent({briefing}); projectid defaults to the
connection's project. It returns data.url, a single-use link to New Task, the Super
Agent's page in level 1 of Kareenos. Open it for the user (macOS open "<url>", Linux
xdg-open "<url>", Windows start "" "<url>"); if you cannot run commands, show it.
What the user sees. Kareenos opens on New Task in level 1, with the project selected (after sign-in if needed), and the Super Agent starts with your briefing. Level 1 is where the agents this phase builds live, beside the team's Inbox and Teams. The Super Agent reads the live project in one call, shows how the operation runs, and proposes at most four AI Agents: for each, what it watches, what it decides, what it does, which channel it speaks on, who it escalates to, and which of the user's own sheets and jobs it rests on. It is honest about what blocks an agent (no contact field, no channel, nobody to escalate to) and says what would have to exist first. The user picks in one form; the Super Agent then builds and wires the chosen agents, runs the loop once and shows what is live.
The link. It runs once: opened again, it fills the message box instead of sending twice. It expires in 7 days, a newer handoff for the same project replaces an unopened older one, and it opens only for users of the account who may use the Super Agent. A link written through a colleague's connection arrives prefilled for review, never sent automatically.
Errors. BRIEFING_TOO_SHORT or BRIEFING_TOO_LONG (rewrite to the eight sections),
BRIEFING_HAS_SECRET (remove the named lines), NOT_FOUND (pass a projectid from
get_k_projects), HANDOFF_LIMIT (30 per account per day: reuse the latest link),
MIGRATION_PENDING or CONSOLE_URL_UNCONFIGURED (the user opens New Task in Kareenos,
picks the project in the top bar and asks "What AI Agents could run this?").
After the handoff. You stay the app builder. When the Super Agent adds agents the
Web-App should call, wire them through the handler K-Jobs (ctx.agents.call) when the user
asks. Values the new agents need (API keys and similar): ask the user and store them with
set_agent_config (you see whether a key is set, never its value), or the user enters them
in Settings, Agent Config.
Running it again. The user can ask the Super Agent "what agents could run this?" in New Task at any time. While a project has data sheets and no agent yet, the same question sits under AI Agents in the level 1 sidebar; builders also find it on the Overview of the Projects page (Advanced). A later audit remembers the earlier one: it leads with what changed since, and it never re-proposes what the user declined without new evidence.