Guide

Event Schemas

Define payload-less and fielded domain events with one decoded Effect Schema boundary.

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:

  • _tag says that the message changed.
  • message says 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

  1. Add author: Schema.String and verify that the old input is rejected until it supplies the field.
  2. Refine message to a non-empty string and exercise both an empty and non-empty value.
  3. Combine Refresh and ChangeMessage into a Schema union and discriminate the decoded result by _tag.