Writing to the Graph and to Devices#

This page describes every path by which a consumer causes data to change — what gets written, where it lands, and what else moves as a consequence. The middleware distinguishes two directions: northbound writes into the knowledge graph (the served datamodel), and southbound writes out to devices through connectors. Understanding which path your code takes determines whether a change persists, whether a device receives a command, and whether unrelated resources get notified.

Terms this page assumes

All Graph Writes Go Through the OGM#

Every knowledge-graph write must go through kapps_ogm.OGM. Use OGM.create for new typed individuals. Use OGM.commit for adding, removing, or replacing properties. No write issues raw SPARQL UPDATE. No write calls kapps_triplestore_interface mutation methods directly. Reads may use the access module (ogm.db) directly; only writes are constrained.

Writes that bypass the OGM skip validation and break atomicity guarantees. A replacement of a cardinality-constrained property requires the removal and insertion to apply in one transaction. The intermediate state — zero possessors during a handover, for example — fails validation without atomicity.

Persistence Writes Name the Changed Field#

A write into persistence carries a second piece of information beside the value: which region of the model actually changed. PersistedConnector.consume and _notify_synced_connectors take it as changed: ConnectionInfo, and the fan-out notifies only the connectors that region covers.

# Correct: names the specific field that changed
consume(value, changed=ConnectionInfo(model_id, contained_model_id, field_id))

# Incorrect: None means "the whole model changed"
consume(value, changed=None)  # notifies everything

Matching is by prefix, not equality. An unspecified level in a ConnectionInfo means every value at that level. A whole-model write reaches every connector. A field-level write reaches only that field’s connectors.

Silent failure: If a write does not name its field, an unscoped notification fans out to every synced connector. Devices get written that nobody touched. For a settable parameter, an unnecessary notification is a fabricated command — a write leg re-deriving at the wrong moment publishes the last value the device reported onto the topic the device treats as a setpoint. The device then reads its own actual state back as a fresh command.

Northbound vs Southbound#

Northbound writes go into the knowledge graph — the served datamodel that peers read over REST. These are OGM.commit operations on Parameter nodes. The Projection removes connection metadata before materializing the northbound view, so peers cannot learn broker addresses or bypass the middleware.

Southbound writes go out to devices through connectors. These happen automatically when a persistence write names a field that has a synced connector. The binding serializes the value and publishes it to the device’s topic or endpoint.

A consumer action determines which direction fires:

  • Calling OGM.commit on a Parameter → northbound only

  • Calling consume on a synced connector → southbound only

  • A PUT to the REST API → both (writes graph, then notifies connectors)

Inbound Messages Replace Whole Parameter Nodes#

An inbound message from a device replaces the entire Parameter node. The binding reassembles static facets — unit, access mode — from cached metadata so a bare scalar does not blank the parts that did not change. Without this reassembly, an inbound scalar would wipe hasUnit and every other facet, because update_persistence_with_value does setattr(contained_model, field_id, value) — it replaces the whole list.

The formatter reassembles the blanknode from cached facets plus the live value. That is a pure function per message. It reads no current state and it needs no framework change.

IRIs and Display#

Any prettified or shortened form of an IRI is display only. Production code carries fully back-resolvable IRIs in their mangled form. The generated API documentation renders a readable form where it can, but nothing parses that form back.

Silent failure: A consumer that parses a displayed IRI, or round-trips one back into a query, gets a wrong answer with no exception raised. Use the mangled form (IRI.lined) for all production operations — REST path segments, datamodel field names, svc:endpoint triples. The mangled form is mechanically reversible to the original IRI. Pretty-printing is a rendering concern with no correctness weight.