add-a-lifecycle-machine
Learn how XState owns a lifecycle while Effect owns its work, errors, and services.
Skill by joelhooks
Install
Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.
npx agentmarket install skill/joelhooks-add-a-lifecycle-machine-cd131b
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions: not checked yet.
What's inside
- Skill name
add-a-lifecycle-machine- Description
- Learn how XState owns a lifecycle while Effect owns its work, errors, and services.
- License
- MIT
- Instructions
- 145 lines in SKILL.md · view on GitHub
Files (2)
- SKILL.md
- lifecycle.model.test.md · templates/
SKILL.md (first lines)
# Add a lifecycle machine
A lifecycle machine defines named states and the events that permit movement between them. Effect describes work with typed failures and required dependencies. A service is a named interface for one job. Schema defines accepted values and their runtime checks.
Copy `packages/core/src/inspect-machine.ts` when the work has states that matter. Keep a direct Effect when it does not.
Before changing Effect or XState code, read `node_modules/effect/AGENTS.md`. Then read the pinned XState and `@xstate/effect` source listed in `AGENTS.md`.
## 1. Write the states first
List the states and final results before writing the machine.
For file inspection, the states are `reading`, `inspected`, and `unreadable`. The final result is either `Inspected` with file stats or `Unreadable` with a `FileStatsError`.
Use an explicit result union when a known error should move the machine into a final state.
## 2. Put side effects in actors
Define each side effect with `createEffectLogic` outside the machine:
```ts
const performWork = createEffectLogic({
effect: ({ input }) => ThingService.use((service) => service.run(input.id)),
schemas: { input: Schema.Struct({ id: Schema.String }) },
});
```
An actor is a running unit that processes events or performs work for a machine. These actors carry typed errors and service dependencies. Declare them through `setupEffect`. Do not return an Effect from an inline XState callback.
## 3. Build the machine
Pass the actors and schemas to `setupEffect`, then call `createMachine`:
```ts
type ThingOutcome =
| { readonly _tag: "Succeeded"; readonly value: ThingResult }
| { readonly _tag: "Failed"; readonly error: ThingError };
interface ThingContext {
readonly id: string;
readonly outcome: ThingOutcome | undefined;
}
export const thingMachine = setupEffect({
actors: { performWork },
schemas: {
context: types<ThingContext>(),
input: Schema.Struct({ id: Schema.String }),
},
}).createMachine({
context: ({ input }) => ({ id: input.id, outcome: undefined }),
initial: "working",
output: ({ context }) => context.outcome,
states: {
working: {
invoke: {
src: "performWork",
input: ({ context }) => ({ id: context.id }),
onDone: {
target: "succeeded",
context: ({ context, event }) => ({
...context,
outcome: { _tag: "Succeeded", value: event.output },
}),
},
onError: {
target: "failed",
context: ({ context, event }) => ({
...context,
outcome: { _tag: "Failed", error: event.error },
}),
},
},
},
succeeded: { type: "final" },
failed: { type: "final" },
},
});
```
XState owns states and moves between them. Effect owns side effects, errors, services, and cleanup.
## 4. Run it inside Effect
Start the machine with `createEffectActor`. Do not use XState's `createActor`. Hand the actor to `watchActor` from `@calendar/capability/actor-watch`, then wait for it with `join` inside `Effect.scoped`:
```ts
export const runThingMachine = Effect.fn("runThingMachine")(function* (
id: string
) {
const actor = yield* createEffectActor(thingMachine, { input: { id } });
yield* watchActor("thingMachine", actor);
// @effect-diagnostics-next-line anyUnknownInErrorContext:off -- join exposes unknown machine failures; unexpected failures become defects with orDie.
const outcome = yield* join(actor).pipe(Effect.orDie);
if (outcome === undefined) {
return yield* Effect.die(new Error("machine completed without an outcome"));
}
if (outcome._tag === "Failed") {
return yield* outcome.error;
}
return outcome.value;
}, Effect.scoped);
```
`watchActor` does nothing until devtools runs the capability. Then `rat_list_actors`, `rat_list_transitions`, and `rat_get_actor` show every state and event. `rat-stack-patterns/watch-effect-actors` fails lint when a runtime module starts an actor without it.
A missing result or machine-level error is a bug in this design. A known product error belongs in the final result. `join` has an `unknown` machine-error channel, so the example uses one narrow diagnostic override before `Effect.orDie`.
Call the runner from the capability handler, the server-side implementation of one named action. Provide its service Layer in `apps/cli/src/cli.ts`. A Layer builds services and supplies their construction dependencies.
## 5. Generate inputs and command histories
Add a property and a model test with each lifecycle. Use `effect/Arbitrary` and `@effect/vitest`. Do not add a separate generator dependency.
Copy the complete test template at `skills/add-a-lifecycle-machine/templates/lifecycle.model.test.md` into `packages/core/test/<name>.model.test.ts`. It runs unchanged for `inspectMachine`. Replace its schemas, service, outcomes, and commands for your lifecycle. The executable reference is `packages/core/test/inspect-machine.model.test.ts`.
- Derive generated inputs, outputs, and failures from the domain schemas.
- Keep the model independent: pending until work settles, then one immutable terminal outcome.
- Use `getShortestPaths` from `xstate/graph` to cover both endings for each generated input and actor result. This export shares the XState pin; do not install `@xstate/graph` for XState 6.
- Use `getPathsFromEvents` to explore generated histories. Check the lifecycle rules after every step. Stop pure traversal at the first terminal event. Only the actor interpreter rejects later delivery.From joelhooks/potion-calendar, MIT.
More from joelhooks/potion-calendar
Signals
- Stars
- 27
- Forks
- 1
- Open issues
- 0
- Last commit (this folder)
- Oct 20, 2026 (10 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- MIT
- Repository
- github.com/joelhooks/potion-calendar
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
Same skill elsewhere
Copies of this SKILL.md published by others; search shows only one of them.
- 0.676score
add-a-lifecycle-machine
joelhooks
Learn how XState owns a lifecycle while Effect owns its work, errors, and services.
SkillAFreshness: A★ 129Claude Code +9 - 0.335score
add-a-lifecycle-machine
ChindanaiNaKub
Learn how XState owns a lifecycle while Effect owns its work, errors, and services.
SkillAFreshness: AClaude Code +9 - 0.323score
add-a-lifecycle-machine
joelhooks
Learn how XState owns a lifecycle while Effect owns its work, errors, and services.
SkillAFreshness: A★ 6Claude Code +9
More from joelhooks
- 0.684score
rat-stack-mode
joelhooks
Route non-trivial rat-stack code, data, design, investigation, and documentation tasks to every matching playbook and principle.
SkillAFreshness: A★ 129Claude Code +9 - 0.682score
uncomplect
joelhooks
Find responsibilities that a rat-stack design combines, separate them into capabilities, cartridges, machines, features, and clients, and enforce that…
SkillAFreshness: A★ 129Claude Code +9 - 0.682score
investigate-a-failure
joelhooks
Find the cause of a crash, wrong output, flaky test, or slow run with evidence, fix it at its source, and leave a notebook a person can learn from.
SkillAFreshness: A★ 129Claude Code +9 - 0.682score
profile-a-worker
joelhooks
Capture a CPU or heap profile of a deployed Cloudflare Worker or Durable Object and read it with pprof.
SkillAFreshness: A★ 129Claude Code +9 - 0.682score
query-design
joelhooks
Design or review SQL reads, writes, scans, backfills, and slow database paths.
SkillAFreshness: A★ 129Claude Code +9 - 0.681score
ship
joelhooks
Learn how to ship a change through merge, CI, release, stage deployment, rollout, verification, rollback and flags.
SkillAFreshness: A★ 129Claude Code +9
Cite this page
add-a-lifecycle-machine skill. Agent Mart. https://agentmart.dev/skills/joelhooks-add-a-lifecycle-machine-cd131b (last updated 2026-10-24)