Query.gen constructs a synchronous query with generator notation. Yield each state you need, use
its facet as an ordinary local value, and return the derived answer.
The generator changes the shape of the authoring code—not the meaning of the query. Strict reads remain checked against the chart topology, and the result remains pure and synchronous.
Let the code follow the reasoning.
Generator notation makes a multi-state query read from top to bottom. It preserves the same pure result and the same topology-checked dependencies as callback composition.
1 · Compose callbacks
The callback tree grows with the query.
Nested Query.zipWith calls can combine all three facets correctly, but the code now follows the composition tree instead of the order in which we reason about the data.
import { Query, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const FinishPacking = Schema.TaggedStruct("FinishPacking", {});
const PackingListInput = Schema.Struct({
orderId: Schema.String,
warehouse: Schema.String,
initialItems: Schema.Array(Schema.String),
});
export class PackingList extends State.Compound<PackingList>()("PackingList", {
orderId: Schema.String,
}) {
static states = States.make(() => [Packing]);
}
export class Packing extends State.Compound<Packing>()("Packing", {
warehouse: Schema.String,
}) {
static states = States.make(() => [Collecting, Ready]);
}
export class Collecting extends State.Atomic<Collecting>()("Collecting", {
items: Schema.Array(Schema.String),
}) {
static transitions = Transitions.make(this, ({ on }) => [
on(FinishPacking, Ready),
]);
}
class Ready extends State.Atomic<Ready>()("Ready") {}
export const PackingListChart = Statechart.make(PackingList, {
input: PackingListInput,
init: ({ input }) => [
new PackingList({ orderId: input.orderId }),
new Packing({ warehouse: input.warehouse }),
new Collecting({ items: input.initialItems }),
],
});
export const packingSummary = PackingListChart.at(Collecting).select(
Query.zipWith(
PackingList,
Query.zipWith(Packing, Collecting, ({ warehouse }, { items }) => ({ items, warehouse })),
({ orderId }, { items, warehouse }) =>
`${orderId} · ${warehouse} · ${String(items.length)} items`,
),
);
2 · Flatten the read
Read from root to leaf.
Query.gen yields PackingList, Packing, and Collecting in sequence. Intermediate values become ordinary local variables while the same strict-read requirements remain attached to the query.
import { Query, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const FinishPacking = Schema.TaggedStruct("FinishPacking", {});
const PackingListInput = Schema.Struct({
orderId: Schema.String,
warehouse: Schema.String,
initialItems: Schema.Array(Schema.String),
});
const packingSummaryQuery = Query.gen(function* () {
const { orderId } = yield* PackingList;
const { warehouse } = yield* Packing;
const { items } = yield* Collecting;
const itemCount = items.length;
return `${orderId} · ${warehouse} · ${String(itemCount)} items`;
});
export class PackingList extends State.Compound<PackingList>()("PackingList", {
orderId: Schema.String,
}) {
static states = States.make(() => [Packing]);
}
export class Packing extends State.Compound<Packing>()("Packing", {
warehouse: Schema.String,
}) {
static states = States.make(() => [Collecting, Ready]);
}
export class Collecting extends State.Atomic<Collecting>()("Collecting", {
items: Schema.Array(Schema.String),
}) {
static transitions = Transitions.make(this, ({ on }) => [
on(FinishPacking, Ready),
]);
}
class Ready extends State.Atomic<Ready>()("Ready") {}
export const PackingListChart = Statechart.make(PackingList, {
input: PackingListInput,
init: ({ input }) => [
new PackingList({ orderId: input.orderId }),
new Packing({ warehouse: input.warehouse }),
new Collecting({ items: input.initialItems }),
],
});
export const packingSummary = PackingListChart.at(Collecting).select(packingSummaryQuery);
Callback composition is still a query
The first frame uses the combinators from the previous page. The outer Query.zipWith reads
PackingList; its inner query combines Packing and Collecting. The result is correct, and the
query retains all three strict-read requirements.
This shape is useful when the composition itself is simple. As the number of reads and intermediate decisions grows, nested callbacks make the reader reconstruct a tree before following the domain calculation.
Yield a state to read its facet
Inside Query.gen, yield* PackingList is a strict read of the active PackingList facet. The same
rule applies to Packing and Collecting. Destructuring each yielded value makes the ownership of
orderId, warehouse, and items visible at the read site.
Strict means the state must be provably active wherever the query is used. The selection is anchored
at Collecting, so its Packing and PackingList ancestors are necessarily active too. Motive
checks that proof when the chart is assembled.
The generator is synchronous
Query.gen resembles Effect.gen, but it describes a different computation. A query reads the
current statechart step or published snapshot synchronously. It does not perform Effects, acquire
services, wait, retry, or introduce an error channel.
The yielded values are Query instructions: state classes, specialized Query instructions, or other
queries. The final return is the ordinary derived value.
Requirements survive the refactor
Both frames produce the same summary from the same three facets. Both selections are present only
while Collecting is active. Sending FinishPacking exits that perspective and changes the live
answer to Absent; generator notation does not widen any facet's lifetime.
Use callback combinators when they say the whole calculation directly. Use Query.gen when several
reads, intermediate names, or control flow are easier to understand from top to bottom.
The next question is what to do when topology cannot prove a state is active. Query.option makes
that uncertainty explicit without weakening strict reads that really are guaranteed.
Test the generator
- Add a
priorityfield toPackingand include it in the returned summary. - Move
warehouseto a sibling state and inspect why the strict read is no longer proven atCollecting. - Yield the nested callback query from inside
Query.genand confirm that its requirements compose.