KDCube
← Engineering
KDCube Engineering · Deep Dive

Your App Needs Another App: Call It, Queue It, or Wake Its Agent?

App A needs something from app B. It can call an API, publish a durable message, enqueue a job, submit an event to app B's conversation, invoke an MCP tool, or mount app B's widget. Every path moves data. They disagree on what completion means, what survives a worker failure, and where authority is checked.

2026-08-12Engineering17 minExperience
app interoperability named services Data Bus MCP cross-app integration app-to-app communication choosing an integration surface

The first version of cross-app integration often begins with a URL. App A needs a report, so it posts to app B. Later the report takes two minutes, the request dies with one worker, a browser socket becomes an accidental queue, or app A starts copying a user's bearer because the target is “internal.”

The problem is not connectivity. The problem is that “call another app” hides several different contracts.

KDCube makes those contracts separate. An immediate result uses a local app operation. A provider-neutral object vocabulary uses a named service. Durable domain work uses Data Bus. Ready background work uses the job stream. An agent turn enters the conversation lane. MCP and REST remain real protocol surfaces. A browser experience composes app-owned widgets.

THE RULE

Choose the semantic contract first; choose the shortest supported transport for that contract second.

This article starts where KDCube Application Integrations leaves off. That article maps what an app provides and consumes. This one asks how app A should actually use a capability owned by app B.

Current code and descriptors still use bundle in literal names such as bundle_id, bundles.yaml, @bundle_entrypoint, and /api/integrations/bundles/.... In builder-facing prose, that deployable unit is an app.

00Implementation highlights

ConceptThe takeaway
Completion semanticsDecide whether app A needs admission, a completed result, durable ownership, or an ordered agent turn.
Local operationA bounded same-KDCube request can call app B's declared @api operation without an HTTP round trip.
Named serviceApp A can use app B's owner-defined object vocabulary without hard-coding its route or storage.
Durable workData Bus, background jobs, and conversation ingress survive beyond the initiating request, but they complete in different ways.
MCP and RESTProtocol compatibility remains a real network contract, even when both apps run in one KDCube.
AuthorityIdentity continuity supplies facts; app B still decides whether this caller may perform this operation.
Cross-KDCube boundaryAnother deployment authenticates its own accepted proof and constructs a new local request context.

01One phrase hides nine different contracts

Suppose app A needs app B to rename an issue, generate a report, find an object, notify an agent, and show an editor. Calling all five “API requests” erases the behavior that matters.

App A's actual needThe contract that fits
Return one bounded result nowlocal app operation
Stream a file or byte response nowlocal streaming operation
Search and act through app B's stable domain vocabularynamed service
Apply a durable mutation after app A exitsData Bus
Give app B ready background workjob stream
Start or continue app B's ordered agent workconversation ingress
Preserve MCP discovery and tools/callMCP
Preserve HTTP behavior or cross a deployment boundaryREST
Put app B's interface inside a scenewidget composition

The decisive question is not “How can I reach app B?” It is “What must be true when app A considers this operation complete?”

Choose the cross-app contract before the transport A warm-lit reader-owned app asks what must be true when its work is done. Five branches select immediate results, durable ownership, an ordered agent turn, protocol compatibility, or browser composition, mapping to local operations and named services, Data Bus and jobs, conversation ingress, MCP and REST, or widgets. CONTRACT SELECTOR YOUR APP · A needs app B one concrete work item ASK FIRST What must be true when app A considers the work complete? return a result now bounded request local operation or named service one alias, or an owner-defined domain vocabulary survive the caller target owns durable work Data Bus or job stream durable domain handling, or ready background work wake app B's agent ordered conversational work conversation ingress admission now; a later worker owns the agent turn keep a protocol visible discovery, schemas, HTTP MCP or REST real transport because compatibility is part of the contract compose the browser app B owns the interface scene / widget mount the UI; route its backend action through the matching contract SEMANTICS FIRST · SHORTEST SUPPORTED TRANSPORT SECOND
Cross-app integration begins with the meaning of completion, not with a URL.

Choosing the wrong path can still produce a working demo. The mismatch appears under failure:

MismatchWhat eventually breaks
A browser relay carries backend worka disconnect becomes lost work
An HTTP request waits for durable processingworker lifetime and timeout become part of the domain contract
Data Bus answers a cheap immediate querythe caller inherits at-least-once delivery and correlation it did not need
App A calls app B's agent loop directlyordered conversation admission and durable turn state are bypassed
An internal URL is treated as permissionreachability is mistaken for authority
Every internal function becomes MCPprotocol overhead appears where no MCP consumer contract exists

02Immediate result: call the declared operation

The smallest honest cross-app integration is one declared operation and one request-bound call.

App B exposes a bounded operation:

task_tracker/entrypoint.pyPYTHON
from typing import Anyfrom kdcube_ai_app.infra.plugin.bundle_loader import api class TaskTrackerEntrypoint:    @api(method="POST", alias="issue_get", route="operations")    async def issue_get(        self, *, issue_id: str, **request_fields: Any    ) -> dict[str, Any]:        return await self.issues.get_visible_issue(            issue_id=issue_id,            request_fields=request_fields,        )

App A calls the alias through the local operation bridge:

app_a/services/task_tracker.pyPYTHON
from kdcube_ai_app.apps.chat.sdk.infra.bundle_operations import (    call_bundle_operation,) result = await call_bundle_operation(    bundle_id="task-tracker@1-0",    operation="issue_get",    route="operations",    http_method="POST",    data={"issue_id": "BUG-123"},)

This is not an import of app B's implementation. The runtime resolves app B's current deployed version and effective descriptor, checks that the app and operation are active for the bound caller, creates app B's request context, and then invokes the operation locally.

The distinction matters. App A does not pass a browser cookie, provider token, role list, or invented user_id as proof. The platform-controlled bridge projects the current caller facts into app B's context. App B still owns domain authorization, including whether that person may read BUG-123.

The same bridge has a stream variant for files and bytes:

app_a/services/reporting.pyPYTHON
from kdcube_ai_app.apps.chat.sdk.infra.bundle_operations import (    call_bundle_operation_stream,) result = await call_bundle_operation_stream(    bundle_id="reporting@1-0",    operation="report_download",    data={"report_id": "report-2026-08"},) async for chunk in result.chunks:    await consume(chunk)

The result carries the media type, filename, headers, and status alongside the chunks. A large report stays a stream instead of becoming base64 inside JSON.

The local operation caller exists inside a KDCube-bound request or task. A free-standing process has no bound caller. A call to an app in another KDCube also cannot use this bridge; it enters the target through an authenticated network surface.

03Stable domain language: ask a named service

A direct operation is right when app A knows the operation it wants. A named service is right when app B owns a domain that several apps, agents, and widgets should explore through one stable vocabulary.

For a task realm, that vocabulary may include:

provider.aboutIdentify the provider and the realm it owns.
provider.capabilitiesDiscover the operations and object kinds currently available.
object.searchFind objects through the provider's own search semantics.
object.schemaInspect the schema before constructing an action.
object.getResolve one provider-owned object reference.
object.actionAsk the provider to apply a declared domain action.

App B owns the namespace, object refs, schemas, search behavior, actions, connected-account requirements, and authorization. App A declares which namespace operations its agent, UI resolver, or service may use. Discovery finds the current owner.

Request-bound app code can ask for the namespace rather than pinning app B's route:

app_a/services/tasks.pyPYTHON
from kdcube_ai_app.apps.chat.sdk.solutions.named_services_providers import (    NamedServiceEndpoint,    call_named_service_endpoint,) response = await call_named_service_endpoint(    NamedServiceEndpoint(namespace="task"),    {        "operation": "object.search",        "namespace": "task",        "query": "blocked authentication issues",        "limit": 20,    },)

Discovery selects a configured same-KDCube bridge:

bundle_registryLoad app B and call its named_services() registry.
bundle_operationCall app B's @api(alias="named_service") facade.
moduleCall an explicit provider module in the same runtime.

The path can change without changing the namespace contract. That is the point: app A depends on task semantics, while app B remains the owner that parses refs and authorizes operations.

Generated code uses the same capability through a trusted relay. The restricted executor sends a named-service request to the supervisor-side path; provider credentials and storage handles stay in trusted services.

Cross-app paths inside one KDCube A warm-lit reader-owned app A can call app B through a local operation, named-service discovery, durable Data Bus job or conversation lanes, or real MCP and REST through private OpenResty. Every path converges on app B's endpoint policy and domain authorization. ONE KDCUBE · FOUR HONEST PATHS EFFECTIVE TENANT / PROJECT RUNTIME YOUR APP · A current caller bound request or task local operation call_bundle_operation → app B @api named-service discovery bundle_registry · bundle_operation · module isolated caller → trusted Data Bus relay durable lanes Data Bus · job stream · conversation ingress target worker or target agent turn real MCP / REST transport private OpenResty → app B @mcp / @api protocol compatibility stays visible APP B target checks context endpoint policy domain authority LOCAL WHERE SEMANTICS ALLOW · PROTOCOL TRANSPORT WHERE SEMANTICS REQUIRE
One deployment, several honest contracts: local where semantics allow it, protocol transport where semantics require it.

04Work that must survive: choose the durable lane

Some work should outlive app A's current request. KDCube has three durable paths because they represent three different owners.

Data Bus: app-owned domain work

Data Bus carries an app-scoped JSON command or event to app B's @data_bus_handler. The stream survives process loss. Workers claim and reclaim messages. serial_per_partition can prevent concurrent handlers for one object while allowing parallel work across objects.

app_a/services/rename_issue.pyPYTHON
from kdcube_ai_app.apps.chat.sdk.runtime.comm_ctx import (    data_bus_publish_and_wait,) result = await data_bus_publish_and_wait(    bundle_id="task-tracker@1-0",    subject="task.issue.rename",    object_ref="task:issue:BUG-123",    message_id=f"task_issue_rename_{request_id}",    idempotency_key=f"task_issue_rename_{request_id}",    reply=True,    payload={"issue_id": "BUG-123", "title": "Fix login"},    timeout_ms=20_000,)

publish_and_wait(...) waits for a correlated handler result. It does not turn the stream into exactly-once RPC. Delivery is at least once, so app B stores idempotency and revision decisions with its domain state.

Job stream: ready background work

Use the background job stream when the work is already ready and app B's one @on_job dispatcher owns it. The queue addresses app B by bundle_id; proc claims the job fairly and rebuilds its execution context.

This is a good fit for a render, import, export, or scheduled maintenance unit whose product identity is “work for app B,” rather than a domain event shared with several handlers.

Conversation ingress: ordered agent work

Use conversation ingress when app B's agent should receive the event as part of an ordered conversation. Submission reserves the event in the conversation lane and returns admission. A later worker runs the turn under app B's agent contract and durable checkpointer.

The caller does not wait for the agent's final answer as if it called a local function. The accepted event and the completed turn are separate moments.

Three durable lanes with different owners A warm-lit reader-owned app A sends durable work through one of three lanes. Data Bus leads to a data bus handler and durable domain handling. The job stream leads to on job and target-owned background work. Conversation ingress admits an event batch and later runs an ordered target agent turn. DURABLE DOES NOT MEAN ONE QUEUE YOUR APP · A hands work over current request may end DATA BUS publish → durable stream → app B @data_bus_handler complete = durable domain handling or correlated result · at least once JOB STREAM enqueue for app B → fair claim → app B @on_job complete = target-owned render, import, export, or maintenance result CONVERSATION INGRESS submit → accepted event batch → later app B agent turn complete now = lane admission · final answer belongs to the later turn LIVE COMMUNICATOR BESIDE THE LANES · DURABLE OWNER INSIDE EACH LANE RETRIES AND IDEMPOTENCY FOLLOW THE SELECTED OWNER
Durable does not mean one queue: domain work, ready jobs, and agent turns have different owners.

The communicator sits beside these paths. It streams progress, results, and UI events to a connected browser. It is a live delivery channel, not the durable record that makes the work survive a disconnect.

05MCP and REST stay protocol surfaces

MCP is the right contract when app A or an external agent needs MCP discovery, tool schemas, and tools/call. REST is the right contract when HTTP status, headers, webhook shape, or an external integration boundary matters.

Those semantics do not disappear when both apps run in one KDCube.

app A MCP consumer
  -> descriptor-configured streamable HTTP endpoint
  -> private OpenResty address inside the deployment
  -> authenticate request and construct app B context
  -> app B @mcp surface
  -> current grant and domain checks

There is currently no app-facing call_bundle_mcp(...) shortcut parallel to call_bundle_operation(...). The MCP consumer speaks an MCP transport. If app A only needs one bounded internal result, the local operation bridge expresses that smaller contract directly.

Managed MCP also separates two values that often look interchangeable:

FieldMeaning
urlthe transport destination the MCP consumer dials
resourcethe protected-resource identity stored on the delegated grant

The target guard compares the observed request path with the configured resource pattern. A private transport URL therefore needs a resource pattern that matches the canonical target path. Changing the host without preserving that identity produces a resource mismatch, not silent access.

REST follows the same authority principle. The deployment can make app B's HTTP surface privately reachable from app A, but network reachability grants no product permission. OpenResty and the app router authenticate the request; app B applies the current endpoint and domain policy.

This is also why an “internal” call should not copy a browser cookie or a provider credential into app A's payload. Local bridges carry bound context. Protocol paths present a credential accepted by the target.

06Widgets compose interfaces, not backend authority

An app may own part of another app's browser experience. A scene can mount app B's widget while app A owns the surrounding layout.

scene host
  -> resolve configured app B + widget alias
  -> mount app B surface
  -> runtime config and authentication handshake
  -> widget calls app B's authenticated backend

The widget remains app B's interface. A scene command or object ref identifies what the browser wants to show or do; app B's backend resolves and authorizes the object.

Live communicator events can notify the widget that durable work progressed. The underlying API, Data Bus, named service, or conversation lane still owns the backend action.

07Identity crosses; authority is decided again

Same-KDCube local paths preserve the represented caller without pretending that app A's decision automatically authorizes app B.

Identity crosses a declared path and authority remains at the target A local platform-controlled lane starts with the warm-lit reader-owned app A and projects the bound caller into a new app B context. A protocol lane starts with a target-accepted credential and target ingress authentication. Both converge on app B endpoint, resource, operation, account, and domain checks before provider code runs. AUTHORITY LIVES AT THE TARGET LOCAL PLATFORM-CONTROLLED PATH YOUR APP · A bound caller facts tenant · actor · user · roles TARGET CONTEXT rebuild for app B represented caller + app B identity MCP / REST PATH PRESENTED PROOF accepted by target session · OAuth · app auth TARGET INGRESS authenticate proof construct a new target-local context APP B · FINAL SAY current authorization endpoint policy resource + operation account requirements domain ownership THEN PROVIDER CODE THE BOUNDARY Identity can cross a declared path. The target still decides whether the operation may run. AN IDENTIFIER LOCATES · A TARGET CHECK AUTHORIZES
Identity can cross a declared path. Authority is still enforced by the target.

Durable paths store actor and authority metadata in their envelopes so a later worker can rebuild the relevant context. A provider id, endpoint URL, object_ref, or stream address locates something. None is proof that the caller may use it.

08Another KDCube is another authority boundary

Two KDCube deployments do not share their local app registry, Data Bus, job stream, conversation lane, process context, or Connection Hub grant store.

app A in KDCube A
  -> HTTPS MCP or REST + credential accepted by KDCube B
  -> KDCube B ingress
  -> B authenticates the proof
  -> B constructs B-local request context
  -> app B enforces B-owned grants and domain rules

A shared identity provider can let both deployments recognize the same person in a browser. That browser identity does not authorize a server-side app call. KDCube B still decides what proof its protected app surface accepts.

The implemented baseline supports a remote call when app A already has a credential accepted by B. B also provides managed MCP/REST authorization, OAuth with PKCE, current grant checks, refresh, and revocation for compatible external MCP consumers.

The source-side adapter that would let a hosted agent in A automatically discover B's challenge, open B's consent, store a per-agent B credential, receive the passive result event, and retry is still design work. It is tracked in kdcube/kdcube#223. The current article does not present that demand-driven cross-KDCube flow as implemented.

09The builder's decision flow

Ask four questions in order. The first yes picks the lane; everything else belongs to the smallest contract there is.

The builder's decision flow Four questions asked in order. An ordered conversation event goes to conversation ingress. Work app B must own after app A exits goes to Data Bus or the job stream. An owner-defined domain contract used by several consumers goes to a named service. A required MCP or HTTP protocol goes to MCP or REST through the target ingress. Everything else uses the request-bound local operation bridge. CONTRACT FIRST · THEN THE SHORTEST TRANSPORT QUESTION 1 Does app B's agent need this as an ordered conversation event? YES CONVERSATION INGRESS admission now · the answer belongs to a later ordered turn NO QUESTION 2 Must app B own the work after app A's request exits? YES DATA BUS · JOB STREAM durable domain event · job stream for ready background work NO QUESTION 3 Is this an owner-defined domain contract used by several consumers? YES NAMED SERVICE one stable vocabulary, many consumers · the owner parses refs and authorizes NO QUESTION 4 Does the consumer require MCP or HTTP protocol compatibility? YES MCP · REST protocol contract through the configured target ingress · authenticated at entry NO EVERYTHING ELSE · LOCAL OPERATION BRIDGE Call app B's declared @api operation, request-bound, one bounded result now. the smallest honest contract · bound caller facts cross · app B still authorizes FIVE ANSWERS DECIDE: SUCCESS MEANING · RETRIES · VOCABULARY · PROTOCOL · TARGET AUTHORITY
The first yes picks the lane. The default is the local operation bridge — the smallest honest contract.

Before calling app B, answer five questions:

  1. What does success mean to app A: admitted, completed, persisted, or delivered to an agent?
  2. Who owns retries and idempotency?
  3. Does the domain need a stable provider-neutral vocabulary?
  4. Does an external protocol need to remain visible?
  5. Which target context and current authority will app B enforce?

Once those answers are explicit, the code becomes smaller. The runtime does not need one universal inter-app transport because the product does not have one universal meaning of “call.”

10The short version

A platform with many apps needs more than connectivity between them. It needs each crossing to say what kind of work is happening.

Call a declared operation for an immediate result. Ask a named service for an owner-defined realm. Put durable domain work on Data Bus. Give ready work to the job stream. Enter the conversation lane when an agent should act. Keep MCP and REST as protocols when compatibility is the requirement. Compose widgets for browser experiences.

Then let the target app authorize the represented caller again.

THE DECISION

Contract first. Transport second.

11Related articles and documentation

Articles

Documentation

KDCube Engineering
2026-08-12 · Experience E3