Skip to content
Looms
Documentation
On this page

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.

bash
npm install @swirls/looms zodnpm install --save-dev wrangler typescript @types/bunmkdir cloudflare

Save this as cloudflare/worker.ts. The environment type is generated from the configuration below.

ts
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.

jsonc
{  "$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.

bash
npx wrangler types --config cloudflare/wrangler.jsonc cloudflare/worker-configuration.d.tsnpx wrangler dev --config cloudflare/wrangler.jsonc --port 8791

Wrangler serves the API locally and persists development storage under .wrangler. In another terminal, create a run and keep the returned runId:

bash
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/pendingApprovals

Stop 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.

bash
npx wrangler deploy --dry-run --config cloudflare/wrangler.jsoncnpx wrangler loginnpx wrangler deploy --config cloudflare/wrangler.jsonc

Add a service binding in your calling application's Wrangler configuration:

jsonc
{  "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:

ts
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.