An event is a decoded domain value that says what occurred. Its Schema owns both the shape admitted at a boundary and the TypeScript type consumed inside a Motive chart.
Define what can happen.
An event Schema gives each occurrence a stable identity and a decoded payload. Start with a tag; add fields only when the occurrence carries information.
1 · Name an occurrence
A payload-less event still has identity.
Schema.TaggedStruct gives Refresh one stable discriminant and no payload fields. The decoder accepts the exact tagged object and rejects values that only look similar.
import { Schema } from "effect";
export const Refresh = Schema.TaggedStruct("Refresh", {});
export type Refresh = typeof Refresh.Type;
export const decodeRefresh = Schema.decodeUnknownOption(Refresh);
export const refresh = decodeRefresh({ _tag: "Refresh" });
2 · Carry information
Fields make the event a domain message.
ChangeMessage adds one decoded string. The tag identifies what occurred; the field carries the information a later transition or action can consume.
import { Schema } from "effect";
export const ChangeMessage = Schema.TaggedStruct("ChangeMessage", {
message: Schema.String,
});
export type ChangeMessage = typeof ChangeMessage.Type;
export const decodeChangeMessage = Schema.decodeUnknownOption(ChangeMessage);
export const changeMessage = decodeChangeMessage({
_tag: "ChangeMessage",
message: "Platform 4",
});
Events are values
Motive does not require events to be classes. Any supported Effect Schema can define an event
vocabulary; Schema.TaggedStruct is the usual choice because it combines a stable _tag with a
decoded payload.
The first frame defines Refresh. It carries no fields, but it is not an unstructured signal. Its
runtime value is exactly { _tag: "Refresh" }, and its type distinguishes it from every other
tagged event.
The tag says what occurred
Schema.TaggedStruct("Refresh", {}) fixes the _tag to the literal "Refresh". Callers do not
choose an arbitrary tag, and consumers can discriminate an event vocabulary with ordinary tagged
union control flow.
The tag is protocol identity. Renaming it changes the admitted message, so treat event tags with the same care as other durable or external protocol names.
Fields carry the relevant facts
The second frame adds message: Schema.String to ChangeMessage. The event now answers two
separate questions:
_tagsays that the message changed.messagesays what the next message is.
Put only facts about the occurrence on the event. Long-lived facts belong on state facets; service dependencies and side-effect implementations belong at the runtime boundary.
Decode at admission
decodeUnknownOption turns an unknown boundary value into either a decoded event or explicit
absence. The live workbench exercises both directions: a string payload is admitted, while the
same tagged object with a numeric payload is rejected.
Decode once where untrusted input enters the application. Transition and action code should receive the decoded event type rather than rechecking loose objects downstream.
Use the decoder that matches the owning boundary. decodeUnknownEffect preserves a typed failure
channel when rejection details belong in the program; decodeUnknownOption is useful when only
admission or absence matters.
Constructor input and decoded type
typeof ChangeMessage.Type is the decoded event value. The Schema also carries its encoded and
constructor shapes, so refinements, defaults, and transformations remain owned by the same
definition instead of being reconstructed in a parallel interface.
That distinction becomes important when an event crosses JSON, persistence, RPC, or another external boundary. Decode the encoded input; use the decoded type inside the chart.
Test the boundary
- Add
author: Schema.Stringand verify that the old input is rejected until it supplies the field. - Refine
messageto a non-empty string and exercise both an empty and non-empty value. - Combine
RefreshandChangeMessageinto a Schema union and discriminate the decoded result by_tag.