oneie
Version:
Build apps, websites, and AI agents in English. Zero-interaction setup for AI agents (Claude Code, Cursor, Windsurf). Download to your computer, run in the cloud, deploy to the edge. Open source and free forever.
1,799 lines (1,507 loc) • 61.5 kB
Markdown
---
title: Effect Patterns Reference
dimension: things
category: plans
tags: ai
related_dimensions: connections, events, knowledge
scope: global
created: 2025-11-03
updated: 2025-11-03
version: 1.0.0
ai_context: |
This document is part of the things dimension in the plans category.
Location: one/things/plans/effect-patterns-reference.md
Purpose: Documents effect.ts patterns reference for one platform
Related dimensions: connections, events, knowledge
For AI agents: Read this to understand effect patterns reference.
---
# Effect.ts Patterns Reference for ONE Platform
**Version:** 1.0.0
**Last Updated:** 2025-10-30
**Status:** Production-Ready Patterns
This is your practical handbook for using Effect.ts in the ONE Platform. Find patterns, copy examples, ship features.
---
## Section 1: Quick Decision Trees
### When to Use Effect vs Plain TypeScript
```
START HERE
│
├─ Does it involve async operations?
│ └─ YES → Continue
│ └─ NO → Use plain TypeScript (no Effect needed)
│
├─ Does it need error handling?
│ └─ YES → Use Effect (tagged errors > try/catch)
│ └─ NO → Continue
│
├─ Does it compose with other operations?
│ └─ YES → Use Effect (Effect.gen, Effect.all)
│ └─ NO → Continue
│
├─ Does it need retry logic?
│ └─ YES → Use Effect (Schedule.exponential)
│ └─ NO → Continue
│
├─ Does it need dependency injection?
│ └─ YES → Use Effect (Context, Layer)
│ └─ NO → Continue
│
└─ Does it manage resources (connections, streams)?
└─ YES → Use Effect (Scope, acquireRelease)
└─ NO → Plain async/await is fine
```
**Simple Rule:** If it's just data transformation or basic async, use plain TypeScript. If it has complex error handling, composition, or resource management → Effect.
### When to Wrap Components vs Use Directly
```
CONVEX COMPONENT DECISION
│
├─ @convex-dev/agent
│ └─ Wrap if: Multi-agent orchestration, custom error handling
│ └─ Direct if: Single agent, simple chat
│
├─ @convex-dev/workflow
│ └─ Wrap if: Business logic in steps needs composition
│ └─ Direct if: Simple sequential steps
│
├─ @convex-dev/rag
│ └─ Wrap if: Composing search with other services
│ └─ Direct if: Standalone vector search
│
├─ @convex-dev/rate-limiter
│ └─ Wrap if: Multiple rate limits, custom retry logic
│ └─ Direct if: Single rate limit check
│
├─ @convex-dev/persistent-text-streaming
│ └─ Direct: Agent component has built-in streaming (use that)
│
├─ @convex-dev/retrier
│ └─ Wrap if: Coordinating with Effect.retry (hybrid approach)
│ └─ Direct if: Action-level retries only
│
├─ @convex-dev/workpool
│ └─ Wrap if: Batch enqueuing, error handling
│ └─ Direct if: Simple task queue
│
└─ @convex-dev/crons
└─ Wrap if: Cron job logic needs composition
└─ Direct if: Simple scheduled mutations
```
**Golden Rule:** Components handle infrastructure (durability, retries, streaming). Effect handles business logic (composition, error handling, orchestration).
### When to Use Confect vs Manual Wrapping
```
CONFECT DECISION TREE
│
├─ Is this a NEW project?
│ └─ YES → Consider Confect (schema-first, full Effect integration)
│ └─ NO → Continue
│
├─ Is your team experienced with Effect?
│ └─ YES → Consider Confect
│ └─ NO → Manual wrapping (easier learning curve)
│
├─ Do you want Option<T> instead of T | null everywhere?
│ └─ YES → Confect
│ └─ NO → Manual wrapping
│
├─ Do you need Effect schemas instead of Convex validators?
│ └─ YES → Confect
│ └─ NO → Manual wrapping
│
└─ Is this an EXISTING Convex project?
└─ YES → Manual wrapping (incremental adoption)
└─ NO → Consider Confect
```
**Recommendation for ONE Platform:** Start with manual wrapping. Confect is powerful but adds learning overhead. Prove the patterns work with manual integration first.
---
## Section 2: Core Patterns with Examples
### Pattern 1: Effect-Wrapped Component ✅ PRODUCTION-PROVEN
**When:** Wrapping Convex components for type-safe error handling and composition.
**Example: Wrapping @convex-dev/agent**
```typescript
// backend/convex/services/agent.service.ts
import { Effect, Context, Layer, Data } from "effect";
import { Agent } from "@convex-dev/agent";
import { openai } from "@ai-sdk/openai";
import { components } from "../_generated/api";
// 1. Define tagged errors
class AgentError extends Data.TaggedError("AgentError")<{
agentName: string;
cause: unknown;
}> {}
// 2. Define service interface
class AgentService extends Context.Tag("AgentService")<
AgentService,
{
readonly generateResponse: (
ctx: ActionCtx,
threadId: string,
prompt: string,
) => Effect.Effect<string, AgentError>;
readonly createThread: (
ctx: ActionCtx,
userId: string,
) => Effect.Effect<string, AgentError>;
}
>() {}
// 3. Implement service layer
const AgentServiceLive = Layer.effect(
AgentService,
Effect.gen(function* () {
// Component handles infrastructure
const agent = new Agent(components.agent, {
name: "Support Agent",
chat: openai.chat("gpt-4o-mini"),
instructions: "You are a helpful assistant.",
tools: {
/* ... */
},
});
return {
generateResponse: (ctx, threadId, prompt) =>
// Effect provides composition
Effect.tryPromise({
try: async () => {
const { thread } = await agent.continueThread(ctx, { threadId });
const result = await thread.generateText({ prompt });
return result.text;
},
catch: (error) =>
new AgentError({
agentName: "Support Agent",
cause: error,
}),
}),
createThread: (ctx, userId) =>
Effect.tryPromise({
try: async () => {
const { threadId } = await agent.createThread(ctx, { userId });
return threadId;
},
catch: (error) =>
new AgentError({
agentName: "Support Agent",
cause: error,
}),
}),
};
}),
);
export { AgentService, AgentServiceLive, AgentError };
```
**Usage in API:**
```typescript
// backend/convex/api/agents.ts
import { action } from "../_generated/server";
import { v } from "convex/values";
import { Effect } from "effect";
import { AgentService, AgentServiceLive } from "../services/agent.service";
export const chat = action({
args: { threadId: v.string(), prompt: v.string() },
handler: (ctx, args) =>
Effect.gen(function* () {
const agentService = yield* AgentService;
const response = yield* agentService.generateResponse(
ctx,
args.threadId,
args.prompt,
);
return { response };
}).pipe(
Effect.provide(AgentServiceLive),
Effect.catchTag("AgentError", (error) =>
Effect.succeed({
response: "I'm having trouble. Please try again.",
error: true,
}),
),
Effect.runPromise,
),
});
```
---
### Pattern 2: Service Layer Pattern (Context.Tag + Layer.effect) ✅ PRODUCTION-PROVEN
**When:** Creating reusable, testable services with dependency injection.
**Example: RAG Service with Dependencies**
```typescript
// backend/convex/services/rag.service.ts
import { Effect, Context, Layer, Data } from "effect";
import { RAG } from "@convex-dev/rag";
import { openai } from "@ai-sdk/openai";
import { components } from "../_generated/api";
// 1. Define errors
class RAGError extends Data.TaggedError("RAGError")<{
operation: string;
cause: unknown;
}> {}
class EmbeddingError extends Data.TaggedError("EmbeddingError")<{
text: string;
cause: unknown;
}> {}
// 2. Define service interface
class RAGService extends Context.Tag("RAGService")<
RAGService,
{
readonly addDocument: (
ctx: ActionCtx,
namespace: string,
content: string,
metadata: Record<string, any>,
) => Effect.Effect<string, RAGError>;
readonly search: (
ctx: ActionCtx,
namespace: string,
query: string,
limit?: number,
) => Effect.Effect<{ text: string; entries: any[] }, RAGError>;
}
>() {}
// 3. Implement service
const RAGServiceLive = Layer.effect(
RAGService,
Effect.gen(function* () {
const rag = new RAG(components.rag, {
textEmbeddingModel: openai.embedding("text-embedding-3-small"),
embeddingDimension: 1536,
});
return {
addDocument: (ctx, namespace, content, metadata) =>
Effect.tryPromise({
try: async () => {
const result = await rag.add(ctx, {
namespace,
text: content,
filterValues: Object.entries(metadata).map(([name, value]) => ({
name,
value,
})),
});
return result.entryId;
},
catch: (error) =>
new RAGError({
operation: "addDocument",
cause: error,
}),
}),
search: (ctx, namespace, query, limit = 10) =>
Effect.tryPromise({
try: async () => {
const results = await rag.search(ctx, {
namespace,
query,
limit,
vectorScoreThreshold: 0.6,
});
return {
text: results.text,
entries: results.entries,
};
},
catch: (error) =>
new RAGError({
operation: "search",
cause: error,
}),
}),
};
}),
);
export { RAGService, RAGServiceLive, RAGError };
```
**Composing Services:**
```typescript
// backend/convex/api/contextual-chat.ts
import { action } from "../_generated/server";
import { Effect } from "effect";
import { AgentService, AgentServiceLive } from "../services/agent.service";
import { RAGService, RAGServiceLive } from "../services/rag.service";
export const contextualChat = action({
args: { userId: v.string(), question: v.string() },
handler: (ctx, args) =>
Effect.gen(function* () {
// Inject both services
const agentService = yield* AgentService;
const ragService = yield* RAGService;
// 1. Fetch context from RAG
const context = yield* ragService.search(
ctx,
args.userId, // User-specific namespace
args.question,
5,
);
// 2. Generate answer with context
const answer = yield* agentService.generateResponse(
ctx,
args.threadId,
`${args.question}\n\nContext: ${context.text}`,
);
return {
answer,
sources: context.entries.map((e) => e.entryId),
};
}).pipe(
// Provide both service implementations
Effect.provide(Layer.merge(AgentServiceLive, RAGServiceLive)),
Effect.catchAll((error) =>
Effect.succeed({
answer: "I'm having trouble finding information.",
error: true,
}),
),
Effect.runPromise,
),
});
```
---
### Pattern 3: Error Handling Pattern (Tagged Errors) ✅ PRODUCTION-PROVEN
**When:** Need type-safe error handling with automatic propagation.
**Example: Layered Error Handling**
```typescript
// backend/convex/domain/agents/errors.ts
import { Data } from "effect";
// Domain errors (business logic)
export class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
userId: string;
}> {}
export class InsufficientCreditsError extends Data.TaggedError(
"InsufficientCreditsError",
)<{
userId: string;
required: number;
available: number;
}> {}
// Infrastructure errors
export class AgentError extends Data.TaggedError("AgentError")<{
agentName: string;
cause: unknown;
}> {}
export class RateLimitError extends Data.TaggedError("RateLimitError")<{
retryAfter: number;
}> {}
export class NetworkError extends Data.TaggedError("NetworkError")<{
endpoint: string;
cause: unknown;
}> {}
```
**Handling Errors at Appropriate Layers:**
```typescript
// backend/convex/api/premium-chat.ts
import { action } from "../_generated/server";
import { Effect, Schedule } from "effect";
import { AgentService, AgentServiceLive } from "../services/agent.service";
import {
UserNotFoundError,
InsufficientCreditsError,
AgentError,
RateLimitError,
} from "../domain/agents/errors";
export const premiumChat = action({
args: { userId: v.string(), prompt: v.string() },
handler: (ctx, args) =>
Effect.gen(function* () {
// 1. Check user credits (can fail with UserNotFoundError or InsufficientCreditsError)
const user = yield* getUserWithCredits(ctx, args.userId);
// 2. Generate response (can fail with AgentError or RateLimitError)
const agentService = yield* AgentService;
const response = yield* agentService.generateResponse(
ctx,
user.threadId,
args.prompt,
);
// 3. Deduct credits
yield* deductCredits(ctx, args.userId, 10);
return { response };
}).pipe(
Effect.provide(AgentServiceLive),
// Handle domain errors specifically
Effect.catchTag("UserNotFoundError", (error) =>
Effect.succeed({
error: `User ${error.userId} not found`,
code: "USER_NOT_FOUND",
}),
),
Effect.catchTag("InsufficientCreditsError", (error) =>
Effect.succeed({
error: `Insufficient credits. Need ${error.required}, have ${error.available}`,
code: "INSUFFICIENT_CREDITS",
}),
),
// Handle infrastructure errors with retry
Effect.catchTag("RateLimitError", (error) =>
Effect.sleep(`${error.retryAfter} seconds`).pipe(
Effect.flatMap(() => premiumChat.handler(ctx, args)),
),
),
Effect.catchTag("AgentError", (error) =>
Effect.retry(
Effect.fail(error),
Schedule.exponential("1 second").pipe(
Schedule.compose(Schedule.recurs(2)),
),
).pipe(
Effect.catchAll(() =>
Effect.succeed({
error: "Agent temporarily unavailable",
code: "AGENT_ERROR",
}),
),
),
),
// Catch-all for unexpected errors
Effect.catchAll((error) => {
console.error("Unexpected error:", error);
return Effect.succeed({
error: "Something went wrong",
code: "UNKNOWN_ERROR",
});
}),
Effect.runPromise,
),
});
```
**Key Pattern:** Handle errors at the layer where you can do something about them. Domain errors → immediate user feedback. Infrastructure errors → retry/fallback logic.
---
### Pattern 4: Parallel Execution Pattern (Effect.all) ✅ PRODUCTION-PROVEN
**When:** Need to run multiple operations concurrently with controlled parallelism.
**Example: Multi-Agent Research Pipeline**
```typescript
// backend/convex/domain/agents/research.ts
import { Effect } from "effect";
import { AgentService } from "../../services/agent.service";
export const conductResearch = (query: string) =>
Effect.gen(function* () {
const agentService = yield* AgentService;
// Run 3 agents in parallel with max concurrency of 2
const [webResults, academicResults, newsResults] = yield* Effect.all(
[
agentService.generateResponse(ctx, "web-agent", `Web search: ${query}`),
agentService.generateResponse(
ctx,
"academic-agent",
`Academic search: ${query}`,
),
agentService.generateResponse(
ctx,
"news-agent",
`News search: ${query}`,
),
],
{
concurrency: 2, // Max 2 concurrent operations
mode: "default", // Fail fast on first error
},
);
// Synthesize results
const synthesis = yield* agentService.generateResponse(
ctx,
"synthesis-agent",
`Synthesize these findings:\n\nWeb: ${webResults}\n\nAcademic: ${academicResults}\n\nNews: ${newsResults}`,
);
return synthesis;
});
```
**Parallel with Graceful Degradation:**
```typescript
// backend/convex/domain/agents/quality-check.ts
import { Effect } from "effect";
export const qualityCheckWithGracefulDegradation = (response: string) =>
Effect.gen(function* () {
// Run quality checks in parallel, continue even if some fail
const results = yield* Effect.all(
[
checkGrammar(response).pipe(
Effect.catchAll(() => Effect.succeed({ score: 1.0, passed: true })),
),
checkFactuality(response).pipe(
Effect.catchAll(() => Effect.succeed({ score: 1.0, passed: true })),
),
checkToxicity(response).pipe(
Effect.catchAll(() => Effect.succeed({ score: 0.0, passed: true })),
),
],
{
concurrency: 3,
mode: "default",
},
);
const [grammar, factuality, toxicity] = results;
return {
overallScore:
(grammar.score + factuality.score + (1 - toxicity.score)) / 3,
passed: grammar.passed && factuality.passed && toxicity.passed,
};
});
```
**Parallel Batch Processing:**
```typescript
// backend/convex/api/batch-process.ts
import { action } from "../_generated/server";
import { Effect } from "effect";
export const processBatch = action({
args: { items: v.array(v.string()) },
handler: (ctx, args) =>
Effect.gen(function* () {
// Process items with controlled parallelism
const results = yield* Effect.all(
args.items.map((item) => processItem(ctx, item)),
{
concurrency: 5, // Process 5 items at a time
mode: "either", // Continue even if some fail
},
);
// Separate successes and failures
const successes = results.filter((r) => Effect.isSuccess(r));
const failures = results.filter((r) => Effect.isFailure(r));
return {
successCount: successes.length,
failureCount: failures.length,
results,
};
}).pipe(Effect.provide(/* layers */), Effect.runPromise),
});
```
---
### Pattern 5: Resource Management Pattern (Effect.scoped) ✅ PRODUCTION-PROVEN
**When:** Managing resources (connections, streams, file handles) that need cleanup.
**Example: Database Connection Pool**
```typescript
// backend/convex/services/database.service.ts
import { Effect, Context, Layer, Scope } from "effect";
class DatabaseService extends Context.Tag("DatabaseService")<
DatabaseService,
{
readonly query: <A>(
sql: string,
params: any[],
) => Effect.Effect<A[], Error>;
readonly transaction: <A>(
fn: (tx: Transaction) => Effect.Effect<A, Error>,
) => Effect.Effect<A, Error>;
}
>() {}
const DatabaseServiceLive = Layer.scoped(
DatabaseService,
Effect.gen(function* () {
// Acquire connection pool (will be released automatically)
const pool = yield* Effect.acquireRelease(
Effect.sync(() => createConnectionPool({ size: 10 })),
(pool) => Effect.sync(() => pool.close()),
);
return {
query: (sql, params) =>
Effect.tryPromise({
try: async () => {
const conn = await pool.acquire();
try {
return await conn.query(sql, params);
} finally {
pool.release(conn);
}
},
catch: (error) => new Error(String(error)),
}),
transaction: (fn) =>
Effect.scoped(
Effect.gen(function* () {
// Acquire transaction (will rollback on error, commit on success)
const tx = yield* Effect.acquireRelease(
Effect.tryPromise({
try: () => pool.beginTransaction(),
catch: (error) => new Error(String(error)),
}),
(tx) => Effect.sync(() => tx.rollback()),
);
const result = yield* fn(tx);
yield* Effect.tryPromise({
try: () => tx.commit(),
catch: (error) => new Error(String(error)),
});
return result;
}),
),
};
}),
);
```
**Example: Stream Processing with Cleanup**
```typescript
// backend/convex/services/streaming.service.ts
import { Effect, Stream } from "effect";
const processStreamWithCleanup = (streamUrl: string) =>
Effect.scoped(
Effect.gen(function* () {
// Acquire stream (will close on completion or error)
const stream = yield* Effect.acquireRelease(
Effect.tryPromise({
try: () => openStream(streamUrl),
catch: (error) => new Error(String(error)),
}),
(stream) => Effect.sync(() => stream.close()),
);
// Process chunks
const results: string[] = [];
yield* Stream.fromAsyncIterable(
stream,
() => new Error("Stream failed"),
).pipe(
Stream.mapEffect((chunk) =>
Effect.gen(function* () {
const processed = yield* processChunk(chunk);
results.push(processed);
return processed;
}),
),
Stream.runDrain,
);
return results;
}),
);
```
---
### Pattern 6: Retry and Timeout Patterns ✅ PRODUCTION-PROVEN
**When:** Calling unreliable external APIs or LLM services.
**Example: Exponential Backoff Retry**
```typescript
// backend/convex/services/llm.service.ts
import { Effect, Schedule } from "effect";
class LLMService extends Context.Tag("LLMService")<
LLMService,
{
readonly complete: (prompt: string) => Effect.Effect<string, LLMError>;
}
>() {}
const LLMServiceLive = Layer.succeed(LLMService, {
complete: (prompt) =>
Effect.tryPromise({
try: () =>
openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
}),
catch: (error) => new LLMError({ cause: error }),
}).pipe(
// Retry with exponential backoff
Effect.retry(
Schedule.exponential("500 millis").pipe(
Schedule.compose(Schedule.recurs(3)), // Max 3 retries
Schedule.compose(
Schedule.elapsed.pipe(
Schedule.whileOutput((duration) => duration < 30_000), // Max 30s total
),
),
),
),
Effect.map((result) => result.choices[0].message.content),
),
});
```
**Example: Timeout with Fallback**
```typescript
// backend/convex/domain/agents/robust-generation.ts
import { Effect, Schedule } from "effect";
export const robustGeneration = (prompt: string) =>
Effect.gen(function* () {
const llmService = yield* LLMService;
// Try primary model with timeout
const result = yield* llmService.complete(prompt).pipe(
Effect.timeout("10 seconds"),
Effect.catchTag("TimeoutError", () =>
// Fallback 1: Reduce prompt length and retry
llmService.complete(truncate(prompt, 1000)).pipe(
Effect.timeout("10 seconds"),
Effect.catchTag("TimeoutError", () =>
// Fallback 2: Use faster model
llmService.completeFast(truncate(prompt, 500)).pipe(
Effect.timeout("5 seconds"),
Effect.catchTag("TimeoutError", () =>
// Fallback 3: Return cached response
getCachedResponse(prompt).pipe(
Effect.catchAll(() =>
Effect.succeed(
"I'm experiencing technical difficulties. Please try again.",
),
),
),
),
),
),
),
),
);
return result;
});
```
**Example: Conditional Retry (Only on Specific Errors)**
```typescript
// backend/convex/services/external-api.service.ts
import { Effect, Schedule } from "effect";
const callExternalAPI = (endpoint: string, body: any) =>
Effect.tryPromise({
try: () => fetch(endpoint, { method: "POST", body: JSON.stringify(body) }),
catch: (error) => new NetworkError({ endpoint, cause: error }),
}).pipe(
// Only retry on rate limit errors
Effect.retry(
Schedule.exponential("1 second").pipe(
Schedule.whileInput(
(error: NetworkError) =>
error.cause instanceof Response && error.cause.status === 429,
),
Schedule.compose(Schedule.recurs(5)),
),
),
);
```
---
## Section 3: Component Integration Checklists
### @convex-dev/agent - Agent Service
**When to Wrap:** Multi-agent orchestration, custom error handling, composing with RAG/rate limiting.
**Pattern:**
```typescript
import { Agent } from "@convex-dev/agent";
import { Effect, Context, Layer } from "effect";
// 1. Define errors
class AgentError extends Data.TaggedError("AgentError")<{
agentName: string;
cause: unknown;
}> {}
// 2. Define service
class AgentService extends Context.Tag("AgentService")<
AgentService,
{
readonly generateResponse: (
ctx,
threadId,
prompt,
) => Effect.Effect<string, AgentError>;
}
>() {}
// 3. Implement with component
const AgentServiceLive = Layer.effect(
AgentService,
Effect.gen(function* () {
const agent = new Agent(components.agent, {
/* config */
});
return {
generateResponse: (ctx, threadId, prompt) =>
Effect.tryPromise({
try: async () => {
const { thread } = await agent.continueThread(ctx, { threadId });
const result = await thread.generateText({ prompt });
return result.text;
},
catch: (error) => new AgentError({ agentName: "...", cause: error }),
}),
};
}),
);
```
**Checklist:**
- [ ] Wrap agent calls in `Effect.tryPromise`
- [ ] Define `AgentError` with tagged class
- [ ] Create service interface with `Context.Tag`
- [ ] Implement with `Layer.effect`
- [ ] Inject dependencies (RateLimitService, RAGService) if needed
- [ ] Handle errors with `Effect.catchTag`
- [ ] Test with mock layer (TestAgentServiceLive)
---
### @convex-dev/workflow - Workflow Service
**When to Use Workflow vs Effect:**
- **Use Workflow for:** Long-running, durable operations (hours/days), need journaling, external actions
- **Use Effect for:** Business logic in workflow steps, error handling within steps, service composition
**Pattern: Effect in Workflow Steps**
```typescript
import { WorkflowManager } from "@convex-dev/workflow";
import { Effect } from "effect";
const workflow = new WorkflowManager(components.workflow);
export const researchWorkflow = workflow.define({
args: { query: v.string() },
handler: async (step, args): Promise<ResearchResult> => {
// Step 1: Fetch context (query)
const context = await step.runQuery(internal.workflows.getContext, {
query: args.query,
});
// Step 2: Generate with Effect composition (action)
const analysis = await step.runAction(
internal.workflows.analyzeWithEffect,
{ query: args.query, context },
);
// Step 3: Quality check in parallel
const [grammar, factuality] = await Promise.all([
step.runAction(internal.workflows.checkGrammar, { text: analysis }),
step.runAction(internal.workflows.checkFactuality, { text: analysis }),
]);
// Step 4: Refine if needed
const refined =
grammar.score < 0.8 || factuality.score < 0.8
? await step.runAction(
internal.workflows.refineResponse,
{ original: analysis, context },
{ retry: { maxAttempts: 2 } },
)
: analysis;
return { result: refined };
},
});
// The action uses Effect internally
export const analyzeWithEffect = internalAction({
args: { query: v.string(), context: v.any() },
handler: async (ctx, args) => {
const program = Effect.gen(function* () {
const agentService = yield* AgentService;
const ragService = yield* RAGService;
const enriched = yield* ragService.search(ctx, "global", args.query, 5);
const result = yield* agentService.generateResponse(
ctx,
"analysis-thread",
`${args.query}\n\nContext: ${args.context}\n\nAdditional: ${enriched.text}`,
);
return result;
});
return await Effect.runPromise(
program.pipe(
Effect.provide(Layer.merge(AgentServiceLive, RAGServiceLive)),
Effect.catchAll((error) => {
console.error("Analysis failed:", error);
return Effect.succeed("Analysis unavailable");
}),
),
);
},
});
```
**Checklist:**
- [ ] Use Workflow component for durability (step-level retries, journaling)
- [ ] Use Effect in actions called by workflow steps
- [ ] Provide service layers in actions
- [ ] Convert Effect to Promise with `Effect.runPromise`
- [ ] Handle errors with `Effect.catchAll` before running
- [ ] Test workflow steps independently with mock layers
---
### @convex-dev/rag - RAG Service
**When to Wrap:** Composing search with other services, custom pre-processing, error handling.
**Pattern:**
```typescript
import { RAG } from "@convex-dev/rag";
import { Effect, Context, Layer } from "effect";
class RAGError extends Data.TaggedError("RAGError")<{
operation: string;
cause: unknown;
}> {}
class RAGService extends Context.Tag("RAGService")<
RAGService,
{
readonly addDocument: (
ctx,
namespace,
content,
metadata,
) => Effect.Effect<string, RAGError>;
readonly search: (
ctx,
namespace,
query,
limit?,
) => Effect.Effect<{ text: string; entries: any[] }, RAGError>;
}
>() {}
const RAGServiceLive = Layer.effect(
RAGService,
Effect.gen(function* () {
const rag = new RAG(components.rag, {
textEmbeddingModel: openai.embedding("text-embedding-3-small"),
embeddingDimension: 1536,
});
return {
addDocument: (ctx, namespace, content, metadata) =>
Effect.tryPromise({
try: async () => {
const result = await rag.add(ctx, {
namespace,
text: content,
filterValues: [],
});
return result.entryId;
},
catch: (error) =>
new RAGError({ operation: "addDocument", cause: error }),
}),
search: (ctx, namespace, query, limit = 10) =>
Effect.tryPromise({
try: async () => {
const results = await rag.search(ctx, { namespace, query, limit });
return { text: results.text, entries: results.entries };
},
catch: (error) => new RAGError({ operation: "search", cause: error }),
}),
};
}),
);
```
**Checklist:**
- [ ] Wrap `rag.add()` and `rag.search()` in `Effect.tryPromise`
- [ ] Define `RAGError` with operation name
- [ ] Create service interface with `Context.Tag`
- [ ] Implement with `Layer.effect`
- [ ] Compose with AgentService for contextual responses
- [ ] Handle errors with `Effect.catchTag`
- [ ] Test with TestRAGServiceLive (mock embeddings)
---
### @convex-dev/rate-limiter - Rate Limit Service
**When to Wrap:** Multiple rate limits, composing with business logic, custom retry.
**Pattern:**
```typescript
import { RateLimiter, MINUTE } from "@convex-dev/rate-limiter";
import { Effect, Context, Layer, Schedule } from "effect";
class RateLimitError extends Data.TaggedError("RateLimitError")<{
type: string;
retryAfter: number;
}> {}
class RateLimitService extends Context.Tag("RateLimitService")<
RateLimitService,
{
readonly checkLimit: (
ctx,
key,
count?,
) => Effect.Effect<boolean, RateLimitError>;
}
>() {}
const RateLimitServiceLive = Layer.effect(
RateLimitService,
Effect.gen(function* () {
const limiter = new RateLimiter(components.rateLimiter, {
requests: { kind: "token bucket", rate: 20, period: MINUTE },
});
return {
checkLimit: (ctx, key, count = 1) =>
Effect.gen(function* () {
const result = yield* Effect.tryPromise({
try: () => limiter.limit(ctx, "requests", { key, count }),
catch: (error) => new Error(String(error)),
});
if (!result.ok) {
return yield* Effect.fail(
new RateLimitError({
type: "requests",
retryAfter: result.retryAfter,
}),
);
}
return true;
}),
};
}),
);
```
**Checklist:**
- [ ] Wrap `limiter.limit()` in `Effect.tryPromise`
- [ ] Check `result.ok` and fail with `RateLimitError` if exceeded
- [ ] Define `RateLimitError` with `retryAfter` field
- [ ] Compose with other services (agent, RAG)
- [ ] Handle rate limit errors with retry or user feedback
- [ ] Test with TestRateLimitServiceLive (always pass or always fail)
---
### @convex-dev/retrier - Retrier Service
**When to Use:** Action-level retries with persistent state vs Effect.retry for in-memory retries.
**Pattern: Hybrid Retrier + Effect.retry**
```typescript
import { ActionRetrier } from "@convex-dev/action-retrier";
import { Effect, Schedule } from "effect";
class ResilientExecutionService extends Context.Tag(
"ResilientExecutionService",
)<
ResilientExecutionService,
{
readonly executeWithRetry: <A>(
ctx,
action,
args,
) => Effect.Effect<A, ExecutionError>;
}
>() {}
const ResilientExecutionServiceLive = Layer.effect(
ResilientExecutionService,
Effect.gen(function* () {
const retrier = new ActionRetrier(components.actionRetrier, {
initialBackoffMs: 250,
base: 2,
maxFailures: 4,
});
return {
executeWithRetry: (ctx, action, args) =>
Effect.gen(function* () {
// Use retrier for action-level retries
const runId = yield* Effect.tryPromise({
try: () => retrier.run(ctx, action, args),
catch: () => new RetrierStartError(),
});
// Poll for completion with Effect schedule
const result = yield* Effect.repeat(
Effect.gen(function* () {
const status = yield* Effect.tryPromise({
try: () => retrier.status(ctx, runId),
catch: () => new StatusCheckError(),
});
if (status.type === "inProgress") {
return yield* Effect.fail(Option.none());
}
return status.result;
}),
Schedule.spaced("500 millis").pipe(
Schedule.compose(Schedule.recurs(60)), // Max 30 seconds
),
);
if (result.type === "success") {
return result.returnValue;
} else {
return yield* Effect.fail(
new ExecutionFailed({ error: result.error }),
);
}
}),
};
}),
);
```
**Checklist:**
- [ ] Use retrier for long-running actions with persistent state
- [ ] Use Effect.retry for in-memory, short-lived operations
- [ ] Poll for completion with `Effect.repeat` and `Schedule.spaced`
- [ ] Handle completion status (success, failed, canceled)
- [ ] Compose with other services
- [ ] Test with TestResilientExecutionServiceLive
---
### @convex-dev/workpool - Task Queue Service
**When to Wrap:** Batch enqueuing, error handling for tasks, coordinating queues.
**Pattern:**
```typescript
import { Workpool } from "@convex-dev/workpool";
import { Effect, Context, Layer } from "effect";
class TaskQueueService extends Context.Tag("TaskQueueService")<
TaskQueueService,
{
readonly enqueue: (
ctx,
priority,
action,
args,
) => Effect.Effect<string, EnqueueError>;
}
>() {}
const TaskQueueServiceLive = Layer.effect(
TaskQueueService,
Effect.gen(function* () {
const highPriority = new Workpool(components.highPriorityWorkpool, {
maxParallelism: 20,
});
const lowPriority = new Workpool(components.lowPriorityWorkpool, {
maxParallelism: 5,
});
return {
enqueue: (ctx, priority, action, args) =>
Effect.tryPromise({
try: async () => {
const pool = priority === "high" ? highPriority : lowPriority;
const taskId = await pool.enqueueAction(ctx, action, args);
return taskId;
},
catch: (error) => new EnqueueError({ cause: error }),
}),
};
}),
);
```
**Batch Enqueue:**
```typescript
export const processBatch = mutation({
args: { items: v.array(v.any()) },
handler: (ctx, args) =>
Effect.gen(function* () {
const taskQueue = yield* TaskQueueService;
const taskIds = yield* Effect.all(
args.items.map((item) =>
taskQueue.enqueue(ctx, "low", internal.tasks.processItem, item),
),
{ concurrency: 10 },
);
return { taskIds, count: taskIds.length };
}).pipe(Effect.provide(TaskQueueServiceLive), Effect.runPromise),
});
```
**Checklist:**
- [ ] Wrap `workpool.enqueueAction()` in `Effect.tryPromise`
- [ ] Support multiple priority queues
- [ ] Use `Effect.all` for batch enqueuing with concurrency control
- [ ] Handle enqueue errors with `Effect.catchTag`
- [ ] Test with TestTaskQueueServiceLive
---
### @convex-dev/persistent-text-streaming - Streaming Service
**When to Use:** Generally, use Agent component's built-in streaming (`saveStreamDeltas: true`). Only wrap if you need custom stream processing.
**Pattern:**
```typescript
import { PersistentTextStreaming } from "@convex-dev/persistent-text-streaming";
import { Effect, Stream } from "effect";
class StreamingService extends Context.Tag("StreamingService")<
StreamingService,
{
readonly processStream: (
ctx,
streamId,
source,
) => Effect.Effect<void, StreamError>;
}
>() {}
const StreamingServiceLive = Layer.effect(
StreamingService,
Effect.gen(function* () {
const streaming = new PersistentTextStreaming(
components.persistentTextStreaming,
);
return {
processStream: (ctx, streamId, source) =>
Effect.gen(function* () {
yield* Stream.fromAsyncIterable(source, () => new StreamError()).pipe(
Stream.mapEffect((chunk) =>
Effect.tryPromise({
try: () => streaming.appendChunk(ctx, streamId, chunk),
catch: () => new ChunkAppendError({ chunk }),
}),
),
Stream.runDrain,
);
yield* Effect.tryPromise({
try: () =>
ctx.runMutation(api.streaming.markComplete, { streamId }),
catch: () => new StreamCompletionError(),
});
}),
};
}),
);
```
**Checklist:**
- [ ] Use Agent component's built-in streaming for most cases
- [ ] Only wrap if you need custom processing (duplicate streams, filtering)
- [ ] Use `Stream.fromAsyncIterable` for async sources
- [ ] Use `Stream.mapEffect` for effectful processing
- [ ] Handle backpressure with `Stream.buffer`
- [ ] Always mark stream complete or failed
---
### @convex-dev/crons - Cron Service
**When to Wrap:** Cron job logic needs composition with other services.
**Pattern:**
```typescript
import { Crons } from "@convex-dev/crons";
import { Effect } from "effect";
// Register cron (mutation)
export const setupDailyCron = internalMutation({
handler: async (ctx) => {
const crons = new Crons(components.crons);
await crons.register(
ctx,
{ kind: "cron", cronspec: "0 0 * * *" },
internal.crons.dailyMaintenanceWithEffect,
{},
"daily-maintenance",
);
},
});
// Cron job uses Effect for logic (action)
export const dailyMaintenanceWithEffect = internalAction({
handler: async (ctx) => {
const program = Effect.gen(function* () {
const ragService = yield* RAGService;
const monitoring = yield* MonitoringService;
// Cleanup old content
yield* Effect.tryPromise({
try: () => ctx.runMutation(internal.rag.cleanupOld, {}),
catch: () => new CleanupError(),
});
// Generate report
const report = yield* monitoring.generateDailyReport(ctx);
// Send email
yield* Effect.tryPromise({
try: () => ctx.runAction(internal.email.sendReport, { report }),
catch: () => new EmailError(),
});
return { success: true };
});
await Effect.runPromise(
program.pipe(
Effect.provide(Layer.merge(RAGServiceLive, MonitoringServiceLive)),
Effect.catchAll((error) => {
console.error("Daily maintenance failed:", error);
return Effect.succeed({ success: false, error });
}),
),
);
},
});
```
**Checklist:**
- [ ] Use Crons component for registration and scheduling
- [ ] Use Effect in the cron job action for business logic
- [ ] Compose with other services (RAG, monitoring, email)
- [ ] Provide service layers in the action
- [ ] Handle errors gracefully (don't crash cron)
- [ ] Log execution results for monitoring
---
## Section 4: Production Patterns
### Pattern: Service Layer Composition (Layer.mergeAll) ✅ PRODUCTION-PROVEN
**Example: Building Application Layer**
```typescript
// backend/convex/services/layers.ts
import { Layer } from "effect";
import { AgentServiceLive } from "./agent.service";
import { RAGServiceLive } from "./rag.service";
import { RateLimitServiceLive } from "./rate-limit.service";
import { MonitoringServiceLive } from "./monitoring.service";
// Compose all service layers
export const AppLayer = Layer.mergeAll(
RateLimitServiceLive,
MonitoringServiceLive,
).pipe(
Layer.provideMerge(RAGServiceLive), // RAG depends on above
Layer.provideMerge(AgentServiceLive), // Agent depends on all above
);
// Usage in any endpoint
export const myAction = action({
args: {
/* ... */
},
handler: (ctx, args) =>
Effect.gen(function* () {
// All services available
const agent = yield* AgentService;
const rag = yield* RAGService;
const rateLimit = yield* RateLimitService;
const monitoring = yield* MonitoringService;
// Your logic here
}).pipe(
Effect.provide(AppLayer), // Single line provides all services
Effect.runPromise,
),
});
```
---
### Pattern: Multi-Agent Orchestration ✅ PRODUCTION-PROVEN
**Example: Research Agent System**
```typescript
// backend/convex/domain/agents/research-orchestration.ts
import { Effect } from "effect";
export const conductResearch = (query: string) =>
Effect.gen(function* () {
const agentService = yield* AgentService;
const ragService = yield* RAGService;
// Step 1: Classify query
const classification = yield* agentService.generateResponse(
ctx,
"classifier-thread",
`Classify this query: ${query}`,
);
// Step 2: Parallel research based on classification
const [webResults, academicResults, newsResults] = yield* Effect.all(
[
agentService.generateResponse(ctx, "web-thread", `Web: ${query}`),
agentService.generateResponse(
ctx,
"academic-thread",
`Academic: ${query}`,
),
agentService.generateResponse(ctx, "news-thread", `News: ${query}`),
],
{ concurrency: 3 },
);
// Step 3: Enrich with RAG context
const ragContext = yield* ragService.search(ctx, "global", query, 5);
// Step 4: Synthesize all findings
const synthesis = yield* agentService.generateResponse(
ctx,
"synthesis-thread",
`Synthesize:\n\nWeb: ${webResults}\n\nAcademic: ${academicResults}\n\nNews: ${newsResults}\n\nContext: ${ragContext.text}`,
);
// Step 5: Quality check in parallel
const [grammar, factuality] = yield* Effect.all(
[checkGrammar(synthesis), checkFactuality(synthesis)],
{ concurrency: 2 },
);
// Step 6: Refine if needed
const final =
grammar.score < 0.8 || factuality.score < 0.8
? yield* agentService.generateResponse(
ctx,
"refine-thread",
`Refine this: ${synthesis}`,
)
: synthesis;
return {
result: final,
sources: {
web: webResults,
academic: academicResults,
news: newsResults,
rag: ragContext.entries,
},
quality: { grammar, factuality },
};
});
```
---
### Pattern: Testing with Layers (Mock Providers) ✅ PRODUCTION-PROVEN
**Example: Test Service Implementations**
```typescript
// backend/convex/services/__tests__/agent.service.test.ts
import { Effect, Layer } from "effect";
import { AgentService } from "../agent.service";
import { RAGService } from "../rag.service";
import { RateLimitService } from "../rate-limit.service";
// Create mock implementations
const TestAgentServiceLive = Layer.succeed(AgentService, {
generateResponse: (ctx, threadId, prompt) =>
Effect.succeed("Mock agent response"),
createThread: (ctx, userId) => Effect.succeed("test-thread-id"),
});
const TestRAGServiceLive = Layer.succeed(RAGService, {
search: (ctx, namespace, query, limit) =>
Effect.succeed({
text: "Mock context",
entries: [],
}),
addDocument: (ctx, namespace, content, metadata) =>
Effect.succeed("mock-entry-id"),
});
const TestRateLimitServiceLive = Layer.succeed(RateLimitService, {
checkLimit: (ctx, key, count) => Effect.succeed(true), // Always pass
});
// Compose test layers
const TestAppLayer = Layer.mergeAll(
TestRateLimitServiceLive,
TestRAGServiceLive,
TestAgentServiceLive,
);
// Write tests
describe("Research Orchestration", () => {
it("should conduct research with all agents", async () => {
const program = conductResearch("What is Effect.ts?");
const result = await Effect.runPromise(
program.pipe(Effect.provide(TestAppLayer)),
);
expect(result.result).toBe("Mock agent response");
expect(result.sources.web).toBe("Mock agent response");
});
it("should handle agent errors", async () => {
// Override one service to fail
const FailingAgentLayer = Layer.succeed(AgentService, {
generateResponse: () =>
Effect.fail(new AgentError({ agentName: "test", cause: "Failed" })),
createThread: () => Effect.succeed("test-thread-id"),
});
const TestLayerWithFailure = Layer.mergeAll(
TestRateLimitServiceLive,
TestRAGServiceLive,
FailingAgentLayer,
);
const program = conductResearch("What is Effect.ts?").pipe(
Effect.provide(TestLayerWithFailure),
Effect.catchTag("AgentError", (error) =>
Effect.succeed({ result: "Error handled", error: true }),
),
);
const result = await Effect.runPromise(program);
expect(result.error).toBe(true);
});
});
```
**Checklist for Testing:**
- [ ] Create test layers with `Layer.succeed` for all services
- [ ] Compose test layers with `Layer.mergeAll`
- [ ] Override specific services to test error paths
- [ ] Use `Effect.runPromise` to execute tests
- [ ] Test both success and failure scenarios
- [ ] Test error handling with `Effect.catchTag`
---
### Pattern: Error Recovery Strategies ✅ PRODUCTION-PROVEN
**Example: Fallback Chain**
```typescript
// backend/convex/domain/agents/robust-generation.ts
import { Effect, Schedule } from "effect";
export const robustGeneration = (prompt: string) =>
Effect.gen(function* () {
const llmService = yield* LLMService;
// Try primary approach
const result = yield* llmService.complete(prompt).pipe(
Effect.timeout("30 seconds"),
// Fallback 1: Reduce context and retry
Effect.catchTag("TimeoutError", () =>
llmService
.complete(truncate(prompt, 1000))
.pipe(Effect.timeout("20 seconds")),
),
// Fallback 2: Switch to faster model
Effect.catchTag("TimeoutError", () =>
llmService
.completeFast(truncate(prompt, 500))
.pipe(Effect.timeout("10 seconds")),
),
// Fallback 3: Check cache
Effect.catchTag("TimeoutError", () =>
getCachedResponse(prompt).pipe(
Effect.orElse(() =>
Effect.succeed("I'm experiencing delays. Please try again."),
),
),
),
// Retry on rate limits
Effect.retry(
Schedule.exponential("1 second").pipe(
Schedule.whileInput((error) => error._tag === "RateLimitError"),
Schedule.compose(Schedule.recurs(3)),
),
),
);
return result;
});
```
---
### Pattern: Monitoring and Observability ✅ PRODUCTION-PROVEN
**Example: Monitoring Service**
```typescript
// backend/convex/services/monitoring.service.ts
import { Effect, Context, Layer } from "effect";
interface UsageData {
serviceName: string;
functionName: string;
duration: number;
tokenCount?: number;
cost?: number;
}
class MonitoringService extends Context.Tag("MonitoringService")<
MonitoringService,
{
readonly trackUsage: (data: UsageData) => Effect.Effect<void>;
readonly trackError: (error: unknown) => Effect.Effect<void>;
}
>() {}
const MonitoringServiceLive = Layer.succeed(MonitoringService, {
trackUsage: (data) =>
Effect.sync(() => {
console.log("Service usage:", data);
// Send to analytics service (Datadog, Honeycomb, etc.)
}),
trackError: (error) =>
Effect.sync(() => {
console.error("Service error:", error);
// Send to error tracking (Sentry, Rollbar, etc.)
}),
});
export { MonitoringService, MonitoringServiceLive };
```
**Wrap Service Calls with Monitoring:**
```typescript
// backend/convex/domain/agents/monitored-generation.ts
export const monitoredGeneration = (prompt: string) =>
Effect.gen(function* () {
const agentService = yield* AgentService;
const monitoring =