Skip to main content

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

RuleTypeDefaultDescription
required_tagsstring[]["cost_center", "business_unit"]Tag names that must appear on context.metadata
fallback_bucketstring"untagged-default"Bucket name used when auto_tag_when_missing fills a missing tag
auto_tag_when_missingbooleanfalseAuto-fill missing tags with fallback_bucket so the cost is still attributable
valid_valuesobject{}Per-tag allowlist, e.g. {"cost_center": ["cc-100", "cc-200"]}
action_on_violationstring"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).

PhaseWhat It ChecksActions
before_workflowEach 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 configuredBLOCK or WARN per action_on_violation
after_workflowNo-op (ALLOW) -- attribution is enforced up frontALLOW

Context Attributes Read

AttributePhasePurpose
context.metadata[tag]before_workflowPrimary tag source
context.<tag>before_workflowFallback -- 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

FieldExample
Categorychargeback-attribution
Actionwarn
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"}
FieldExample (invalid value)
Reason"Chargeback tags missing: []; invalid: [{'tag': 'cost_center', 'value': 'cc-999', 'allowed': ['cc-100', 'cc-200']}]"

Common Gotchas

  1. short_circuit_on_block = False. Even with action_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.
  2. Empty string counts as missing. metadata = {"cost_center": ""} triggers the missing-tag path, not the invalid-value path.
  3. Auto-tag mutates context.metadata in 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.
  4. valid_values is per-tag. A tag not listed in valid_values accepts any non-empty string. If you want a strict allowlist, every required tag must have its own array.
  5. required_tags = [] makes the handler a no-op. All runs ALLOW. Use this to soft-disable without removing the policy.

Next Steps