# How OpenSEO Integrates with the Agents SDK for AI-Driven Workflows

> Discover how OpenSEO integrates with the Agents SDK, using a three-layer architecture for AI-driven workflows, conversational AI, and real-time streaming interfaces.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: how-to-guide
- Published: 2026-06-26

---

**OpenSEO leverages Cloudflare's Agents SDK through a three-layer architecture that routes HTTP requests to Durable Objects, implements conversational AI with tool calling, and provides React hooks for real-time streaming interfaces.**

The `every-app/open-seo` repository demonstrates a production-ready integration between an SEO platform and Cloudflare's Agents SDK. This architecture enables persistent, stateful AI conversations that can call external tools and survive page reloads while maintaining project-specific context.

## Three-Layer Architecture Overview

The integration follows a clear separation of concerns across three distinct layers:

- **HTTP Routing Layer**: Intercepts `/agents/*` requests and forwards them to Durable Objects using `routeAgentRequest` with custom authorization hooks
- **Durable Object Layer**: Implements the conversational logic, LLM streaming, and tool execution within `OnboardingChatAgent` while storing history in SQLite
- **Client-Side Layer**: Provides React hooks (`useAgent`, `useAgentChat`) that bind UI components to the persistent Durable Object instance keyed by project ID

## HTTP Routing with routeAgentRequest

In [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts), the Cloudflare Worker detects paths beginning with `/agents/` and delegates to the SDK's `routeAgentRequest` function. This handler manages WebSocket upgrades, locates the appropriate Durable Object by name (using the **project ID** as the instance identifier), and establishes bidirectional streams.

The integration injects custom authorization hooks (`onBeforeConnect` and `onBeforeRequest`) that verify organization-level permissions before the connection reaches the Durable Object.

```typescript
// Forward every /agents/* request to a Durable Object that implements the chat.
if (pathname.startsWith("/agents/")) {
  return routeOnboardingChatAgent(publicRequest, env);
}

// ...

async function routeOnboardingChatAgent(
  request: Request,
  env: Env,
): Promise<Response> {
  const response = await routeAgentRequest(request, env, {
    // Authorize the user before any WS connection reaches the DO.
    onBeforeConnect: (req, lobby) => authorizeOnboardingChat(req, lobby.name),
    onBeforeRequest: (req, lobby) => authorizeOnboardingChat(req, lobby.name),
  });
  return response ?? new Response("Not found", { status: 404 });
}

```

The routing uses `routeAgentRequest` from the Agents SDK and injects custom auth hooks to protect the Durable Object endpoints.

## Durable Object Agent Implementation

The `OnboardingChatAgent` class in [`src/server/features/onboarding/OnboardingChatAgent.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/onboarding/OnboardingChatAgent.ts) extends `AIChatAgent` from the SDK. It defines the system prompt, registers a **ToolSet** containing `read_website`, `get_seo_metrics`, and `research_keywords`, and handles billing-gate checks through the Autumn subscription service.

The Durable Object stores chat history in its internal **SQLite** database, guaranteeing persistence across reloads. Before each turn, the agent checks the organization's credit balance; if the free-question cap is exceeded, it returns a static assistant message.

```typescript
export class OnboardingChatAgent extends AIChatAgent {
  maxPersistedMessages = 60; // cap history

  async onChatMessage(
    onFinish: StreamTextOnFinishCallback<ToolSet>,
    options?: OnChatMessageOptions,
  ): Promise<Response | undefined> {
    // ... billing checks omitted for brevity ...

    const model = await getOnboardingModel();

    const result = streamText({
      model,
      system: buildSystemPrompt(project.domain),
      messages: await convertToModelMessages(this.messages),
      abortSignal: options?.abortSignal,
      maxOutputTokens: 1600,
      stopWhen: stepCountIs(5),
      onFinish: async (event) => {
        // Meter LLM cost against Autumn credits.
        if (creditCustomerId) {
          const costUsd = event.steps.reduce(
            (sum, step) => sum + openRouterCostUsd(step.providerMetadata),
            0,
          );
          await trackUsageCreditSpend({
            customer: billingCustomer,
            customerId: creditCustomerId,
            creditFeature: "onboarding",
            costUsd,
            monthlyRemaining,
            properties: { provider: "openrouter" },
          });
        }
        await onFinish(event); // persist turn in DO SQLite
      },
      tools: {
        read_website: tool({ /* ... */ }),
        get_seo_metrics: tool({ /* ... */ }),
        research_keywords: tool({ /* ... */ }),
      } as ToolSet,
    });

    return result.toUIMessageStreamResponse({
      onError: (error) => "The assistant hit an error. Please try again.",
    });
  }
}

```

The Durable Object extends `AIChatAgent`, registers SEO-specific tools, streams LLM output through `streamText`, and records usage costs against the organization's credit balance.

## Client-Side React Integration

On the frontend, [`src/client/features/onboarding/OnboardingChatConversation.tsx`](https://github.com/every-app/open-seo/blob/main/src/client/features/onboarding/OnboardingChatConversation.tsx) uses the `useAgent` hook to create a typed handle pointing at the Durable Object instance (`name: projectId`). The `useAgentChat` hook provides a streaming chat API that exposes `messages`, `sendMessage`, and `status`.

The component renders **ToolBadge** components to display progress for each tool call, keeping users informed while the Durable Object performs network calls (e.g., to DataForSEO).

```tsx
export function OnboardingChatConversation({ projectId, domain }: {
  projectId: string;
  domain: string;
}) {
  // Bind the React component to the Durable Object instance.
  const agent = useAgent({ agent: "onboarding-chat", name: projectId });
  const { messages, sendMessage, status } = useAgentChat({ agent });

  // Send a user message to the DO.
  const sendText = (text: string) => void sendMessage({ text });

  // UI renders messages, tools, typing indicator, etc.
  return (
    <div className="flex min-h-0 flex-1">
      {/* ... UI omitted for brevity ... */}
      <ChatComposer busy={status === "submitted" || status === "streaming"}
                    onSend={sendText} />
    </div>
  );
}

```

The `useAgent` hook creates a handle to the Durable Object, while `useAgentChat` provides a streaming chat API that respects the Agents SDK's message format and connection state.

## Extending AI Workflows Beyond Onboarding

Because the Durable Object follows the Agents SDK contract, OpenSEO exposes additional workflows using the same pattern. The [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) file also exports `SiteAuditWorkflow` and `RankCheckWorkflow` as Agent-compatible Durable Objects. Any new workflow can be exposed by routing `"/agents/<name>"` to a new Durable Object class that extends the SDK's base classes, enabling rapid expansion of AI-driven SEO automation.

## Summary

- **HTTP Routing**: [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) uses `routeAgentRequest` to handle `/agents/*` paths, upgrade connections to WebSockets, and inject `onBeforeConnect` authorization hooks before reaching the Durable Object
- **Stateful Agents**: `OnboardingChatAgent` extends `AIChatAgent` to implement streaming LLM responses, tool calling (`read_website`, `get_seo_metrics`, `research_keywords`), and credit-based billing gates through Autumn
- **Persistence**: Chat history survives reloads through the Durable Object's built-in SQLite storage, with the project ID serving as the unique instance name for isolation
- **React Integration**: `useAgent` and `useAgentChat` hooks provide type-safe connections to Durable Objects, handling real-time message streaming and tool execution UI with `ToolBadge` components
- **Extensibility**: The architecture supports multiple AI workflows (e.g., `SiteAuditWorkflow`, `RankCheckWorkflow`) through the same SDK primitives without reimplementing WebSocket or persistence logic

## Frequently Asked Questions

### What is the Agents SDK's role in OpenSEO?

The Agents SDK abstracts low-level WebSocket handling, Durable Object lifecycle management, and LLM streaming protocols. This allows OpenSEO to focus on domain-specific SEO prompts, tool implementations against DataForSEO, and billing logic rather than infrastructure plumbing.

### How does authentication work with the Agents SDK integration?

OpenSEO implements `onBeforeConnect` and `onBeforeRequest` callbacks within the `routeAgentRequest` configuration in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts). These hooks call `authorizeOnboardingChat` to perform organization-level authorization before the WebSocket connection reaches the Durable Object, ensuring only authorized users access specific project chats.

### How is chat history persisted across sessions?

The Durable Object stores conversation history in its internal SQLite database via the SDK's persistence layer. Because each project uses its project ID as the Durable Object instance name, returning users automatically reconnect to the same stateful agent instance with full message history intact.

### Can developers add custom AI workflows beyond onboarding?

Yes. OpenSEO already exposes `SiteAuditWorkflow` and `RankCheckWorkflow` using the same pattern. Developers can create new Durable Object classes extending `AIChatAgent`, implement the `onChatMessage` method with custom tools, and route new `/agents/<workflow-name>` endpoints in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) to enable additional AI-driven workflows.