Skip to main content
This guide builds a two-step workflow that researches a topic and writes a report using the low-level Effect builder.

Step 1: Define Models and Services

Create workflow.ts:
import { Context, Effect, Layer, Schedule, Schema } from "effect";
import { Model } from "@effect/sql";
import { Smithers } from "smithers-orchestrator";

class Input extends Schema.Class<Input>("Input")({
  topic: Schema.String,
}) {}

class Research extends Model.Class<Research>("Research")({
  summary: Schema.String,
  keyPoints: Schema.Array(Schema.String),
}) {}

class Report extends Model.Class<Report>("Report")({
  title: Schema.String,
  body: Schema.String,
  wordCount: Schema.Number,
}) {}

class Researcher extends Context.Tag("Researcher")<
  Researcher,
  {
    readonly research: (
      topic: string,
    ) => Effect.Effect<Research>;
  }
>() {}

class Writer extends Context.Tag("Writer")<
  Writer,
  {
    readonly write: (
      research: Research,
    ) => Effect.Effect<Report>;
  }
>() {}

Step 2: Build the Workflow Graph

export const ResearchReport = Smithers.workflow({
  name: "research-report",
  input: Input,
}).build(($) => {
  const research = $.step("research", {
    output: Research,
    run: ({ input }) =>
      Effect.gen(function* () {
        const researcher = yield* Researcher;
        return yield* researcher.research(input.topic);
      }),
    retry: Schedule.exponential("250 millis").pipe(
      Schedule.intersect(Schedule.recurs(2)),
    ),
  });

  const report = $.step("report", {
    output: Report,
    needs: { research },
    run: ({ research }) =>
      Effect.gen(function* () {
        const writer = yield* Writer;
        return yield* writer.write(research);
      }),
  });

  return $.sequence(research, report);
});
The important builder concepts are:
  • research is a typed step handle, not a string id
  • needs: { research } declares an explicit dependency edge
  • services are resolved through Context.Tag, not a global workflow context

Step 3: Provide Live Services

const ResearcherLive = Layer.succeed(Researcher, {
  research: (topic) =>
    Effect.succeed(
      new Research({
        summary: `${topic} is a systems programming topic.`,
        keyPoints: [
          "Prioritizes explicitness over hidden behavior.",
          "Tooling aims to improve reliability.",
          "Often discussed in low-level programming contexts.",
        ],
      }),
    ),
});

const WriterLive = Layer.succeed(Writer, {
  write: (research) =>
    Effect.succeed(
      new Report({
        title: "Research Summary",
        body: `${research.summary}\n\nKey points:\n- ${research.keyPoints.join("\n- ")}`,
        wordCount: 43,
      }),
    ),
});

export const AppLive = Layer.mergeAll(
  Smithers.sqlite({ filename: "./smithers.db" }),
  ResearcherLive,
  WriterLive,
);

Step 4: Execute

Create main.ts:
import { Effect } from "effect";
import { AppLive, Input, ResearchReport } from "./workflow";

const result = await Effect.runPromise(
  ResearchReport.execute(
    new Input({ topic: "The history of the Zig programming language" }),
  ).pipe(Effect.provide(AppLive)),
);

console.log(result.title);
console.log(result.body);
Run it:
bun run main.ts

Next Steps