kortecxdocs
Apps

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 automationHosted web app
Schema tagkortecx.app/v1kortecx.experience/v1
Internal nameFunctionalExperience
What it holdsa blueprint — the step graph agents runa project file tree plus a framework choice
What it is forwork that runs on a trigger or on demanda web page you open in a browser on your own machine
Can it be a schedule targetyesno

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/v1 envelope must carry a blueprint object and must not carry a hosted config;
  • a kortecx.experience/v1 envelope must carry a hosted config and a branch_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:

Model — an optional route. Empty means "whatever this instance serves".
Tools — the tool ids the App wants, plus requested network and filesystem scope, plus a reach selector.
Context — content refs, context-bundle handles, and dataset refs to ground on. Retrieval over a dataset needs an hnsw build — see Datasets.
Guards — turn budget, tool-call budget, whether TLS is required for outbound calls, and the names of the secrets the App expects to be in scope.

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/mcp fails validation whether or not it has a scheme:// 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-assistant
apps/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 --wait

Pass inputs with --arg, repeated once per value:

kx app run apps/local/echo-demo --arg topic=kortecx --wait

Write the stored envelope back out to a file to inspect or edit it:

kx app get apps/local/echo-demo --output echo.app.json
import 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 it

prompt, 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-demo

A 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.

Next