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 projectname = "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 = trueThe 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
$ 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.