effect-ts-patterns

star 16

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.

tylerjrbuell By tylerjrbuell schedule Updated 3/15/2026

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)

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)

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)

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>

Scoped Resources: Layer.scoped for cleanup

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

// 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

// 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

// 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

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 services use Context.Tag + Layer.effect
  • 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

See CODING_STANDARDS.md for the full authoritative reference.

Install via CLI
npx skills add https://github.com/tylerjrbuell/reactive-agents-ts --skill effect-ts-patterns
Repository Details
star Stars 16
call_split Forks 3
navigation Branch main
article Path SKILL.md
More from Creator
tylerjrbuell
tylerjrbuell Explore all skills →