Apps
An App is the named, saved unit of work in the agentic runtime — a kortecx.app/v1 envelope that carries references and intent, never authority.
An App is the thing you name, save, and come back to. It is the unit of work the rest of this site talks about: you build one, you run it, you schedule it, you edit it, you hand it to someone else.
Under the surface an App is a small JSON document called an envelope. The envelope does not contain your data, your passwords, or your permissions. It contains pointers — the name of a prompt, the id of a tool, the name of a credential — plus your stated intent for how the run should go. Everything that could actually let something happen is resolved by the runtime, from your own permissions, at the moment you run it.
The two lanes
There are exactly two kinds of App. Which lane an App is in is read off the schema string at
the top of its envelope — there is no kind field on the wire.
| Scheduled automation | Hosted web app | |
|---|---|---|
| Schema tag | kortecx.app/v1 | kortecx.experience/v1 |
| Internal name | Functional | Experience |
| What it holds | a blueprint — the step graph agents run | a project file tree plus a framework choice |
| What it is for | work that runs on a trigger or on demand | a web page you open in a browser on your own machine |
| Can it be a schedule target | yes | no |
Deriving the lane from the schema tag is deliberate: the hosted lane's hosted config is
omitted entirely from a scheduled App, so adding the lane changed zero bytes of every
envelope that already existed. No saved App's identity moved.
Validation enforces the split rather than trusting it:
- a
kortecx.app/v1envelope must carry ablueprintobject and must not carry ahostedconfig; - a
kortecx.experience/v1envelope must carry ahostedconfig and abranch_handle(the project tree), and must not carry a blueprint.
A hosted app cannot be scheduled
Because a hosted App carries no blueprint, there is nothing for a schedule to run. The runtime
refuses it when you register the trigger, not later when it fires, with
hosted (experience) apps are not schedulable; scheduling requires a functional app. You find
out at the moment you make the mistake, instead of collecting silent failures.
Hosted apps are a non-default build feature. They run on loopback — http://127.0.0.1:<port>/
— with no reverse proxy and no sandbox. See Hosted apps for what that means
in practice, and Scheduled apps for the other lane.
What is inside the envelope
Here is a minimal scheduled App, in its canonical form — sorted keys, compact, no decimals:
{"blueprint":{"steps":[]},"name":"x","schema":"kortecx.app/v1","version":"1"}Everything else is optional and is omitted when unset, which is why that document is so short. A fuller App adds four groups:
References — the by-name, by-pointer rail. Context items, tool ids, connection descriptors, dataset references, and a small artifact rail of prompts, rules, memory notes, and skills. A reference is a label plus a content pointer (a 64-character hex ref) or a registry id. Bodies are never inlined; the text lives in the content store and the envelope only names it.
Steering config — four axes of intent that the server re-resolves when you run:
reach selector.Replay — per-step intent for a re-run: re-bind the committed bytes, or run the step fresh.
Branch handle — the name of the App's own project file tree, when it has one.
Byte-identical across three languages
The envelope is canonical JSON: sorted keys, compact, integers only — floats are rejected outright. The Rust CLI, the Python SDK, and the TypeScript SDK are all held to the same committed corpus of canonical strings, so the App's identity does not depend on which one you used.
An App carries no authority
This is the property the whole design hangs on, so it is worth stating plainly.
There is no warrant, no grant, no secret value, no password, no API key, and no run identity
anywhere in an envelope. A test in the runtime walks the envelope's structure and fails if a key
with any of those meanings appears. What the envelope can carry is a credential name — the
string MCP_TOKEN, not the token — which the server resolves against your own registered
connections at the moment of the call.
Two more guards sit on the same boundary:
- A connection descriptor is rejected if it smuggles credentials into a URL.
user:pw@host/mcpfails validation whether or not it has ascheme://in front of it. - Every declared tool id must be a well-formed id and every tool version must be a whole number, so a hand-edited envelope cannot display a capability it never legitimately requested.
The consequence for you: saving an App, importing someone else's App, or running an App can never widen what you are allowed to do. The worst a bad envelope can do is ask for something and be refused.
Wanting is not having
An App declares a request for capability. The runtime grants only the intersection with what you — the person running it — can actually do. The intersection only ever narrows. A wish never becomes authority.
kx app manifest shows you that comparison before you run, so a missing piece is a sentence on
your screen rather than a failure ten minutes into a run:
kx app manifest apps/local/research-assistantapps/local/research-assistant — capability manifest
model: (served default) [served]
tools (reach: explicit):
retrieve@1 [satisfied]
gmail/search@1 [MISSING — not granted or not fireable]
connections:
mcp+stdio://gmail [MISSING — register with `kx connections add`]It is read-only. It gates nothing — the runtime computes the same intersection itself when the run starts.
Reach. The tools axis carries a reach selector. explicit is the default: the App gets
exactly the tools it enumerated, intersected with your policy. inherit_principal instead asks
for the whole set of tools you are allowed to fire — convenient for a personal App that should
adapt to whatever you have set up, and still bounded by your policy, never wider.
Model route. If an App names a model route and this instance does not serve it, the run fails closed with a clear error rather than quietly running somewhere else. The manifest flags that before you run.
Registering a tool is not the same as having a working integration. kx connections is the path
that actually dials out — see Connections.
Working with Apps
Author an envelope, save it to the catalog, and run it. kx app new runs offline — it needs no
running server, unless you attach a catalog skill with --skill, which has to be resolved
against one. The rest of the subcommands talk to a server (kx serve --dev-allow-local for a
local instance).
# Author an envelope from a blueprint file. No server needed.
kx app new "Echo Demo" --from-blueprint echo.dag.json --max-turns 8 --max-tool-calls 6 --tag demo --output echo.app.json
# Save it to your catalog. The handle defaults to apps/local/echo-demo.
kx app save echo.app.json
kx app list
kx app get apps/local/echo-demo
kx app manifest apps/local/echo-demo
kx app run apps/local/echo-demo --waitPass inputs with --arg, repeated once per value:
kx app run apps/local/echo-demo --arg topic=kortecx --waitWrite the stored envelope back out to a file to inspect or edit it:
kx app get apps/local/echo-demo --output echo.app.jsonimport kortecx as kx
app = (
kx.app("research-assistant")
.blueprint(kx.flow().agent("Research the topic.", tools=["kortecx.diagnostics.echo"]))
.rule("no-pii", body="Never reveal personal data.")
.steer(max_turns=8, max_tool_calls=6)
.describe("A grounded research agent")
)
app.save() # persist to the catalog
app.run({"topic": "kortecx"}) # compile the blueprint and run itprompt, rule, and memory are named text artifacts in the content store. Pass a body with
body=... (uploaded when you save) or a content ref with ref=... if you uploaded it already.
import { app, flow } from "@kortecx/sdk";
const a = app("research-assistant")
.blueprint(flow().agent("Research the topic.", { tools: ["kortecx.diagnostics.echo"] }))
.rule("no-pii", { body: "Never reveal personal data." })
.steer({ maxTurns: 8, maxToolCalls: 6 });
await a.save();
await a.run({ topic: "kortecx" });The Node entrypoint configures its own client. The browser entrypoint
(@kortecx/sdk/web) is explicit by design — pass a client to save and run.
kx app run is the runtime as a function: it reads the saved App server-side, re-compiles its
blueprint, and warrants every step from your own grants.
Freeze an App
Once an App does what you want, you can stop it changing. A lock freezes both halves of it: file
edits inside the App's project tree, and structure saves from the editor. Both are refused
server-side with the code LOCKED_BRANCH.
kx app lock apps/local/echo-demo
kx app unlock apps/local/echo-demoA lock is a policy decision, held off the truth path. It is deliberately an availability gate, not an integrity gate: if the lock store is lost, branches read as unlocked — editing is restored rather than the App being bricked. A console pre-disables the write controls on a locked App, but the runtime is the gate that counts.
One App at a time
An App does not call another App. There is no mechanism for that at any layer. Chaining happens inside one App: its blueprint is a graph of agent steps that hand off to each other. See Workflows.