What Sigil is
About 1136 wordsAbout 4 min
2026-09-24
Sigil is a small language for writing rules. Your Go program hands a policy some typed input, the policy's rules look at it, and the answer is a typed decision: page the on-call, turn a feature on, approve a deploy, grant a role, apply a discount. Every decision carries a reason and the data the program needs to act on it.
A policy that routes alerts reads like this:
policy checkout.alerts: AlertRouting@1
when alert.severity == critical {
page(reason: critical_alert, target: team.oncall)
}
when alert.severity == warning {
notify(reason: routine, channel: team.channel)
}when blocks are rules. page(...) and notify(...) are decisions, and the decisions a policy may make, their reasons and their fields are fixed by a contract your Go code defines, called the kind. A typo in a field, a misspelled severity or a missing payload field is a compile error, not a rule that quietly never matches.
Project status
Sigil is nearly complete. The language, the Go API, composition, the CLI, the language server and editor support are built, and fuzzing covers every layer. Two pieces remain: loading a kind from a file at run time (policy.LoadKind) and host-ordered types such as versions. The roadmap tracks them. The language can still change; report problems through GitHub issues.
What you can decide with it
Sigil doesn't know what an alert, a deploy or a discount is. The kind says what the input looks like and which decisions exist, and the language only evaluates rules against it. The same few constructs cover very different problems:
The running example of these docs. Page someone, post to a channel, or drop the alert:
decision page { reason: critical_alert | sustained target: string }
decision drop { reason: muted | not_production }
decision notify { reason: routine | unrouted channel: string = "#alerts" }when alert.severity == warning and alert.firing_for >= 30m {
page(reason: sustained, target: team.oncall)
}
when alert.name in ["CheckoutCanaryLatency"] {
drop(reason: muted)
}Turn a feature on for enterprise customers, beta testers and a percentage of everyone else, but only in regions where it's ready:
policy flags.new_checkout: FeatureRollout@1
param percent: int = 20, min: 0, max: 100
when user.region not in ["eu-1", "eu-2"] {
disable(reason: region_not_ready)
}
when user.beta {
enable(reason: beta_tester, variant: "redesign")
}
when bucket < percent {
enable(reason: rollout)
}Decide which discount applies to a cart. The kind ranks the reasons, so a loyal customer's 20% wins over the first-order 10%:
policy shop.discounts: Discount@1
when customer.orders == 0 {
discount(reason: first_order, percent: 10)
}
when cart.items >= 10 and cart.total >= 100.0 {
discount(reason: bulk, percent: 15)
}
when customer.tier in [gold, platinum] {
discount(reason: loyalty, percent: 20)
}Approve a release, send it to review, or deny it. Per-team policies builds the full version:
when release.soak < min_soak and not release.hotfix {
deny(reason: soak_too_short)
}
when service.tier in tiers and actor.teams any in service.owners {
review(reason: service_owner, approvers: approvers)
}A kind can collect every decision that holds instead of picking one, here the roles someone holds at once:
when team_member {
reader(reason: team_member)
deployer(reason: team_member)
}
when on_call {
deployer(reason: oncall, ttl: 2h)
}The first three are small enough to write in an afternoon. The last two come from deploygate, a complete service that uses both kinds.
How it fits together
- The kind is Go code. You describe the input as structs, declare the decisions with their reasons and payloads, and say which decision wins when several rules fire. Nobody writes a kind by hand.
- Policies are
.sigilfiles written against the kind. They can live in the service's repository, in a separate one, or in a ConfigMap. - Your service compiles the policies once and evaluates them for every request, from as many goroutines as it likes. A compiled policy is immutable.
- The
sigilCLI checks, evaluates and tests policies without your Go code. It reads the kind from a file your service exports.
The evaluator doesn't loop, recurse or call anything the kind doesn't declare, so a policy always halts. Rule order never matters: every rule is evaluated, and the kind's precedence picks the winner. Design goals explains why.
What it isn't
Sigil isn't a general-purpose language, and it isn't meant to replace OPA or Cedar for org-wide authorization. It's for decisions that live inside one application. Go programs embed it natively; programs in other languages run the same engine compiled to WebAssembly, as Embed Sigil in TypeScript shows.
Install
The Go library:
go get github.com/spechtlabs/sigil@latestThe sigil CLI, for checking and testing policies. People who only write policies need the CLI, not Go:
brew install --cask spechtlabs/tap/sigilOr go install github.com/spechtlabs/sigil/cmd/sigil@latest. The releases also have signed archives for Linux and macOS.
Learn it step by step
The tour reads one complete policy in five minutes and lets you route alerts through it in the browser. After that, these steps build the alert router from an empty Go module. Each one adds one idea:
Define the input and evaluate a policy
Describe the input and the decisions in Go, load one policy and act on its decision.
Rules, lets, nesting, precedence and the compile errors that catch mistakes.
Write the contract to a file, so policies can be checked without your Go code.
Check, evaluate and test with the CLI
sigil check,sigil evaland test files that pin the decisions.A library of shared helpers and policies that teams invoke with their own values.
sigil explainflattens a composed policy into the rules it can fire.Rules the host requires of every policy, so no team can switch them off.
Steps 1 to 4 are all a service needs when one team owns its policies. Steps 5 to 7 are for when several teams write policies against the same kind. The same router, grown into a complete service, is examples/alert-routing: a TypeScript app on Sigil's WebAssembly build, with an operator console, an observability stack and load tests.
