Skip to content

Getting Started

This page is the shortest complete path through harmos: declare a dependency, define one change, boot a runtime, commit, read the position that commit reached, and shut down without losing anything. At the end you have a running journal-as-truth application and the four lines you will keep writing.

Every Rust block below is embedded from examples/minischematic/, so what you read is what compiles. mise run run:minischematic runs the whole example.

An application depends on harmos and names nothing beneath it: every kit in the workspace is a feature of this crate that opens a module of the same name. Harmos is not published to a registry, so that dependency is declared by path or by git revision:

[dependencies]
# By path, for a checkout beside your project:
harmos = { path = "../harmos/crates/harmos" }
# Or by git, against the remote you cloned from. Pin `rev` rather than tracking
# a branch: harmos is pre-1.0 and its public surface still moves.
harmos = { git = "<the harmos remote>", rev = "<a commit you tested against>" }

Inside this workspace the same dependency is a workspace entry, which is what the schematic example's application crate declares:

# Compiled for the host and nothing else, which is what lets the adapter, the
# JSON codec and the guest host sit in one unconditional list. The api crate is
# here because `services/` is where an autorouter's request is held to the
# transaction it is decoded as.
[dependencies]
minischematic-api = { path = "../api" }
harmos = { workspace = true, features = ["guest"] }
jsonl-adapter = { path = "../../jsonl-adapter" }
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tracing.workspace = true

harmos owns the runtime, while serde and thiserror support the state and its refusals. tracing instruments the entry verbs and nothing else — installing a subscriber over them is a shell's decision, so tracing-subscriber is not here. The adapter that reads a recording back is an ordinary dependency too, because this crate is compiled for the host and only the host.

Four crates, and the names say which is which. examples/minischematic/app is the package called minischematic-app: what the editor is and how a runtime runs it — the state, the catalogs, the identity and the entry verbs. examples/minischematic/api is minischematic-api: the editor's contract with its own plugins, and nothing else. examples/minischematic/cli is minischematic-cli, the shell that chooses a document and prints a transcript, and examples/minischematic/plugin is minischematic-plugin, the Wasm guest. The direction is one way and never bends: api below app below the shell, api below the plugin, and nothing at all depending on the plugin.

The api crate is small on purpose:

# The editor's serializable contract with its own plugins, and nothing else.
[dependencies]
serde.workspace = true

Harmos with every default feature off, and serde. That is the whole list, because a plugin compiles this crate and only this crate — so anything that opened a file or booted a runtime would be a guest build that cannot be made. It is a separate crate rather than a module for exactly that reason: a crate boundary is the only kind a compiler enforces, mise run check:wasm builds it for wasm32-unknown-unknown on every run, and neither it nor the app crate carries a single cfg(target_arch) gate.

Every optional capability is a cargo feature, and a default build compiles none of them. Enable what you use and nothing else.

CrateFeatureWhat it adds
harmos(default)the journal, streams, work, evidence — no optional dependency
harmosartifactharmos::artifact, the declaration contract both carriers name (Load Artifacts)
harmossidecarharmos::sidecar, native-process authoring and supervision (Isolate a Sidecar)
harmosguestharmos::guest, the Wasm host loader beside the authoring kit (Load Guests and Sidecars)
harmoscomponentizethe component encoder in harmos::guest, without the host runtime a packaging tool never runs
harmossignalharmos::signal, measurement kernels over plain &[f64]
harmospluginharmos::plugin, packaging and installation for both carriers (Ship the Runtime)
harmossimthe seeded simulation harness under harmos::sim (Simulate)

There is no storage row in that table, and no second dependency beneath harmos. Harmos publishes one order and one fold contract — Storage, Source, Series, Vault, and the encode boundary beside them — and ships no engine that implements them. An application that persists writes the adapter it wants, against ports the runtime already declares, and the runtime never learns that JSON, or a directory, or a file exists.

You do not start from nothing. examples/jsonl-adapter is a complete, durable, tested implementation of those ports, published so you can read one end to end and copy what you need: a JSONL journal copy, a crash-recovery sidecar, a snapshot producer, a JSON Lines stream capture, and an evidence vault. It is a worked example rather than a supported dependency — vendor the files you want, own them, and change the on-disk layout freely (reference).

Harmos spawns one writer task, so a Tokio runtime has to exist. It enables the Tokio features it needs itself; what your own crate adds is the entrypoint:

tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread", "time"] }

The examples use the ordinary #[tokio::main] entrypoint. That keeps the learning surface focused on harmos and gives hosted work the multi-thread runtime it may need without an application-specific executor choice.

A transaction owns a permanent wire id, a precondition, and an infallible application after that precondition succeeds. The runtime records it only after apply completes.

#[harmos::transaction(id = "symbol.place", version = 1, irreversible)]
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct PlaceSymbol {
/// The name the placement takes, for as long as it exists.
pub symbol: SymbolId,
/// What the drawing calls it.
pub reference: String,
/// Where it goes.
pub at: Point,
/// How far it is turned.
pub rotation: Rotation,
}
impl Transaction<Schematic> for PlaceSymbol {
type Error = SchematicError;
fn check(&self, schematic: &Schematic) -> Result<(), Self::Error> {
schematic.require_symbol_free(self.symbol)
}
fn apply(&self, schematic: &mut Schematic) {
schematic.symbols.insert(
self.symbol,
Symbol {
reference: self.reference.clone(),
position: self.at,
rotation: self.rotation,
},
);
}
}

Application names the state, the closed transaction, record, and stream catalogs carried by this runtime, and the resources its jobs borrow. The types remain application types.

/// The editor's identity card: this state, changed by this language, narrated
/// by these facts, and publishing these streams.
#[derive(Clone, Copy, Debug)]
pub struct SchematicEditor;
impl harmos::Application for SchematicEditor {
type State = Schematic;
type Transaction = Transactions;
type Record = Records;
type Streams = StreamCatalog;
type Resources = ();
}

The builder receives the origin state first and the declared resources second. It freezes the catalogs, restores any attached history, opens the writer, and returns only when the runtime is ready. The schematic names that declaration once, as app::builder, and it is deliberately environment-free: it reads no variable and opens no file, so every way the editor is stood up — live, memory-only, or simulated — starts from the same three registrations.

/// Declares the canonical editor: its catalogs, and nothing else.
///
/// Infallible and environment-free. It reads no variable, opens no file, and
/// chooses no storage, which is what lets [`launch`] and [`simulate`] — and a
/// memory-only teaching boot — all start from the same declaration.
pub fn builder(origin: impl Into<Origin>, resources: ()) -> Builder<SchematicEditor> {
harmos::Runtime::builder(origin, resources)
.register(Transactions::catalog())
.register(Records::catalog())
.register(StreamCatalog::catalog())
}

commit accepts the leaf transaction type. The generated conversion places it in the application catalog, and the returned receipt names its position in the one history.

let moved = handle
.commit(MoveSymbol {
symbol: R1,
to: Point::new(10, 45),
})
.await
.expect("R1 is placed");
println!(" [{}] moved R1 to (10, 45)", moved.position);

A read takes a short closure and returns an owned At<R>. The value is paired with the journal position the closure observed, so a caller can compare it directly with the commit receipt.

/// One line of positioned state, as every read hands it back.
async fn read(runtime: &Runtime) -> String {
let read = runtime
.journal
.read(|schematic| {
let symbols = schematic.symbols.len();
let nets = schematic.nets.len();
let position = schematic.symbol(R1).position;
let name = schematic.net(N1).name.to_string();
format!("{symbols} symbols, {nets} net · R1 at {position} · net 1 is {name}")
})
.await
.expect("reads are open");
format!("read @{} · {}", read.position, read.value)
}

stop consumes the runtime and waits for the writer to finish what it admitted. It can only do that once the last sender is gone — and every Journal clone holds one. So drop your handles first:

drop(handle); // and every other Journal clone
let settled = runtime.stop().await; // the position every fold reached

Get that wrong and stop does not fail; it waits, for a sender nobody is going to release. The diagnostic for it is runtime.notices(), which publishes the number of live Journal clones when the journal join crosses its grace window. Read that number as an ownership bug in your own code — a longer grace window does not fix it. Ship the Runtime works the whole exit order through.

From the repository root:

Terminal window
mise run run:minischematic

The transcript begins with the memory-only boot, prints each receipt position, and prints the positioned read after the edits. The same command continues through streams, undo, durable facts, shutdown, and recovery; the first two stops are the path above.

Read Model the State for the rules that keep replay honest, then Assemble and Launch for the complete builder contract. Keep Vocabulary open — several words on this page mean something narrower here than elsewhere.

Stokker Technologies markDesigned and built by Stokker Technologies