Guide: SDK Patterns

Status: Active Source: SDK-OPERATIONS.md L2 advisory patterns, EXPLORATION-SDK-ACCESS-MODEL.md


What This Guide Covers

Patterns for building applications on the entity system SDK. These are advisory — the SDK operations spec (SDK-OPERATIONS.md) defines the normative interface. This guide describes how to use those operations effectively.


1. Scoped Handles

A scoped handle binds a peer, a tree prefix, and a capability context. All operations through the handle are relative to the prefix.

scope = peer.scope("app/browser/")

scope.get("settings/theme")
; → resolves to /{peer_id}/app/browser/settings/theme

scope.put("workspace/layout", layout_entity)
; → resolves to /{peer_id}/app/browser/workspace/layout

scope.watch("workspace/*", on_change)
; → watches /{peer_id}/app/browser/workspace/*

scope.execute("system/query", "find", {path_filter: "workspace/"})
; → query scoped to /{peer_id}/app/browser/workspace/

Benefits:

Path canonicalization. Scoped handles accept peer-relative paths. The SDK canonicalizes internally: relative_path/{peer_id}/{prefix}/{relative_path} per the absolute path model (V7 §1.4). The application works with short paths; the protocol sees absolute paths. See SDK-OPERATIONS.md §2.5.

The SDK SHOULD provide scoped handles. The SDK MUST also provide unscoped operations with absolute or peer-relative paths.


2. Entity-Backed Application State

Application state stored as typed entities in the tree. This is the universal pattern across entity-native applications — UI state, settings, selection, layout are all entities at known paths.

Convention Paths

app/{app-id}/workspace/     UI state (windows, layout, selection)
app/{app-id}/settings/      Application configuration

The {app-id} scoping lets multiple applications coexist on one peer without colliding.

Convention Types

ConceptType name
Generic settingapp/state/setting
Selection stateapp/state/selection
Window configurationapp/state/window
The live window setapp/state/window-index

The app/state/ prefix is language-neutral. Not rust_workspace/settings, not go_workspace/state.

GUIDE-ENTITY-WORKBENCH-APP.md §4.2 is the authority for this set; the rows above are the common ones, not the whole table. An earlier app/state/layout row is retired: window arrangement — splits, ratios, sizing, stacking — is renderer decoration and stays per-implementation. What is portable is window membership, which app/state/window-index carries (§4.2a there).

Usage

; Write a setting
peer.put("app/browser/settings/theme", "app/state/setting", {key: "theme", value: "dark"})

; Read it back
setting = peer.get("app/browser/settings/theme")

; Within a scoped handle:
scope = peer.scope("app/browser/")
scope.put("settings/theme", "app/state/setting", {key: "theme", value: "dark"})

Why Entity-Backed State

This is preferred over in-memory or file-based state because the entity system's infrastructure (sync, history, query, subscription) works on it for free.


3. Type Rendering

Status: advisory app-side pattern. Type rendering is intrinsically per-presentation-layer (CLI, GUI, web — all have different idioms). The three-level chain and ValueKind enum below are recommended patterns for app-side renderer libraries to converge on; they are not an SDK contract. SDKs MAY ship a renderer-registry helper, but apps SHOULD NOT depend on one being present. This section reads as conventions for cross-app interop on rendered output, not as required SDK surface area.

Applications displaying entities to users SHOULD implement the three-level degradation chain:

  1. Type-specific renderer. A registered render function for the entity's type. Best experience.
  2. Structural renderer. Reads the type definition at system/type/{type_path} and renders field-by-field using field metadata. Meaningful but generic.
  3. CBOR diagnostic. Renders raw CBOR data. Always available as fallback.

ValueKind

Renderer-neutral semantic tagging for formatted values:

ValueKind := "null" | "bool" | "string" | "number" | "bytes"
           | "hash" | "path" | "key" | "index" | "error" | "unknown"

The SDK SHOULD provide:


4. The Reactive Cycle

Individual SDK operations compose into a cycle that drives entity-native applications:

put(path, entity)
  → emit pathway fires (SYSTEM-COMPOSITION.md §1)
    → subscription engine matches patterns
      → notification delivered to subscriber's inbox (local or remote)
        → inbox triggers continuation advance
          → continuation dispatches next step (e.g., fetch + merge)
            → resulting puts trigger further emit...
              → watch/event stream delivers to application UI
                → application re-renders

This is the core reactive pattern: write → emit → notify → process → write. The SDK operations (put, watch, execute) are the application's entry and exit points. The emit pathway, subscription engine, inbox, and continuation machinery run between them.

For local-only use (no cross-peer), the cycle simplifies to: put → emit → watch fires → application re-renders.

Change Detection Mechanisms

The SDK provides three mechanisms suited to different application architectures:

MechanismDeliveryBest for
subscribe_events()All events, unfilteredEvent-driven architectures (callbacks, event loops)
watch(prefix, callback)Filtered by prefixFine-grained reactive UI (specific component watches specific paths)
generation()Poll in render loopImmediate-mode UI (egui, game loops — check if anything changed since last frame)

All three ultimately observe the emit pathway's Phase 2 async notifications (SYSTEM-COMPOSITION.md §1.3).


5. Connection Lifecycle Patterns

Manual

Store transport address, connect explicitly, execute:

peer.tree_put("system/peer/transport/{remote_id}",
    entity("system/network/transport-address", {peer_id: remote_id, address: "192.168.1.10:9100"}))
peer.connect("192.168.1.10:9100")
peer.execute("entity://{remote_id}/system/tree", "get", ...)

Works for development, static networks, CLI tools.

Automated (Network Extension)

The network extension's maintain-peer builds continuation pipelines:

peer.execute("system/network", "maintain-peer", {peer_id: remote_id, address: "192.168.1.10:9100"})
; → connects, builds continuation graph:
;   - subscription on system/peer/status/{remote_id} for disconnect detection
;   - on disconnect: reconnect with exponential backoff
;   - on reconnect: restore subscriptions that were active before disconnect

Entity-native: connection state is in the tree (system/peer/status/*), continuations are entities (system/inbox/network/*), subscriptions trigger the lifecycle. Inspectable, debuggable, composable.

See SDK-EXTENSION-OPERATIONS.md §9 for the full network extension surface.


6. Multi-Peer Applications

The SDK supports multiple independent peers in one process. Common patterns:

Application peer + device peers. The application creates one internal peer (in-process, Level 0+1 access). Device peers run as separate processes or services — the application connects to them via WebSocket/TCP (Level 2 access).

Multiple internal peers. The application creates several in-process peers for isolation — each has its own identity, storage, and grants. Useful for testing, sandboxing, or separating concerns.

The EntitySDK container (or equivalent) manages multiple PeerContexts. Each PeerContext wraps one local peer. Backend/network peers are registered as metadata — reached through a local peer's Level 2 routing. The container provides peer(id) to access a specific context and default_peer() for the primary one.


7. Entity-Native Handler Pattern

When handler logic needs to be transferable, inspectable, or survive restart without re-registration, write it as a compute expression entity and register a handler manifest pointing to it. This is the entity-native model from SDK-OPERATIONS §11.3.

Shape:

1. Build the expression bottom-up (literals → operators → control flow → root).
   For each sub-expression: encode → store in tree → record content hash.
   See GUIDE-COMPUTE.md §8.1 and GUIDE-COMPUTE-PROGRAMMING.md.

2. Write the root expression entity to a tree path you control (e.g., app/handlers/{name}/body).

3. Register the handler manifest via the system/handler:register protocol operation:
   {
     pattern: "app/{name}",
     expression_path: "app/handlers/{name}/body",
     interface: { operations: [...] },
   }

4. The manifest is a regular tree entity — no SDK primitive is needed because
   the body is already in the tree. Dispatch evaluates the expression at the
   declared path (V7 §6.6).

When to use:

When not to use:

Comparison (SDK-OPERATIONS §11.3 has the full table): entity-native is transferable, inspectable, hot-swappable, restart-surviving, statically auditable; SDK language-native is faster but ephemeral and opaque.


8. Reactive Computation Pattern

When derived state needs to stay current as inputs change, install a reactive compute subgraph. The engine watches the expression's dependencies, re-evaluates on change, and writes the result to a result path. This is the spreadsheet model (GUIDE-COMPUTE §5).

Shape:

1. Build the root expression (typically a compute/lookup/tree on input + transform).
   Store the expression entity in the tree.

2. Install the subgraph:
   sdk.compute.install(
     root: expression_hash,
     result_path: "app/views/{name}",
     triggers: { auto },                // engine walks audit_subgraph for triggers
     grant: ctx.installation_grant,     // attenuated cap covering deps + result_path
   ) → SubgraphHandle

3. Reads from app/views/{name} return the current value. As inputs change,
   the engine re-evaluates and updates the result entity at result_path.

4. To tear down: sdk.compute.uninstall(subgraph_handle.path)

When to use:

Key properties:

Composes with §2 entity-backed state (the result path is an entity-backed state slot) and §4 reactive cycle (the result-path update emits, which can be watched, which can trigger further reactive computations or UI re-renders).

See GUIDE-COMPUTE §5 for the install/trigger/result model, GUIDE-COMPUTE §7.2 for the freeze error codes, and GUIDE-COMPUTE-PROGRAMMING.md for debugging frozen subgraphs.


9. Capability Grant Pattern

Grant configuration is where the kernel-vs-handler principle (SDK-OPERATIONS §2.7.2) meets practice. The rule: grant handler create operations, not raw tree:put to handler-managed namespaces. Handler operations validate embedded capabilities at creation time; tree:put bypasses this validation.

Per-extension shape:

; Antipattern — broad tree:put on handler-managed namespace
{handlers: {include: ["system/tree"]},
 operations: {include: ["put"]},
 resources: {include: ["system/continuation/suspended/*"]}}

; Coherent — route through the handler's create op
{handlers: {include: ["system/continuation"]},
 operations: {include: ["install"]},
 resources: {include: ["*"]}}

The per-extension checklist (full list in GUIDE-CAPABILITIES §5):

ExtensionGrant thisNot this
Continuationsystem/continuation:installsystem/tree:put on system/continuation/suspended/*
Subscriptionsystem/subscription:subscribesystem/tree:put on system/subscription/*
Rolesystem/role:assignsystem/tree:put on system/role/{ctx}/assignment/*
Computesystem/compute:installsystem/tree:put on system/compute/processes/*

Grant progression (SDK-OPERATIONS §11.2A; full progression in GUIDE-CAPABILITIES §7):

Common pitfalls (GUIDE-CAPABILITIES §8 has the full list): broad tree:put on a system namespace; embedding a delegated cap as your own dispatch capability; mixing role definition (template writes) with role assignment (token derivation); forgetting chain reachability when serializing requests.


These patterns are advisory. The SDK operations spec (SDK-OPERATIONS.md) defines what the operations are. This guide describes how to use them well.