Guides

Cloudflare Workers

ulang-lite has no dependencies and uses no Node.js APIs, so it runs in Workers unchanged. Wrangler bundles the TypeScript source, and the contract is imported as a text module.

Configuration

wrangler.tomlexample project
name = "deploy-policy"
main = "src/worker.ts"
compatibility_date = "2026-09-01"

# Bundle contracts as text modules: `import source from '../policy/deploy.ul'`.
[[rules]]
type = "Text"
globs = ["**/*.ul"]
fallthrough = true

The Text rule makes import source from '../policy/deploy.ul' return the file's contents as a string. For TypeScript, declare the module:

src/ul.d.tsexample project
// Lets TypeScript type `import source from './x.ul'` (Wrangler bundles .ul files as text).
declare module '*.ul' {
  const source: string;
  export default source;
}

The Worker

src/worker.tsexample project
// The same decision endpoint as a Cloudflare Worker. The contract is bundled as text (see wrangler.toml).
import source from '../policy/deploy.ul';
import { loadPolicy } from './policy.ts';

const policy = loadPolicy(source, 'policy/deploy.ul'); // compiled once per isolate, not per request

export default {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    if (request.method !== 'POST' || url.pathname !== '/decide') return Response.json({ error: 'POST /decide' }, { status: 404 });
    let body: { action?: unknown; facts?: unknown } | null;
    try { body = await request.json(); } catch { return Response.json({ error: 'invalid JSON' }, { status: 400 }); }
    if (typeof body?.action !== 'string') return Response.json({ error: 'action must be a string' }, { status: 400 });
    const outcome = policy.decide(body.action, body.facts);
    return Response.json(outcome, { status: 'error' in outcome ? 400 : 200 });
  },
};

It reuses src/policy.ts from the Node.js guide. The contract is compiled once per isolate, when the module loads, not per request.

Bundle it

example projectreal output
$ wrangler deploy --dry-run --outdir dist-worker

 ⛅️ wrangler 4.59.2 (update available 4.151.0)
──────────────────────────────────────────────
Total Upload: 53.36 KiB / gzip: 14.16 KiB
No bindings found.
--dry-run: exiting now.

Requests and responses

The build imports that bundle and calls its fetch handler with these requests. It runs in Node.js, with the .ul module loaded as a string, as Wrangler bundles it:

POST /decide

{
  "action": "deploy",
  "facts": {
    "DeployRequest": {
      "environment": "production",
      "tests_passed": true,
      "approvals": 2,
      "author_approved_own_change": false,
      "error_rate_pct": 0.4,
      "change_freeze": false
    }
  }
}

200

{"verdict":"PERMIT","clause_id":"ProductionDeploy.deploy.permit","contract_sha256":"686358ddc154b77b5b7b3a24870b8dd6f79b23d376ed039d6824a923284e4059"}

POST /decide

{
  "action": "deploy",
  "facts": {
    "DeployRequest": {
      "environment": "production",
      "tests_passed": true,
      "approvals": 2,
      "author_approved_own_change": false,
      "error_rate_pct": 0.4,
      "change_freeze": true
    }
  }
}

200

{"verdict":"DENY","clause_id":"ProductionDeploy.change_freeze","contract_sha256":"686358ddc154b77b5b7b3a24870b8dd6f79b23d376ed039d6824a923284e4059"}

POST /decide

{
  "action": "rollback",
  "facts": {
    "DeployRequest": {
      "environment": "production",
      "tests_passed": true,
      "approvals": 2,
      "author_approved_own_change": false,
      "error_rate_pct": 0.4,
      "change_freeze": true
    }
  }
}

200

{"verdict":"PERMIT","clause_id":"ProductionDeploy.rollback.permit","contract_sha256":"686358ddc154b77b5b7b3a24870b8dd6f79b23d376ed039d6824a923284e4059"}

POST /decide

{
  "action": "deploy",
  "facts": {
    "DeployRequest": {
      "environment": "production",
      "tests_passed": true,
      "approvals": 2.5,
      "author_approved_own_change": false,
      "error_rate_pct": 0.4,
      "change_freeze": false
    }
  }
}

400

{"error":"invalid type or domain for DeployRequest.approvals"}

POST /decide

{
  "action": "hotfix",
  "facts": {
    "DeployRequest": {
      "environment": "production",
      "tests_passed": true,
      "approvals": 2,
      "author_approved_own_change": false,
      "error_rate_pct": 0.4,
      "change_freeze": false
    }
  }
}

400

{"error":"ProductionDeploy does not govern hotfix"}

POST /decide

{"action": "deploy", 

400

{"error":"invalid JSON"}

GET /

no body

404

{"error":"POST /decide"}

Deploy with wrangler deploy.