> ## Documentation Index
> Fetch the complete documentation index at: https://smithers.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Effect API

> Build Smithers workflows as first-class graph values, no JSX or React.

The Effect API is a lower-level authoring surface for teams already modeling
application logic with `Effect`, `Layer`, and `Schema`. It shares the JSX
runtime (SQLite persistence, no rerun of completed work on resume,
schema-validated outputs, dependency-driven scheduling), differing only in
authoring style: every step, approval, sequence, parallel block, match,
branch, loop, worktree, and scope is an ordinary value you can export, return
from a function, or compose with others.

Use JSX for most workflows; use the Effect API inside an Effect service, when
step bodies should return `Effect` values directly, or for a React-free API
over generated workflow definitions.

## Minimal workflow

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Smithers } from "smthrs";
import { Effect, Schema } from "effect";

const inputSchema = Schema.Struct({
  repo: Schema.String,
  sha: Schema.String,
});

const analysisSchema = Schema.Struct({
  summary: Schema.String,
  risk: Schema.Literals(["low", "medium", "high"]),
});

const reportSchema = Schema.Struct({
  markdown: Schema.String,
});

const G = Smithers.workflow({
  name: "repo-review",
  input: inputSchema,
});

const analyze = G.step("analyze", {
  output: analysisSchema,
  timeout: "2m",
  retry: { maxAttempts: 3, backoff: "exponential", initialDelay: "1s" },
  run: ({ input, heartbeat }) =>
    Effect.gen(function* () {
      heartbeat({ phase: "analyzing" });
      yield* Effect.log(`Reviewing ${input.repo}@${input.sha}`);
      return { summary: "Found one risky migration.", risk: "medium" as const };
    }),
});

const report = G.step("report", {
  needs: { analyze },
  output: reportSchema,
  run: ({ analyze }) => ({
    markdown: `# Review\n\n${analyze.summary}\n\nRisk: ${analyze.risk}`,
  }),
});

export const reviewWorkflow = G.from(G.sequence(analyze, report));

const result = await Effect.runPromise(
  reviewWorkflow
    .execute(
      { repo: "acme/api", sha: "abc123" },
      { runId: "review-abc123" },
    )
    .pipe(Effect.provide(Smithers.sqlite({ filename: "smithers.db" }))),
);
```

`Smithers.workflow(opts)` returns a typed handle `G`. Every constructor
(`G.step`, `G.approval`, `G.sequence`, `G.parallel`, `G.match`, `G.branch`,
`G.loop`, `G.worktree`, `G.scope`) returns a graph value. `G.from(graph)`
finalizes the workflow into an executable one.

`execute()` returns an `Effect` whose success value is the decoded output of
the final graph node: a step's own output, a sequence's last child, a
parallel block's tuple. If it stops on an approval or timer instead, the
value is a `RunResult` with waiting status.

## Steps and dependencies

Steps are values:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const analyze = G.step("analyze", {
  output: analysisSchema,
  run: ({ input }) => analyzeRepo(input.repo, input.sha),
});
```

`input` is typed from the workflow's input schema; the step's output type is
inferred from `output` and flows into anything listing this step in `needs`:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const report = G.step("report", {
  needs: { analyze },
  output: reportSchema,
  run: ({ analyze }) => ({
    markdown: renderReport(analyze.summary, analyze.risk),
  }),
});
```

Step IDs are durable: changing one creates a new task and leaves the old
persisted output behind. A step may return a plain value, `Promise`, or
`Effect`; Smithers decodes the result with the step's `output` schema before
writing it.

Step context: `input`, dependency values, `executionId`, `stepId`, `attempt`,
`iteration`, `signal`, `heartbeat(data)`, and `lastHeartbeat`.

## Control flow

`G.sequence(...nodes)` runs ordered work; `G.parallel(...nodes, { maxConcurrency })`
runs concurrent work and returns a tuple of child results.

`G.match(source, { when, then, else })` selects between two statically-known
branches based on a completed step's output; both compile into the graph, but
only the matching one executes.

`G.branch({ condition, needs, then, else })` is `G.match` for an arbitrary
`needs` context instead of a single source step.

`G.loop({ id, children, until, maxIterations, onMaxReached })` repeats a fragment
(not nestable) until the predicate returns true. `onMaxReached`
(`'fail'` or `'return-last'`) governs `maxIterations` overflow; the default
returns the last iteration's outputs rather than failing.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
export const reviewWorkflow = G.from(
  G.sequence(
    analyze,
    G.match(analyze, {
      when: (analysis) => analysis.risk === "high",
      then: G.approval("approve-high-risk", {
        needs: { analyze },
        request: ({ analyze }) => ({
          title: "Approve high-risk review",
          summary: analyze.summary,
        }),
        onDeny: "fail",
      }),
      // else: report (omitting `else` means the match falls through to the next sibling in the sequence)
    }),
    report,
  ),
);
```

## Worktrees

`G.worktree({ id, path, branch, skipIf, needs, children })` runs `children`
inside a git worktree, created beforehand and torn down after.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
G.worktree({
  id: "review-shard",
  path: "scratch/review",
  children: G.sequence(read, summarize),
});
```

## Reuse

Static reuse is a graph value:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const reviewShard = G.sequence(read, summarize);
```

Parameterized reuse is a function returning a graph value:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const makeReviewShard = (params: { path: string }) =>
  G.sequence(
    G.step("read", {
      output: diffSchema,
      run: ({ input }) => readDiff(input.repo, params.path),
    }),
    G.step("summarize", {
      needs: { read },
      output: summarySchema,
      run: ({ read }) => summarizeDiff(read),
    }),
  );
```

Multi-mount reuse is `G.scope(instanceId, fragment)`: the compiler prefixes
every step and approval ID in the fragment with `instanceId.`, so
`G.scope('api', makeReviewShard(...))` produces `api.read` and `api.summarize`.
The same fragment mounts under multiple scopes without collision:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
G.parallel(
  G.scope("api", makeReviewShard({ path: "packages/api" })),
  G.scope("web", makeReviewShard({ path: "apps/web" })),
);
```

## Cross-workflow fragments

Fragments that need to live across workflows with different inputs are built
with `Smithers.fragment(inputSchema)`:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const F = Smithers.fragment(diffInputSchema);

const readDiff = F.step("read-diff", {
  output: diffSchema,
  run: ({ input }) => readDiff(input.path),
});

const summarize = F.step("summarize", {
  needs: { readDiff },
  output: summarySchema,
  run: ({ readDiff }) => summarizeDiff(readDiff),
});

export const reviewShard = F.sequence(readDiff, summarize);
```

`Smithers.fragment` exposes the same constructors as a workflow handle (`step`,
`approval`, `sequence`, `parallel`, `match`, `branch`, `loop`, `worktree`,
`scope`) but no `from`: fragments are values, compiled only once mounted into
a real workflow:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const G = Smithers.workflow({ name: "repo-review", input: workflowInputSchema });

export const reviewWorkflow = G.from(
  G.parallel(
    G.scope("api", reviewShard),
    G.scope("web", reviewShard),
  ),
);
```

A fragment's input schema lets TypeScript infer each step's `input` type; at runtime it's never read or validated, since steps receive the host workflow's input directly. That input must be assignable to the fragment's schema, or TypeScript errors at compile time when mounting a fragment with fields the host input doesn't satisfy.

## Operational notes

* Provide exactly one persistence layer with `Effect.provide(Smithers.sqlite({ filename }))`.
* Keep step IDs stable across releases; use new IDs for materially different work.
* Use `heartbeat()` in long-running steps and honor `signal` in external calls.
* Use `retry`, `retryPolicy`, `timeout`, `skipIf`, and `cache` as on JSX tasks
  (see [JSX Task options](/components/task#retry) for the shared option shape).
* All graph values support `.pipe(...fns)` for future data-last combinators.
* Prefer idempotent step bodies: for external side effects, use `executionId`,
  `stepId`, and `attempt` when constructing idempotency keys.
* `G.match` selects graph topology: both branches must be statically knowable
  so durable IDs stay stable across resume, unlike Effect's `Match` module,
  which does runtime value pattern matching.
