Skip to content
Agentmart
Submit a tool
This entry duplicates add-a-lifecycle-machine, which is the one we list in search.

add-a-lifecycle-machine

Learn how XState owns a lifecycle while Effect owns its work, errors, and services.

Skill by joelhooks

IndexedMIT★ 6
Write a review

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

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.

We use your email only for this newsletter. Privacy

About

Learn how XState owns a lifecycle while Effect owns its work, errors, and services.

No 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 `@rat-stack/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/mischief, MIT.

Signals

Stars
6
Forks
0
Open issues
0
Last commit (this folder)
Oct 10, 2026 (19 days ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

Same skill elsewhere

Copies of this SKILL.md published by others; search shows only one of them.

More from joelhooks

Cite this page

add-a-lifecycle-machine skill. Agent Mart. https://agentmart.dev/skills/joelhooks-add-a-lifecycle-machine (last updated 2026-10-24)