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.

Step 1: the factstut-1
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().

Step 2: the actiontut-2
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.

Step 3: a first ruletut-3
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
  }
}
RequestActionChangedVerdictClause
a normal deploydeployas abovePERMITProductionDeploy.deploy.permit
the author approved their own changedeployauthor_approved_own_change: trueDENYProductionDeploy.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.

Step 4: guaranteestut-4
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
  }
}
RequestActionChangedVerdictClause
a normal deploydeployas abovePERMITProductionDeploy.deploy.permit
tests failingdeploytests_passed: falseDENYProductionDeploy.tests_green
one approvaldeployapprovals: 1DENYProductionDeploy.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.

Step 5: named, sourced constantstut-5
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
  }
}
RequestActionChangedVerdictClause
a normal deploydeployas abovePERMITProductionDeploy.deploy.permit
error rate 2.5%deployerror_rate_pct: 2.5DENYProductionDeploy.error_budget
error rate exactly 1.5%deployerror_rate_pct: 1.5PERMITProductionDeploy.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.

Step 6: combining conditionstut-6
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
  }
}
RequestActionChangedVerdictClause
a normal deploydeployas abovePERMITProductionDeploy.deploy.permit
freeze, productiondeploychange_freeze: trueDENYProductionDeploy.change_freeze
freeze, stagingdeployenvironment: "staging" change_freeze: truePERMITProductionDeploy.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.

Step 7: a second actiontut-7
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
  }
}
RequestActionChangedVerdictClause
roll back productionrollbackas abovePERMITProductionDeploy.rollback.permit
roll back an unknown environmentrollbackenvironment: "dev"DENYProductionDeploy.known_environments
roll back during a freezerollbackchange_freeze: truePERMITProductionDeploy.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.

policy/deploy.ultut-final
// 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:

  1. the first FORBID for the action whose condition holds, in source order;
  2. otherwise, the first ENSURE for the action whose condition fails;
  3. 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
  }
}
RequestActionChangedVerdictClause
a normal deploydeployas abovePERMITProductionDeploy.deploy.permit
freeze and self-approved: the first FORBID winsdeployauthor_approved_own_change: true change_freeze: trueDENYProductionDeploy.change_freeze
self-approved and tests failing: FORBID before ENSUREdeploytests_passed: false author_approved_own_change: trueDENYProductionDeploy.no_self_approval
tests failing and one approval: the first ENSURE that failsdeploytests_passed: false approvals: 1DENYProductionDeploy.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