Hosting & storage
Choose persistent storage, keep one writer per run, and configure timers and signals to wake parked work. Cloudflare Durable Objects supply per-run SQLite and durable alarms. With a local Bun host, you manage storage and wake scheduling yourself.
Run the quickstart on Cloudflare#
Start with review-definition.ts from the quickstart. The same agent, workflow, and approval modules run inside a Durable Object. This example uses deterministic tools and simulated publication, so it requires no model API key.
npm install @swirls/looms zodnpm install --save-dev wrangler typescript @types/bunmkdir cloudflareSave this as cloudflare/worker.ts. The environment type is generated from the configuration below.
import { LoomsDurableObject, routeToDurableObject, type LoomsDurableObjectConfig,} from '@swirls/looms/cloudflare'import { modules } from '../review-definition'export class LoomsRun extends LoomsDurableObject<Env> { override configure(): LoomsDurableObjectConfig { return { modules } }}// Private service: put authentication and per-run authorization in the calling app.// The accompanying config disables public workers.dev and preview URLs.export default { fetch: (request, env): Promise<Response> => routeToDurableObject(env.LOOMS_RUN, request).then( (response) => response ?? new Response('Not Found', { status: 404 }), ),} satisfies ExportedHandler<Env>Save this as cloudflare/wrangler.jsonc. The migration creates SQLite-backed objects. Keep migration history when changing an existing deployment; do not replace it with a new initial migration.
{ "$schema": "../../node_modules/wrangler/config-schema.json", "name": "looms-review", "main": "worker.ts", "compatibility_date": "2026-09-15", "workers_dev": false, "preview_urls": false, "durable_objects": { "bindings": [{ "name": "LOOMS_RUN", "class_name": "LoomsRun" }], }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["LoomsRun"] }], "observability": { "enabled": true },}The Worker has no public route, workers.dev address, or preview URL. Deploy it as a private service and call it from your application Worker after authentication and per-run authorization. Disabling public URLs does not implement tenant authorization inside your application.
Set maxPendingRunOperations in configure() to bound queued work for a hot run. The object rejects excess operations instead of growing an unbounded in-memory queue. maxWakeIterations bounds one wake cycle; alarms resume work that remains.
npx wrangler types --config cloudflare/wrangler.jsonc cloudflare/worker-configuration.d.tsnpx wrangler dev --config cloudflare/wrangler.jsonc --port 8791Wrangler serves the API locally and persists development storage under .wrangler. In another terminal, create a run and keep the returned runId:
curl -X POST http://localhost:8791/runs \ -H 'content-type: application/json' \ -d '{"kind":"workflow","definitionName":"release-v1","input":{"topic":"Launch"}}'curl http://localhost:8791/runs/RUN_ID/projections/pendingApprovalsStop Wrangler, start it again with the same configuration, and read that pending approval again. Replace RUN_ID with the returned identifier. Inspect the events or send an approval decision through the HTTP API; the client guide covers application integration.
Deploy a private execution service#
Choose a unique Worker name, validate the bundle, and deploy to your Cloudflare account. This provisions Durable Object storage. Use a separate Worker name and namespace for staging.
npx wrangler deploy --dry-run --config cloudflare/wrangler.jsoncnpx wrangler loginnpx wrangler deploy --config cloudflare/wrangler.jsoncAdd a service binding in your calling application's Wrangler configuration:
{ "services": [{ "binding": "EXECUTION", "service": "looms-review" }]}After validating the current user's permission for the operation and run, forward a Request through env.EXECUTION.fetch(request). Use the Looms route path, such as /runs/:id/events, even if your application exposes it at a different prefix. Apply the same checks to SSE and read endpoints.
For a real model, configure the provider adapter in the Worker and store credentials with npx wrangler secret put OPENROUTER_API_KEY --config cloudflare/wrangler.jsonc. Add local credentials to an ignored cloudflare/.dev.vars. Never place provider keys in browser code. See model integration and Cloudflare's Durable Objects guide.
Host within a Bun application#
For a local HTTP host, install @swirls/looms, import the actor and core subpaths, then use the quickstart's modules. This complete server binds to localhost:
import { mkdir } from 'node:fs/promises'import { join } from 'node:path'import { createLocalActorHost } from '@swirls/looms/actor'import { bunSqliteEventStore } from '@swirls/looms/core/bun-sqlite'import { modules } from './review-definition'const runsDir = join(process.cwd(), '.looms', 'runs')await mkdir(runsDir, { recursive: true })const host = createLocalActorHost({ modules, createStore: async (runId) => bunSqliteEventStore({ path: join(runsDir, encodeURIComponent(runId) + '.sqlite'), }),})Bun.serve({ hostname: '127.0.0.1', port: 8791, fetch: (request) => host.fetch(request) })Keep the database directory on persistent storage and run one host for those files. The local host rescans timers when a run cell opens; it does not discover every database on startup. Maintain a run index and reopen runs at boot for unattended timer recovery. Durable Objects provide host-managed alarms.
Snapshots, recovery, and retention#
Actor cells recover from a snapshot plus subsequent events. A parked wake cycle writes a snapshot; snapshotEvery defaults to 200 durable events for checkpoints during a wake. Recovery time depends on history size, reducers, and host conditions.
trimAfterSnapshot can prune events needed by historical replay and rebuilding projections. Establish retention and backup requirements first. Post-commit projectors can build cross-run indexes and archives, but downstream failures require their own monitoring and recovery policy. See operations and reliability.
Other hosts#
createLooms defaults to an in-memory store; the quickstart configures SQLite explicitly. For another actor platform, use createActorCell and supply storage and wake scheduling while preserving one writer per run. Compatibility with self-hosted Workers runtimes such as celld needs validation against a specific version; this guide does not provide a verified celld deployment.
Parking does not imply zero hosting cost. Account for stored events, requests, alarms, and connected clients when estimating capacity.