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 aLanguageModelfor 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 aLanguageModelfor 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 aLanguageModelfor 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)