Skip to content
Docs menu / Execution Planning

Execution Planning

Imagine that we’ve refactored our generateDadJoke program from our Getting Started guide. Now, instead of handling all errors internally, the code can fail with domain-specific issues like network interruptions or provider outages:

import type { LanguageModel } from "@effect/ai"
import { OpenAiLanguageModel } from "@effect/ai-openai"
import { Data, Effect } from "effect"
class NetworkError extends Data.TaggedError("NetworkError") {}
class ProviderOutage extends Data.TaggedError("ProviderOutage") {}
declare const generateDadJoke: Effect.Effect<
LanguageModel.GenerateTextResponse<{}>,
NetworkError | ProviderOutage,
LanguageModel.LanguageModel
>
const main = Effect.gen(function* () {
const response = yield* generateDadJoke
console.log(response.text)
}).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o")))

This is fine, but what if we want to:

  • Retry the program a fixed number of times on NetworkErrors
  • Add some backoff delay between retries
  • Fallback to a different model provider if OpenAi is down

How can we accomplish such logic?

Planning LLM Interactions

The ExecutionPlan module from Effect provides a robust method for creating structured execution plans for your Effect programs. Rather than making a single model call and hoping that it succeeds, you can use ExecutionPlan to describe how to handle errors, retries, and fallbacks in a clear, declarative way.

This is especially useful when:

  • You want to fall back to a secondary model if the primary one is unavailable
  • You want to retry on transient errors (e.g. network failures)
  • You want to control timing between retry attempts

Creating Execution Plans

To create an ExecutionPlan, we can use the ExecutionPlan.make constructor.

Example (Creating an ExecutionPlan for LLM Interactions)

import type { LanguageModel } from "@effect/ai"
import { OpenAiLanguageModel } from "@effect/ai-openai"
import { Data, Effect, ExecutionPlan, Schedule } from "effect"
9 collapsed lines
class NetworkError extends Data.TaggedError("NetworkError") {}
class ProviderOutage extends Data.TaggedError("ProviderOutage") {}
declare const generateDadJoke: Effect.Effect<
LanguageModel.GenerateTextResponse<{}>,
NetworkError | ProviderOutage,
LanguageModel.LanguageModel
>
const DadJokePlan = ExecutionPlan.make({
provide: OpenAiLanguageModel.model("gpt-4o"),
attempts: 3,
schedule: Schedule.exponential("100 millis", 1.5),
while: (error: NetworkError | ProviderOutage) => error._tag === "NetworkError",
})
// ┌─── Effect<void, NetworkError | ProviderOutage, OpenAiClient>
// ▼
const main = Effect.gen(function* () {
const response = yield* generateDadJoke
console.log(response.text)
}).pipe(Effect.withExecutionPlan(DadJokePlan))

This plan contains a single step which will:

  • Provide OpenAi’s "gpt-4o" model as a LanguageModel for the program
  • Attempt to call OpenAi up to 3 times
  • Wait with an exponential backoff between attempts (starting at 100ms)
  • Only re-attempt the call to OpenAi if the error is a NetworkError

Adding Fallback Models

To make your interactions with large language models resilient to provider outages, you can define a fallback models to use. This will allow the plan to automatically fallback to another model if the previous step in the execution plan fails.

Use this when:

  • You want to make your model interactions resilient to provider outages
  • You want to potentially have multiple fallback models

Example (Adding a Fallback to Anthropic from OpenAi)

import type { LanguageModel } from "@effect/ai"
import { AnthropicLanguageModel } from "@effect/ai-anthropic"
import { OpenAiLanguageModel } from "@effect/ai-openai"
import { Data, Effect, ExecutionPlan, Schedule } from "effect"
9 collapsed lines
class NetworkError extends Data.TaggedError("NetworkError") {}
class ProviderOutage extends Data.TaggedError("ProviderOutage") {}
declare const generateDadJoke: Effect.Effect<
LanguageModel.GenerateTextResponse<{}>,
NetworkError | ProviderOutage,
LanguageModel.LanguageModel
>
const DadJokePlan = ExecutionPlan.make(
{
provide: OpenAiLanguageModel.model("gpt-4o"),
attempts: 3,
schedule: Schedule.exponential("100 millis", 1.5),
while: (error: NetworkError | ProviderOutage) => error._tag === "NetworkError",
},
{
provide: AnthropicLanguageModel.model("claude-4-sonnet-20250514"),
attempts: 2,
schedule: Schedule.exponential("100 millis", 1.5),
while: (error: NetworkError | ProviderOutage) => error._tag === "ProviderOutage",
},
)
// ┌─── Effect<..., ..., AnthropicClient | OpenAiClient>
// ▼
const main = Effect.gen(function* () {
const response = yield* generateDadJoke
console.log(response.text)
}).pipe(Effect.withExecutionPlan(DadJokePlan))

This plan contains two steps.

Step 1

The first step will:

  • Provide OpenAi’s "gpt-4o" model as a LanguageModel for the program
  • Attempt to call OpenAi up to 3 times
  • Wait with an exponential backoff between attempts (starting at 100ms)
  • Only attempt the call to OpenAi if the error is a NetworkError

If all of the above logic fails to run the program successfully, the plan will try to run the program using the second step.

Step 2

The second step will:

  • Provide Anthropic’s "claude-4-sonnet-20250514" model as a LanguageModel for the program
  • Attempt to call Anthropic up to 2 times
  • Wait with an exponential backoff between attempts (starting at 100ms)
  • Only attempt the fallback if the error is a ProviderOutage

End-to-End Usage

The following is the complete program with the desired execution plan fully implemented:

import type { LanguageModel } from "@effect/ai"
import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic"
import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"
import { NodeHttpClient } from "@effect/platform-node"
import { Config, Data, Effect, ExecutionPlan, Layer, Schedule } from "effect"
class NetworkError extends Data.TaggedError("NetworkError") {}
class ProviderOutage extends Data.TaggedError("ProviderOutage") {}
declare const generateDadJoke: Effect.Effect<
LanguageModel.GenerateTextResponse<{}>,
NetworkError | ProviderOutage,
LanguageModel.LanguageModel
>
const DadJokePlan = ExecutionPlan.make(
{
provide: OpenAiLanguageModel.model("gpt-4o"),
attempts: 3,
schedule: Schedule.exponential("100 millis", 1.5),
while: (error: NetworkError | ProviderOutage) => error._tag === "NetworkError",
},
{
provide: AnthropicLanguageModel.model("claude-4-sonnet-20250514"),
attempts: 2,
schedule: Schedule.exponential("100 millis", 1.5),
while: (error: NetworkError | ProviderOutage) => error._tag === "ProviderOutage",
},
)
const main = Effect.gen(function* () {
const response = yield* generateDadJoke
console.log(response.text)
}).pipe(Effect.withExecutionPlan(DadJokePlan))
const Anthropic = AnthropicClient.layerConfig({
apiKey: Config.redacted("ANTHROPIC_API_KEY"),
}).pipe(Layer.provide(NodeHttpClient.layerUndici))
const OpenAi = OpenAiClient.layerConfig({
apiKey: Config.redacted("OPENAI_API_KEY"),
}).pipe(Layer.provide(NodeHttpClient.layerUndici))
main.pipe(Effect.provide([Anthropic, OpenAi]), Effect.runPromise)