Skip to content
Back to skills

Effect Ts Patterns

ASecurity

Mandatory Effect-TS coding patterns for the Reactive Agents framework. Use when writing any TypeScript code, creating services, defining types, handling errors, or composing layers in this project.

  • 28 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
ai-agentstypescriptsqlreactapidatabase

Works with

  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tylerjrbuell/reactive-agents-ts --skill effect-ts-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Effect Ts Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Effect Ts Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tylerjrbuell-effect-ts-patterns/badge)](https://www.skillsdirectory.com/skills/tylerjrbuell-effect-ts-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: effect-ts-patterns
description: Mandatory Effect-TS coding patterns for the Reactive Agents framework. Use when writing any TypeScript code, creating services, defining types, handling errors, or composing layers in this project.
user-invocable: false
---

# Effect-TS Patterns — Reactive Agents

Every file in this project MUST follow these patterns. Violations will break the build and type system.

## Types: Schema.Struct (NEVER plain interfaces)

```typescript
import { Schema } from "effect";

// Branded IDs
export const AgentId = Schema.String.pipe(Schema.brand("AgentId"));
export type AgentId = typeof AgentId.Type;

// Struct definitions
export const AgentSchema = Schema.Struct({
  id: AgentId,
  name: Schema.String,
  status: Schema.Literal("idle", "running", "completed", "failed"),
  description: Schema.optional(Schema.String),
  tags: Schema.Array(Schema.String),
  metadata: Schema.optional(
    Schema.Record({ key: Schema.String, value: Schema.Unknown }),
  ),
});
export type Agent = typeof AgentSchema.Type;
```

**Rules:**

- ALWAYS use `Schema.Struct` for data shapes — never `interface` or `type { ... }`
- ALWAYS use `Schema.brand()` for ID types
- ALWAYS use `Schema.Literal()` for union/enum values
- ALWAYS use `Schema.optional()` for optional fields — never `?:`
- ALWAYS derive the TypeScript type with `typeof XxxSchema.Type`

## Errors: Data.TaggedError (NEVER throw)

```typescript
import { Data } from "effect";

export class AgentError extends Data.TaggedError("AgentError")<{
  readonly message: string;
}> {}

export class TaskError extends Data.TaggedError("TaskError")<{
  readonly message: string;
  readonly taskId: string;
}> {}

// Union type for service signatures
export type CoreErrors = AgentError | TaskError;
```

**Rules:**

- NEVER use `throw new Error()`
- ALWAYS use `Data.TaggedError("UniqueTag")<{ ... }>`
- The tag string MUST match the class name exactly
- All fields MUST be `readonly`
- ALWAYS create a union type for each package's errors

## Services: Context.Tag + Layer.effect (NEVER OOP classes)

```typescript
import { Effect, Context, Layer, Ref } from "effect";

// Step 1: Define the service tag with its interface
export class MyService extends Context.Tag("MyService")<
  MyService,
  {
    readonly doWork: (input: string) => Effect.Effect<string, MyError>;
    readonly getState: () => Effect.Effect<ReadonlyMap<string, string>, never>;
  }
>() {}

// Step 2: Create the Live layer
export const MyServiceLive = Layer.effect(
  MyService,
  Effect.gen(function* () {
    // Resolve dependencies
    const dep = yield* OtherService;

    // Create mutable state via Ref
    const state = yield* Ref.make(new Map<string, string>());

    // Return the service implementation
    return {
      doWork: (input) =>
        Effect.gen(function* () {
          yield* Ref.update(state, (m) => new Map(m).set(input, input));
          return yield* dep.process(input);
        }),
      getState: () => Ref.get(state),
    };
  }),
);
```

**Rules:**

- Services ALWAYS extend `Context.Tag("ServiceName")<ServiceTag, Interface>()`
- The tag string MUST match the class name
- Implementations ALWAYS use `Layer.effect(Tag, Effect.gen(...))`
- State ALWAYS uses `Ref` — never mutable variables
- Dependencies are resolved with `yield* OtherService` inside `Effect.gen`
- All methods return `Effect.Effect<Success, Error>`

## Services: Effect.Service (app-level shortcut, since Effect 3.9)

`Effect.Service` fuses the tag + layer into one class. Use it for **app-level services with exactly one concrete implementation and no swap-out need** (e.g. a single package's internal orchestrator). Keep the two-step `Context.Tag` + `Layer.effect` pattern above for **library-exported services** where multiple implementations (live/test/mock) are expected — most of this framework's cross-package services fall in that bucket, so `Effect.Service` should stay the exception, not the default.

```typescript
import { Effect } from "effect";

export class Greeter extends Effect.Service<Greeter>()("Greeter", {
  effect: Effect.gen(function* () {
    const dep = yield* OtherService;
    return {
      greet: (name: string) => Effect.succeed(`Hello ${name}`),
    };
  }),
}) {}

// Usage: Greeter.Default is the auto-generated Layer
Effect.gen(function* () {
  const greeter = yield* Greeter;
  return yield* greeter.greet("world");
}).pipe(Effect.provide(Greeter.Default));
```

**Rules:**

- Only for services with one canonical implementation, internal to a package
- Do NOT use for any service that ships a `Live`/`Test` layer pair — use `Context.Tag` + `Layer.effect` instead
- Still returns `Effect.Effect<Success, Error>` methods; still no `throw`, no raw mutable state

## Scoped Resources: Layer.scoped for cleanup

```typescript
export const MemoryDatabaseLive = Layer.scoped(
  MemoryDatabase,
  Effect.acquireRelease(
    Effect.sync(() => {
      const db = new Database(dbPath, { create: true });
      db.exec("PRAGMA journal_mode=WAL");
      return db;
    }),
    (db) => Effect.sync(() => db.close()),
  ).pipe(
    Effect.map((db) => ({
      query: db.query.bind(db),
      exec: db.exec.bind(db),
      close: () => Effect.sync(() => db.close()),
    })),
  ),
);
```

**Rules:**

- Use `Layer.scoped` + `Effect.acquireRelease` for resources needing cleanup (DB, files, connections)
- Acquire returns the resource; release cleans it up
- Both acquire and release must be wrapped in `Effect.sync` or `Effect.tryPromise`

## Synchronous Operations: Effect.sync

```typescript
// bun:sqlite is synchronous — use Effect.sync
const rows = yield * Effect.sync(() => db.query("SELECT * FROM t").all());

// Pure computations
const value = yield * Effect.sync(() => computeHash(input));
```

## Async Operations: Effect.tryPromise

```typescript
// HTTP calls, file I/O, external APIs
const data =
  yield *
  Effect.tryPromise({
    try: () => fetch(url).then((r) => r.json()),
    catch: (e) => new FetchError({ message: String(e) }),
  });
```

**Rules:**

- ALWAYS provide a `catch` function that returns one of your `Data.TaggedError` types
- NEVER use raw `await`

## Layer Composition: Layer.mergeAll + Layer.provide

```typescript
// Merge independent layers
export const createMyLayer = () => Layer.mergeAll(ServiceALive, ServiceBLive);

// Provide dependencies from one layer to another
export const createMyLayer = () =>
  Layer.mergeAll(ServiceALive, ServiceBLive.pipe(Layer.provide(ServiceALive)));

// Full package layer factory
export const createCoreLayer = () =>
  Layer.mergeAll(
    EventBusLive,
    AgentServiceLive.pipe(Layer.provide(EventBusLive)),
    TaskServiceLive.pipe(Layer.provide(EventBusLive)),
    ContextWindowManagerLive,
  );
```

**Rules:**

- Every package exports a `createXxxLayer()` factory function
- Use `Layer.provide()` to wire dependencies between services
- Use `Layer.mergeAll()` to combine independent services
- The factory function takes configuration params if needed

## Optional Dependencies: Effect.serviceOption

```typescript
import { Context } from "effect";

// When a dependency may not be present in the runtime
const maybeReasoning =
  yield *
  Effect.serviceOption(
    Context.GenericTag<ReasoningServiceInterface>("ReasoningService"),
  );

const result = Option.match(maybeReasoning, {
  onNone: () => executeDefaultPath(),
  onSome: (svc) => svc.selectStrategy(context),
});
```

## Pattern Checklist

Before committing any file, verify:

- [ ] All types use `Schema.Struct`, not interfaces (exception: strategy input interfaces with Effect types)
- [ ] All IDs use `Schema.brand()`
- [ ] All errors use `Data.TaggedError`
- [ ] All library-exported services use `Context.Tag` + `Layer.effect`; single-impl internal services may use `Effect.Service`
- [ ] All state uses `Ref`
- [ ] No `throw`, no raw `await`, no mutable variables
- [ ] No `@ts-ignore` or `@ts-expect-error` — ever
- [ ] `as any` only in test mocks or untyped meta bags (comment why)
- [ ] New behavior emits EventBus events
- [ ] Computable fields are deterministic (not LLM-generated)
- [ ] Package exports a `createXxxLayer()` factory
- [ ] `index.ts` re-exports all public types, errors, services, and the layer factory
- [ ] File names kebab-case, types PascalCase, functions camelCase

## Version Note (checked 2026-09-03)

Repo pins `effect@^3.10.0`; installed `3.19.18`; npm latest `3.22.1`. No breaking changes found in that range — safe to bump lockfile, but run full build+test after (`bun run build && bun test`), don't assume. `Data.TaggedError` remains the default error pattern; `Schema.TaggedError` is only worth reaching for when the error itself needs schema decode/encode across a wire boundary — no changelog evidence it's now "preferred" generally.

See `CODING_STANDARDS.md` for the full authoritative reference.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.