← All AI Engineer talks

AI Engineer World's Fair 2025

Building Reliable Support Agents Using the Effect TypeScript Library - Michael Fester

Read the talk

Building reliable support agents with Effect

At 14.ai, customer support agents combine uncertain model decisions with deterministic workflows, typed service dependencies, and explicit failure handling in TypeScript.

From a talk by Michael Fester

Before you start: Familiarity with TypeScript, API calls, and LLM tool calling will help; prior experience with Effect is not required.

Reliability when the agent talks directly to customers

A customer support agent interacts directly with an end user, but its next step may depend on an unreliable API or a nondeterministic model response. Add dependencies between services and workflows that run for a long time, and TypeScript’s static types alone do not describe everything that can go wrong. This is the setting at 14.ai, the AI-native customer support platform Michael Fester introduces as its co-founder and CTO.

The team uses Effect to make those operational concerns composable: type guarantees across the stack, concurrency, streaming, interruption, retries, structured errors, and dependency injection. These are pieces for building a system whose behavior remains understandable when individual operations fail. Effect also integrates with OpenTelemetry; useful observability still requires instrumentation and SDK/exporter configuration.

Slide titled “Why we use Effect” lists six capabilities in two columns, with the presenter on the right.
Why use Effect: type guarantees, composition, concurrency, error modeling, dependency injection, and observability.

Adoption does not require replacing an entire application. Fester’s team chose Effect partly because it can enter an existing TypeScript codebase gradually, while supporting more stable, testable, and maintainable code as the platform grows.

0:010:16
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

0:01 · section reference included

One schema contract across the stack

Effect spans the platform rather than sitting only around model calls. The React frontend covers dashboards, the agent IDE, knowledge management, insights, analytics, and SDKs. Behind it, different interfaces serve application traffic, public integrations, data ingestion, and agent execution.

ComponentImplementation and responsibility
Internal application serverEffect RPC, paired with a modified TanStack Query frontend
Public APIEffect HTTP, with OpenAPI documentation generated from annotated schemas
Data processing engineSyncs CRMs, documents, and databases for real-time analytics and reporting
Agent workflowsCustom Effect DSL mixing deterministic and nondeterministic behavior
StoragePostgres for data and vectors; Effect SQL for queries

These components give the platform distinct places to handle application logic, external data, workflow execution, and persistence.

The shared contract is Effect Schema. Fester describes modeling everything with schemas to support runtime validation, encoding, decoding, typed inputs and outputs, and generated documentation. A schema describes the data; decoding or validation operations must actually interpret that description at runtime. The same contract can therefore support both TypeScript consumers and checks at external boundaries. On top of this stack, the agents operate as planners.

1:221:38
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

1:22 · section reference included

Let the planner choose a deterministic procedure

The agent receives user input, forms a plan, chooses an action, workflow, or sub-agent, executes it, and repeats until the task is complete. An action is a small unit of execution, such as fetching payment information or searching logs—the equivalent of a focused tool call.

A workflow packages a deterministic sequence. Canceling a subscription, for example, can require:

  1. Collecting the customer’s reason.
  2. Offering a retention option, if applicable.
  3. Checking eligibility.
  4. Performing the cancellation.

The planner can choose this workflow without having to improvise its internal procedure each time.

Sub-agents group related actions and workflows into domain modules, such as billing or log retrieval. To express the behavior inside those modules, the team built a custom DSL on Effect’s functional, pipe-based composition system. It represents branching, sequencing, retries, state transitions, and memory. The architecture combines flexible selection of what to do with explicit control over how a selected procedure proceeds.

2:302:48
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

2:30 · section reference included

Remember failed providers and split response streams

If one LLM provider fails, the platform can fall back to another with suitable performance characteristics. Fester gives GPT-4o mini falling back to Gemini Flash 2.0 for tool calling as an example. That pairing reflects the team’s engineering judgment; the talk supplies no measured comparison of their tool-calling performance.

The retry policy carries state. The team tracks failed providers so that subsequent attempts avoid them, rather than repeatedly selecting a provider already known to have failed. That exclusion behavior belongs to their routing policy: scheduled retries alone do not automatically maintain a failed-provider set.

Response delivery has another requirement: tokens need to reach the customer while also being retained internally. The team duplicates the token stream, sending one branch directly to the user and another to storage for uses such as analytics. Effect supplies the composition tools for this arrangement; the talk does not specify its buffering or persistence guarantees.

3:313:47
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

3:31 · section reference included

Swap dependencies without rewriting the agent

Failure handling needs deliberate tests. The team uses dependency injection to replace live LLM providers with mocks and simulate failure scenarios without changing system internals. This makes a provider outage something the test can arrange, rather than something engineers must wait to encounter.

The development process starts with input, output, and error schemas, including their encoding and decoding behavior. Those definitions make contracts explicit and support generated documentation. Services are then composed and supplied at system entry points, keeping the choice of implementation separate from the logic that consumes it.

Effect represents required services in the type of a computation. A caller must supply those requirements before running it, while modular service implementations can be replaced or overridden. The following TypeScript example illustrates that boundary using current Effect v3 APIs, rather than reconstructing the talk’s unspecified library version. The request stays the same; supplying a different Llm implementation changes whether the computation produces a plan or a typed failure.

typescript

import { Context, Effect } from "effect";

type ProviderFailure = {
  readonly _tag: "ProviderFailure";
  readonly message: string;
};

class Llm extends Context.Tag("Llm")<
  Llm,
  {
    readonly plan: (
      request: string
    ) => Effect.Effect<string, ProviderFailure>;
  }
>() {}

const planCancellation = Effect.gen(function* () {
  const llm = yield* Llm;
  return yield* llm.plan("Cancel my subscription");
});

const available: typeof Llm.Service = {
  plan: () => Effect.succeed("Collect cancellation reason"),
};

const unavailable: typeof Llm.Service = {
  plan: () =>
    Effect.fail({
      _tag: "ProviderFailure",
      message: "Provider unavailable",
    }),
};

const planned = Effect.provideService(
  planCancellation,
  Llm,
  available
);

const failed = Effect.provideService(
  planCancellation,
  Llm,
  unavailable
);

export const scenarios = Effect.all({
  available: Effect.either(planned),
  unavailable: Effect.either(failed),
});

Both scenarios use identical consuming logic. The successful mock returns a proposed next step, not an executed cancellation; the failing mock exposes an error outcome for inspection.

Fester also reports that these guardrails help engineers who are new to TypeScript become productive. There is an initial learning curve, but once engineers understand the framework, its constraints make common mistakes and bad patterns harder to introduce.

4:094:24
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

4:09 · section reference included

Clean composition can still hide failures

Effect makes happy-path code look clean and explicit, but that readability can create a false sense of safety. An error caught upstream—or somewhere a reader does not see—can silently conceal an important failure. A well-typed composition still needs an intentional error policy. Engineers must understand which failures remain visible and which handlers consume them.

Dependency injection creates a related navigation problem at scale. Replacing a service is straightforward in a small example; tracing where it was provided becomes harder across multiple layers and subsystems. Modularity does not remove the need to understand the application’s service wiring.

The ecosystem itself is substantial. Its concepts and tools can overwhelm newcomers, even though Fester finds that the benefits compound once they start to click. His qualification is practical: Effect helps build predictable, resilient systems, but it does not replace engineering judgment.

“Lessons learned” slide with three columns covering false safety on happy paths, difficult service tracing, and the learning curve; a closing line says Effect is not magic.
Lessons learned: happy paths can mislead, dependency injection can get messy, and the learning curve is real.
5:305:44
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

5:30 · section reference included

Start with one service or endpoint

The practical entry point is a single service or endpoint. Introduce Effect there, learn how its contracts and failure handling fit the application, and expand from a working boundary. A system does not need to adopt the entire ecosystem on day one.

LLM applications make this approach especially useful because nondeterministic behavior increases the value of predictable, observable surrounding systems. Effect brings functional-programming rigor into production TypeScript without requiring developers to become functional-programming purists. Start small, then let the benefits accumulate as adoption expands.

6:356:45
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

6:35 · section reference included

Resources

From the talk

Read the complete timestamped transcript
  1. 0:01

    Hi, I'm Michael, co-founder and CTO of 14.ai. We're building an AI native customer support platform. Our systems interact directly with end users, rely on LLMs in production, and have to operate reliably under uncertain conditions.

  2. 0:16

    To manage that complexity, we use Effect, a TypeScript library designed for building robust, type safe, and composable systems. The TypeScript language itself gives us a great foundation, but it starts to fall short when we're dealing with unre-reliable APIs, complex dependencies between systems, non-deterministic model outputs, or long-running workflows.

  3. 0:38

    Effect gives us the tools to handle such situations confidently as our platform evolves. Among other things, it provides strong type guarantees across the stack, powerful composition primitives, built-in concurrency, streaming, interruptions, and retry mechanisms, structured error modeling, and a clean dependency injection system that makes testing and modularization much easier.

  4. 1:02

    It also offers very easy observability via OpenTelemetry.

  5. 1:08

    In addition to helping us build more stable, testable, and maintainable code at scale, we chose Effect also because it can be gradually adopted in an existing code base and feels like a natural extension of TypeScript itself.

  6. 1:22

    So quickly about our architecture. We use Effect across our entire stack. Here are how the main components fit together. We have a React front end that powers everything from dashboards to the agent's ID, knowledge management, insights, analytics, and SDKs.

  7. 1:38

    Then we have an internal RPC server that handles app logic built on Effect RPC and a modified version of TanStack Query on the front end. Our public API server uses Effect HTTP with an OpenAPI docs auto-generated from annotated schemas.

  8. 1:54

    Our data processing engine syncs data from CRMs, docs, and databases and processes it for real-time analytics and reporting. Agent workflows are written in a custom DSL built on Effect, allowing us to mix deterministic and non-deterministic behavior.

  9. 2:09

    And finally, we use a Postgres database for both data and vector storage with Effect SQL handling queries.

  10. 2:16

    Everything is modeled using Effect schemas, so we get runtime validation, encoding, decoding, type-safe input and output handling across the stack, and even auto-generated documentation for free. Our agents are basically planners.

  11. 2:30

    They take input from the user, come up with a plan, choose the right action, workflow, or sub a-agent, execute them, and repeat until the task is complete. Actions are small, focused units of execution, like fetching payment info or searching through logs.

  12. 2:48

    You can think of them like tool calls. Workflows are deterministic multi-step processes. For instance, canceling a subscription might involve collecting a reason, offering a retention option if applicable, checking eligibility, and then performing the cancellation.

  13. 3:04

    Finally, sub-agents group together related actions and workflows into larger domain-specific modules, like a billing agent or a log retrieval agent.

  14. 3:13

    In order to model this complexity, we built a domain-specific language for workflows using Effect's functional pipe-based system as the foundation. And this lets us express things like branching, sequencing, retries, state transitions, and memory in a very clear and composable way.

  15. 3:31

    Now, as you're powering mission-critical systems, reliability really is key. If one LLM provider fails, for instance, we fall back to another provider with similar performance characteristics. For instance, GPT-4o mini could fall back to Gemini Flash 2.0 for tool calling.

  16. 3:47

    We model this with retry policies which also track state, so we avoid retrying failed providers. Oftentimes, answers are streamed to the end user. We, for that purpose, duplicate token streams, one directly to the user and one for storage on our side, for instance, analytics.

  17. 4:05

    Effect allows us to do that very easily.

  18. 4:09

    For testing, we make heavy use of dependency injection to mock LLM providers and simulate failure scenarios. With this DI approach, we can easily swap services providers with mock versions without actually affecting the internals of our system.

  19. 4:24

    This brings us to developer experience. With all these tools available, the developer experience for building agentic systems in Effect really is excellent. First of all, it's schema-centric, so we define input, output, and error types upfront, and those schemas come with powerful encoding and decoding built in.

  20. 4:40

    They provide strong type safety guarantees, and they're automatically documented, so we can get clarity and consistency without extra effort. Dependency injection, obviously, is also a strong point. So services are provided at the entry point of our systems that can be composed however we need and easily mocked for testing.

  21. 4:58

    Dependencies are present at the type level, guarantee that compile time that all required services are provided. Services themselves are modular and composable, which makes it easy to override behavior or swap in different implementations without affecting the internals of our system.

  22. 5:14

    And finally, Effect provides very strong guardrails. Engineers who are new to TypeScript can become productive quickly because the framework helps prevent common mistakes. Once you get past the initial learning curve, it's harder to fall into bad patterns.

  23. 5:30

    A bit about the lessons learned here. So Effect is powerful, but using it well takes discipline. It's really nice to write code for the happy path. Things look really clean and explicit, and everything seems to just flow.

  24. 5:44

    But that can also give you a false sense of safety. It's easy to accidentally catch errors somewhere upstream or out of sight and silently lose important failures if you're not careful.

  25. 5:56

    Dependency injection is another area, I would say, where it can be hard sometimes to grasp at scale. It's great in principle, but tracing where services are provided, especially when you're dealing with multiple layers or subsystems, can become kind of hard to follow.

  26. 6:11

    The learning curve is real. Effect is a big ecosystem with a lot of concepts and tools. It can be overwhelming at first, but really once you get past this initial bump, things just somehow start to click, and from that point on, the benefits compound.

  27. 6:27

    So at the end of the day, Effect helps us build systems that are predictable and resilient, but it's not magic. You still have to think.

  28. 6:35

    One of the great things about Effect is that you don't have to go all in on day one. You can adopt it incrementally. Start with a single service or endpoint and build from there.

  29. 6:45

    Effect is especially useful for LLM and AI-based systems where reliability and coping with non-determinism really matter. You want your systems to be predictable and observable, and Effect gives you the right tools to make it happen.

  30. 6:59

    It also brings the rigor of functional programming into real-world TypeScript, but in a way that's practical for production use. You don't have to be a functional programming purist to get a lot of value.

  31. 7:09

    Just start small and let the benefits build up over time.

  32. 7:14

    So if you're building with agents, LLMs, or exploring Effect, feel free to get in touch. Thank you.