Authoring Invariants
An invariant starts as intent text — a plain-language claim about a system property. Forgecroft translates that intent into a set of evidence checks and surfaces any honest gaps. This page covers the full authoring lifecycle: preview, create, update, re-analyze, and retire.
Before You Start
Section titled “Before You Start”You need write access at the org level. Preview and create both consume one AI inference; the cost class is returned in the preview result so you can evaluate it before committing.
Previewing an Invariant
Section titled “Previewing an Invariant”Preview processes your intent without persisting anything. Use it to inspect the evidence checks, review honest gaps, and check the estimated cost class before creating.
forgecroft invariants preview --intent "no public S3 buckets"You can also pass intent on stdin:
echo "no public S3 buckets" | forgecroft invariants previewOr as a positional argument:
forgecroft invariants preview "no public S3 buckets"The preview output shows the evidence checks, honest gaps, and token usage:
Processed by anthropic (claude-sonnet-4-5)Estimated cost class: lowTokens used: 1240 in, 310 out
Evidence checks (1): 1. type=policy engine=opa_rego/v1
Honest gaps (1): - [RUNTIME_NOT_OBSERVABLE] Bucket ACL configuration applied outside of Terraform cannot be detected from plan output alone.Add --json to get the full structured response.
Go SDK
Section titled “Go SDK”preview, err := client.PreviewInvariant(ctx, "no public S3 buckets")if err != nil { return err}fmt.Printf("Cost class: %s\n", preview.EstimatedCostClass)for _, gap := range preview.HonestGaps { fmt.Printf("Gap [%s]: %s\n", gap.Code, gap.Message)}POST /invariants/previewContent-Type: application/json
{ "intent_text": "no public S3 buckets"}Response:
{ "compiled_mix": { ... }, "honest_gaps": [ { "code": "RUNTIME_NOT_OBSERVABLE", "message": "Bucket ACL configuration applied outside of Terraform cannot be detected from plan output alone." } ], "estimated_cost_class": "low", "ai_provider": "anthropic", "ai_model": "claude-sonnet-4-5", "input_tokens": 1240, "output_tokens": 310}Preview Response Fields
Section titled “Preview Response Fields”| Field | Description |
|---|---|
compiled_mix | The evidence-check plan Forgecroft produced — the evaluation recipe for this invariant |
honest_gaps | Aspects of the intent the available evidence sources cannot fully prove (see below) |
estimated_cost_class | Relative per-evaluation cost: low, medium, or high |
ai_provider | The AI provider that processed the intent |
ai_model | The specific model used |
input_tokens / output_tokens | Token usage for this preview call |
Honest Gaps
Section titled “Honest Gaps”Honest gaps are aspects of your intent that the current evidence sources cannot fully prove. They are surfaced at authoring time so you know upfront what the invariant can and cannot establish.
A gap entry has a code (machine-readable identifier) and a message (human-readable explanation). For example:
RUNTIME_NOT_OBSERVABLE— the claim requires runtime state that is not available from plan output.PARTIAL_COVERAGE— the intent covers multiple resource types; evidence is available for some but not all.
Honest gaps are a feature. They mean Forgecroft is precise about what it can prove. An invariant with gaps still evaluates — it may produce Compliant verdicts where evidence exists and Needs evidence verdicts where it does not.
If a gap is unacceptable, you have two options: reword the intent to focus on what is provable, or accept the gap and plan to add the missing evidence source later.
Creating an Invariant
Section titled “Creating an Invariant”Create processes the intent and persists the invariant in one call. There are no drafts.
forgecroft invariants create --intent "no public S3 buckets"Via stdin:
echo "no public S3 buckets" | forgecroft invariants createOutput:
Created invariant inv-01j4k... (status=active, source=customer, version=1)Intent: no public S3 bucketsGo SDK
Section titled “Go SDK”inv, err := client.CreateInvariant(ctx, "no public S3 buckets")if err != nil { return err}fmt.Printf("Created %s\n", inv.ID)POST /invariantsContent-Type: application/json
{ "intent_text": "no public S3 buckets"}Returns 201 Created with the full invariant record:
{ "id": "inv-01j4k...", "org_id": "org-uuid", "intent_text": "no public S3 buckets", "intent_version": 1, "status": "active", "source": "customer", "compiled_mix": { ... }, "created_at": "2026-05-11T10:00:00Z", "last_compiled_at": "2026-05-11T10:00:00Z"}Listing Invariants
Section titled “Listing Invariants”forgecroft invariants listGo SDK
Section titled “Go SDK”invs, err := client.ListInvariants(ctx)GET /invariantsReturns all active invariants for the org. Retired invariants are excluded.
Getting a Single Invariant
Section titled “Getting a Single Invariant”forgecroft invariants get <id>Go SDK
Section titled “Go SDK”inv, err := client.GetInvariant(ctx, id)GET /invariants/{id}Updating an Invariant
Section titled “Updating an Invariant”PATCH accepts intent_text (triggers re-analysis, bumps intent_version), status, or both.
There is no dedicated patch subcommand. Use the API or SDK for intent updates; use delete to retire.
Go SDK
Section titled “Go SDK”newIntent := "no public S3 buckets in any region"inv, err := client.PatchInvariant(ctx, id, sdk.PatchInvariantInput{ IntentText: &newIntent,})To change status only:
retired := "retired"inv, err := client.PatchInvariant(ctx, id, sdk.PatchInvariantInput{ Status: &retired,})PATCH /invariants/{id}Content-Type: application/json
{ "intent_text": "no public S3 buckets in any region"}Updating intent_text triggers a fresh analysis and increments intent_version. The compiled_mix is replaced. Updating status alone does not re-analyze.
Accepted status values: active, retired.
Retiring (Deleting) an Invariant
Section titled “Retiring (Deleting) an Invariant”Delete is a soft delete — the row is retained for audit, but evaluators skip it on subsequent runs.
forgecroft invariants delete <id>Output:
Retired invariant inv-01j4k...Go SDK
Section titled “Go SDK”err := client.DeleteInvariant(ctx, id)DELETE /invariants/{id}Returns 204 No Content. The invariant’s status is set to retired.
Re-analyzing an Invariant
Section titled “Re-analyzing an Invariant”Re-analysis re-runs the intent against the current state of evidence sources without changing the intent_text. Use this when you want an existing invariant to take advantage of newly available evidence sources without changing the intent. (The underlying endpoint is named /recompile for historical reasons.)
intent_version is not incremented — only the compiled_mix and last_compiled_at are updated.
forgecroft invariants recompile <id>Go SDK
Section titled “Go SDK”inv, err := client.RecompileInvariant(ctx, id)POST /invariants/{id}/recompileAPI Endpoints
Section titled “API Endpoints”| Method | Path | Description |
|---|---|---|
POST | /invariants/preview | Process intent without persisting |
POST | /invariants | Create an invariant |
GET | /invariants | List active invariants |
GET | /invariants/{id} | Get a single invariant |
PATCH | /invariants/{id} | Update intent or status |
DELETE | /invariants/{id} | Retire an invariant (soft delete) |
POST | /invariants/{id}/recompile | Re-analyze against current evidence sources |
Related Guides
Section titled “Related Guides”- Invariants Overview — what invariants are and how they differ from governance layers
- Scorecard — reading per-run evaluation results