Chargeback Attribution Policy
The chargeback-attribution policy enforces that every governed run carries the cost-attribution tags an org needs for internal chargeback. Without these tags, finance cannot tell which business unit, cost center, or project an LLM bill belongs to. This is a common ask for enterprises that already chargeback compute spend back to internal customers.
Maps to ISO 42001 A.10.2 (cost transparency), ISO 27001 A.8.2 (asset classification), and internal SOX-like cost allocation controls.
Rules
| Rule | Type | Default | Description |
|---|---|---|---|
required_tags | string[] | ["cost_center", "business_unit"] | Tag names that must appear on context.metadata |
fallback_bucket | string | "untagged-default" | Bucket name used when auto_tag_when_missing fills a missing tag |
auto_tag_when_missing | boolean | false | Auto-fill missing tags with fallback_bucket so the cost is still attributable |
valid_values | object | {} | Per-tag allowlist, e.g. {"cost_center": ["cc-100", "cc-200"]} |
action_on_violation | string | "warn" | Either block or warn |
How It Works
The chargeback-attribution handler runs at before_workflow. It is not re-evaluated post-action (the run is already attributed).
| Phase | What It Checks | Actions |
|---|---|---|
before_workflow | Each required tag is present on context.metadata (or as a direct context attribute), and its value (when set) is in valid_values if an allowlist is configured | BLOCK or WARN per action_on_violation |
after_workflow | No-op (ALLOW) -- attribution is enforced up front | ALLOW |
Context Attributes Read
| Attribute | Phase | Purpose |
|---|---|---|
context.metadata[tag] | before_workflow | Primary tag source |
context.<tag> | before_workflow | Fallback -- e.g. context.cost_center if metadata is missing |
When auto_tag_when_missing is true, the handler writes back to context.metadata in-place: metadata[tag] = fallback_bucket and adds metadata["chargeback_fallback"] = fallback_bucket so finance can flag the row as unattributed.
Example Policy
{
"name": "Require Cost Attribution",
"category": "chargeback-attribution",
"rules": {
"required_tags": ["cost_center", "business_unit", "project_code"],
"valid_values": {
"cost_center": ["cc-100", "cc-200", "cc-300"],
"business_unit": ["risk", "marketing", "engineering"]
},
"auto_tag_when_missing": true,
"fallback_bucket": "unattributed-2026q2",
"action_on_violation": "warn"
},
"enabled": true
}
SDK Integration
import waxell_observe as waxell
waxell.init()
@waxell.observe(
agent_name="quote-generator",
enforce_policy=True,
metadata={
"cost_center": "cc-100",
"business_unit": "engineering",
"project_code": "P-7842",
},
)
async def generate_quote(rfp_text: str) -> str:
return await draft_response(rfp_text)
Observability
| Field | Example |
|---|---|
| Category | chargeback-attribution |
| Action | warn |
| Reason | "Chargeback tags missing: ['business_unit']" |
| Metadata | {"missing": ["business_unit"], "invalid": [], "auto_tagged": true, "fallback_bucket": "unattributed-2026q2", "iso_42001": "A.10.2", "iso_27001": "A.8.2"} |
| Field | Example (invalid value) |
|---|---|
| Reason | "Chargeback tags missing: []; invalid: [{'tag': 'cost_center', 'value': 'cc-999', 'allowed': ['cc-100', 'cc-200']}]" |
Common Gotchas
short_circuit_on_block = False. Even withaction_on_violation: "block", the policy manager will not halt other handlers in the chain. The tag violation is informational by default. If you genuinely want chargeback to halt the run, ensure no later policies override the block.- Empty string counts as missing.
metadata = {"cost_center": ""}triggers the missing-tag path, not the invalid-value path. - Auto-tag mutates
context.metadatain place. Downstream policies and the run record both see the fallback bucket. This is intentional -- the audit row is still chargeable -- but be aware that the original "missing" state is lost after this handler runs. valid_valuesis per-tag. A tag not listed invalid_valuesaccepts any non-empty string. If you want a strict allowlist, every required tag must have its own array.required_tags = []makes the handler a no-op. All runs ALLOW. Use this to soft-disable without removing the policy.
Next Steps
- Budget Policy -- Pair with chargeback to cap spend per cost center
- Rate-Limit Policy -- Throttle by tenant or agent
- Policy Categories