About stigg-governance
stigg-governance is a skill for modeling and configuring per-entity usage limits in Stigg, for teams managing customer-defined units and hierarchies. It explains entity types, entities, entitlements, usage reporting, and enforcement, while distinguishing setup through MCP from runtime checks.
Key features
- 01Model customer-defined entity hierarchies
- 02Configure entity types and attribution keys
- 03Set feature or credit usage limits
- 04Report and attribute usage across trees
- 05Distinguish MCP setup from runtime enforcement
Use cases
- 01Limit usage by project, tenant, or other units
- 02Keep Stigg entities synchronized with a source model
- 03Attribute reported usage to entities and ancestors
- 04Explore governance configuration through Stigg MCP
MCP support
The skill uses Stigg MCP tools such as `execute` and `search_docs` for governance setup, exploration, and test checks. Live request-time enforcement runs separately through the Stigg SDK or edge.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ stiggio/ skills --skill stigg-governanceAfter installing, type/stigg-governancein Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/enforcement-and-usage.mdenforcement-vs-observability.mdentity-entitlements.mdentity-lifecycle.mdentity-model.mdDetails
From the project READMEStigg Governance — Per-Entity Usage Limits
Governance layers per-entity limit enforcement on top of Stigg credits and features. Where credits answer "how much can this customer consume?", governance answers "how much can this customer's own sub-units consume?" — whatever the vendor needs to limit, with hierarchical roll-up and hard-limit enforcement. The entity model is arbitrary and customer-defined — Stigg imposes no fixed hierarchy, so don't assume one (see Step 1).
You drive Stigg governance through the Stigg MCP — the execute tool runs SDK code, search_docs resolves exact shapes. That's how the agent explores, configures, and test-checks governance: entity types, entities, entity entitlements, tree queries, and one-off checks all go through execute using the SDK's v1 namespace (client.v1.…). Production runtime enforcement is separate: the live entitlements check that gates real customer requests runs on the vendor's hot path via the Stigg SDK/edge, not through the MCP — the MCP is for agent-side setup and exploration, not the request-time gate. This skill owns the mental model; it does not cover connection, keys, or setup — those belong to the stigg-mcp skill.
Governance is GA on the
v1API. Use theclient.v1.*calls. Theclient.v1Beta.*governance calls still work but are deprecated. One exception: the governance tree query has no v1 version and stays onclient.v1Beta. For existing v1-beta code, see Moving from v1-beta below.
Step 1 — Elicit the Model: ASK, Don't Assume (do this BEFORE defining entity types)
The governance entity model is fully arbitrary and customer-defined. There is no default hierarchy — Stigg ships none, and any dept/team/agent shape is just one possibility, not a template. Before defining a single entity type, ask the customer what they want to govern and build strictly from their answer. Do not propose or assume a hierarchy. Ask:
- What units do you limit? (their own vocabulary — teams, projects, regions, tenants, sub-accounts, cost centers, agents, workspaces…)
- How do they nest / roll up? (which contains which, and how deep — the tree caps at 8 levels)
- Which levels actually carry limits? (only levels that hold a limit or need roll-up belong in the tree; fold the rest away)
- What identifies a unit on a usage event? (the dimension key(s) your events already emit — these become the attribution keys)
Model from their answer: "we limit by project, and projects belong to a client" → build client → project. Every worked example below and in the references is one possible model — it teaches the API's shape, never the model to adopt.
Sequence: elicit the model (this step) → entity types → entities → entity entitlements → runtime.
Prerequisites — depend on what you're limiting
Governance limits sit on top of an existing feature or credit currency; they don't replace it. What's required differs for a feature vs. a credit currency — the two follow different paths, so read the row that matches your entity entitlement (featureId or currencyId).
-
Feature (
featureId) — needs an entitling active subscription. The gating check can pass only when the customer has an ACTIVE subscription that entitles the feature — otherwise it denies with a standard reason (NoActiveSubscription;NoFeatureEntitlementInSubscription= subscribed but the plan lacks the feature). Governance still runs andchainscan still come back, but the check cannot pass. Entitle the feature via an active subscription BEFORE checks can pass. The entity entitlement upsert does no entitlement validation — it accepts a limit for an un-entitled feature, so the gap surfaces only later at check time. Order it: catalog + subscription first (seestigg-pricing-modeling/stigg-subscriptions), then limits. -
Credit currency (
currencyId) — does NOT require a subscription. Credits run on a different path: the check reads the customer's credit pool directly, and credits can be granted and consumed with no active subscription or assigned plan. What a credit limit needs instead is a credit currency that exists and a funded pool (a grant — recurring, prepaid, promotional, or manual). Set the currency up viastigg-credits, then add per-entity limits.
Setting up a feature catalog (plan, feature, subscription — a FREE plan needs charges.pricingType before publishing) is not governance's job: do it via stigg-pricing-modeling (catalog) and stigg-subscriptions (provisioning); credit currencies and grants live in stigg-credits. Then add per-entity limits.
Before You Start
Per the umbrella stigg skill: search first. Confirm operation names, field names, and body shapes via search_docs (or the Mintlify Stigg docs MCP) before authoring. This skill owns the mental model; the live surface owns the shapes.
The Four Pieces
entity type → entity → entity entitlement → governance tree — a type (which declares the attribution keys) is defined once per environment and instantiated per customer as entities; an entity entitlement limits one (entity, featureId|currencyId) pair, optionally narrowed by a scope; the tree reports usage-vs-limit and never gates. An entity entitlement is what v1-beta called an assignment. Each piece is detailed below.
Entity Types
An entity type declares a kind of sub-customer unit — whatever the customer named in Step 1 (a project, region, tenant, cost center, agent, …) — and, critically, its attribution keys: the event-dimension keys whose values identify which entity a usage event belongs to. An entity type is defined once per environment and shared across every customer in that environment. Operations: list, upsert (bulk PUT, idempotent), archive, and checkActiveEntities. Archive fails with 400 while any entity of that type is still active (in any customer): call checkActiveEntities(id) first, archive the entities, then the type. An upsert of an archived type restores it; list hides archived types unless you pass includeArchived: 'true'. Design attribution keys before reporting any usage; retro-attributing old events is not a thing.
Entities
Entities are the instances of those types, created per customer (under a customer) — many per customer, up to 8 levels deep, in whatever shape Step 1 surfaced (e.g. client → project, or region → tenant → workspace). To place or move a node in the tree, set parentId on the entity entitlement upsert (client.v1.customers.entities.entitlements.upsert) — tri-state: omit = leave the parent unchanged, null = make it a root, an entity id = set/change it. parentId is the entity's single parent (one position per entity), set on the entity entitlement, not per feature/currency. (Practical note: the entity upsert rejects parentId, and metadata has no structural role, so parenting happens on the entity entitlement upsert.) Operations: list / get / upsert (bulk PUT) / archive. There is no unarchive: an upsert of an archived entity restores it. An entity on an archived entity type cannot be upserted (400). Re-parenting is leaf-only (a non-leaf move is rejected, and an archived child still counts as a child); archive is NOT leaf-gated — archiving a parent orphans its still-active children, so archive/re-home descendants bottom-up yourself. Full rules: references/entity-model.md.
Keeping Entities in Sync
Entities are an ongoing mirror of the vendor's source of truth, not a one-time import — the hierarchy lives in your system first; the tree is a projection. Wire governance ops into the code paths that mutate your model: register → entity upsert; rename/attribute-change → idempotent re-upsert; deprovision/delete → archive (bottom-up); reactivate → re-upsert the archived entity; restructure → set parentId on the entity's entity entitlement upsert to move the node (leaf-only). Since upsert is idempotent bulk PUT, a declarative "push the desired tree each sync" pattern works. Full mapping + the unshipped allocation-return caveat: references/entity-lifecycle.md.
Entity Entitlements — the Limits
An entity entitlement sets a usage limit for one (entity, featureId|currencyId) pair:
entityId— The entity being limited (any level of the customer's tree).featureIdorcurrencyId— What's being limited — exactly one: afeatureId(metered feature) or acurrencyId(credit currency). Mutually exclusive.usageLimit— The limit for the cadence window.null= unlimited: no ceiling, usage is still tracked.cadence— A single-unit ISO-8601 duration for the reset window, e.g.'P1M'(monthly),'P10D'(every 10 days) or'PT1H'(hourly). It must be one unit only — a composite like'P1M10D'(1 month and 10 days) is not allowed.scopeEntityIds— Optional — defines a dimension-scoped sub-limit; it narrows when the limit applies.[](empty) = a node-wide limit that always applies. Non-empty (e.g.['model-gpt4o']) = the limit applies only when every listed entity is present in the request's resolved set (the ancestors + dimension matches from attribution). At most 5 ids, no duplicates. Use it instead of everything under the entity for specific (typically dimensional) entities.
Operations: list / upsert, at most 100 rows per upsert. The upsert body key is entityEntitlements (v1-beta used assignments). A row is matched by (entityId, featureId|currencyId, scopeEntityIds): a match is updated (omitted usageLimit / cadence are kept), anything else is created (both are required). So the same feature under two different scopes is two separate limits. A currencyId entity entitlement limits credit consumption — unrelated to billing currency (USD/EUR). Worked examples: references/entity-entitlements.md.
Reporting Usage — the Meter-Type Fork
Ingest never gates; it records consumption and rolls up the tree. The endpoint depends on the target feature's meter type:
- Calculated / incremental meters (your app computes the number) →
client.v2.usage.report: synchronous. Its answer lists each measurement's usage and, indata.entities, the usage of each entity entitlement it counted towards. Required:customerId,featureId(a feature id; a credit currency id fails withFeatureNotFound),value. (client.v1.usage.reportis deprecated.) - Raw-events meters (Stigg aggregates raw events — count/sum/unique) →
client.v1.events.report(reportEvent): the required path for these today (v2.usage.reportcannot report raw-events–metered features — useevents.report/reportEvent instead); high-volume, eventually-consistent. reportEvent has nofeatureId— the meter is matched byeventName(+idempotencyKey).
Both carry attribution in dimensions (the entity type's attributionKey values, e.g. { "projectId": "proj-atlas", "regionId": "eu-west" }) → attributes to the matching entity and every ancestor.
Read the feature's meter to pick the path. The Feature type exposes
hasMeter/meter/meterType— inspect themeterconfig to tell a raw-events meter (→events.report) from a calculated/incremental one (→v2.usage.report). NotemeterTypealone doesn't flag "raw events" (a raw-events feature can readINCREMENTAL), so read themeterconfig and confirm fields viasearch_docs. Keep a definitive fallback: onCannotReportUsageForEntitlementWithMeterErrorfromv2.usage.report, it's a raw-events meter — switch toevents.report. Shapes + examples:references/enforcement-and-usage.md.
Enforcement and Usage Reads
Two distinct reads — do not mix them up. This is the hot-path (enforcement) vs cold-path (observability) split; the full framing is in references/enforcement-vs-observability.md:
- Access (gates requests): the standard entitlements check —
client.v1.customers.checkEntitlement(customerId, { ... }), passingfeatureIdorcurrencyId, optionalrequestedUsage(default 1), and attributiondimensions. Withdimensions, the governance limits are checked too. When at least one entity entitlement applies, the response'sdata.chainslists each limit on the path, and a governance denial readsRequestedUsageExceedingLimit. An entity is blocked when ANY ancestor has hit its hard limit — a child with limit left is still blocked if any of its parents is exhausted. A feature (featureId) check also needs an entitling active subscription; a credit (currencyId) check does not (see Prerequisites). - Usage vs limit (observability): the governance tree query —
client.v1Beta.customers.governance.list(customerId, { ... }), still on v1-beta and paginated — returns the entity nodes; remaining limit =usageLimit − currentUsage. By default it runs tree-only:usageLimit/currentUsage/utilization/cadencecome backnull; passfeatureIdsand/orcurrencyIds(arrays) to hydrate config + usage for those features / credit currencies. Its usage figures may lag by a few seconds (more for raw events) — use it for dashboards and alerts, never to gate access. Full semantics:references/enforcement-and-usage.md.
Surface Map
Operations run through the MCP execute tool as @stigg/typescript SDK calls (bulk upserts are idempotent; bulk upserts and archives take at most 100 items per call). Each operation → its SDK call (via MCP execute):
- Entity types — list / upsert (bulk) / archive / checkActiveEntities —
client.v1.entityTypes.* - Entities — list / retrieve / upsert (bulk, also restores archived) / archive —
client.v1.customers.entities.*(retrieve(entityId, { id: customerId })) - Entity entitlements — list / upsert (body key
entityEntitlements) —client.v1.customers.entities.entitlements.* - Entitlements check (not gated by governance enablement — it still gates access) —
client.v1.customers.checkEntitlement(customerId, { ... }) - Governance tree query (pass
featureIds/currencyIdsto hydrate; paginated; v1-beta only) —client.v1Beta.customers.governance.list(customerId, { ... }) - Ingest — calculated/incremental meter —
client.v2.usage.report - Ingest — raw-events meter (by
eventName) —client.v1.events.report
Don't guess operation names or shapes — confirm exact operations via search_docs.
API vs UI — different entity entitlement models, by design. The API targets one feature or credit currency per entity entitlement ((entity, featureId|currencyId)). When you create an entity under a parent in the dashboard UI, it writes one unlimited entity entitlement (usageLimit: null) for each feature or credit currency you selected, to hold the placement (you edit limits per row). A top-level entity gets none. So UI- and API-created entities look different — don't be surprised, and don't "repair" one to match the other.
Via the Stigg MCP: the MCP exposes only execute (sandboxed TypeScript over the SDK) and search_docs — no one-tool-per-endpoint mapping. Governance ops go through execute using the SDK namespaces above. Connection, keys, and environment setup are the stigg-mcp skill's job — if execute or search_docs won't connect, that's where the troubleshooting lives, not here.
Moving from v1-beta
The client.v1Beta governance calls are deprecated. Each maps to a v1 call, and the fields inside each row keep their names:
v1Beta.entityTypes.list / upsert→v1.entityTypes.list / upsert(v1 addsarchiveandcheckActiveEntities)v1Beta.customers.entities.list / retrieve / upsert / archive→ the same names onv1.customers.entitiesv1Beta.customers.entities.unarchive→v1.customers.entities.upsertwith the archived entity's idv1Beta.customers.assignments.list / upsert→v1.customers.entities.entitlements.list / upsert; the upsert body key changes fromassignmentstoentityEntitlementsv1Beta.customers.entitlements.check→v1.customers.checkEntitlement(samefeatureId/currencyId/requestedUsage/dimensionsparams)v1Beta.customers.governance.list→ no change (no v1 version)v1.usage.report(deprecated) →v2.usage.report, whose answer addsdata.entities
Raw REST moves the same way, /api/v1-beta/... → /api/v1/..., with one rename: /api/v1-beta/customers/{id}/assignments → /api/v1/customers/{id}/entities/entitlements.
When NOT to Use This Skill
- Account-level credit currencies, grants, pools, ledger, auto-recharge →
stigg-credits. - Feature gating / usage reporting with no per-entity limits →
stigg-entitlements. - Modeling features / plans / credit currencies in the catalog →
stigg-pricing-modeling. - Provisioning / canceling subscriptions →
stigg-subscriptions.
Common Mistakes
Each mistake — the fix:
- Writing new code on
client.v1Beta— Those governance calls are deprecated. Use the v1 calls (see Moving from v1-beta); only the tree query stays onclient.v1Beta. - Sending
assignmentsto the v1 upsert — The v1 body key isentityEntitlements. - Calling
unarchive— v1 has none. Re-upsert the archived entity (same id) to restore it. - Archiving an entity type that still has active entities — The whole request fails with 400. Check
checkActiveEntities(id), archive the type's entities first, then the type. - Gating access from the tree query — Its usage figures may lag by a few seconds. Gate with the entitlements check only.
- Debugging "child has limit left but is blocked" as a bug — Ancestor semantics: any exhausted ancestor blocks the whole subtree. Check the tree upward.
- Building more than 8 hierarchy levels — A hard limit; a deeper placement is rejected. Redesign the model (e.g. fold a level into
scopeEntityIds). - Reporting usage without attribution-key dimensions — Usage won't attribute to any entity — limits never accrue. Design dimensions up front.
- Always calling
v2.usage.report, or alwaysevents.report— It's a meter-type fork. Calculated/incremental →v2.usage.report; raw-events →events.report(reportEvent, byeventName). Ifv2.usage.reportreturnsCannotReportUsageForEntitlementWithMeterError, switch to reportEvent. - Passing a credit currency id as the usage report's
featureId— It takes feature ids only and fails withFeatureNotFound. Draw credits directly through the credits API (stigg-credits). - Passing a
featureIdto reportEvent — reportEvent targets the meter byeventName(+idempotencyKey), notfeatureId; attribution rides indimensions. - Creating a feature (
featureId) entity entitlement and expecting checks to pass — The upsert does no entitlement validation. The feature must be entitled via an active subscription first, else check denies withNoActiveSubscription/NoFeatureEntitlementInSubscription. - Assuming a credit (
currencyId) limit needs a subscription like a feature does — It doesn't — credits are a different path. The check reads the credit pool directly; credits can be granted/consumed with no subscription. It needs a credit currency + funded pool, not a subscription (stigg-credits). - Reading
usageLimit: nullas zero —nullmeans unlimited; usage still accrues and rolls up. - Reading the tree and getting
nulllimits/usage — Tree-only mode. PassfeatureIds/currencyIdsto hydrate config + usage. - Treating an MCP connection error as a governance problem — Connection/setup issues (bad key, wrong environment) are
stigg-mcp's domain — fix them there, then re-run the governance op. - Re-parenting a node that still has children — Re-parent is leaf-only — a non-leaf move is rejected. Move the children to another parent, or detach them with
parentId: null, bottom-up; then move the former parent. Archiving a child does not help: an archived child still counts. The rejection is not transient, so don't retry it. - Assuming archive is leaves-only — It isn't — archiving a parent orphans its active children. Archive/re-home descendants bottom-up yourself; the platform won't stop you.
- Putting
parentIdon the entity upsert (or inmetadata) — The entity upsert rejectsparentId, andmetadatahas no structural role. SetparentIdon the entity entitlement upsert to place/move the node. - Treating entities as a one-time import — They're an ongoing mirror of your source of truth — wire upsert/archive/re-parent into your create/update/delete/restructure paths (
references/entity-lifecycle.md). - Reading
currencyIdas a billing currency — It's a credit currency (seestigg-credits), not USD/EUR. - Expecting per-endpoint MCP tools for governance — The MCP has only
execute+search_docs. Drive the SDK throughexecute.