# How OpenSEO Addresses SiteAuditWorkflow and AuditScratchpad DO Instances

> Discover how OpenSEO manages SiteAuditWorkflow and AuditScratchpad DO instances with a dedicated Cloudflare worker and helper functions. Learn about the open-seo-audit integration.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-09-12

---

**OpenSEO addresses SiteAuditWorkflow and AuditScratchpad DO instances through a dedicated auxiliary Cloudflare worker (`open-seo-audit`) that binds the workflow cross-script as `SITE_AUDIT_WORKFLOW` and resolves the scratchpad Durable Object via the `getAuditScratchpad(auditId)` helper using the `AUDIT_SCRATCHPAD` namespace.**

OpenSEO runs site-audit jobs inside a specialized auxiliary Cloudflare Worker that hosts two tightly-coupled components: a **Cloudflare Workflow** for orchestration and a **Durable Object (DO)** for transient crawl state. Understanding how these SiteAuditWorkflow and AuditScratchpad DO instances are created, addressed, and cleaned up is essential for developers working with the every-app/open-seo codebase.

## Architecture Overview

The system separates concerns between the main application worker and an auxiliary audit worker named `open-seo-audit`. This auxiliary worker, defined in [`src/audit-worker.ts`](https://github.com/every-app/open-seo/blob/main/src/audit-worker.ts), re-exports the `SiteAuditWorkflow` and hosts the `AuditScratchpad` Durable Object class. Cross-script bindings declared in `wrangler.audit.jsonc` and `wrangler.jsonc` enable the main worker to trigger workflow instances while the auxiliary worker manages the scratchpad lifecycle.

Two critical environment bindings enable instance addressing:

- **`SITE_AUDIT_WORKFLOW`**: A cross-scriptWorkflow binding that allows the main worker to create and retrieve workflow instances.
- **`AUDIT_SCRATCHPAD`**: A Durable Object namespace binding that provides access to scratchpad instances via string-derived IDs.

## Creating and Addressing SiteAuditWorkflow Instances

The **SiteAuditWorkflow** is instantiated through the `SITE_AUDIT_WORKFLOW` binding. When a user initiates an audit, the `AuditService` class creates a new workflow instance using the `create()` method.

According to the source code in [`src/server/features/audit/services/AuditService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/AuditService.ts) (lines 109-126), the workflow creation follows this pattern:

```typescript
await env.SITE_AUDIT_WORKFLOW.create({
  workflow: "SiteAuditWorkflow",
  payload: {
    auditId,
    billingCustomer,
    projectId,
    startUrl,
    config,
  },
});

```

This spawns a durable Cloudflare Workflow instance that owns the entire lifecycle of the audit. To retrieve an existing instance for status checks or termination, the code uses `env.SITE_AUDIT_WORKFLOW.get(auditId)`, which returns a workflow handle for that specific audit ID.

## Managing AuditScratchpad DO Instances

While the workflow orchestrates phases, the **AuditScratchpad** Durable Object stores transient crawl state—including the frontier queue, page mirrors, and link graph—in an embedded SQLite database. Each audit receives its own isolated scratchpad instance.

The addressing mechanism lives in [`src/server/features/audit/AuditScratchpad.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/AuditScratchpad.ts) (lines 48-56). The helper function `getAuditScratchpad(auditId)` resolves the DO instance:

```typescript
import { getAuditScratchpad } from "@/server/features/audit/AuditScratchpad";

const scratchpad = getAuditScratchpad(auditId);

```

Internally, this function accesses the `env.AUDIT_SCRATCHPAD` namespace and derives the DO ID from the audit identifier:

```typescript
const id = namespace.idFromName(auditId);
return namespace.get(id);

```

The scratchpad exposes RPC methods including `seedStart`, `seedSitemapUrls`, `claimChunk`, `recordBatch`, and `runFinalizeChecks` that the workflow calls during execution phases defined in [`src/server/workflows/siteAuditWorkflowPhases.ts`](https://github.com/every-app/open-seo/blob/main/src/server/workflows/siteAuditWorkflowPhases.ts).

## Lifecycle Management and Cleanup

Both components implement explicit cleanup strategies to prevent resource leakage.

**Workflow termination** occurs naturally when the workflow reaches its final step, though the instance remains addressable via `get()` for historical queries.

**Scratchpad destruction** happens through two pathways:

1. **Explicit cleanup**: The workflow instructs the auxiliary worker to destroy the scratchpad via the `destroyScratchpad(auditId)` function exposed in [`src/audit-worker.ts`](https://github.com/every-app/open-seo/blob/main/src/audit-worker.ts) (lines 29-33).

2. **Self-healing alarms**: If the workflow crashes, the scratchpad implements a self-cleanup alarm (defined in [`src/server/features/audit/AuditScratchpad.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/AuditScratchpad.ts), lines 88-98) that wipes its storage after 7 days.

```typescript
// Inside the auxiliary worker or workflow finalization
await getAuditScratchpad(auditId).destroy();

```

## Integration Code Examples

The following patterns demonstrate how these instances interact during a typical audit:

**Initializing the crawl state:**

```typescript
const scratchpad = getAuditScratchpad(auditId);
await scratchpad.seedStart(normalizedStartUrl);
await scratchpad.seedSitemapUrls(sitemapUrls);

```

**Processing batches during the crawl phase:**

```typescript
const { urls, isRetry } = await scratchpad.claimChunk(chunkNumber, 100);
// ... perform crawling ...
await scratchpad.recordBatch(results);

```

**Final cleanup:**

```typescript
// Within the workflow's finalize phase
await env.SITE_AUDIT_WORKFLOW.get(auditId).destroyScratchpad(auditId);

```

## Summary

- **Cross-script bindings** in `wrangler.audit.jsonc` and `wrangler.jsonc` link the main worker to the auxiliary `open-seo-audit` worker via `SITE_AUDIT_WORKFLOW`.
- **Workflow instances** are created via `env.SITE_AUDIT_WORKFLOW.create()` and retrieved via `.get(auditId)` in [`src/server/features/audit/services/AuditService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/AuditService.ts).
- **Scratchpad DO instances** are addressed through the `getAuditScratchpad(auditId)` helper using the `AUDIT_SCRATCHPAD` namespace with `idFromName(auditId)`.
- **Transient state** is stored in the scratchpad's embedded SQLite database, accessed through RPC methods like `claimChunk` and `recordBatch`.
- **Resource cleanup** combines explicit `destroy()` calls with automatic 7-day alarms to ensure scratchpads do not persist indefinitely.

## Frequently Asked Questions

### How does the main OpenSEO worker communicate with the SiteAuditWorkflow?

The main worker communicates through the `SITE_AUDIT_WORKFLOW` binding declared in `wrangler.jsonc`. This cross-script binding allows the main worker to call `env.SITE_AUDIT_WORKFLOW.create()` to start new audits and `env.SITE_AUDIT_WORKFLOW.get(auditId)` to retrieve existing workflow instances, even though the workflow logic resides in the separate `open-seo-audit` auxiliary worker.

### What is the relationship between the SiteAuditWorkflow and the AuditScratchpad?

The **SiteAuditWorkflow** is a Cloudflare Workflow that orchestrates the audit phases (discovery, crawling, Lighthouse analysis, finalization), while the **AuditScratchpad** is a Durable Object that stores transient crawl-specific state. The workflow addresses the scratchpad via the `getAuditScratchpad(auditId)` helper to maintain the frontier queue and link graph, ensuring that crawl state persists across workflow steps without polluting the main database.

### How are AuditScratchpad DO instances isolated between different audits?

Each audit receives its own isolated scratchpad instance because the `getAuditScratchpad` helper generates a unique Durable Object ID using `namespace.idFromName(auditId)`, where `auditId` is a unique string identifier. This ensures that the SQLite database and state for one audit never intersects with another, providing complete data isolation at the DO level.

### What happens if a SiteAuditWorkflow crashes before completing cleanup?

If a workflow crashes before calling `destroyScratchpad()`, the **AuditScratchpad** Durable Object includes a self-cleanup mechanism. As implemented in [`src/server/features/audit/AuditScratchpad.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/AuditScratchpad.ts) (lines 88-98), the scratchpad sets an alarm that automatically wipes its embedded SQLite storage after 7 days, preventing orphaned data from consuming resources indefinitely.