Typed against a contract
Your Go code defines the kind, with its inputs, enums, functions and decisions. A typo like alert.severty, or a severity spelled critcal, fails at compile time with a fix hint instead of silently switching a rule off.
Write rules that turn your program's input into a typed decision, such as paging the on-call, rolling out a feature or approving a deploy. Every decision carries a reason and a payload, and every policy is checked against a contract your Go code defines.

Most services grow a rule engine by accident, one YAML matcher at a time. Sigil is a language small enough to read on first contact and strict enough to trust with the rules that matter.
The same rules, expressed two ways.
Stringly typed, checked at runtime, if at all
Typed, halting, and explainable
An alert router asks a policy what to do with each alert: page the on-call, drop it, or post it to a channel. Your Go code defines the contract, a policy decides, and your code acts on a typed result.
Define the input and the decisions in Go
type Input struct {
Alert Alert `policy:"alert"`
Team Team `policy:"team"`
}
var (
Page = policy.NewDecision[PageData]("page", "critical_alert", "sustained")
Drop = policy.NewDecision[policy.None]("drop", "muted", "not_production")
Notify = policy.NewDecision[NotifyData]("notify", "routine", "unrouted")
)
var Kind = policy.NewKind[Input]("AlertRouting",
policy.WithVersion(1),
policy.WithEnum(Critical, Warning, Info),
policy.WithDecisions(Page, Drop, Notify), // a page beats a drop beats a notification
policy.WithDefault(Notify.Reason("unrouted")),
// ...
)Write the rules
policy checkout.alerts: AlertRouting@1
let pre_production = alert.labels["env"] in ["staging", "dev"]
when not pre_production and alert.severity == critical {
page(reason: critical_alert, target: team.oncall)
}
when not pre_production and alert.severity == warning {
when alert.firing_for >= 30m {
page(reason: sustained, target: team.oncall)
}
notify(reason: routine, channel: team.channel)
}
when pre_production {
drop(reason: not_production)
}Load once, evaluate per alert, act on the typed result
p, err := Kind.Load(policies, "checkout.alerts")
// ...
res, err := p.Eval(ctx, Input{Alert: alert, Team: team})
// ...
switch d := res.Value().(type) {
case PageData:
pageOncall(d.Target, res.Reason) // d is a typed PageData
case NotifyData:
notifySlack(d.Channel, res.Reason)
case policy.None: // drop
log.Info("dropped", "reason", res.Reason)
}A warning that has been firing for 45 minutes matches two rules. Both become candidates, and the page wins because the kind ranks it above the notification:
checkout.alerts: page(reason: sustained)
target = "checkout-primary"
trace: 2 candidates
* page(reason: sustained) checkout/alerts.sigil:11:5
when not pre_production and alert.severity == warning
and alert.firing_for >= 30m
target = "checkout-primary"
notify(reason: routine) checkout/alerts.sigil:14:3
channel = "#checkout-alerts"The tour reads this policy line by line and lets you route alerts through it in the browser.
Sigil doesn't know what an alert is. The kind says what the input looks like and which decisions exist, and the same rules, lets and precedence decide feature rollouts, discounts, deploy approvals or the roles someone holds. What Sigil is shows five of them side by side, and deploygate is a complete service that decides deploy approvals and access grants with two kinds.
Teams that share a kind can share rules too: a platform team publishes a library of helpers and policies with typed params, product teams invoke them with their own values, and the host can require the rules no team may switch off. Share rules across teams builds that from scratch.
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.
Select your Platform