KDCube is built around apps

Everything you ship on KDCube lives inside an app. An app is the versioned unit the runtime discovers, configures, and serves. It may host an agent, wrap an agent you already have, expose APIs or services, provide UI, run background work, or combine only the surfaces your product needs. The SDK includes the framework-neutral Agent Harness Runtime, the built-in ReAct agent, and reusable isolated execution, but none is required for an API-only, UI-only, or job-only app.

Surfaces (Widget, Copilot, API, Pipeline, Dashboard, MCP, named-service provider) compose an App. One or more apps compose a product. KDCube is the runtime that hosts every app.

That single decision shapes most of the SDK's ergonomics:

  • One synchronized application contract. Runtime decorators, interface declarations, configuration templates, storage ownership, tests, release metadata, and stable documentation describe the same app. UI, chat, MCP, named services, Data Bus handlers, jobs, and ReAct are conditional modules, not mandatory folders.
  • Deployable and reloadable. Apps can come from Git or a mounted path and can be updated independently of the platform services that host them.
  • Identity carried through the runtime. Admission resolves tenant, project, user, app, conversation, and turn context. Trusted app and tool code must use that carried context and the scoped SDK contracts; isolated generated code receives only its prepared workspace and supervisor bridge.
  • Accountable execution. Model, search, tool, and other metered calls made through platform pathways produce spending events attributed to the user, app, conversation, turn, and agent.
  • Framework-agnostic inside. An app can use the built-in descriptor-selected ReAct v2/v3 loop, LangGraph, CrewAI-authored flows, the separate ISO runtime for controlled execution, plain Python, or no LLM at all. The platform guarantees the surrounding runtime; what happens inside the app is your choice.
  • Provider and consumer surfaces. surfaces.as_provider declares what the app exposes. surfaces.as_consumer declares which tools, skills, MCP servers, and named-service namespaces its agents may call. An app can be a provider, a consumer, both, or neither.

Deployment-scope rule: one running deployment is bound to one effective tenant/project and may serve many users and operator-approved apps. Shared backing services can enforce logical namespaces, but stronger customer or lifecycle isolation requires separate deployments or dedicated infrastructure.

Terminology: KDCube now calls this unit an app. Internal identifiers and descriptors still use the older word bundle, for example @bundle_entrypoint, bundle_id, bundles.yaml, and /bundles/....

The rest of this page is a tour of how you actually build one: what an app contains, how it plugs into the platform, what the built-in primitives (storage, tools, app events, named services, hosted files, SDK integrations, widgets, APIs, @mcp, @cron, @on_job, @venv) look like, and how you deploy it.

What Is an App?

An app is your application package. It combines Python backend code with optional TypeScript UI widgets and views into a single deployable unit. The package becomes a KDCube app by subclassing the platform's base class (BaseEntrypoint or a derived variant) and registering with the current internal decorator @bundle_entrypoint.

An app is the end-to-end application unit inside one tenant/project environment. It can include backend execution logic, authenticated or public APIs, widget UI, main UI, scheduled jobs, background job handlers, named-service providers, deployment-scoped config, deployment-scoped secrets, and optional per-user state. One environment can host many apps at the same time.

App UI is a platform-served surface, not a special iframe concept. An app may expose widget UI with @ui_widget(...) and main UI with @ui_main / ui.main_view. KDCube builds and serves those assets through the integrations/static routes; a consuming frontend can render them directly or embed them, and the KDCube control plane often uses iframes for isolation. That iframe is a client display choice, not an app object.

App code can be delivered either from git or from a mounted local path. In deployed environments the registry usually points at a git repo, ref, and subdirectory. In local descriptor-driven development, use kdcube init --tenant T --project P --descriptors-location ... to stage a concrete runtime under the platform default base (~/.kdcube/kdcube-runtime/<tenant>__<project>/), then iterate with kdcube bundle reload <bundle_id> --tenant T --project P. Rebuild platform images later with kdcube refresh --tenant T --project P --build; add --path /path/to/kdcube-ai-app when refresh should copy a local platform checkout first, or add exactly one of --latest, --upstream, or --release <ref> when the existing runtime should move to another platform source while preserving descriptors. Reapply seed app descriptors with kdcube bundle config apply --descriptors-location ..., not with a full platform refresh.

The reference workspace app consumes the shared Agent Harness through the ReAct adapter. The deployment descriptor selects ai.react.react_agent_version; the checked-in reference assembly.yaml currently selects v2, while v3 is available when explicitly selected. ReAct provides tools, skills, continuous conversations, a source pool, ANNOUNCE, named-service tools, lifecycle events, followup, and steer. In safe_fanout mode, strategy traits govern whether ordered actions may coexist. Ordinary accepted actions preserve their order; a fully validated call with the supported neutral, detached execution profile may begin as soon as its streamed action closes and run in parallel with continued generation. Configure each agent under config.react.<agent_id>, with fallback to default_agent. Apps are not required to use ReAct: they can host LangGraph, another agent runtime, plain Python, or no LLM.

App Events, Policies, and Object Rehosting

App events are the authoring model for story-aware UI such as side chat, wizard, canvas, and snapshot flows. Tools are a special case of event sources: for a tool call, tool_id == event_source_id and tool_call_id == event_id. App UI can also submit authored external events, for example a saved wizard draft, uploaded evidence, canvas review request, or current snapshot notification.

OriginEvent identityTypical use
Tool callevent_source_id = tool_id
event_id = tool_call_id
Tool result becomes timeline blocks through event-source policies.
Widget or main UI eventpayload.external_event.event_source_id
payload.target.agent_id
Wizard, canvas, and chat events land on the selected agent lane.
Externally tracked artifact refext:... or another registered namespacereact.pull invokes the namespace rehoster and returns conversation-owned conv:fi: refs.

Use explicit events for meaningful product transitions: saved draft, file attached, file deleted, assistance requested, canvas review requested, snapshot available, and chat message. Store fast-changing UI state through app APIs or app-owned storage, then send a compact event with a snapshot or artifact ref when the agent should see the state.

{
  "payload": {
    "target": {
      "agent_id": "default.react.agent",
      "story_kind": "case_wizard",
      "story_id": "case:draft-123"
    },
    "external_event": {
      "event_source_id": "case_workspace.wizard.assistance.requested",
      "story_id": "case:draft-123",
      "routing": {
        "reactive": true,
        "iteration_credit": 1
      },
      "data": {
        "section_id": "observed_behavior",
        "snapshot_ref": "ext:case-workspace/draft-123/snapshots/current.yaml"
      }
    }
  }
}

Event-source policies are registered by phase. The first implemented production family is block_production, which converts a tool or external-event result into timeline blocks and artifact rows. The same source can bind later phases such as timeline_projection, announce_production, and compaction_projection so the app controls how its events appear in visible context, ANNOUNCE, and compaction.

Named-service providers extend this pattern from events to objects. A provider can expose search scopes, object schema, object actions, default open effects, and block.produce/block.render policies. react.pull materializes the object into the ReAct artifact space while preserving meta.object_ref, so react.read and timeline projection can select the namespace-owned policies.

Custom artifact namespaces are resolved by registered namespace rehosters. A rehoster materializes approved bytes into the current turn and returns a qualified conv:fi: ref. Physical destinations are role-based: editable project state under turn_<id>/git/projects/..., produced files under turn_<id>/files/..., workflow snapshots under turn_<id>/git/snapshots/..., uploads under attachments/..., and rehosted evidence under external/.... After react.pull(paths=["ext:..."]), agents continue with the returned logical ref or materialized path.

i
Reference: the authoring contract is documented in bundle-events-README.md, with phase details in event-source-README.md and workspace/rehosting rules in ReAct Agent.

Example Apps

Start from the examples folder in sdk/examples/bundles/. The folder name still uses the internal term. The concrete reference app used on this page is workspace@2026-03-31-13-36 — a production-style sample that combines ReAct orchestration, app-local tools, widget decorators, REST operations, public endpoint examples, Telegram webhook and Mini App patterns, attachment handling, ReAct-stream-to-Telegram delivery, and custom UI. It is the current reference app for app authoring docs, but not the reference for @cron or @venv.

AppDescription
workspace@2026-03-31-13-36 Reference app for this page. Demonstrates ReAct orchestration, app-local and MCP tools, decorators for widgets and APIs, public endpoint examples, Telegram hook and webapp patterns, attachment handling, economics-aware entrypoint patterns, a custom main view under ui/main/, widget UI under ui/widgets/, and integrations operations that a real UI can call.

Platform Architecture for App Developers

See also: Platform Overview

KDCube is an AI application framework with an integrated, self-hosted runtime. One running deployment has one effective tenant/project scope and can serve many apps and concurrent users. Shared infrastructure may host several deployments, but each deployment keeps its own schemas, namespaces, prefixes, configuration, and runtime context.

An app is the deployable unit. It can host one or many agents, expose backend or browser surfaces, provide named services, consume other apps or MCP servers, run jobs, or simply wrap code you already operate. It does not have to be a chat app.

💡
Start with one useful boundary. An app can be a small service, a UI surface, an agent host, or a larger product slice. Add platform components only where they remove work you would otherwise build and operate yourself.

Big Picture: How It All Fits Together

Big picture platform overview diagram
Big Picture: How It All Fits Together Complete platform overview showing all components from client through ingress, processor, apps, storage, event and data buses, and integrations User Browser / App SSE + REST SSE/WS REST /bundles/{t}/{p}/{b}/operations/{op} Ingress Auth & Rate Limit App Routing SSE Emitter Task Enqueueing enqueue Redis Queue Task Buffer dequeue Processor (proc) App Loader Harness Adapters Communicator (async) Operations API (REST) GET/POST /bundles/{t}/{p}/{b}/operations/{op} invoke chat·steer·ops·jobs YOUR APP Entrypoint Workflow Tools Skills ReAct Agent Widgets / UI Storage · Config · Economics Communicator Firewall Settings SSE stream Communicator (async streaming → client) PostgreSQL Conversations Redis Props / Cache S3 / Local FS Artifacts / Files Platform-managed persistence layer

Event Bus and Data Bus

These names describe different directions and must not be used as size classes. The Conversation Event Bus is ordered ingress into a conversation lane: follow-ups, steer, consent transitions, provider events, and other accepted inputs can wake or fold into an agent turn. The Data Bus carries app-owned domain commands to configured @data_bus_handler consumers. It is for application work and mutations, not a generic channel for “heavy data.”

Outbound live UI delivery uses ChatCommunicator, which emits post-firewall envelopes to SSE/Socket.IO clients and optional recorders. Files and provider objects use storage plus authorized refs; they do not become Data Bus messages merely because they contain bytes. Event declarations live in sdk/events, transport and wake lanes live in service communication, and runtime object resolution lives separately in the Agent Harness.

Classical Runtime and Agent Harness

sdk/runtime contains classical process/runtime facilities: tool execution, isolated and external execution, service clients, and process-local support. sdk/runtime/harness is the shared framework-neutral layer used by ReAct, hosted LangGraph and future adapters, conversation services, chat, canvas, and namespace integrations.

Harness scopeOwnsDoes not own
harness/eventsAuthorized object/event-ref resolution and byte or action materializationEvent transport, scheduling, partitioning, or turn wake-up
harness/workspacePer-turn paths, conv:fi: refs, pull/materialization, change detection, and WorkspaceArtifactTimeline ordering or model-facing ReAct tools
harness/timelineEvent identity, conv.timeline.v1, TurnLog, turn views, and ownership-fenced provider projectionCommunicator streams or Event Bus queues

ReAct and hosted LangGraph are sibling harness consumers. Conversation persistence and search, including ContextRAGClient, belong to sdk/solutions/conversation. A ref is a locator, not a credential: tenant, project, user, authority, and grants come from trusted runtime context.

Two Services, One Platform

Ingress (chat-ingress)

Handles inbound chat traffic. Authenticates users, enforces rate limits, validates the target app, enqueues the task, and opens an SSE stream back to the client. Your app rarely needs to know about this — the platform wires it up automatically.

Processor (chat-proc)

Dequeues tasks, loads your app entrypoint, and calls execute_core(). It also hosts the Operations API — the REST endpoint that your UI widgets call directly when no SSE stream is needed.

App Anatomy

An app is a self-describing package. Its runtime composition, human documentation, machine-readable interfaces, configuration, storage ownership, tests, release metadata, and implementation journal must stay synchronized.

ContractPurpose
entrypoint.pyThin runtime composition root: app identity, decorators, defaults, surface wiring, and lifecycle hooks.
README.mdHuman overview of the product role, boundaries, surfaces, dataflow, and package map.
AGENTS.mdOperational implementation contract for coding agents: read order, invariants, forbidden shortcuts, synchronization rules, and validation commands. It is not another product README.
config/Complete non-secret descriptor template with safe defaults, plus a secret template containing placeholders only.
interface/Human and machine-readable declarations for every exposed surface family. OpenAPI may have no HTTP paths while x-kdcube-surfaces declares MCP, widgets, named services, Data Bus handlers, or jobs.
docs/storage/README.mdOwnership matrix distinguishing durable, read-through, cached, secret, provider-owned, and ephemeral state.
tests/Manifest, interface, authorization, configuration, storage, and primary product-path checks.
release.yaml and docs/journal/Release metadata and dated implementation decisions. Neither replaces stable documentation or runtime configuration.

Only add conditional source folders such as ui/, agents/, tools/, surfaces/, events/, or integration modules when the app actually provides them. The illustration below shows one agent-oriented package, not a mandatory tree.

App anatomy file structure diagram
App Anatomy File structure of a KDCube app package showing entrypoint, tools, skills, and config files my-app@1-0/ entrypoint.py REQUIRED — @bundle_entrypoint orchestrator/workflow.py BaseWorkflow subclass agents/gate.py Optional: custom intent gate (demo pattern) tools/my_tools.py Custom tool implementations config/bundles.template.yaml Per-agent tool inventory and policy skills/product/kdcube/SKILL.md Agent instruction sets skills_descriptor.py Which skills are visible to which agents knowledge/resolver.py Optional: docs/code knowledge space resources.py User-facing error messages event_filter.py Optional: allow/suppress events

Minimal Entrypoint

from kdcube_ai_app.infra.plugin.bundle_loader import bundle_entrypoint, bundle_id
from kdcube_ai_app.apps.chat.sdk.solutions.chatbot.entrypoint import BaseEntrypoint
from langgraph.graph import StateGraph, START, END
from typing import Dict, Any

BUNDLE_ID = "my-bundle@1-0"

@bundle_entrypoint(name="My App", version="1.0.0")
@bundle_id(BUNDLE_ID)
class MyBundle(BaseEntrypoint):

    def configuration_defaults(self) -> Dict[str, Any]:
        return {
            "role_models": {
                "solver.react.v2.decision.v2.strong": {
                    "provider": "anthropic",
                    "model": "claude-sonnet-4-6"
                }
            }
        }

    async def on_bundle_load(self, **kwargs):
        # Called ONCE per process when bundle first loads.
        self.logger.info("App loaded!")

    async def execute_core(self, state, thread_id, params):
        graph = StateGraph(dict)
        graph.add_node("run", self._run_node)
        graph.add_edge(START, "run"); graph.add_edge("run", END)
        return await graph.compile().ainvoke(state)

    async def _run_node(self, state):
        await self._comm.delta(text="Hello from my app!", index=0, marker="answer")
        return {"final_answer": "Done", "followups": []}

BaseEntrypoint Lifecycle

Method / PropertyWhen CalledTypical Use
configuration_defaults()When effective app configuration is resolvedDeclare code-owned defaults; the platform's configuration property deep-merges effective descriptor props over them
on_bundle_load(**kwargs)Once per process, per tenant/projectBuild knowledge index, connect external services, warm caches
on_apply_props(props)Every effective-props application, after platform-reserved pathsApply app-specific props to runtime objects; return True only when the model service must be rebuilt
on_props_changed(...)When effective app props changedReconcile long-lived side effects after prop refresh
pre_run_hook(state)Before each turnPer-turn setup, state enrichment, request-local validation
execute_core(state, thread_id, params)Every turnBuild + invoke your LangGraph workflow
post_run_hook(state, result)After successful turn executionFast final bookkeeping after the main result
on_turn_completed(...)After completion, error, or cancellationCleanup that must run even when a turn fails
handle_job(**kwargs)When a background job is dispatchedReusable dispatcher for @on_job and mixin-owned background work
rebind_request_context(...)On cached singleton reuseRefresh request-bound state (comm, user, etc.)

Runtime hooks are normal entrypoint methods, not manifest decorators unless explicitly documented as decorators. If an app uses SDK mixins, call super() from completion and job hooks so mixin cleanup and background dispatch still run.

App Lifecycle

An app goes through a well-defined lifecycle from discovery to shutdown. Understanding these phases helps you place initialization logic, manage state, and handle configuration changes correctly.

Discovery and Loading

App source can come from git or from a mounted local path. The platform resolves the configured app source, imports the module, and finds the class decorated with @bundle_entrypoint. The loader extracts interface metadata (decorators for widgets, APIs, message handler, and background job handler) and builds an internal interface manifest that the REST layer and processor use for routing.

Singleton Instance Model

By default each incoming turn or operation creates a fresh entrypoint instance. When the registry sets singleton=true, the platform caches and reuses one instance per loaded app spec in the current process worker.

i
Even with singleton reuse, treat runtime memory as ephemeral. Request-bound surfaces (self.comm, current actor, conversation/turn ids) are rebound per invocation via rebind_request_context() and must never be cached across requests. Durable state belongs in app storage, Redis, or app props — not on self. If an app needs instance-local filesystem state, it should resolve that location through the platform storage helper instead of creating ad hoc runtime folders next to app source code. Singleton reuse is keyed by the loaded app spec (path + module), not by tenant/project and not by bundle_id alone.

Initialization Hooks

HookFrequencyTypical Use
on_bundle_load(**kwargs)Once per process, per tenant/projectBuild indexes, warm caches, clone repos, prepare local read-only assets, trigger UI build
on_apply_props(props)Every props refresh on turn, REST, widget, MCP, and local-operation surfacesApply app-specific configuration after platform-owned role_models, embedding, and services.llm.custom paths
on_props_changed(...)When effective props changed for the active instanceInvalidate prop-derived caches, mark helpers dirty, reconcile side effects after live prop updates
pre_run_hook(state)Every invocationLast-minute validation or reconciliation before execution
execute_core(state, thread_id, params)Every invocationMain app logic (chat turn or operation handling)
post_run_hook(state, result)Every invocationFinal bookkeeping after execution completes
on_turn_completed(...)Every invocation exit pathCleanup after success, error, or cancellation
handle_job(**kwargs)Background job dispatchHandle mixin-owned or app-owned work_kind values after proc claims a job
on_memory_reconciliation_request(request)Before a memory reconciliation job is stored/enqueuedValidate or augment one request's memory reconciliation controls
rebind_request_context(...)Singleton reuse onlyRefresh request-local handles on cached instance before the current call

on_bundle_load() must be deterministic and idempotent. It runs before any request depends on the app and is the right place for bounded preparation work. Do not store request-local state there.

on_apply_props(props) is the public async seam for applying app-specific configuration to live runtime objects. It runs after platform-interpreted paths on every effective-props refresh. Do not override the internal synchronous _apply_bundle_props_overrides; doing so can silently disable platform model, embedding, or custom-provider application.

on_props_changed(...) is different: it runs after effective app props changed for the active instance. Use it for long-lived side effects such as prop-derived caches, sidecar wrapper state, or other runtime helpers that must track app props. Do not use it for request-local validation or heavy one-time install/build work.

on_memory_reconciliation_request(request) is available on memory-enabled entrypoints. Return None or a JSON-safe patch to merge into the request; return {"ok": false, "error": "...", "message": "..."} to reject the request before a job is created.

Portable App Call Context

bundle_call_context is the app-owned, request-scoped context room. The internal name remains for compatibility. Use it for JSON-safe metadata that must follow nested agents, tools, background handlers, and isolated runtimes without asking the model to pass those values as tool arguments.

from kdcube_ai_app.apps.chat.sdk.runtime.comm_ctx import (
    bind_current_bundle_call_context_patch,
    get_current_bundle_call_context,
    update_current_bundle_call_context,
)

# Visible for the rest of the current invocation.
update_current_bundle_call_context({
    "my_bundle": {"selected_mode": "lite"},
})

# Temporarily override one named agent role for one nested run.
with bind_current_bundle_call_context_patch({
    "role_models": {
        "my.named.agent": {
            "provider": "anthropic",
            "model": "claude-haiku-4-5-20251001",
        },
    },
}):
    await self.run_named_agent(...)

current = get_current_bundle_call_context()

Read it from self.comm_context.bundle_call_context or get_current_bundle_call_context() in entrypoints, APIs, widget operations, and @on_job handlers. In in-process tools, bundle_tool_context.scope()["bundle_call_context"] is a tool-side reader for the same room. Isolated Docker/Fargate runtimes restore the same context through the runtime globals snapshot.

This context is not durable storage. It survives the current execution graph and child runtime boundaries; it does not automatically survive a later request. If a later background job needs the same decision, store it in the job payload/metadata or another durable app-owned location, then rebind it when that job runs. PORTABLE_SPEC_JSON is platform-built; app-owned per-call data belongs in the technically named bundle_call_context.

bundle_call_context.role_models is a reserved request-scoped overlay interpreted by the model router. Precedence is current call context first, then effective app props role_models, then platform defaults.

Hot-Reload on Config Change

When you update an app's ref in bundles.yaml and re-apply, the platform detects the configuration change via a content hash of the app directory. The updated app is loaded in the current process without a service restart. on_bundle_load() runs again for the new version, and subsequent requests are served by the refreshed instance.

For props-only live updates, the active app instance refreshes effective props on the next invocation, and on_props_changed(...) fires only if the effective props actually changed. Already-loaded singleton apps in the current worker also receive that hook on live bundles.props.update events.

Shutdown and Cleanup

The platform does not expose a dedicated shutdown hook. Because app state should live in external storage (Redis, S3, local shared storage), process termination is safe by design. Transient per-invocation state in OUT_DIR / workdir is scoped to the request and cleaned up automatically.

Storage Surfaces Across Phases

Surface / ScopeRead / write APILive authority todayExampleExport / ejection path
Platform/global propsread: get_settings()
raw read: get_plain("...")
write: none from normal app code
Deployment descriptors such as assembly.yaml and gateway.yaml; process-environment keys are generated runtime projections or implementation aliases, not the operator write surfacePorts, auth ids, storage backends, runtime path rootsOutside kdcube export; manage through deployment descriptors
Platform/global secretsread: await get_secret("canonical.key")
write: none from normal app code
Configured secrets provider; in local secrets-file mode this is secrets.yamlDeployment-wide API keys and auth secretsOutside kdcube export; manage through deployment secret workflows
bundle_props / bundle_prop(...)read: self.bundle_prop(...)
write: await set_bundle_prop(...)
The configured descriptor provider; file mode persists to bundles.yaml. Redis is a derived runtime cache, never the authority.App feature flags, cron config, model selection, UI configExported to bundles.yaml by kdcube export
App secretsread: await get_secret("b:...")
write: await set_bundle_secret(...)
Configured secrets provider; in local secrets-file mode this is bundles.secrets.yamlApp-scoped webhook secrets and shared API tokensExported to bundles.secrets.yaml when the provider/export flow can reconstruct app secrets
User app propsawait get_user_prop(..., default=...), await get_user_props(), await set_user_prop(...), await delete_user_prop(...)PostgreSQL <SCHEMA>.user_bundle_propsPer-user non-secret preferences and app stateNever exported to descriptors or app export
User app secretsread: await get_secret("u:...")
write/delete: await set_user_secret(...), await delete_user_secret(...)
Configured user-scoped secrets providerPer-user secret material such as personal access tokensNever exported to descriptors or app export
Redis KV cachedeveloper-defined read/write keysRedis onlyLightweight distributed state, flags, small cachesNo descriptor export
BundleArtifactStorageartifact storage APIsS3 or localfs backend, depending on deploymentPersistent app artifacts addressed through the storage APIOutside descriptor export
Shared local storage (BUNDLE_STORAGE_ROOT)self.bundle_storage_root() or bundle_storage_dir(...)Host-local or shared instance-visible storageLarge local caches, cloned repos, indexes, mutable local workspaces, cron stateOutside descriptor export
OUT_DIR / workdirfilesystem read/writeCurrent invocation onlyTransient turn files, generated artifactsEphemeral; not exported
Hosted conversation filesret.artifact_type == "files" or host_files(...)Conversation store plus current-turn file event metadataUser-visible downloads and attachments produced by tools or isolated executionCurrent conversation artifact surface; not descriptor state

Only deployment-scoped app state belongs to app descriptors and app export. Platform/global deployment state and all user-scoped state stay outside kdcube export.

In supervised Docker/Fargate execution, the trusted supervisor receives and materializes descriptor payloads before tool bootstrap. App tools therefore use the normal SDK helpers. The claim that generated code receives no descriptor files, provider secrets, or network applies to the reference Docker split executor; local subprocess, combined Docker, and Fargate have different physical boundaries. Set execution.runtime.descriptor_payload_scope: active_bundle when the supervisor should receive only the active app sections of bundles.yaml and bundles.secrets.yaml.

Reference docs: bundle-runtime-configuration-and-secrets-README.md, runtime-read-write-contract-README.md, how-to-configure-and-run-bundle-README.md.

await get_secret("b:...") resolves the current app from bound runtime context; await get_secret("u:...") additionally resolves the current user. Outside a bound request or app runtime, pass explicit scope or use a fully qualified deployment secret path. User prop values pass through asyncpg JSON/JSONB codecs as native Python values. Compatibility readers decode a second JSON layer only when a decoded string begins with { or [, preserving literal strings such as false and de-DE.

Node / TypeScript Backend Inside an App

If your application backend already exists in Node or TypeScript, the supported pattern is to keep the public KDCube app surface in Python and run the Node backend as an app-local sidecar.

That split is intentional:

  • Python app shell owns decorators, auth, role gating, app props, app secrets, and the public API / widget / MCP / cron contract.
  • Node backend owns internal domain logic behind a narrow route boundary.
my.bundle@1-0/
  entrypoint.py
  backend_src/
    package.json
    src/
      bridge_app.ts

The Python side starts the sidecar through ensure_local_sidecar(...). Current runtime behavior:

  • one sidecar instance per worker for the active loaded app spec and tenant/project scope
  • app code reload stops the sidecar and the next call starts it fresh
  • props-only updates do not proactively restart the sidecar at publish time
  • startup-config changes restart lazily on next use; live config can be pushed lazily through POST /__kdcube/reconfigure

This lets you wrap an existing Node backend without replacing the Python app shell. The app remains the KDCube application unit; Node is one internal implementation part of that app.

Reference docs: bundle-node-backend-bridge-README.md and node-backend-sidecar-README.md.

Interface: In & Out

An app's interface is the union of the surface families it actually declares: operations or public APIs, MCP, named-service providers, widgets and main views, reactive conversation handling, external events, Data Bus handlers, cron jobs, background jobs, integrations, and artifact namespaces. The package's decorators, descriptor config, interface/README.md, OpenAPI/x-kdcube-surfaces, tests, and journal must agree.

The chat contract below applies only to conversation-capable apps. API-only, service-only, UI-only, and job-only apps do not receive a ChatTaskPayload and do not need a communicator.

📥 Conversation Input

A scheduled conversation turn arrives as a ChatTaskPayload (Pydantic). Key fields:

  • request.message — user's text
  • request.chat_history — prior messages
  • request.payload — arbitrary JSON (for REST ops)
  • actor — tenant_id, project_id
  • routing — conversation_id, turn_id, bundle_id
  • user — user_id, user_type, roles, timezone
  • continuation — follow-up or steer type

📤 Conversation Streaming

A conversation-capable app streams through the Communicator. The client receives ChatEnvelope events in real time over SSE or Socket.IO.

  • delta — streaming text chunks (thinking / answer)
  • step — tool calls, status updates, timeline events
  • complete — turn finished with final data
  • error — propagate errors cleanly
  • event — custom events (artifacts, reactions, etc.)

Using the Communicator

# Stream answer text
await self._comm.delta(text="chunk...", index=0, marker="answer")

# Announce a step
await self._comm.step(step="web_search", status="started", title="Searching the web...")
await self._comm.step(step="web_search", status="completed")

# Emit follow-up suggestions
await self._comm.followups(["Tell me more", "Show examples"])

# Final complete signal
await self._comm.complete(data={"answer": "..."})

Recording comm events and sending event batches

Apps can record selected post-firewall communicator envelopes into a bounded, scoped buffer and send the batch to a configured event sink. Recording is additive and scoped: a workflow, API handler, MCP endpoint, job handler, or tool can add a JSON-serializable scope without replacing the outer scope.

async with self.comm.recording(
    EVENT_SELECTOR,
    scope={"owner": "workflow", "bundle": bundle_id},
    mode="replace",
    max_events=500,
    sink=event_sink,
    send_on_exit=True,
):
    await self.run_react(...)

Configured isolated child tool runtimes receive portable active scopes through COMM_SPEC. Child-added scopes are recorded in the child buffer, written to comm_recorded_events.json, merged by the host, and sent by the host sink. Sink callbacks are live Python objects and are not serialized into child runtimes.

Open the recording scope at the boundary that owns the invocation: @on_message workflows, @api methods, @mcp methods, and @on_job handlers can all record when a communicator is bound. @cron is normally headless, so use comm recording only when the cron path invokes a comm-bound flow; otherwise enqueue a job or persist durable operational facts directly.

Reference docs: bundle-event-recording-and-sinks-README.md, comm-recording-event-sinks-README.md.

REST Operations (for UI Widgets)

Your app exposes additional REST operations through the Operations API hosted by the Processor:

POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation}
GET  /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation}

The explicit bundle_id form is the preferred route. The processor resolves the target method from the app's decorator-discovered interface surface rather than assuming every operation is just workflow.run(). A default-app compatibility shortcut still exists at POST /api/integrations/bundles/{tenant}/{project}/operations/{operation}.

Runtime Availability And Visibility

Bundle availability and resource visibility are deployment-scoped bundle props. The whole bundle is gated through the canonical enabled.bundle prop resolved by the platform. APIs and widgets declare code defaults for user types and roles, and can declare prop paths that the Bundle Admin UI can override. Resource enabled flags are stored in bundle props/admin state; do not pass the removed enabled_config argument to @api or @mcp.

@bundle_entrypoint(
    name="Ops",
    version="1.0.0",
    allowed_roles=("kdcube:role:viewer",),
    allowed_roles_config="visibility.bundle.allowed_roles",
)
class OpsBundle(BaseEntrypoint):

    @api(
        alias="report",
        user_types=("registered",),
        user_types_config="visibility.api.report.user_types",
        roles_config="visibility.api.report.roles",
    )
    async def report(self, **kwargs):
        ...

    @ui_widget(
        alias="admin",
        icon={"lucide": "Settings"},
        user_types=("privileged",),
        roles_config="visibility.widget.admin.roles",
    )
    async def admin_widget(self, **kwargs):
        ...

    @mcp(alias="automation", transport_config="mcp.automation.transport")
    def automation_mcp(self, **kwargs):
        ...

    @cron(alias="news-sync", expr_config="jobs.news_sync.cron")
    async def news_sync(self) -> None:
        ...

The visibility dot paths are resolved against effective bundle props. Missing or invalid values fall back to the decorator defaults. Empty user-type or role selections are intentional overrides that mean no restriction for that selector. Cron jobs use expr_config; blank or disable values disable the job. MCP endpoint request authorization belongs to the bundle-served MCP app, not to user_types / roles on the @mcp decorator.

⚙️
Precedence: when enabled.bundle is falsy, bundle operations, widgets, and MCP endpoints return 404, and proc skips scheduled jobs for that bundle.

Because these values live in bundle props, they work with hot bundle reload and live props updates. This is the mechanism that lets Bundle Admin change bundle visibility, API/widget visibility selectors, resource enabled flags, and scheduled-job expressions without a redeploy.

Continuation Types

TypeDescription
regularNormal new message
followupUser clicked a suggested follow-up
steerUser is redirecting the ongoing turn

Classic vs. Interactive Turn Execution

ModeBehavior
Classic turn execution The turn runs to completion before the user can affect the next agent step. From the user perspective, input is effectively blocked until the turn finishes and emits its final result.
Interactive turn execution While a turn is running, the user can still contribute to the same conversation. followup and steer are written into the conversation event lane. A live React turn can consume them immediately: followup stays on the current turn, while steer redirects the current turn at the next safe checkpoint. Idle-turn handling is controlled by the reactive-event admission path rather than by bundle UI code.
↪️
Interactive execution uses the existing continuation kinds: followup and steer. A busy conversation accepts these into the shared event lane rather than dropping them. followup is a live “continue on this turn” input, while steer is a live “redirect this turn” signal. When React owns the turn it consumes these directly on the current timeline.

Long ReAct turns may also emit chat_compaction transport events with semantic type chat.compaction. These mark context compaction start/completion while the turn continues; clients should render them as progress/activity items, not as final answers.

Storage

KDCube separates storage by ownership and lifecycle. The three primitives below are not interchangeable, and an app may use none, one, or several of them.

Storage tiers diagram
Storage Tiers Three app storage contracts: the neutral artifact storage API, the scoped filesystem root, and Redis cache Artifact Storage API BundleArtifactStorage file:// or s3:// Survives restarts & scaling Per tenant / project / bundle Read/write arbitrary keys CB_BUNDLE_STORAGE_URL Filesystem Root bundle_storage_root() descriptor-resolved stable root tenant / project / app id Knowledge indexes, caches local or mounted shared filesystem pathlib.Path (filesystem) ⚡ Redis Cache self.kv_cache / self.redis In-memory, fast derived config cache Session state, rate limits Pub/sub for bundle updates aioredis client

Cloud Storage (BundleArtifactStorage)

from kdcube_ai_app.apps.chat.sdk.storage.bundle_artifact_storage import BundleArtifactStorage

storage = BundleArtifactStorage(
    tenant="my-tenant", project="my-project",
    bundle_id="my-bundle@1-0",
    storage_uri="s3://my-bucket"  # or file:///data/bundle-storage
)

storage.write("reports/latest.json", data='{"count": 42}')
content = storage.read("reports/latest.json", as_text=True)
keys = storage.list("reports/")

App Filesystem Storage

# In entrypoint or workflow
root = self.bundle_storage_root()  # pathlib.Path
index_path = root / "knowledge_index"
index_path.mkdir(exist_ok=True)

Path namespaced: {BUNDLE_STORAGE_ROOT}/{tenant}/{project}/{bundle_id}/. bundle_storage_root() is a filesystem path, never an S3 API. Local runtime uses local or mounted storage; multi-worker cloud deployments normally mount shared filesystem storage such as EFS when workers must see the same files. Use BundleArtifactStorage separately when the app needs the backend artifact API backed by S3 or localfs.

Document the owner of every dataset. For example, connected-account metadata may live in app filesystem storage while provider tokens stay in user-scoped secrets; delegated KDCube grants and sessions use server-side grant/session records. Provider-owned mail and Slack bytes remain provider-owned unless an explicit operation materializes them.

Redis Cache

# Low-level Redis client (aioredis)
await self.redis.set("my:key", "value", ex=3600)
val = await self.redis.get("my:key")

# KVCache wrapper
await self.kv_cache.set("user_prefs", {"theme": "dark"}, ttl=86400)

Workflow Orchestration

KDCube does not require one orchestration framework. An app can use LangGraph, LangChain, CrewAI-authored logic, the built-in KDCube ReAct agent, plain Python, or another runtime behind its app boundary. The example below uses LangGraph and BaseWorkflow.

ReAct and hosted/ported agents are sibling consumers of the shared Agent Harness. A run-to-completion LangGraph adapter can use harness workspace, resolver, timeline, and conversation-recorder contracts without inheriting ReAct's live mid-turn event folding or round protocol. In concurrent multiuser serving, keep graph objects turn-scoped: build the graph for the turn, reuse durable connections or checkpointers where appropriate, and release the graph when the turn ends.

A fresh ReAct runtime object is built for each accepted turn from app code, the administrator capability ceiling, conversation-scoped user selection, and durable timeline/workspace/memory/runtime/event-lane state. Construction is per turn; state is not.

from kdcube_ai_app.apps.chat.sdk.solutions.chatbot.base_workflow import BaseWorkflow
from kdcube_ai_app.apps.chat.sdk.runtime.tool_config import agent_tool_config_from_bundle_props
from kdcube_ai_app.apps.chat.sdk.runtime.skill_config import agent_skill_config_from_bundle_props

class MyWorkflow(BaseWorkflow):
    def __init__(self, *args, bundle_props=None, **kwargs):
        super().__init__(*args, bundle_props=bundle_props, **kwargs)

    async def process(self, payload):
        scratchpad = self.start_turn(payload)
        try:
            client_id = self.runtime_ctx.agent_id
            tools = agent_tool_config_from_bundle_props(
                self.bundle_props, client_id, bundle_root=self.bundle_root()
            )
            skills = agent_skill_config_from_bundle_props(
                self.bundle_props, client_id, bundle_root=self.bundle_root()
            )
            tools, skills = await self.apply_user_agent_selection(tools, skills)
            tools = await self.apply_delegated_tool_claims(tools)
            react = self.build_react(
                scratchpad=scratchpad,
                mod_tools_spec=tools.tool_specs,
                mcp_tools_spec=tools.mcp_tool_specs,
                tools_runtime=tools.tool_runtime,
                tool_traits=tools.tool_traits,
                custom_skills_root=skills.custom_skills_root,
                skills_visibility_agents_config=skills.agents_config,
            )
            result = await react.run()
            self.finish_turn(scratchpad, ok=True)
            return result
        except Exception as e:
            self.finish_turn(scratchpad, ok=False); raise
🚀
Use BaseWorkflow for the quickest path. It wires up ConvMemories, TurnStatus, ContextRAG, ApplicationHosting, and gives you build_react() which assembles the full ReAct agent with all tools and skills resolved.

build_react() automatically honors the current agent's react.<agent_id>.additional_instructions and appends that administrator customization last inside the shared hard-override envelope. Instruction profiles are declared under the same agent block; the picker carries IDs only, while the runtime resolves each selected profile to its body or blocks such as xlite:workspace_exec.

build_react() respects descriptor-backed runtime selection through ai.react.react_agent_version (projected as AI_REACT_AGENT_VERSION). The checked-in reference descriptor and code-level fallback currently select v2; choose v3 explicitly for its streamed-lane governance and optional multi-action path. If AI_REACT_AGENT_MULTI_ACTION=safe_fanout is enabled, accepted multi-action rounds are still executed sequentially unless a fully validated neutral call carries the supported detached early-execution traits. The base round cap resolves from app props, then assembly projection, then fallback 15.

BaseWorkflow Key Parameters

ParameterTypeDescription
conv_idxConvIndexConversation vector index for semantic search
storeConversationStoreFile/S3-backed conversation storage
commChatCommunicatorChat streaming channel for SSE or Socket.IO delivery
model_serviceModelServiceBaseLLM registry / router
ctx_clientContextRAGClientConversation persistence/search client owned by sdk/solutions/conversation
bundle_propsDictBundle runtime configuration
graphGraphCtxOptional knowledge graph context

Simplest Agentic Workflow Pattern

A bundle can use any orchestration pattern — it is not required to use the ReAct agent. This is one common pattern that combines a Gate agent with the ReAct agent:

Agentic workflow pattern diagram
Simplest Agentic Workflow Pattern Workflow diagram combining a Gate agent with the ReAct agent for agentic task routing Simplest Agentic Workflow Pattern (optional gate + react layers) User Message Gate Agent (Optional layer) Intent classify Route decision ReAct Agent Decision loop Tool calls Self-planning Answer Gen (Optional layer) Final answer Follow-ups 📜 Timeline — streamed to client in real time tool_calls · answer · thinking · artifacts · plan blocks — all streaming

Tools System

ToolNamespaceDescription
web_searchweb_toolsNeural web search with ranking (Brave / DuckDuckGo)
web_fetchweb_toolsFetch + parse web pages (readability-enabled)
execute_code_pythonexec_toolsProfile-selected Python execution: local subprocess containment, Docker combined compatibility, reference Docker split isolation, or external Fargate transport
write_pdfrendering_toolsGenerate PDF from Markdown + table of contents
write_pngrendering_toolsRender HTML/SVG to PNG image
write_docxrendering_toolsGenerate DOCX from Markdown
write_pptxrendering_toolsGenerate PPTX slide deck
write_htmlrendering_toolsGenerate standalone HTML artifact
fetch_ctxctx_toolsFetch visible objects by qualified logical ref, including conv:ar:, conv:fi:, and external namespaces.
readreactreact.readRead visible logical refs such as conv:fi:, conv:ar:, conv:so:, conv:ws:, conv:su:, and conv:ev:. A bare fi: ref is invalid.
writereactreact.writeAuthor current-turn content. Produced files belong under the turn's files/ role; editable project state belongs in the active workspace under git/projects/.
pullreactreact.pullResolve and materialize visible owner refs such as mem:, task:, cnv:, or another registered namespace into the current turn. Returned conversation-owned file refs use conv:fi:.
planreactreact.planCreate / update / close the current turn plan (shown in ANNOUNCE)
patchreactreact.patchPatch an existing current-turn text artifact. Unified diffs are supported; full replacement is used when the patch body is plain file content. Display line numbers in previews are never part of patch content.
checkoutreactreact.checkoutConstruct or update editable project state under turn_<id>/git/projects/... from qualified conv:fi: refs. mode="replace" seeds from scratch; mode="overlay" imports selected historical files.
memsearchreactreact.memsearchSemantic search in past conversation turns
rgreactreact.rgSearch materialized artifact files by name and text-like files by content regex; returns read-ready ranges for react.read.
hidereactreact.hideReplace timeline snippet with placeholder

App-local tools use @kernel_function from Semantic Kernel and are connected to each agent through config.surfaces.as_consumer.agents.<agent_id>.tools. The app configuration is the agent's inventory ceiling: a user may narrow it, but cannot add a tool the app did not grant. Tool results should use the common {ok, error, ret} envelope. When a tool produces user-visible files, ret must use the strict file artifact protocol.

Tool calls and event sources may share block-production and timeline machinery, but they are opposite directions and are not interchangeable. Tool direction is model → fence → provider → result → timeline. Event direction is provider/event lane → block production → timeline → model. Event-source configuration binds owner discovery, block production, and pull policy; it does not transport events and does not create a model-callable tool.

# tools/my_tools.py
from typing import Annotated
import semantic_kernel as sk
from semantic_kernel.functions import kernel_function

class MyTools:
    @kernel_function(name="search", description="Search product catalog")
    async def search(self,
        query: Annotated[str, "Search query"],
        limit: Annotated[int, "Max results"] = 5
    ) -> str:
        # your logic here
        return "results..."
# bundles.yaml (or the app's config template)
config:
  surfaces:
    as_consumer:
      agents:
        main:
          tools:
            - id: web
              kind: python
              module: kdcube_ai_app.apps.chat.sdk.tools.web_tools
              alias: web_tools
              allowed: [web_search]
            - id: product
              kind: python
              ref: tools/my_tools.py
              alias: my_tools
              discovery: semantic_kernel
              allowed: [search]
              tool_traits:
                search:
                  strategy: [exploration]

File-producing tools have two supported paths. They can return local files declaratively:

{
  "ok": true,
  "error": null,
  "ret": {
    "artifact_type": "files",
    "files": [
      {
        "path": "report.pdf",
        "filename": "report.pdf",
        "mime": "application/pdf",
        "visibility": "external",
        "description": "Generated report"
      }
    ]
  }
}

Or a trusted app/catalog tool can host files itself with bundle_tool_context.host_files(...) and return the already-hosted rows. In an active conversation, both paths emit chat.files with object refs; the browser resolves bytes at click time under the user's session, and the model sees a delivery note rather than a signed URL. A turn-less MCP caller has no chat lane, so the original URL-bearing result remains unchanged. Generated executor code should call a catalog tool through agent_io_tools.tool_call(...) when it needs hosted files; host_files(...) is for trusted tool code.

host_files(...) works only after the SDK has prepared the tool runtime: active ToolSubsystem, hosting service, tenant, project, user id, conversation id, turn id, conversation storage, and output directory. Normal React workflows prepare this through BaseWorkflow.build_react(...); isolated execution prepares it through bootstrap_bind_all(...). If that context is missing, the helper raises a runtime error instead of creating an unscoped artifact.

Custom domain artifact refs can also be exposed through namespace rehosters. A loaded tool or event module can register @artifact_namespace_rehoster(namespace="ext"); then react.pull(paths=["ext:..."]) resolves that opaque domain ref and returns a materialized conv:fi: ref for later react.read, react.rg, or generated-code use.

See custom-tools-README.md and the reference app's per-agent tool configuration.

Consumed MCP services and the tools visible to each agent both live in the app consumer surface. A tool entry's server_id must match a server under surfaces.as_consumer.mcp.services.

# bundles.yaml — connection plus per-agent inventory
config:
  surfaces:
    as_consumer:
      mcp:
        services:
          mcpServers:
            knowledge:
              transport: streamable-http
              url: https://mcp.example.com
              auth:
                type: bearer
                secret: b:mcp.knowledge.token
      agents:
        main:
          tools:
            - id: knowledge
              kind: mcp
              server_id: knowledge
              alias: knowledge
              allowed: ["*"]
              tool_traits:
                "*":
                  strategy: [exploration]

For a hosted agent acting for the signed-in user, configure the connection with delegated: true, claims in scopes, and a resource that byte-matches the delegated-resource catalog ID; url remains the concrete endpoint to dial. The runtime reuses the agent's server-side grant bearer per turn and drops a consent-pending connection before contacting the server. The agent identity is kdcube-agent:<app>:<agent>, so sibling agents never share grants.

See mcp-README.md and consume-mcp-service-README.md for transports, authentication, and delegated hosted-agent connections. KDCube negotiates the current MCP 2026-07-28 flow and can fall back to the legacy 2025-11-25 protocol for older peers.

Named-Service Tools

Named-service tools are the preferred way for agents to work with app-owned object systems. Instead of adding separate direct tools for every subsystem, a provider exposes a namespace with about/schema/search/action/upsert/delete operations and optional block policies. Consumers configure which namespaces and operations are allowed.

Provider-backed account authorization has two live boundaries. Delegated to KDCube establishes or upgrades the user's provider account; Delegated by KDCube grants one hosted agent access to the exact account and operation. Capability is checked first so connect or claim-upgrade demands do not misroute into the agent-grant flow. Missing consent is raised at the concrete operation attempt, and revoking either boundary stops the tool.

Provider metadata

provider.about, object.schema, search filters, guarded use cases, connected-account requirements, and the required presentation layer form one self-description for two readers: agents work the realm; users understand, narrow, and consent from its service card.

Consumer policy

The tool connection declares the configuration ceiling for allowed namespaces and operations. Strategy and execution traits govern ordering and scheduling, not authorization. Before an action, the agent reads the provider's schema and uses only payload keys that contract declares.

Object projection

react.pull materializes object refs, react.read uses block production policies, and timeline rendering can call block.render for provider-owned blocks.

See Object Ecosystem & Ontologic Contracts and ReAct named-service flow.

Artifact Path Families

Conversation-owned refs use the strict form conv:<family>:<body>. The conv: prefix is an owner namespace; a body segment such as conv_123 is a physical conversation identifier and must not be rewritten. ReAct tools resolve these refs under the current request identity before any bytes are materialized.

Ref or pathMeaningExample
conv:fi:Conversation-owned file bytes or a materialized workspace file.conv:fi:conv_123.turn_456.files/report.pdf
conv:ar:, conv:tc:, conv:so:Replica records, tool records, and source-pool rows.conv:ar:conv_123.turn_456.assistant.completion
conv:ws:, conv:su:, conv:ev:Working summaries, summary records, and accepted timeline events.conv:ev:conv_123.turn_456.event_7
turn_<id>/git/projects/...Editable project/workspace state.turn_456/git/projects/app/src/main.py
turn_<id>/files/...Produced files and deliverables.turn_456/files/report.pdf
turn_<id>/git/snapshots/...Canvas, wizard, story, or workflow snapshots.turn_456/git/snapshots/current.yaml
attachments/..., external/...Current-turn uploads and pulled or rehosted external evidence.turn_456/external/vendor/evidence.json
mem:, task:, cnv:, other ownersExternal owner refs. react.pull resolves authorized content and rehosts exact bytes as conv:fi:.task:shipment:42

Skills System

Skills are reusable instruction sets that give agents specialized capabilities. A skill bundles a natural-language instruction (SKILL.md), tool references, and source references.

The skill registry is broader than a bundle's own skills/ folder. For each agent consumer, the runtime resolves core SDK skills, SDK solution skills, and bundle-local skills, then applies the bundle's AGENTS_CONFIG and the current tool catalog before showing the final list to the agent.

Built-in Platform Skills

Skill IDNamespaceDescription
url-genpublicForm clean human-facing source URLs for fetch tools; not a file-delivery mechanism
pdf-presspublicPDF generation and manipulation
docx-presspublicDOCX document generation
pptx-presspublicPPTX presentation generation
png-presspublicPNG image rendering from HTML/SVG
svg-presspublicSVG illustration and diagram production
mermaidpublicMermaid diagram generation
link-evidenceinternalCitation and evidence linking
sources-sectioninternalAutomatic sources section generation

SDK solution skills, such as task-focused skills, are discovered by the same registry and filtered by the same AGENTS_CONFIG and required-tool rules. They do not need to live in the bundle's skills/ folder.

Custom Bundle Skill

# skills/product/kdcube/SKILL.md
You are an expert in our product catalog.
When asked about products, use the `product_search` tool to find relevant items.
Always include pricing and availability.
# skills/product/kdcube/tools.yaml
tools:
  - id: product_search
    role: search
    why: Search the product catalog
    required: true
# skills_descriptor.py
AGENTS_CONFIG = {
    "solver.react.v2.decision.v2.strong": {
        "enabled": ["product.kdcube", "public.pdf-press", "public.url-gen"],
        "disabled": []
    }
}

Creating a SKILL.md File

Each skill lives in its own folder under the bundle's skills/ directory. The required file is SKILL.md (or skill.yml), which contains YAML front-matter and a natural-language instruction body.

# skills/product/kdcube/SKILL.md
---
name: "Product Catalog Expert"
id: "kdcube"
namespace: "product"
description: "Search and present product information"
version: "1.0"
tags: ["catalog", "products"]
when_to_use: "When the user asks about products, pricing, or availability"
imports: []
agent_disclosure: "normal"
---

You are an expert in the product catalog.
When asked about products, use the `product_search` tool.
Always include pricing and availability in your response.

Optional companion files in the same folder:

  • compact.md — a shorter instruction variant for context-constrained agents
  • sources.yaml — sources injected into sources_pool when the skill loads (exposed through qualified conv:so: refs)
  • tools.yaml — recommended or required tools for the skill, used by planners and runtime eligibility checks

Skill Visibility Configuration

The skills_descriptor.py file controls which skills are visible to which agent consumers. It exposes two key variables:

# skills_descriptor.py
import pathlib

BUNDLE_ROOT = pathlib.Path(__file__).resolve().parent

# Points to the skills folder layout: <root>/<namespace>/<skill_id>/SKILL.md
CUSTOM_SKILLS_ROOT = BUNDLE_ROOT / "skills"

AGENTS_CONFIG = {
    # Allow-list for the ReAct decision agent
    "solver.react.v2.decision.v2.strong": {
        "enabled": ["product.kdcube"]
    },
    "solver.react.v2.decision.v2.regular": {
        "enabled": ["product.kdcube", "public.*"]
    },
    # Deny-list for a generator agent
    "answer.generator.strong": {
        "disabled": ["public.*"]
    },
}
RuleBehavior
enabled: [...]Allow-list: only listed skills are visible to that consumer
disabled: [...]Deny-list: listed skills are hidden from that consumer
WildcardsSupported in both lists ("public.*", "public.docx-*", "*")
Missing consumer entryNo descriptor filtering — registered skills may still be removed by required-tool eligibility

Runtime Skill Eligibility

AGENTS_CONFIG is only the first filter. ReAct also compares each skill's required tool references with the active tool catalog for the current agent and turn. If a skill declares a required tool that is not available, the skill is omitted from the visible catalog, SK1/SK2 short ids, imports, and react.read("sk:...") for that runtime context.

# skills/public/pdf-press/tools.yaml
tools:
  - id: rendering_tools.write_pdf
    role: render pdf
    required: true

Use required: true for subsystem skills that would confuse the agent without the matching tools. For example, PDF/DOCX/PPTX skills depend on rendering tools, and task skills depend on task tools. If those tools are not registered for a bundle, the corresponding skills disappear automatically without adding deny-list entries.

Hidden Operational Skills

Some skills are useful as imported operational guidance but should not be advertised when a user asks what the agent can do. Add agent_disclosure: hidden in the skill front matter for that case.

---
name: memory-journal
namespace: product
description: Operational guidance for durable memory tools.
agent_disclosure: hidden
imports: []
---

A hidden-disclosure skill remains loadable by exact id or import when it is otherwise eligible, but it is excluded from visible catalogs and short-id mappings. If it is explicitly loaded, the active skill block uses a redacted heading and a non-disclosure rule. This is prompt-disclosure control, not authorization; use AGENTS_CONFIG and tool gating for actual availability.

If a parent skill imports an optional subsystem skill, keep the subsystem-specific instructions inside the imported skill. The registry can skip an ineligible import, but it cannot rewrite arbitrary subsystem instructions that were duplicated in the parent skill body.

Skill Loading and Resolution

The runtime auto-detects <bundle_root>/skills as the custom skills root when CUSTOM_SKILLS_ROOT is not set. Skills are loaded into the skill registry and resolved by fully qualified id (namespace.skill_id). Consumer agents reference eligible visible skills via short-id tokens (SKx) or explicit sk:<id> references. When a skill is loaded with react.read("sk:<skill>"), its sources are merged into sources_pool and citation tokens are rewritten to match.

i
Setting CUSTOM_SKILLS_ROOT = None does not reliably disable bundle-local skills — the runtime falls back to auto-detection. To truly disable them, remove the skills/ folder or set CUSTOM_SKILLS_ROOT to a non-existent path.

Best Practices for Skill Authoring

  • Keep SKILL.md instructions focused — one skill per capability domain
  • Use when_to_use front-matter to help the agent decide when to activate the skill
  • Include tools.yaml to pair skills with the tools they need, and mark hard dependencies with required: true
  • Use AGENTS_CONFIG to restrict skill visibility rather than deleting skill files — this keeps skills available for future consumers
  • Use agent_disclosure: hidden only for loadable operational guidance that must not be advertised in self-description
  • Test visibility filtering per consumer: a skill hidden from solver.react.v2.decision.v2.strong is still visible to unfiltered consumers

Widgets, APIs, MCP & Custom UI

Apps expose a decorator-discovered interface. Widgets, REST APIs, app-served MCP endpoints, chat message handlers, named-service providers, and background job handlers are separate surfaces: widgets are UI entrypoints intended for the platform shell, APIs are programmatic operations reachable under the integrations routes, @mcp(...) exposes an MCP-native surface for external MCP clients, and @on_job receives ready work claimed by proc. An app may also define a custom main view and a dedicated message entrypoint.

💡
Think in interface surfaces: widgets, APIs, MCP endpoints, named services, ui_main, on_message, and on_job. The loader scans them from decorators and app registration hooks and exposes them as an app interface manifest.
Widget and custom UI integration diagram
Widgets and Custom UI Widget integration flow showing decorator-discovered widgets, app APIs, and app-served MCP endpoints UI Panel Loads widget GET /widgets/{alias} Integrations API /bundles/{t}/{p}/{b}/ widgets / operations / mcp Your App @ui_widget / @ui_main @api / @mcp / @on_message / @on_job Widget / Main View Rendered in UI panel UI calls /operations UI uses /operations; external MCP clients use /mcp on the same explicit bundle_id path

Buildable React/Vite Widgets

New widget apps should be source folders declared per alias under ui.widgets. The descriptor should pass the loader output destination through the standard placeholder, and the widget build config must consume it as an environment value:

ui:
  widgets:
    task_memo_webapp:
      enabled: true
      src_folder: widgets/task_memo_webapp
      build_command: npm install --no-package-lock && OUTDIR=<VI_BUILD_DEST_ABSOLUTE_PATH> npm run build
    task_webapp:
      enabled: true
      src_folder: widgets/task_webapp
      build_command: npm install --no-package-lock && OUTDIR=<VI_BUILD_DEST_ABSOLUTE_PATH> npm run build

For Vite widgets, configure output from process.env.OUTDIR and use relative assets. Do not pass the destination path as vite build <path>. ui.widgets is the build/source contract only; the runtime widget surface is still declared by a matching @ui_widget(alias="...") method. A configured folder without a decorator is not a visible widget.

export default defineConfig({
  base: './',
  build: {
    outDir: process.env.OUTDIR || 'dist',
    emptyOutDir: true,
  },
})

If runtime logs show vite build /.../.ui.build.tmp... or Vite reports UNRESOLVED_ENTRY for .ui.build.tmp.../index.html, the output directory leaked into Vite as a positional project/root argument. Fix the widget build contract or update to a platform runner that treats <VI_BUILD_DEST_ABSOLUTE_PATH> as an environment value. Do not manually copy built files into bundle storage.

App Interface Decorators

DecoratorAttaches ToPurposeKey args
@bundle_entrypoint(...)classMarks the app entrypoint for loader discoveryname, version, priority, allowed_roles, allowed_roles_config
@bundle_id(...)classDeclares the canonical internal app id from codeid
@ui_widget(...)methodDeclares a UI widget discoverable under the widgets endpointsicon, alias, user_types, user_types_config, roles, roles_config
@api(...)methodDeclares an app API method under /operations/{operation} or /public/{operation}method=POST|GET, alias, route, user_types, user_types_config, roles, roles_config, public_auth, csrf
@mcp(...)methodDeclares an app-served MCP endpoint under /mcp/{alias} or /public/mcp/{alias}alias, route, transport
@ui_mainmethodMarks the app's main UI entrypoint returned in the interface manifestno args
@on_reactive_eventmethodDeclares the canonical chat/agent turn ingress for an accepted external-event batchone per entrypoint
@on_jobmethodMarks the async ready-job handler called by proc after claiming a background job stream itemno args; method receives the job envelope and dispatch kwargs
@cron(...)methodDeclares scheduled app work reconciled by the proc-owned scheduleralias, cron_expression, expr_config, timezone, tz_config, span
@data_bus_handler(...)methodDeclares a handler for durable app-owned Data Bus messagessubject, partition_by, ordering, idempotency, user_types, roles
@public_content(...)methodDeclares a public discoverable-content alias: the platform serves crawlable pages, JSON-LD, and a per-alias sitemap from the app's content registryalias, schema_type

App identity can come from the registry entry or be declared explicitly with @bundle_id("my.bundle@version"). The loader turns discovered interface decorators into the app's HTTP, UI, conversation, and job contract.

For proc-owned API and widget routes, user_types use the threshold anonymous < registered < paid < privileged, while roles names raw external roles such as kdcube:role:super-admin. If both are declared, both must pass. Declared visibility is the source of additional role requirements; a normal operation with no declared visibility remains available to authenticated registered users. Admin-configurable variants live in app props. Public APIs separately require an explicit public_auth mode.

MCP has its own boundary. With surfaces.as_provider.mcp.<alias>.auth.mode: managed, proc applies the Connection Hub delegated-credential guard before dispatch. Otherwise the app's MCP subapp owns request authentication and authorization. @on_job is not URL-addressable: proc invokes the async handler only after claiming admitted background work. SDK mixins should receive the job first through await super().handle_job(**kwargs).

Example: Declare widget, API, MCP, and UI entrypoints

from fastapi import HTTPException, Request
from kdcube_ai_app.apps.chat.sdk.config import get_secret
from kdcube_ai_app.infra.plugin.bundle_loader import bundle_entrypoint, bundle_id, api, mcp, ui_widget, ui_main, on_message

@bundle_entrypoint(name="my-bundle", version="1.0.0", priority=100)
@bundle_id("my.bundle@1.0.0")
class MyBundle(BaseEntrypointWithEconomics):

    @ui_widget(icon={"tailwind": "heroicons-outline:swatch"}, alias="dashboard", user_types=("registered", "privileged"))
    def dashboard_widget(self, **kwargs):
        return ["<div id='root'></div>"]

    @api(alias="refresh_dashboard", method="POST", user_types=("registered", "privileged"), csrf=True)
    def refresh_dashboard(self, **kwargs):
        return {"ok": True}

    @api(alias="telegram_webhook", method="POST", route="public", public_auth="bundle")
    async def telegram_webhook(self, request: Request, **kwargs):
        header_name = self.bundle_prop("telegram.webhook.auth.header_name", "X-Telegram-Bot-Api-Secret-Token")
        expected_token = await get_secret("b:telegram.webhook.auth.shared_token")
        if request.headers.get(header_name) != expected_token:
            raise HTTPException(status_code=401, detail=f"Missing or invalid {header_name}")
        return {"ok": True}

    @mcp(alias="tools", route="operations", transport="streamable-http")
    async def tools_mcp(self, request: Request):
        from mcp.server.fastmcp import FastMCP

        header_name = self.bundle_prop("mcp.inbound.auth.header_name", "X-App-MCP-Token")
        expected_token = await get_secret("b:mcp.inbound.auth.shared_token")
        if request.headers.get(header_name) != expected_token:
            raise HTTPException(status_code=401, detail=f"Missing or invalid {header_name}")

        server = FastMCP("my-bundle")

        @server.tool()
        def ping() -> str:
            return "pong"

        return server

    @ui_main
    def main_view(self, **kwargs):
        return ["<div id='app'></div>"]

    @on_message
    async def run_message(self, **kwargs):
        return await self.run(**kwargs)
i
App-owned public API hook: use the technically named public_auth="bundle" when the app, not proc, must verify the inbound request. Keep the client-facing header name in app config, the verification token in app secrets, and validate the request before doing work.
i
MCP authentication: use auth.mode: managed when Connection Hub should validate a delegated bearer credential and tool grants. Use app-owned mode for custom headers, API keys, signatures, or JWT schemes, and validate them inside the MCP provider before returning the subapp.

Integrations HTTP Surface

EndpointPurposeNotes
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}Return the app interface manifestIncludes ui_widgets, api_endpoints, mcp_endpoints, ui_main, on_message
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/widgetsList visible widgets for the current userReturns alias, icon, user_types, roles
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/widgets/{widget}Render or fetch one widgetResolved via @ui_widget
POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation}Call an app API operationResolved via @api; preferred explicit form
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation}/csrfMint an operation-bound CSRF tokenFor cookie-authenticated POST operations that opt into csrf: true; returns csrf_required and a 10-minute single-use token
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/operations/{operation}GET variant for idempotent operations and static-like API readsResolved via the same interface manifest
POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/{operation}Call a public app API operationResolved only via @api(route="public", ...); public methods declare public_auth; public_auth="bundle" is the compatibility name for app-owned verification
GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/{operation}GET variant for public idempotent app APIsSupports the same route-scoped manifest contract
GET|POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/mcp/{alias}Call an app-served MCP endpointResolved via @mcp(route="operations", ...); managed mode applies delegated-credential checks, while non-managed mode delegates authentication to the app
GET|POST /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/mcp/{alias}Call a public app-served MCP endpointResolved via @mcp(route="public", ...); the route family does not replace the selected managed or app-owned auth policy
POST /api/integrations/bundles/{tenant}/{project}/operations/{operation}Default-app shortcutCompatibility path when bundle_id is omitted
GET /api/integrations/static/{tenant}/{project}/{bundle_id}/{path}Serve app-scoped static assetsUseful for app-shipped frontend assets and media
i
Public app APIs are a deliberate platform surface. Use route="public" only for intentionally public endpoints and declare public_auth: "none", proc-side header_secret, or the compatibility value "bundle" for app-owned verification.
i
Operation CSRF: browser code calling a cookie-authenticated POST surface with csrf: true first fetches .../operations/{operation}/csrf, then sends the returned value as X-KDCube-CSRF-Token. The token is bound to subject, tenant, project, app, operation, and method; it is single-use and fails closed if the shared Redis backend is unavailable.
i
MCP auth model: managed mode lets proc validate Connection Hub delegated credentials and selected tool grants. Non-managed MCP forwards the raw request to the app, which owns any custom header, cookie, signature, or JWT verification.

How to configure an app-owned public API hook

The app defines the non-secret client contract in app props, stores verification material in app secrets, and checks the incoming request itself.

Server-side configuration

# bundles.yaml
bundles:
  version: "1"
  items:
    - id: "partner.tools@1-0"
      config:
        telegram:
          webhook:
            auth:
              header_name: "X-Telegram-Bot-Api-Secret-Token"
# bundles.secrets.yaml
bundles:
  version: "1"
  items:
    - id: "partner.tools@1-0"
      secrets:
        telegram:
          webhook:
            auth:
              shared_token: "replace-in-real-deployment"

What to share with the hook caller

  • the public route: /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/telegram_webhook
  • the header name from app props, for example X-Telegram-Bot-Api-Secret-Token
  • the shared token provisioned in app secrets

App-authenticated public API client call

curl -X POST \
  "http://localhost:5173/api/integrations/bundles/<tenant>/<project>/<bundle_id>/public/telegram_webhook" \
  -H "X-Telegram-Bot-Api-Secret-Token: <shared-token>" \
  -H "Content-Type: application/json" \
  -d '{"update_id":1}'

App-owned MCP authentication example

This example uses app-owned header verification. For delegated automation, configure surfaces.as_provider.mcp.<alias>.auth.mode: managed instead; proc then resolves the bearer handle to its server-side grant record before dispatch.

Server-side configuration

# bundles.yaml
bundles:
  version: "1"
  items:
    - id: "workspace@2026-03-31-13-36"
      config:
        surfaces:
          as_provider:
            mcp:
              preferences_tools:
                auth:
                  mode: bundle
                  header_name: "X-Workspace-Preferences-MCP-Token"

# bundles.secrets.yaml
bundles:
  version: "1"
  items:
    - id: "workspace@2026-03-31-13-36"
      secrets:
        surfaces:
          as_provider:
            mcp:
              preferences_tools:
                auth:
                  shared_token: "<rotate-me>"

App code

@mcp(alias="preferences_tools", route="operations", transport="streamable-http")
async def preferences_tools_mcp(self, request: Request, **kwargs):
    header_name = self.bundle_prop(
        "surfaces.as_provider.mcp.preferences_tools.auth.header_name",
        "X-Workspace-Preferences-MCP-Token",
    )
    expected_token = await get_secret("b:surfaces.as_provider.mcp.preferences_tools.auth.shared_token")
    if request.headers.get(header_name) != expected_token:
        raise HTTPException(status_code=401, detail=f"Missing or invalid {header_name}")
    return build_preferences_mcp_app(...)

What to share with the MCP client

  • the MCP route: /api/integrations/bundles/{tenant}/{project}/{bundle_id}/mcp/{alias}
  • the header name from app props, for example X-Workspace-Preferences-MCP-Token
  • the token provisioned in app secrets

Authenticated MCP client call

curl -X POST \
  "http://localhost:5173/api/integrations/bundles/demo-tenant/demo-project/workspace@2026-03-31-13-36/mcp/preferences_tools" \
  -H "X-Workspace-Preferences-MCP-Token: <shared-token>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"1","method":"tools/list"}'

Public MCP client call

curl -X POST \
  "http://localhost:5173/api/integrations/bundles/demo-tenant/demo-project/my.bundle/public/mcp/public_tools" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"1","method":"tools/list"}'
i
Route semantics: route="operations" and route="public" select only the URL family. Authentication comes from the surface policy: managed delegated credentials, app-owned verification, or an intentionally open provider.

Interface Manifest Discovery

Decorators on the app class are the source of truth for its HTTP, UI, conversation, and job contract. At load time the loader scans the entrypoint class and builds the technically named BundleInterfaceManifest:

# Internal manifest shape (built automatically by the loader)
BundleInterfaceManifest(
    bundle_id="workspace@2026-03-31-13-36",
    ui_widgets=(UIWidgetSpec(alias="preferences", icon={...}, user_types=(...), roles=(...)),),
    api_endpoints=(APIEndpointSpec(alias="preferences_exec_report", http_method="POST", route="operations", user_types=(...), roles=(...)),),
    mcp_endpoints=(MCPEndpointSpec(alias="tools", route="operations", transport="streamable-http"),),
    ui_main=UIMainSpec(method_name="main_ui"),
    on_message=OnMessageSpec(method_name="run"),
)

The manifest is returned by GET /api/integrations/bundles/{tenant}/{project}/{bundle_id} and drives all route resolution. Only decorated methods are remotely callable — there is no same-name fallback for undecorated methods.

Consumed MCP Tools vs App-Served MCP

These are different surfaces and they solve opposite directions of integration.

Use caseHow you declare itWhat it doesWhere it is live
Use an external MCP server inside an agentconfig.surfaces.as_consumer.mcp.services plus a kind: mcp entry under that agent's toolsAdds only the configured server and allowed tools to that agent's inventoryInside agent/tool execution rounds
Expose an MCP-native surface from your app@mcp(...) on an entrypoint methodMounts a FastMCP or MCP-ready ASGI app through proc under /mcp/{alias} or /public/mcp/{alias}At the integrations HTTP layer, callable by external MCP clients

Widget UI Model

Widget methods (@ui_widget) declare app UI surfaces. Source-folder widgets are built and served by KDCube; legacy widgets may return small HTML fragments directly. The widget fetch endpoint resolves the alias, checks user_types and raw roles visibility, and invokes the method. user_types use the same threshold rule here: anonymous < registered < paid < privileged. If the same widget must also be callable through the operations route (for legacy clients), decorate it with both @ui_widget and @api:

@api(alias="task-board", route="operations", user_types=("registered",))
@ui_widget(alias="task-board", icon={"tailwind": "heroicons-outline:check-badge"}, user_types=("registered",))
def task_board(self, **kwargs):
    return ["<div id='root'></div>"]

Widget and main UI browser code should follow the same runtime config bridge as the working platform widgets. First try the platform config endpoint GET /api/cp-frontend-config; if it returns usable config, do not wait for the parent frame. If the endpoint is unavailable or the host app owns the config, fall back to postMessage: send CONFIG_REQUEST to the parent frame and accept both CONN_RESPONSE and CONFIG_RESPONSE. Build app operation URLs from baseUrl, defaultTenant, defaultProject, and defaultAppBundleId. Do not infer app identity from a source-folder name or an internal static URL. Widget load should stay read-only by default; use an explicit in-widget action when the widget needs a syncing operation. For platform widgets, the preferred POST /operations/{alias} body shape is { "data": { ... } }; proc also accepts a raw JSON object body and treats it as data for webhook-style integrations. Clients should unwrap the returned {alias} field from the integrations response envelope.

When embedded in an iframe outside the platform shell, app UIs should also report their own size to the parent after mount and after layout changes: window.parent.postMessage({ type: "kdcube-resize", height: document.documentElement.scrollHeight, width: document.documentElement.scrollWidth }, "*"). The parent must still validate the message origin before applying the resize.

Use the SDK reference page for the exact widget/runtime config pattern and example: bundle-widget-integration-README.md.

Static File Serving for Custom UIs

Apps that ship a custom frontend (typically a Vite/React SPA in ui/main/ for the main view or ui/widgets/<alias>/ for widgets) have their built assets served from a dedicated static route:

GET /api/integrations/static/{tenant}/{project}/{bundle_id}/{path}

The endpoint serves files from the app's stable filesystem-backed bundle_storage_root()/ui/ subtree. Missing paths fall back to index.html for client-side routing. A <base> tag is injected into index.html so relative assets resolve correctly.

i
UI builds are shared supervised operations, not request-owned background tasks. An HTTP request may start a build, but a strong process-local task registry shields the build from client disconnect. Source and node_modules live in worker-local temporary storage; shared app storage holds only build outputs, signatures, temporary output, and cross-worker locks. npm/Vite runs in a dedicated process group that is terminated and reaped on timeout or explicit administrative cancellation.

One worker heartbeats the shared lock while building, publishes artifacts atomically, records the signature, and releases the lock before potentially expensive local cleanup. If that worker crashes, the heartbeat stops and lock TTL lets another worker retry. A request-time build fallback is supported when no current artifact exists. Bundle Admin's effective-props read is deliberately non-lifecycle: inspecting props never loads, evicts, cancels, or restarts an app or UI build and never calls on_bundle_load().

Application-Hosted Websites

An app can declare config.ui.main_view.site to expose its complete built main-view tree as a website. Site declarations are validated into a versioned ApplicationSiteCatalog; Redis distributes atomic generations, while each proc performs request-time host or alias lookup from an immutable in-memory snapshot. Direct aliases use /sites/{alias}. A CDN can preserve the viewer hostname and rewrite clean paths to /api/integrations/site-root/{path}, but it does not own the catalog.

This is different from @public_content: a site serves an app's files, directory indexes, and SPA fallback; public content serves indexed records, metadata, JSON-LD, and sitemaps.

Public Discoverable Content (SEO Pages & Sitemap)

Widgets and custom UIs are client-rendered — a crawler that fetches them sees an SPA shell. When an app publishes content that should be discoverable (articles, docs, catalog entries), it declares a public content alias with @public_content(alias=..., schema_type=...) and enables it in config. The app publishes, updates, and retracts items through the SDK registry (kdcube_ai_app.apps.chat.sdk.pub); the platform then serves the full discoverability layer with no further app code — crawlable HTML pages with real title/meta/body (verifiable with curl, no JS), rel=canonical + Open Graph/Twitter metadata, JSON-LD (declared @type plus BreadcrumbList), a per-alias sitemap.xml with accurate lastmod, and 410 Gone after retraction:

GET /api/integrations/bundles/{tenant}/{project}/{bundle_id}/public/__content__
      → machine-readable sitemap descriptor list (host federation)
GET .../public/__content__/{alias}/sitemap.xml
      → the per-alias sitemap (published items only)
GET .../public/__content__/{alias}/{slug...}
      → crawlable item page (200) · 410 retracted · 404 unknown

Exposure is explicit: the alias must be enabled in the app's public_content.<alias> config block, and each item carries a published/retracted state — there are no per-user audience selectors on this surface. canonical_base in the same block decouples the canonical URL from the serving route: the operator maps a clean prefix at the CDN/proxy (a rewrite, never a redirect — a URL answering 3xx cannot be a canonical), and page canonicals, JSON-LD, and sitemap entries all use it. The widget URL stays a widget shell; the crawlable page is a separate platform-rendered artifact.

i
Content-provider apps should declare singleton: true: every crawler request resolves the app instance for the declaration/config gate (no app operation code runs on the serving path), so a non-singleton app would construct a fresh entrypoint per crawled page.

Reference docs: public-content-provider-README.md (the app-surface contract), public-content-solution-README.md (the cdn-pub solution: registry tiers, serving, split-origin CDN deployment), and the step-by-step publish-discoverable-content-README.md recipe.

Main View Configuration

To define a custom main view, declare ui.main_view in app configuration and mark a method with @ui_main. Code can supply defaults; the app descriptor can override them.

def configuration_defaults(self):
    return {
        "ui": {
            "main_view": {
                "src_folder": "ui/main",
                "build_command": "npm install && OUTDIR=<VI_BUILD_DEST_ABSOLUTE_PATH> npm run build",
            }
        }
    }

The built SPA communicates with the backend through the app operations endpoint and receives runtime config from GET /api/cp-frontend-config with parent-frame postMessage as fallback. The UI is a normal platform client: if it needs app-originated events targeting one exact connected peer, it must propagate the connected peer id on REST requests per the client communication contract. Embedded pages should emit the same kdcube-resize height/width message used by widgets.

Memory Widget and Reconciliation

Apps that inherit BaseEntrypointWithMemory or BaseEntrypointWithEconomicsAndMemory get the SDK memory widget operations: list/create/update/delete, snapshots, snapshot export/restore preview, and reconciliation jobs. Enable the widget through app props under memory.enabled and memory.widget.enabled, and configure the built widget source under ui.widgets.memories.

Manual reconciliation is submitted through memories_widget_reconcile_run. The request may include agent_type with one of lite, regular, or strong. At execution time that selector maps the logical memory.reconciler role to memory.reconciler.lite, memory.reconciler.regular, or memory.reconciler.strong for that job only. Configure those role models in the app's role_models props.

config:
  role_models:
    memory.reconciler.lite:
      provider: anthropic
      model: claude-haiku-4-5-20251001
    memory.reconciler.regular:
      provider: anthropic
      model: claude-sonnet-4-6
    memory.reconciler.strong:
      provider: anthropic
      model: claude-opus-4-1

If the app needs extra per-job controls, send them in reconciliation_context and override on_memory_reconciliation_request(request=...) to validate or augment them before the job is stored. The SDK persists this context with the job and rebinds it under bundle_call_context.memory.reconciliation.context when the background reconciler runs.

async def on_memory_reconciliation_request(self, *, request: dict) -> dict | None:
    context = dict(request.get("reconciliation_context") or {})
    context.setdefault("policy", "strict")
    return {
        "agent_type": request.get("agent_type") or "regular",
        "reconciliation_context": context,
    }

Registry vs. Interface Discovery

GET /api/admin/integrations/bundles returns configured app registry entries: id, repo, ref, module, path, version, and related deployment metadata. The richer interface discovery surface lives on the per-app integrations endpoint above, where the runtime scans decorators and returns the current manifest.

Common App Operations

OperationDescription
ai_bundlesApp admin dashboard (technical operation name retained) — available through the platform base entrypoint
control_planeControl-plane dashboard — via the platform base entrypoint
economic_usage / opexEconomics and operational usage views — via BaseEntrypointWithEconomics
memories and memories_widget_*Memory UI plus memory operations — via BaseEntrypointWithMemory
suggestionsSuggested prompts for new conversations
inventory-reorder-apiExample structured API for an inventory widget: list low-stock items, create reorder requests, schedule replenishment scans, trigger immediate execution

Deploying Your App

Option A: With the KDCube Platform

  1. 1

    Push your app to Git

    git push origin v1.0.0
  2. 2

    Add to bundles.yaml

    - id: "my-bundle@1-0"
      repo: "git@github.com:org/my-bundle.git"
      ref: "v1.0.0"
      module: "my_bundle.entrypoint"
  3. 3

    Initialize secrets and apply

    kdcube init --tenant <tenant> --project <project> \
      --descriptors-location /path/to/descriptors \
      --set-secret services.openai.api_key "sk-..." \
      --set-secret services.anthropic.api_key "sk-ant-..." \
      --set-secret services.brave.api_key "..." \
      --set-secret services.git.http_token "github_pat_..." \
      --set-secret git.http_token "github_pat_..."
    
    kdcube info --tenant <tenant> --project <project>

    The CLI writes these values into the staged active config/secrets.yaml, resolves descriptor placeholders such as tenant/project/domain where the descriptor set supports it, and then the runtime reads that staged copy.

  4. 4

    Set the default app in the Admin Dashboard

    Open the app dashboard at the technically named ai_bundles operation. Your registered app appears in the list. Set default_bundle_id for the tenant/project. The change is applied immediately; no restart is needed.

Option B: Standalone (Without Platform)

App logic can also run outside the platform. The SDK is a Python package, so you can import selected building blocks from your own FastAPI service or custom image. KDCube adds the integrated hosting, identity, streaming, storage, economics, and UI runtime around that logic; adopting those capabilities remains incremental.

App Source Git Authentication

Modebundles.yamlSecret
SSH keygit@github.com:org/repo.gitSSH key mounted in container
HTTPS tokenhttps://github.com/org/repo.gitservices.git.http_token / git.http_token in secrets.yaml

Fast Local App Development

KDCube supports a descriptor-driven local app loop optimized for fast changes. Initialize once from descriptors, edit the app under the mounted app source root, then reload only that app without reinstalling the platform. The CLI retains bundle in these command names for compatibility.

# one-time initialization
kdcube init --tenant <tenant> --project <project> \
  --descriptors-location /path/to/descriptors \
  --set-secret services.openai.api_key "sk-..." \
  --set-secret services.anthropic.api_key "sk-ant-..."

kdcube start --tenant <tenant> --project <project>

# then during development
# edit files under assembly.paths.host_bundles_path
kdcube bundle reload my.bundle@1.0.0 --tenant <tenant> --project <project>

# rebuild platform images later (descriptors preserved)
kdcube refresh --tenant <tenant> --project <project> --build
kdcube refresh --tenant <tenant> --project <project> --path /path/to/kdcube-ai-app --build
kdcube refresh --tenant <tenant> --project <project> --release <ref> --build

# reapply seed bundle descriptors only
kdcube bundle config apply --tenant <tenant> --project <project> \
  --descriptors-location /path/to/descriptors --dry-run
kdcube bundle config apply --tenant <tenant> --project <project> \
  --descriptors-location /path/to/descriptors --reload

The important path rule is that the host app lives under paths.host_bundles_path in assembly.yaml, while bundles.yaml points to its container-visible path:

# assembly.yaml
paths:
  host_bundles_path: "/Users/you/dev/bundles"

# bundles.yaml
bundles:
  items:
    - id: "my.bundle@1.0.0"
      path: "/bundles/my.bundle"
      module: "entrypoint"

Use this flow for local code changes, descriptor-backed app config edits, and quick widget/API iteration. init stages the active descriptor set under the concrete runtime workdir. bundle reload replays that staged descriptor, clears the app cache, and makes the next request load updated code. bundle config apply stages only bundles.yaml and optional bundles.secrets.yaml; it does not rebuild images or restart Docker. Before replacing live descriptors with older seed files, use kdcube export --out-dir ... to write reviewable snapshots.

Expose a CLI-Started Local Runtime with Ngrok

For Telegram webhooks, Telegram Mini Apps, Cognito callbacks, or any other external callback into a local KDCube runtime, run KDCube with the CLI first and expose the CLI web proxy port with ngrok. Do not expose proc separately.

kdcube start --tenant <tenant> --project <project>

# use the port printed by kdcube start, commonly 5173 for local descriptors
ngrok http --host-header=rewrite 5173

The CLI-started Docker Compose stack already includes the KDCube web proxy, which routes /api/integrations/* to proc, /api/* and /sse/* to ingress, and browser routes to the frontend. After ngrok gives you https://<ngrok-domain>, use that single origin in CORS/Cognito callback settings and app integration URLs such as Telegram webhooks. If assembly.yaml changes, restart with kdcube refresh --tenant T --project P; use --path, --latest, --upstream, or --release <ref> only when platform source should change. If app config or secrets change, use kdcube bundle reload <bundle_id> or reapply seed descriptors with kdcube bundle config apply --descriptors-location ... --reload.

Reference recipe: ngrok-README.md.

App-Scoped @venv(...) Helpers

Apps can mark dependency-heavy helper functions with @venv(...). The platform creates a cached per-app subprocess venv, overlays the runtime packages already present in proc, then installs the app's requirements.txt on top. The venv is rebuilt only when the requirements file changes.

from kdcube_ai_app.infra.plugin.bundle_loader import api, venv

@venv(requirements="requirements.txt", timeout_seconds=120)
def parse_external_document(payload: dict) -> dict:
    ...

class MyBundle(BaseEntrypoint):
    @api(alias="ingest_document", route="operations")
    async def ingest_document(self, **kwargs):
        result = parse_external_document(kwargs)
        await self.comm.event(
            type="bundle.ingest.completed",
            step="ingest_document",
            status="completed",
            title="Document processed",
            data={"pages": result.get("pages", 0)},
        )
        return result

Boundary rule: @venv(...) is for plain serialized inputs and outputs only. Keep communicator use, request context, DB pools, Redis clients, and other live proc-bound runtime objects outside the venv helper. If only requirements.txt changes, the next helper call rebuilds the cached venv lazily. If Python source changes, use the normal app reload path.

App-Scheduled @cron(...) Jobs

Apps can declare proc-owned scheduled jobs directly in app code with @cron(...). This binds periodic app logic to the platform lifecycle without requiring a separate external scheduler for every routine.

from kdcube_ai_app.infra.plugin.bundle_loader import cron

class MyBundle(BaseEntrypoint):
    @cron(
        alias="rebuild_index",
        expr_config="routines.rebuild.cron",
        cron_expression="0 */6 * * *",
        span="instance",
    )
    async def rebuild_index(self):
        ...

The shipped contract is: @cron(alias=..., cron_expression=..., expr_config=..., span=...). Use cron_expression for an inline schedule or expr_config for a dot-separated config path such as apps.app1.routines.cron. If both are provided, expr_config wins. If the resolved config value is missing, blank, or disable, the job is inert and nothing is scheduled. span controls exclusivity across process, instance, or system.

A practical example is a scheduled inventory routine that materializes the next reorder queue and publishes an operational event for the UI:

class InventoryBundle(BaseEntrypoint):
    @cron(alias="refresh-reorder-queue", cron_expression="0 6 * * *", span="system")
    async def refresh_reorder_queue(self):
        items = await self.rebuild_reorder_queue()
        await self.comm.event(
            type="bundle.inventory.reorder_queue_refreshed",
            step="refresh_reorder_queue",
            status="completed",
            title="Reorder queue refreshed",
            data={"items": len(items)},
        )

Background Job Stream and @on_job

For work that should not run inside a scheduler tick or widget request, enqueue a background job and handle it with @on_job. The producer creates the durable app-owned record first, then writes a ready-work envelope to Redis Streams. Proc claims jobs fairly across workers, builds the app runtime context, and invokes the app's async @on_job handler.

Background job lifecycle from cron or API producer to app @on_job handler
Background job lifecycle A producer creates a domain record, enqueues a Redis Stream job, proc claims it, and the app @on_job handler executes it. Producer @cron / API create Domain Record execution / status enqueue Redis Stream ready job envelope claim Proc fair worker invoke @on_job async
from kdcube_ai_app.infra.plugin.bundle_loader import cron, on_job

class TaskBundle(BaseEntrypoint):
    @cron(alias="due-scan", cron_expression="*/5 * * * *", span="system")
    async def due_scan(self):
        await self.tasks.enqueue_due_jobs()

    @on_job
    async def on_job(self, job: dict, **kwargs):
        del kwargs
        if job.get("work_kind") == "task.execution.due":
            return await self.tasks.run_execution(job["payload"]["execution_id"])
        return {"ok": False, "error": {"code": "unsupported_job"}}

@on_job is not a public route and not a widget operation. It should validate work_kind, load durable ids from payload, and update app-owned execution or result state. Until proc acknowledges the stream message, retry is possible.

Reference Apps

Technical app IDUse it forKey surfaces
workspace@2026-03-31-13-36Ready ReAct workspace and full app compositionChat, scene, canvas, tools, skills, MCP consumers, Telegram, main view, widgets, files, and economics
ported-langgraph-agents@2026-07-13Bring an existing LangGraph/LangChain agentTwo agents behind one app, per-turn graph rebuild, conversation streaming, capabilities, isolated code execution, and shared-schema persistence
kdcube-services@1-0Service-oriented app packageNamed-service providers, MCP facade, Data Bus relay, public file hosting routes, OpenAPI, storage ownership, and contract tests
connection-hub@1-0Identity and delegated connectionsConnected accounts, OAuth/OIDC, delegated automation, consent, widgets, APIs, grants, and user-secret resolution
website@2026-07-12Application-hosted websiteMain-view site, alias/host declaration, multipage files, SPA fallback, injected site context, and CDN origin routing
workspace — widgets, Telegram, operations, and custom main-view pattern

The workspace sample shows how one app can combine a ReAct conversation workflow with decorated widget endpoints, APIs, Telegram webhook/Mini App surfaces, attachment handling, and a custom SPA main view. It is the best current public reference for how entrypoint methods, per-agent consumer configuration, ui/main/, and ui/widgets/ fit together.

View entrypoint.py on GitHub →

Documentation Reference

Current Capability Index

This table summarizes implemented app surfaces. The owning repository docs remain authoritative for exact contracts and configuration.

FeatureStatusNotes
App-declared widgets Available Apps mark their UI widgets explicitly with @ui_widget. The platform exposes that widget manifest via GET /api/integrations/bundles/{tenant}/{project}/{bundle_id} and /widgets, so frontends can render app-defined launchers from discovered metadata instead of hardcoded platform lists.
Custom main view Available Apps can provide a custom main UI through ui.main_view config and a discovered @ui_main entrypoint. A main view can be a full SPA using REST, SSE streaming, widgets, downloads, and any richer client behavior the app needs.
Static asset serving from an app Available App-scoped static files can be served directly via GET /api/integrations/static/{tenant}/{project}/{bundle_id}/{path}. GET operations are also available for idempotent app APIs under /operations/{operation}.
Public app APIs Available App APIs can be exposed under /public/{operation} through @api(..., route="public"). Public methods declare public_auth, including intentionally open access, proc-side header-secret verification, and app-owned verification.
App-served MCP endpoints Available Apps can expose MCP-native HTTP endpoints through @mcp(...). Proc routes them under /mcp/{alias} or /public/mcp/{alias}. Managed mode applies delegated-credential checks; otherwise the app owns MCP authentication and authorization.
App surface access Available Where proc owns the route, app listing and per-method visibility use declared allowed_roles, user_types, and raw roles. user_types use threshold semantics. Public APIs additionally declare public_auth. MCP uses either managed delegated credentials or app-owned authentication, not proc-side user_types.
Admin app interface discovery Available /api/admin/integrations/bundles returns registry metadata enriched with scanned app manifest data such as widgets, apis, on_message, on_job, and scheduled jobs. Admins can see what an app exposes without opening its code.
Fast local app development Available Point assembly.yaml at paths.host_bundles_path, define the app in bundles.yaml with a container path under /bundles, initialize once, then iterate with the compatibility command kdcube bundle reload <bundle_id>. The live local descriptor authority is the staged runtime copy under workdir/config/. See how-to-configure-and-run-bundle-README.md.
Live app reload Available App code is loaded per process and may be cached as a singleton, but local development does not require restarting proc. kdcube bundle reload <bundle_id> replays the staged descriptor and clears the app cache for the next request. Admin updates persist into the live descriptor or secret authority before reload applies them.
on_props_changed(...) lifecycle hook Available BaseEntrypoint exposes on_props_changed(...) for reconciling long-lived side effects after effective app props change. Already-loaded singleton apps in the current worker receive it on live bundles.props.update events.
on_apply_props(props) configuration hook Available BaseEntrypoint calls this public async hook after platform-reserved props on every turn, REST, widget, MCP, and local-operation apply path. Return True only when the model service must be rebuilt; do not override the internal _apply_bundle_props_overrides.
App versioning Available Update the ref (branch, tag, or commit) through the configured descriptor write path: file-backed bundles.yaml, App Admin, or CLI apply. Redis and provider-generated runtime YAML projections are derived views, not alternate write authorities. See bundle-runtime-configuration-and-secrets-README.md.
Deployment-scoped app props and secrets Available Apps read deployment-scoped non-secret config with self.bundle_prop(...) and app secrets with await get_secret("b:..."). Non-secret props belong in the configured descriptor authority; secrets belong in the configured secrets provider. In local file mode those are bundles.yaml and bundles.secrets.yaml. See bundle-runtime-configuration-and-secrets-README.md.
User Settings Available Async typed stores over user_bundle_props hold durable user choices; user secrets remain in the configured user-scoped secrets provider. Agent model, instruction profile, tool, skill, MCP, named-service, and subagent selections are saved per conversation under conversation:<conversation_id>:agent_selection:<agent_id>. An optional baseline seeds a new conversation once, and picker drafts persist only on Save changes.
Connection Hub and per-agent delegation Available Connected provider accounts (Delegated to KDCube) remain separate from hosted-agent, external-client, and automation grants (Delegated by KDCube). Hosted agents use kdcube-agent:<app>:<agent>; concrete operations re-check provider capability and caller/account binding and raise scoped consent demands at attempt time.
Governed generated-code execution Available The reference Docker split profile runs generated code in a separate networkless, read-only-root executor with narrow workspace mounts. Tool calls cross to a trusted supervisor carrying identity and credentials. Local subprocess is development containment, combined Docker is a legacy shared-container profile, and Fargate uses a different one-task physical boundary.
Economics and accounted service use Available Deployment and live Eco Admin policy cover plans, quotas, budgets, reservations, prices, trials, wallet credit, and participating model, embedding, web-search, and custom service calls. Coverage applies only to integrated accountable calls, not arbitrary unintegrated spending.
User-reconcilable durable memory Available Apps can expose durable user memories with evidence, snapshots, create/update/delete operations, export/restore preview, and explicit background reconciliation. Conversation history, durable user memory (mem), and the conversation named-service realm (cnv) remain distinct.
Runtime feature gating and configurable visibility Available Apps can gate the whole app through the technically named enabled.bundle prop and configure API/widget visibility with allowed_roles_config, user_types_config, and roles_config. Do not use removed resource-level enabled_config decorator arguments. See bundle-platform-integration-README.md.
App-scoped @venv helpers Available Apps can run selected dependency-heavy helper functions in a cached per-app subprocess venv. The venv inherits platform runtime packages, installs the app's requirements.txt, and rebuilds only when the requirements hash changes. It is for leaf work, not live proc-bound runtime objects.
App-local Node / TS backend sidecar Available Apps can keep the public KDCube surface in Python and run selected backend logic as an app-local Node or TypeScript sidecar. This wraps existing Node backends without discarding them. Startup-config drift triggers lazy restart on next use.
App-scheduled @cron jobs Available Apps declare scheduled jobs with @cron(alias=..., cron_expression=..., expr_config=..., span=...). The scheduler resolves configured or inline schedules and supports process, instance, and system exclusivity spans.
App background @on_job handlers Available Apps can declare one async @on_job handler for ready work claimed by proc from the Redis background job stream. @cron or an operation can enqueue; @on_job validates work_kind, reads durable ids, and updates app-owned result state.
Git-backed React workspace Available ReAct supports a sparse git-backed workspace with lineage-isolated history, explicit materialization through react.pull(...), and workspace construction through react.checkout(mode="replace"|"overlay", ...). Editable state lives under turn_<id>/git/projects/..., deliverables under turn_<id>/files/..., and snapshots under turn_<id>/git/snapshots/.... Conversation-owned refs use qualified conv:fi: form.
App event sources and artifact rehosters Available External/provider events declare owner discovery, pull, and phase-bound block-production policies; they are not model-callable tools or event transport. Tool results can use related block-production contracts. Registered namespace rehosters let a framework adapter materialize authorized refs such as ext:... into qualified conv:fi: refs.
Per-agent ReAct runtime configuration Available Apps configure each ReAct agent under config.react.<agent>, including instructions, ID-based instruction profiles, supported models, role models, and iteration bounds. Tool, skill, MCP, named-service, and subagent inventory lives under surfaces.as_consumer.agents.<id>; users can narrow that administrator ceiling per conversation but cannot widen it.
Claude Code agent integration Available Apps can integrate the Claude Code agent as an accountable LLM runtime, including per-turn model, token, cache, and spend reporting plus optional git-backed session continuity.
Interactive steer / followup execution Available Busy-turn followup and steer enter the shared conversation event lane. The live ReAct runtime folds them into the owned turn; authored app events use the same lane identity with explicit agent_id, event_source_id, and routing.reactive.
Streaming for operations Available App operations can stream to connected clients. REST calls can target the initiating peer with KDC-Stream-ID, and app code can emit deltas, steps, and widget updates through the communicator over SSE or Socket.IO while the operation is running.
Tool traits Available strategy traits govern ordered multi-action compatibility; execution traits govern completed-call scheduling and replay. The supported early profile is fully validated, neutral, detached, parallel_with_generation, and at_most_once_per_round.
ReAct subagents Available When administrator config and user selection both allow it, react.delegate launches a chartered helper as its own fair-scheduled child conversation. Denial prevents spawner installation, the tool, and delegation guidance. Contributions and terminal events return to the parent timeline; this is distinct from fenced runtime execution.
Application-hosted websites Available An app can expose its built main view as a complete site. A validated ApplicationSiteCatalog is distributed through Redis generations and served from a proc-local immutable snapshot with alias, host-first/default routing, multipage files, SPA fallback, and CDN origin support.
💬
Have feedback or want to prioritize something? Open an issue on the GitHub repo.