io.Connect offers various APIs for apps to communicate that are covered in Key io.Connect Integration Concepts Explained. This article goes deeper into each one with code samples, flow diagrams, and notes on when to use it and when not to.
1. Pub/Sub
Apps publish messages on a named topic. All currently-subscribed apps receive it. No persistence - a late joiner misses everything.
Use when:
-
Migrating apps already built on pub/sub
-
Fire-and-forget events, no state needed
Avoid when:
-
Building new integrations - use Shared Contexts
-
Late joiners need the last-known value
// Provider
io.bus.publish("mkt.update", { ticker: "AAPL" });
// Consumer
await io.bus.subscribe("mkt.update", data => updateUI(data));
App A (publisher)
│ publish("mkt.update", {ticker: "AAPL"})
▼
Gateway (fan-out broker — no state)
├──▶ App B (subscriber) → {ticker: "AAPL"} delivered ✓
└──╳ App C (late joiner) → no history — missed ✗
2. Shared Contexts
A named global object stored in the Gateway. Any app can read, write, or subscribe. Late joiners receive the current state immediately.
API surface: set() · get() · update() · subscribe() · all() · setPath() · setPaths() · destroy()
Use when:
-
Syncing a selected entity across apps (developer-driven)
-
Late-joining apps must get current state immediately
-
Multiple apps read and write the same shared data
Avoid when:
-
You need user-driven grouping → use Color Channels
-
You need Workspace-level isolation → use Workspace Contexts
// Provider
await io.contexts.update("selectedClient", { id: "C-001" });
// Consumer
await io.contexts.subscribe("selectedClient", ctx => load(ctx));
// set() replaces fully; update() merges; setPath() targets one key
App A (writer)
│ update("selectedClient", {id: "C-001"})
▼
Gateway (stores state, persists)
├──▶ App B (subscribed) → notify → {id: "C-001"} ✓
└──▶ App C (late joiner) → subscribe → {id: "C-001"} immediately ✓
3. Color Channels
Shared Contexts plus a Channel Selector UI. Users assign apps to a named colour; all apps on the same colour sync. Supports directional mode: users can restrict a window to publish-only or subscribe-only.
API surface: all() · join() · leave() · publish() · subscribe() · get()
Use when:
-
The user decides which apps are linked at runtime
-
Multiple independent sync groups coexist on screen
-
FDC3 User Channels compatibility is needed
Avoid when:
-
Linking is fixed in code → use Shared Contexts instead
-
Full global visibility across all apps is required
Note: Color Channels are syntactic sugar around Shared Contexts - each channel maps to a dedicated context object.
// Provider
await io.channels.join("Red");
await io.channels.publish({ ticker: "AAPL" });
// Consumer
io.channels.subscribe(data => updateUI(data));
App A (joins Red)
│ join("Red") → publish({ticker: "AAPL"})
▼
Gateway (Red channel context)
├──▶ App B (joins Red) → {ticker: "AAPL"} to Red members ✓
└──╳ App C (on Blue) → wrong channel = no delivery ✗
4. Workspace Contexts
Lets apps inside a Workspace share state with each other. The context is scoped to that Workspace, travels with it, and can be persisted with the layout.
API surface: workspace.setContext() · workspace.getContext() · workspace.updateContext() · workspace.onContextUpdated()
Use when:
-
Running parallel workflows (e.g. two client portfolios in separate Workspaces)
-
Context should save and restore with the Workspace Layout
-
Hard isolation between Workspaces is required
Avoid when:
-
Apps outside the Workspace need the data → use Shared Contexts
-
Global visibility across all apps is required
Note: Workspace Contexts are syntactic sugar around Shared Contexts - each Workspace gets its own scoped context.
// Provider
await workspace.setContext({ clientId: "C-001" });
// Consumer
workspace.onContextUpdated(ctx => load(ctx.clientId));
App A (WS1)
│ setContext({clientId: "C-001"}) - WS1
▼
Gateway (scoped routing)
├──▶ App B (WS1) → notify WS1 members ✓
└──╳ App C (WS2) → WS2 isolated = no delivery ✗
(WS2 has its own independent context, e.g. {clientId: "C-042"})
5. Interop Methods
Explicit, service-like capability. The caller knows exactly what it needs. An app registers a named function and others can invoke it and get a result.
API surface: register() · unregister() · invoke() · methods() · servers()
Use when:
-
You need a well-defined service contract with a return value
-
The target app is already running
-
Precise functions: pricing, calculations, trade execution
Avoid when:
-
Target may not be running → use Intents instead
-
You need to broadcast state - not a service call pattern
// Provider (Risk App)
await io.interop.register("calcRisk", args => ({ risk: calc(args.id) }));
// Consumer (Blotter)
const res = await io.interop.invoke("calcRisk", { id: "C-001" });
Risk App (server)
│ register("calcRisk")
│ Gateway (method registry)
│ ▲
Blotter (client) │
│ invoke("calcRisk", {id: "C-001"}) ──▶ routed to server
│ ◀── return {risk: 0.73} response delivered
6. Intents
Raise an action, not a target. The caller says “handle this” —> the platform finds matching handlers and can launch the app if not running.
API surface: register() · raise() · all()
Use when:
-
App-to-app handoffs, launch/activate workflows, user choice
-
The caller doesn’t need to know the exact destination app
-
Handler may need to be launched first
Best mental model: Phone “Share / Open with…” UI for desktop workflows.
Note: When an intent is ambiguous (multiple handlers match), the Intents Resolver UI is displayed, letting the user pick.
// Provider
await io.intents.register("ViewChart", ctx => showChart(ctx.data));
// Consumer
await io.intents.raise({ intent: "ViewChart", context: { type: "Instrument", data: { ticker: "AAPL" } } });
Chart App (not running)
▲ launch + deliver {RIC: "AAPL"}
│
Gateway (intent registry + launcher)
▲
│
Blotter (raiser)
│ raise("ViewChart", {RIC: "AAPL"})
7. Streams
Continuous, real-time data push. Best for live alerts, telemetry, and any producer->many-subscribers flow. The server controls who can subscribe via branches.
API surface: createStream() · stream.push() · stream.branches() · subscribe() · subscription.close() · stream.close()
Use when:
-
Data changes continuously and polling would be wasteful
-
Many consumers need the same live data
-
Per-subscriber filtering is needed
Avoid when:
-
Data updates only occasionally → use Shared Contexts
-
You need a one-off response → use Interop Methods
Branches: Named sub-channels within a stream (e.g. per region). The provider pushes to a branch and only subscribers on that branch receive the data.
// Provider
const stream = await io.interop.createStream("prices");
stream.push({ ticker: "AAPL", bid: 182.50, ask: 182.55 });
// Consumer
const sub = await io.interop.subscribe("prices", { onData: d => update(d) });
Price Svc (provider)
│ createStream("prices")
│ push({ticker, bid}) ∞
▼
Gateway (stream registry)
├──▶ Blotter → fan-out continuously
└──▶ Chart App → fan-out continuously
8. Window Context
Per-window state and launch context. Scoped to one window. Best for restoring or initialising a specific window. Can be saved as part of a Global Layout.
API surface: window.setContext() · window.getContext() · window.updateContext() · window.onContextUpdated()
Use when:
-
Passing initial launch payload to a window
-
Context should persist with a saved Global Layout
-
FDC3 Private Channels scenario
Avoid when:
-
Dynamic inter-app messaging is needed after launch
-
Multiple apps need access to the same data
// Provider (Blotter - opening a chart)
await io.windows.open("chartApp", url, { context: { ticker: "AAPL" } });
await win.updateContext({ ticker: "MSFT" });
// Consumer (Chart App - reading its own context)
const ctx = await io.windows.my().getContext();
io.windows.my().onContextUpdated(ctx => reload(ctx));
Blotter (opener)
│ open("chartApp", {ticker: "AAPL"})
│ updateContext({ticker: "MSFT"})
▼
Gateway (window launcher)
└──▶ Chart App (launched)
getContext() → {ticker: "AAPL"}
onContextUpdated → {ticker: "MSFT"}