Your first machine¶
This counter accepts an increment event. A guard keeps the count at or below 10.
The machine and its state are ordinary data owned by your program.
Define the bundle¶
A bundle is one YAML or JSON document containing shared event contracts and one or
more machines. This bundle contains one machine named counter.
format: 1
namespace: tutorial.first
events:
increment:
direction: input
payload:
amount: { type: int, required: true }
machines:
- machine_id: counter
version: 1
root:
type: composite
variables:
count: { type: int, init: 0 }
initial: { transition_to: running }
states:
running:
on_events:
increment:
guard: count + event.payload.amount <= 10
action:
- assign: { count: count + event.payload.amount }
The bundle declares:
- the exact format;
- a namespace and machine identity;
- an input event with a required integer payload;
- an integer variable initialized to zero;
- an initial transition to
running; - a guarded action that computes the next value with portable CEL.
The schema and semantic loader reject misspelled fields, invalid event payloads, and type errors before the machine runs.
Run it with Python¶
Install the released engine:
The complete program is below. create returns the initial state. dispatch returns
the resulting state: a handled step may produce a new immutable state, while unhandled
or rejected processing preserves the exact prior state object. Neither call retains
hidden work.
from pathlib import Path
import sys
import determa.state as ds
machine_path = Path(sys.argv[1])
bundle = ds.load_bundle(machine_path.read_text())
created = ds.create(
bundle,
machine_id="counter",
root_instance_id="tutorial-counter",
creation_id="tutorial-counter:create",
bindings={},
)
assert created["status"] == "running"
state = created["state"]
target = {
"root": {
"root_instance_id": state["root_instance_id"],
"root_runtime_id": state["root_runtime_id"],
}
}
accepted = ds.dispatch(
bundle,
state,
{
"input": {
"event": "increment",
"event_id": "tutorial-counter:increment:1",
"target": target,
"payload": {"amount": 3},
}
},
)
assert accepted["status"] == "running"
assert accepted["disposition"] == "handled"
state = accepted["state"]
root = state["runtimes"][state["root_runtime_id"]]
assert root["scopes"]["root"]["count"] == 3
rejected_by_guard = ds.dispatch(
bundle,
state,
{
"input": {
"event": "increment",
"event_id": "tutorial-counter:increment:2",
"target": target,
"payload": {"amount": 8},
}
},
)
assert rejected_by_guard["status"] == "running"
assert rejected_by_guard["disposition"] == "unhandled"
assert rejected_by_guard["state"] is state
print("count=3; next increment was unhandled")
From this repository, extract the authored fences and run the generated files:
make extract
python .cache/examples/python/first_counter.py \
.cache/examples/machines/first-counter.yaml
Expected output:
unhandled is not an engine fault. It means no enabled handler accepted that event in
the active state hierarchy. Here, the guard correctly prevented 3 + 8.
Run the same trace with Rust¶
Create a small Cargo project with the released crate.
[package]
name = "determa-first-counter"
version = "0.2.0"
edition = "2021"
publish = false
[dependencies]
determa-state = "=0.2.0"
The Rust program uses the same machine file and asserts the same count and dispositions.
use determa_state::{
create, dispatch, load_bundle, Bindings, Delivery, Disposition, Envelope, Target,
Value,
};
use std::{collections::BTreeMap, env, fs};
fn input(state: &determa_state::AggregateState, amount: i64, sequence: u8) -> Delivery {
Delivery::Input(Envelope {
event: "increment".to_string(),
event_id: format!("tutorial-counter:increment:{sequence}"),
target: Target::Root {
root_instance_id: state.root_instance_id.clone(),
root_runtime_id: state.root.runtime_id.clone(),
},
payload: BTreeMap::from([("amount".to_string(), Value::Int(amount))]),
correlation_id: None,
})
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let machine_path = env::args().nth(1).expect("machine path");
let bundle = load_bundle(&fs::read_to_string(machine_path)?)?;
let created = create(
&bundle,
"counter",
"tutorial-counter",
"tutorial-counter:create",
&Bindings::default(),
);
let initial = created.state.expect("creation succeeds");
let accepted = dispatch(&bundle, &initial, Some(input(&initial, 3, 1)));
assert_eq!(accepted.disposition, Some(Disposition::Handled));
let state = accepted.state.expect("handled dispatch returns state");
assert_eq!(state.root.visible_variables()["count"], Value::Int(3));
let blocked = dispatch(&bundle, &state, Some(input(&state, 8, 2)));
assert_eq!(blocked.disposition, Some(Disposition::Unhandled));
assert_eq!(
blocked.state.expect("unhandled dispatch returns prior state"),
state
);
println!("count=3; next increment was unhandled");
Ok(())
}
Run it with:
cargo run \
--manifest-path .cache/examples/rust/first-counter/Cargo.toml \
-- .cache/examples/machines/first-counter.yaml
Both examples are executed during repository validation. They are not illustrative pseudocode.
Next¶
Continue through core statecharts, CEL and actions, components and spawning, and effects, faults, and hosting. Finish with the SQLite persistence tutorial and its advanced migration reference lab. The coverage status maps the complete released format-1 surface.
Normative references: bundle grammar §4, CEL §5, and dispatch §6.