Get started
Tutorial: your first contract
You will build policy/deploy.ul, the contract the guides use, one step at a time. Each step is a complete source, checked by ulang-lite, shown with the decisions it makes. Paste any of them into the playground to experiment.
The scenario: before every deploy or rollback, a deploy service asks whether it may go ahead.
Step 1: the facts
A contract starts with the facts it decides on: one entity with typed fields. The service will send these six values with every request.
CONTRACT ProductionDeploy {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES DeployRequest {
environment: string
tests_passed: bool
approvals: int
author_approved_own_change: bool
error_rate_pct: float
change_freeze: bool
}
}
}
rejected semantic[trigger-required] at 1:1 explicit TRIGGER action list required
This is not a contract yet. ulang-lite reports the first problem it finds, with a stable code: a contract must say which actions it governs.
Step 2: the action
TRIGGER lists the actions, each written action().
CONTRACT ProductionDeploy {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES DeployRequest {
environment: string
tests_passed: bool
approvals: int
author_approved_own_change: bool
error_rate_pct: float
change_freeze: bool
}
}
TRIGGER {
deploy()
}
}
rejected semantic[ungoverned-action] at 13:3 every trigger action requires an executable clause
Every listed action needs at least one rule. An action without rules would always be permitted, which is almost never what anyone meant, so it is an error.
Step 3: a first rule
A FORBID clause denies the action when its condition holds. The tag after it, [SOURCE:no_self_approval], is the clause's id, and decisions name it.
CONTRACT ProductionDeploy {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES DeployRequest {
environment: string
tests_passed: bool
approvals: int
author_approved_own_change: bool
error_rate_pct: float
change_freeze: bool
}
}
TRIGGER {
deploy()
}
PROHIBITION {
FORBID deploy WHEN DeployRequest.author_approved_own_change == true [SOURCE:no_self_approval]
}
}
valid ProductionDeploy 1 clause · sha256 4b1ab28648aa677d0e199d0c53d3573bd48ecd26746e7123b63d29b9d14335b2
Facts of the first request; each other request changes only the fields shown.
{
"DeployRequest": {
"environment": "production",
"tests_passed": true,
"approvals": 2,
"author_approved_own_change": false,
"error_rate_pct": 0.4,
"change_freeze": false
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| a normal deploy | deploy | as above | PERMIT | ProductionDeploy.deploy.permit |
| the author approved their own change | deploy | author_approved_own_change: true | DENY | ProductionDeploy.no_self_approval |
When no rule denies, the decision is PERMIT, named <Contract>.<action>.permit.
Step 4: guarantees
ENSURE states what must hold for the action to go ahead: the action is denied when the condition fails. Note the parentheses: ENSURE deploy(), but FORBID deploy.
CONTRACT ProductionDeploy {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES DeployRequest {
environment: string
tests_passed: bool
approvals: int
author_approved_own_change: bool
error_rate_pct: float
change_freeze: bool
}
}
TRIGGER {
deploy()
}
GUARANTEE {
ENSURE deploy() WHEN DeployRequest.tests_passed == true [SOURCE:tests_green]
ENSURE deploy() WHEN DeployRequest.approvals >= 2 [SOURCE:two_approvals]
}
PROHIBITION {
FORBID deploy WHEN DeployRequest.author_approved_own_change == true [SOURCE:no_self_approval]
}
}
valid ProductionDeploy 3 clauses · sha256 64a8766dab399915424f1024b1b38b353f7c27bc0dbfe03702bff58654149e49
Facts of the first request; each other request changes only the fields shown.
{
"DeployRequest": {
"environment": "production",
"tests_passed": true,
"approvals": 2,
"author_approved_own_change": false,
"error_rate_pct": 0.4,
"change_freeze": false
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| a normal deploy | deploy | as above | PERMIT | ProductionDeploy.deploy.permit |
| tests failing | deploy | tests_passed: false | DENY | ProductionDeploy.tests_green |
| one approval | deploy | approvals: 1 | DENY | ProductionDeploy.two_approvals |
Step 5: named, sourced constants
The 2 in step 4 is a policy number. A constant gives it a name, a type and a source, and keeps it in one place.
CONTRACT ProductionDeploy {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES DeployRequest {
environment: string
tests_passed: bool
approvals: int
author_approved_own_change: bool
error_rate_pct: float
change_freeze: bool
}
CONSTANTS {
min_approvals: int == 2 [SOURCE:ENG-HANDBOOK-7.1]
max_error_rate_pct: float == 1.5 [SOURCE:SRE-SLO-3]
}
}
TRIGGER {
deploy()
}
GUARANTEE {
ENSURE deploy() WHEN DeployRequest.tests_passed == true [SOURCE:tests_green]
ENSURE deploy() WHEN DeployRequest.approvals >= min_approvals [SOURCE:two_approvals]
}
PROHIBITION {
FORBID deploy WHEN DeployRequest.author_approved_own_change == true [SOURCE:no_self_approval]
FORBID deploy WHEN DeployRequest.error_rate_pct > max_error_rate_pct [SOURCE:error_budget]
}
}
valid ProductionDeploy 4 clauses · sha256 ed486438406d7f6025ff25851429dce5708b2d83bb7ef4d2b5ec7fb83f6cc8d8
Facts of the first request; each other request changes only the fields shown.
{
"DeployRequest": {
"environment": "production",
"tests_passed": true,
"approvals": 2,
"author_approved_own_change": false,
"error_rate_pct": 0.4,
"change_freeze": false
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| a normal deploy | deploy | as above | PERMIT | ProductionDeploy.deploy.permit |
| error rate 2.5% | deploy | error_rate_pct: 2.5 | DENY | ProductionDeploy.error_budget |
| error rate exactly 1.5% | deploy | error_rate_pct: 1.5 | PERMIT | ProductionDeploy.deploy.permit |
Constants are hard (==), typed int or float, and always sourced. A constant is compared with facts, never with a literal: min_approvals == 2 would be refused, because it does not depend on the request.
Step 6: combining conditions
AND and OR combine comparisons. AND binds tighter than OR, and parentheses group. Here the freeze applies only in production.
CONTRACT ProductionDeploy {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES DeployRequest {
environment: string
tests_passed: bool
approvals: int
author_approved_own_change: bool
error_rate_pct: float
change_freeze: bool
}
CONSTANTS {
min_approvals: int == 2 [SOURCE:ENG-HANDBOOK-7.1]
max_error_rate_pct: float == 1.5 [SOURCE:SRE-SLO-3]
}
}
TRIGGER {
deploy()
}
GUARANTEE {
ENSURE deploy() WHEN DeployRequest.tests_passed == true [SOURCE:tests_green]
ENSURE deploy() WHEN DeployRequest.approvals >= min_approvals [SOURCE:two_approvals]
}
PROHIBITION {
FORBID deploy WHEN DeployRequest.change_freeze == true AND DeployRequest.environment == "production" [SOURCE:change_freeze]
FORBID deploy WHEN DeployRequest.author_approved_own_change == true [SOURCE:no_self_approval]
FORBID deploy WHEN DeployRequest.error_rate_pct > max_error_rate_pct [SOURCE:error_budget]
}
}
valid ProductionDeploy 5 clauses · sha256 c6011a1c7c5a6458f4baeeb28608decb7f3f5fe6f4e6a4e27343b768ba526f3c
Facts of the first request; each other request changes only the fields shown.
{
"DeployRequest": {
"environment": "production",
"tests_passed": true,
"approvals": 2,
"author_approved_own_change": false,
"error_rate_pct": 0.4,
"change_freeze": false
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| a normal deploy | deploy | as above | PERMIT | ProductionDeploy.deploy.permit |
| freeze, production | deploy | change_freeze: true | DENY | ProductionDeploy.change_freeze |
| freeze, staging | deploy | environment: "staging" change_freeze: true | PERMIT | ProductionDeploy.deploy.permit |
There is no NOT. Write the opposite comparison: != for ==, >= for <, == false for == true. Patterns has the full table.
Step 7: a second action
One contract can govern several actions over the same facts. Each clause belongs to one action, so the freeze does not block rollbacks.
CONTRACT ProductionDeploy {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES DeployRequest {
environment: string
tests_passed: bool
approvals: int
author_approved_own_change: bool
error_rate_pct: float
change_freeze: bool
}
CONSTANTS {
min_approvals: int == 2 [SOURCE:ENG-HANDBOOK-7.1]
max_error_rate_pct: float == 1.5 [SOURCE:SRE-SLO-3]
}
}
TRIGGER {
deploy()
rollback()
}
GUARANTEE {
ENSURE deploy() WHEN DeployRequest.tests_passed == true [SOURCE:tests_green]
ENSURE deploy() WHEN DeployRequest.approvals >= min_approvals [SOURCE:two_approvals]
}
PROHIBITION {
FORBID deploy WHEN DeployRequest.change_freeze == true AND DeployRequest.environment == "production" [SOURCE:change_freeze]
FORBID deploy WHEN DeployRequest.author_approved_own_change == true [SOURCE:no_self_approval]
FORBID deploy WHEN DeployRequest.error_rate_pct > max_error_rate_pct [SOURCE:error_budget]
FORBID rollback WHEN DeployRequest.environment != "production" AND DeployRequest.environment != "staging" [SOURCE:known_environments]
}
}
valid ProductionDeploy 6 clauses · sha256 972a43946ba6393d9189b1f896d9411feabc11ae5d3e30747fe79dc7c2b374de
Facts of the first request; each other request changes only the fields shown.
{
"DeployRequest": {
"environment": "production",
"tests_passed": true,
"approvals": 2,
"author_approved_own_change": false,
"error_rate_pct": 0.4,
"change_freeze": false
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| roll back production | rollback | as above | PERMIT | ProductionDeploy.rollback.permit |
| roll back an unknown environment | rollback | environment: "dev" | DENY | ProductionDeploy.known_environments |
| roll back during a freeze | rollback | change_freeze: true | PERMIT | ProductionDeploy.rollback.permit |
Strings compare exactly, by code point: "Production" is not "production".
Step 8: which clause decides
The finished contract adds a comment, and an OBSERVABILITY block that tells callers what to log; it does not affect decisions. This is policy/deploy.ul.
// Production deploys: who may ship, and when.
CONTRACT ProductionDeploy {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES DeployRequest {
environment: string
tests_passed: bool
approvals: int
author_approved_own_change: bool
error_rate_pct: float
change_freeze: bool
}
CONSTANTS {
min_approvals: int == 2 [SOURCE:ENG-HANDBOOK-7.1]
max_error_rate_pct: float == 1.5 [SOURCE:SRE-SLO-3]
}
}
TRIGGER {
deploy()
rollback()
}
GUARANTEE {
ENSURE deploy() WHEN DeployRequest.tests_passed == true [SOURCE:tests_green]
ENSURE deploy() WHEN DeployRequest.approvals >= min_approvals [SOURCE:two_approvals]
}
PROHIBITION {
FORBID deploy WHEN DeployRequest.change_freeze == true AND DeployRequest.environment == "production" [SOURCE:change_freeze]
FORBID deploy WHEN DeployRequest.author_approved_own_change == true [SOURCE:no_self_approval]
FORBID deploy WHEN DeployRequest.error_rate_pct > max_error_rate_pct [SOURCE:error_budget]
FORBID rollback WHEN DeployRequest.environment != "production" AND DeployRequest.environment != "staging" [SOURCE:known_environments]
}
OBSERVABILITY {
LOG "deploy decision" ON decision
}
}
valid ProductionDeploy 6 clauses · sha256 686358ddc154b77b5b7b3a24870b8dd6f79b23d376ed039d6824a923284e4059
When several rules apply, the decision names exactly one clause:
- the first
FORBIDfor the action whose condition holds, in source order; - otherwise, the first
ENSUREfor the action whose condition fails; - otherwise,
PERMIT.
Facts of the first request; each other request changes only the fields shown.
{
"DeployRequest": {
"environment": "production",
"tests_passed": true,
"approvals": 2,
"author_approved_own_change": false,
"error_rate_pct": 0.4,
"change_freeze": false
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| a normal deploy | deploy | as above | PERMIT | ProductionDeploy.deploy.permit |
| freeze and self-approved: the first FORBID wins | deploy | author_approved_own_change: true change_freeze: true | DENY | ProductionDeploy.change_freeze |
| self-approved and tests failing: FORBID before ENSURE | deploy | tests_passed: false author_approved_own_change: true | DENY | ProductionDeploy.no_self_approval |
| tests failing and one approval: the first ENSURE that fails | deploy | tests_passed: false approvals: 1 | DENY | ProductionDeploy.tests_green |
So put the prohibition with the most useful explanation first. The digest under the contract identifies this exact contract: any change that could alter a decision changes it.
Next
- Run it: Quickstart.
- Serve it: Node.js or Cloudflare Workers.
- Test it: Testing contracts.
- See everything else the language can do: Language tour.