Custom Action Builder Example
This example demonstrates how to use a custom action builder when application inputs do not map directly to policy fields.
This is one of the most important advanced integration patterns because it gives you full control over how runtime inputs become Actra actions.
Perfect for:
- framework request objects
- nested payloads
- legacy APIs
- MCP tool inputs
- queue envelopes
- AI tool normalization
Core Mental Model
Application input -> custom builder -> policy action -> decision
Instead of relying on default field extraction, you explicitly decide which fields become part of policy evaluation.
Example
- Python
- JavaScript
"""
Custom Action Builder Example
"""
from actra import Actra, ActraPolicyError
from actra.runtime import ActraRuntime
schema_yaml = """
version: 1
actions:
refund:
fields:
amount: number
actor:
fields:
role: string
snapshot:
fields:
fraud_flag: boolean
"""
policy_yaml = """
version: 1
rules:
- id: block_large_refund
scope:
action: refund
when:
subject:
domain: action
field: amount
operator: greater_than
value:
literal: 1000
effect: block
"""
policy = Actra.from_strings(schema_yaml, policy_yaml)
runtime = ActraRuntime(policy)
runtime.set_actor_resolver(lambda ctx: {"role": "support"})
runtime.set_snapshot_resolver(lambda ctx: {"fraud_flag": False})
def build_refund_action(action_type, args, kwargs, ctx):
return {
"type": action_type,
"amount": kwargs["amount"],
}
@runtime.admit(action_builder=build_refund_action)
def refund(amount: int, currency: str):
print(f"Refund executed: {amount} {currency}")
print("\nAllowed call")
refund(amount=200, currency="USD")
print("\nBlocked call")
try:
refund(amount=1500, currency="USD")
except ActraPolicyError as e:
print("Refund blocked by policy")
print("Rule:", e.matched_rule)
import { Actra, ActraRuntime, ActraPolicyError } from "@getactra/actra";
const schemaYaml = `
version: 1
actions:
refund:
fields:
amount: number
actor:
fields:
role: string
snapshot:
fields:
fraud_flag: boolean
`;
const policyYaml = `
version: 1
rules:
- id: block_large_refund
scope:
action: refund
when:
subject:
domain: action
field: amount
operator: greater_than
value:
literal: 1000
effect: block
`;
const policy = await Actra.fromStrings(schemaYaml, policyYaml);
const runtime = new ActraRuntime(policy);
runtime.setActorResolver(() => ({ role: "support" }));
runtime.setSnapshotResolver(() => ({ fraud_flag: false }));
function buildRefundAction(actionType: string, kwargs: Record<string, any>) {
return {
amount: kwargs.amount,
};
}
function refund(amount: number, currency: string) {
console.log(`Refund executed: ${amount} ${currency}`);
}
const protectedRefund = runtime.admit("refund", refund, {
builder: buildRefundAction,
});
console.log("\nAllowed call");
await protectedRefund(200, "USD");
console.log("\nBlocked call");
try {
await protectedRefund(1500, "USD");
} catch (e) {
if (e instanceof ActraPolicyError) {
console.log("Refund blocked by policy");
console.log("Rule:", e.matchedRule);
}
}
Why This Example Matters
Custom builders are the highest-control action mapping surface in both SDKs.
Use them when:
- request payloads are nested
- framework objects include extra metadata
- legacy parameters need remapping
- queue messages use envelopes
- tool schemas differ from policy schemas
This is especially valuable for AI tool governance and MCP integrations.
Python Advantage
The Python builder receives:
(action_type, args, kwargs, ctx)
This makes it excellent for:
- FastAPI request extraction
- Celery task envelopes
- typed function remapping
- selective field exposure
JavaScript Advantage
The JavaScript builder is ideal for:
- payload-first frameworks
- object normalization
- request body mapping
- edge function adapters
Best Practice
Only include fields that are intentionally part of the policy schema.
This prevents:
- sensitive transport metadata
- framework internals
- request wrappers
- hidden agent state
from leaking into governance decisions.