Skip to main content
Smithers does not invent a service container. Workflows use Effect’s native dependency injection: Context.Tag for service definitions and Layer for composition.

Defining a Service

A service is a typed capability:
import { Context, Effect } from "effect";

class Coder extends Context.Tag("Coder")<
  Coder,
  {
    readonly analyze: (description: string) => Effect.Effect<Analysis>;
    readonly fix: (analysis: Analysis) => Effect.Effect<Fix>;
  }
>() {}
The service is consumed inside step run functions:
const analyze = $.step("analyze", {
  output: Analysis,
  run: ({ input }) =>
    Effect.gen(function* () {
      const coder = yield* Coder;
      return yield* coder.analyze(input.description);
    }),
});

Providing a Service

Services are provided through Layer:
import { Layer } from "effect";

const CoderLive = Layer.succeed(Coder, {
  analyze: (description) =>
    Effect.succeed(
      new Analysis({
        summary: `Analysis of: ${description}`,
        severity: "medium",
      }),
    ),
  fix: (analysis) =>
    Effect.succeed(
      new Fix({
        patch: `Fix for: ${analysis.summary}`,
        explanation: "Applied standard fix pattern.",
      }),
    ),
});

Composing Layers

Use Layer.mergeAll to compose multiple service layers:
const AppLive = Layer.mergeAll(
  Smithers.sqlite({ filename: "./smithers.db" }),
  CoderLive,
  ReviewerLive,
  DeployerLive,
);

const result = await Effect.runPromise(
  MyWorkflow.execute(input).pipe(Effect.provide(AppLive)),
);

Layer Dependencies

Services can depend on other services:
const ReviewerLive = Layer.effect(
  Reviewer,
  Effect.gen(function* () {
    const coder = yield* Coder;
    return {
      review: (code) =>
        Effect.gen(function* () {
          const analysis = yield* coder.analyze(code);
          return new Review({
            approved: analysis.severity === "low",
            feedback: analysis.summary,
          });
        }),
    };
  }),
);

// Compose with dependency
const AppLive = Layer.mergeAll(
  Smithers.sqlite({ filename: "./smithers.db" }),
  ReviewerLive.pipe(Layer.provide(CoderLive)),
);

Common Service Patterns

Model Provider

class ModelProvider extends Context.Tag("ModelProvider")<
  ModelProvider,
  {
    readonly generate: (prompt: string) => Effect.Effect<string>;
    readonly generateStructured: <A>(
      prompt: string,
      schema: Schema.Schema<A>,
    ) => Effect.Effect<A>;
  }
>() {}

Repository

class TicketRepo extends Context.Tag("TicketRepo")<
  TicketRepo,
  {
    readonly get: (id: string) => Effect.Effect<Ticket>;
    readonly update: (id: string, data: Partial<Ticket>) => Effect.Effect<void>;
  }
>() {}

VCS Adapter

class VCS extends Context.Tag("VCS")<
  VCS,
  {
    readonly createBranch: (name: string) => Effect.Effect<Branch>;
    readonly commit: (message: string) => Effect.Effect<Commit>;
    readonly diff: () => Effect.Effect<string>;
  }
>() {}

Testing with Services

Replace live services with test implementations:
const CoderTest = Layer.succeed(Coder, {
  analyze: (description) =>
    Effect.succeed(
      new Analysis({
        summary: "test analysis",
        severity: "low",
      }),
    ),
  fix: (analysis) =>
    Effect.succeed(
      new Fix({
        patch: "test patch",
        explanation: "test explanation",
      }),
    ),
});

const TestLive = Layer.mergeAll(
  Smithers.sqlite({ filename: ":memory:" }),
  CoderTest,
);
The workflow definition does not change. Only the provided layers differ between production and test.

Services in TOON

TOON workflows can use Effect services through handler: functions and the imports: block:
imports:
  services[1]{from,use}:
    ./services/coder.ts,Coder

steps[1]:
  - id: analyze
    handler: ./handlers/analyze.ts#analyzeWithCoder
    output:
      summary: string
      severity: "low" | "medium" | "high"
The handler function accesses the service through normal Effect patterns:
export const analyzeWithCoder = ({ input }) =>
  Effect.gen(function* () {
    const coder = yield* Coder;
    return yield* coder.analyze(input.description);
  });

Design Principles

  • Services own capabilities, not workflow data. Step context provides workflow data; services provide infrastructure.
  • Layers compose. Build your service graph with Layer.mergeAll, Layer.provide, and Layer.effect.
  • Test by swapping layers. The same workflow runs against live and mock services without code changes.
  • Keep services focused. A service should represent one capability (model calls, VCS, tickets), not a grab-bag of utilities.

Next Steps

  • Context — How services interact with step context.
  • Builder API — Using services inside builder steps.
  • Installation — Setting up Effect and provider packages.