cloudflare stack

why

platform entrypoints are discovery surface, not business owners. once a Cloudflare workflow or durable object shell absorbs domain logic, that logic can only be exercised by deploying the runtime, and every platform concern change (retries, idempotency, state transitions) becomes a domain edit. a thin shell keeps business behavior testable with ordinary unit tests regardless of runtime.

rule

apply framework entrypoints to Cloudflare-discovered declarations. use a leading ~ folder prefix for non-business entrypoints at a source root: ~workflows/, ~durable/, ~agents/, ~containers/ — the prefix makes exceptional runtime surface visible without polluting business domain folders.

keep the shell thin: validate input, establish context, delegate to a named domain owner returning a command or result, then shape the platform response. anything more hides platform concerns (retries, idempotency, state transitions) inside the shell and forces a full runtime deploy to test or debug.

resource families

folderuse it forkeep inside
~workflows/one Cloudflare WorkflowEntrypoint or Think workflow class per namesake file.workflow shell, platform input, delegation.
~durable/one DurableObject or container-facing class per namesake file.stateful coordination, rpc/websocket boundary, delegation.
~agents/one Think or Agents SDK resource class per namesake file.agent entrypoint, callable boundary, delegation.
~containers/a self-contained Cloudflare container build context.Dockerfile and that container's isolated Node runtime source.

ordinary bindings — D1, KV, R2, Vectorize, queues, environment values — get no ~ folder; their runtime access belongs in the relevant domain or responsibility owner.

placement decision

the prefix groups files Cloudflare discovers or invokes directly; it does not mean backend, shared, or non-business logic.

candidate fileplacement
class registered directly with a workflow, Agent, or Durable Object runtimematching ~ family; thin shell only.
provider client, database repository, mapper, or domain servicenormal responsibility/domain owner.
route handler that validates then delegatesframework-required route location, not a ~ family by default.
standalone task with no Cloudflare discovery contractordinary domain or technical folder.

entrypoint flow

  1. Cloudflare invokes the thin prefixed entrypoint.
  2. the entrypoint establishes execution context and validates platform-shaped input.
  3. it converts that input to a typed domain command or reference.
  4. a domain, service, or adapter owns business behavior and durable writes.
  5. the entrypoint returns the platform-shaped result or schedules the next durable operation.

verification

binding, entrypoint class, file name, and folder family must name the same resource. exercise the actual platform boundary, then inspect the shell for repository, provider, or domain policy — those belong outside the prefixed family.

good and bad

bad

// ~workflows/runProcess.workflow.ts
export const runProcessWorkflow = Workflow.define("runProcess", async (input) => {
const process = await db.query(`SELECT * FROM processes WHERE id = ?`, [input.processId]);
if (!process) {
throw new Error("Not found");
}
// repository queries, provider calls, retry policy all embedded
const result = await provider.runWithRetry(process.name, process.config);
if (result.failed) {
await db.update("processes", { status: "failed", error: result.message });
}
return result;
});

business logic and durable writes are embedded in the platform shell: testing retry policy or error handling requires a deployed workflow runtime, and a schema move becomes a two-step change (migrate, then update the embedded query string).

good

// ~workflows/runProcess.workflow.ts
export const runProcessWorkflow = Workflow.define("runProcess", async (input) => {
const command = await validateAndBuildRunCommand(input);
const result = await runProcessService.execute(command);
return mapResultForWorkflowResponse(result);
});
// domains/process/runProcess.ts
export async function validateAndBuildRunCommand(input: unknown) {
const validated = RunProcessInputSchema.parse(input);
const process = await processRepo.readRequired(validated.processId);
return { processId: process.id, name: process.name, config: process.config };
}
export async function execute(command: RunProcessCommand) {
const result = await providerClient.runWithRetry(command.name, command.config);
if (result.failed) {
await processRepo.updateStatus(command.processId, {
status: "failed",
error: result.message,
});
}
return result;
}

the workflow shell adapts platform input, builds a domain command, and delegates to an ordinary service: business logic is testable outside the workflow runtime, and schema changes touch only the persistence layer.

search pages

go to any page