/SKILL.md
# effect-machine Skill Quick reference for AI agents working with effect-machine. ## What It Is Type-safe state machines for Effect. Schema-first API. ## Core Pattern ```ts import { Machine, State, Event, Slot } from "effect-machine"; // 1. Define schemas const MyState = State({ Idle: {}, Loading: { url: Schema.String }, Done: { data: Schema.Unknown }, }); const MyEvent = Event({ Start: { url: Schema.String }, Complete: { data: Schema.Unknown }, }); // 2. Build machine const ma
npx skillsauth add cevr/effect-machine effect-machineInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick reference for AI agents working with effect-machine.
Type-safe state machines for Effect. Schema-first API.
import { Machine, State, Event, Slot } from "effect-machine";
// 1. Define schemas
const MyState = State({
Idle: {},
Loading: { url: Schema.String },
Done: { data: Schema.Unknown },
});
const MyEvent = Event({
Start: { url: Schema.String },
Complete: { data: Schema.Unknown },
});
// 2. Build machine
const machine = Machine.make({
state: MyState,
event: MyEvent,
initial: MyState.Idle,
})
.on(MyState.Idle, MyEvent.Start, ({ event }) => MyState.Loading({ url: event.url }))
.on(MyState.Loading, MyEvent.Complete, ({ event }) => MyState.Done({ data: event.data }))
.final(MyState.Done);
| Method | Purpose |
| -------------------------------------- | --------------------------------------- |
| .on(state, event, handler) | Add transition |
| .on([stateA, stateB], event, h) | Multi-state transition |
| .onAny(event, handler) | Wildcard (any state, specific .on wins) |
| .reenter(state, event, handler) | Force lifecycle on same-state |
| .spawn(state, handler) | State-scoped effect (auto-cancelled) |
| .timeout(state, { duration, event }) | State timeout (gen_statem) |
| .postpone(state, event/events) | Postpone event in state (gen_statem) |
| .background(handler) | Machine-lifetime effect |
| .final(state) | Mark final state |
Construct state from existing source:
// Per-variant: preserve fields, override specific ones
State.Active.derive(state, { count: state.count + 1 });
// Cross-state: picks only target fields
State.Shipped.derive(processingState, { trackingId: "TRACK-123" });
// Empty variant
State.Idle.derive(anyState); // → { _tag: "Idle" }
// Union-level: dispatches to correct variant based on _tag
// Preserves specific variant subtype — no switch needed
const updated = MyState.derive(state, { queue: newQueue });
const MySlots = Slot.define({
canRetry: Slot.fn({ max: Schema.Number }, Schema.Boolean),
fetch: Slot.fn({ url: Schema.String }),
});
const machine = Machine.make({ state, event, slots: MySlots, initial }).on(
State.X,
Event.Y,
({ slots }) =>
Effect.gen(function* () {
if (yield* slots.canRetry({ max: 3 })) {
yield* slots.fetch({ url: "/api" });
}
return State.Z;
}),
);
// Slot implementations provided at spawn time — handlers take only params
const actor =
yield *
Machine.spawn(machine, {
slots: {
canRetry: ({ max }) => attempts < max,
fetch: ({ url }) => Http.get(url),
},
});
Simple (no registry):
const program = Effect.gen(function* () {
const actor = yield* Machine.spawn(machine);
yield* actor.send(Event.Start({ url: "/api" }));
const state = yield* actor.waitFor(MyState.Done);
});
Effect.runPromise(Effect.scoped(program));
With registry/persistence:
const program = Effect.gen(function* () {
const system = yield* ActorSystemService;
const actor = yield* system.spawn("id", machine);
// ...
});
Effect.runPromise(Effect.scoped(program.pipe(Effect.provide(ActorSystemDefault))));
| Method | Description |
| -------------------------------- | ------------------------------------------- |
| actor.send(event) | Fire-and-forget (queue event) |
| actor.cast(event) | Alias for send (OTP gen_server:cast) |
| actor.call(event) | Request-reply, returns ProcessEventResult |
| actor.ask(event) | Typed reply (event must have Event.reply) |
| actor.waitFor(State.X) | Wait for state (constructor or fn) |
| actor.sendAndWait(ev, State.X) | Send + wait for state |
| actor.awaitFinal | Wait for final state |
| actor.watch(other) | Completes when other actor stops |
| actor.drain | Process remaining queue, then stop |
| actor.snapshot | Get current state |
| actor.sync.send(event) | Sync fire-and-forget (for UI) |
| actor.sync.stop() | Sync stop |
| actor.sync.snapshot() | Sync get state |
| actor.sync.matches(tag) | Sync check state tag |
| actor.sync.can(event) | Sync can handle event? |
| actor.subscribe(fn) | Sync callback, returns unsubscribe |
| actor.system | Access the actor's ActorSystem |
| actor.children | Child actors (ReadonlyMap) |
Events declare reply schemas via Event.reply(). Handlers use Machine.reply():
const MyEvent = Event({
GetCount: Event.reply({}, Schema.Number), // askable
Reset: {}, // not askable
});
.on(State.Active, Event.GetCount, ({ state }) =>
Machine.reply(state, state.count),
)
const count = yield* actor.ask(Event.GetCount); // number — type inferred from schema
// actor.ask(Event.Reset) — compile error (no reply schema)
Deferred replies via Machine.deferReply() — spawn handler settles later via self.reply(value).
Fails with NoReplyError if handler doesn't reply, ActorStoppedError on stop.
// State timeout — timer auto-cancelled on state exit
machine.timeout(State.Loading, {
duration: Duration.seconds(30),
event: Event.Timeout,
});
// Event postpone — buffered, drained on next state change
machine.postpone(State.Connecting, [Event.Data, Event.Cmd]);
Returned by actor.call(event):
interface ProcessEventResult<S> {
newState: S;
previousState: S;
transitioned: boolean;
lifecycleRan: boolean;
isFinal: boolean;
hasReply: boolean;
reply?: unknown;
postponed: boolean;
}
// Sync callback — ActorSpawned / ActorStopped events
const unsub = system.subscribe((event) => console.log(event._tag, event.id));
// Sync snapshot of all registered actors
const actors: ReadonlyMap<string, ActorRef> = system.actors;
// Async stream (late subscribers miss prior events)
system.events.pipe(Stream.take(10), Stream.runCollect);
// Simulate (no spawn effects)
const result = yield * simulate(machine, [Event.Start, Event.Complete]);
expect(result.finalState._tag).toBe("Done");
// Assert path
yield * assertPath(machine, events, ["Idle", "Loading", "Done"]);
// Real actor — call-based testing
const actor = yield * Machine.spawn(machine);
const result = yield * actor.call(Event.Start);
expect(result.transitioned).toBe(true);
expect(result.newState._tag).toBe("Loading");
State.Idle not State.Idle()yield* Effect.yieldNow to process events.reenter() to forceyield* Effect.fail().onAny() is fallback: Specific .on() always takes priorityMachine.spawn(machine, { slots: { ... } }) — not on the buildersend/cast = fire-and-forget, call = request-reply, ask = typed replyactor.sync.* (not top-level sendSync/snapshotSync)call/ask Deferreds settled on stopWire machines to @effect/cluster for distributed actors:
import { toEntity, EntityMachine, PersistenceAdapter } from "effect-machine/cluster";
const OrderEntity = toEntity(orderMachine, { type: "Order" });
const OrderEntityLayer = EntityMachine.layer(OrderEntity, orderMachine, {
initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
persistence: { strategy: "journal" }, // or "snapshot" (default)
});
| Export | Purpose |
| --------------------------------------------- | ------------------------------------------------------------------- |
| toEntity(machine, { type }) | Generate Entity definition with Send/Ask/GetState/WatchState RPCs |
| EntityMachine.layer(entity, machine, opts?) | Wire machine to cluster Entity layer |
| makeEntityActorRef(client, id) | Typed client wrapper (send/ask/snapshot/watch/waitFor) |
| PersistenceAdapter | Service tag for storage backend |
| makeInMemoryPersistenceAdapter | In-memory adapter for testing |
Persistence strategies:
EntityMachineOptions: initializeState, hooks, maxIdleTime, mailboxCapacity, disableFatalDefects, defectRetryPolicy, persistence
| File | Purpose |
| ------------------------------- | -------------------------------------- |
| machine.ts | Machine builder |
| schema.ts | State/Event + derive |
| slot.ts | Slot.define/Slot.fn |
| actor.ts | ActorSystem, event loop |
| testing.ts | simulate, harness |
| internal/runtime.ts | Shared runtime kernel (entity-machine) |
| cluster/entity-machine.ts | Entity-machine adapter + persistence |
| cluster/persistence.ts | Adapter interface, types, service tag |
| cluster/adapters/in-memory.ts | In-memory persistence adapter |
| cluster/entity-actor-ref.ts | Typed entity client wrapper |
| cluster/to-entity.ts | Entity definition generator |
development
Type-safe state machines for Effect. Use when building state machines with effect-machine — defining states/events, transition handlers, spawn effects, timeouts, postpone, actors, typed ask/reply, testing, recovery/durability lifecycle. Triggers on effect-machine imports, Machine.make, Machine.spawn, actor.start, Machine.replay, Machine.reply, Event.reply, State/Event definitions, ActorRef usage, Recovery, Durability, Lifecycle.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.