A Query is a synchronous, pure description of an answer and the active state it depends on. It
does not copy facets into a second store. Its read requirements remain attached to the query so
Motive can check them against the assembled topology.
Begin with the callback-shaped combinators. They keep one- and two-state projections short; the generator form belongs on the next page, when several dependent reads need a more linear shape.
Ask the active configuration a question.
A query derives an answer from active facets without taking ownership of those facts. Its dependencies stay visible to the type system and the assembled chart.
1 · Project one facet
Start with a callback.
Query.map reads Collecting and gives its facet to a pure callback. The result is an item count whose dependency on Collecting remains part of 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,
initialItems: Schema.Array(Schema.String),
});
export class PackingList extends State.Compound<PackingList>()("PackingList", {
orderId: 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 Collecting({ items: input.initialItems }),
],
});
export const itemCount = PackingListChart.at(Collecting).select(
Query.map(Collecting, ({ items }) => items.length),
);
2 · Combine active facts
Let the topology prove both reads.
Query.zipWith reads PackingList and Collecting together. The Collecting perspective proves that the child and its root ancestor are coactive.
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,
initialItems: Schema.Array(Schema.String),
});
export class PackingList extends State.Compound<PackingList>()("PackingList", {
orderId: 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 Collecting({ items: input.initialItems }),
],
});
export const packingSummary = PackingListChart.at(Collecting).select(
Query.zipWith(
PackingList,
Collecting,
({ orderId }, { items }) => `${orderId} · ${String(items.length)} items`,
),
);
Queries describe reads
Query.map(Collecting, callback) names one strict read. When the query runs, the callback receives
the current Collecting facet and returns an ordinary value—in this case, the number of items.
The callback is synchronous and pure. It does not fetch the state, run an Effect, or subscribe to changes. The query is a reusable value that describes how to derive an answer when its required state is available.
A perspective proves presence
PackingListChart.at(Collecting) establishes the configuration in which the query is meaningful.
Calling .select(query) attaches the derived answer to that perspective. While Collecting is
active, the selection evaluates to Some(answer); after FinishPacking exits the state, it
evaluates to None.
This absence belongs to the perspective, not to the callback result. The callback receives a real
Collecting facet or does not run at all—it never receives a fabricated partial state.
Combine facts that are coactive
The second frame uses Query.zipWith to derive one summary from the root PackingList facet and
the child Collecting facet. The Collecting perspective proves both reads: an active child always
has its root ancestor active with it.
Changing one read to a sibling state would not be justified by that perspective. Motive carries the requirements through the query and rejects an impossible strict read when the chart is assembled.
Read hierarchy as hierarchy
Configuration is not a bag of state-path strings. A Compound state has exactly one active immediate executable child, while a Parallel state has one active child in every region. Queries can preserve those different shapes:
const phase = yield * Query.substate(Checkout);
const regions = yield * Query.substates(Fulfilling);
Query.substate(Checkout) returns the exact child union declared by Checkout.states, so
phase._tag is exhaustive. Query.substates(Fulfilling) returns a region-keyed object whose
values retain each region's exact child union. Adding a child or region changes the result type
instead of hiding new topology behind an unchecked string.
Use Query.option(...) around either instruction when the containing state is not proven active.
Optionality describes whether the owner is present; Compound versus Parallel still determines the
shape once it is.
Queries preserve ownership
A query does not widen a facet's lifetime. The summary can mention orderId and items together
while both owners are active, but it disappears when Collecting exits even though the root order
id remains.
That is the central distinction: facets own facts; queries derive answers. When a derivation needs
several reads, branching, or intermediate values, Query.gen gives the same requirements a linear
authoring form.
Test the read
- Change the
Query.mapcallback to return the first item instead of the count. - Add another field to
PackingListand include it in theQuery.zipWithsummary. - Replace
CollectingwithReadyin the strict read and inspect the topology error at assembly.