Cookbook 03 · Foundations
Policy-Safe Decision Tool
Deterministic business logic first, then approval-gated side effects, then an explanation layer.
FoundationsCore40–60 min
What you build. A refund decision flow: a deterministic eligibility node, a hard approval gate before the (mocked) refund write, and an optional explanation that can never contradict the decision.
Surfaces: Tools Workflows Judges Evaluations
What you learn
- Register a shipped callable as a tool from the Spec form (no code)
- Author deterministic logic as a visual decision table
- See write tools auto-mock in the sandbox + gate in a workflow
- Drive the human-approval gate in the Run Monitor
- Score an explanation with an LLM judge — after the deterministic lane is green
Implementation flow
flowchart LR
RI[/run input JSON/] --> DR[Data Transform: ordered decision table]
DR --> HA{{Human Approval · gates every run when enabled}}
HA -->|approve| IR[Tool: initiate_refund · mocked + gated]
DR -.optional.-> EX[Explanation prompt] -.as a scorer.-> JF[Judge: ExplanationFaithfulness]
LO[Tool: lookup_order] -.Tools sandbox demo.-> SBX[Fixtures + Hardening]Step-by-step UI only
Each step shows the exact navigation path, the fields to fill, the button to click, and a snapshot of the CALIBER screen you'll be on.
- Register the read-only order lookup. Clicking Register Tool opens the tool wizard. A registered Tool must point at importable Python, so you give it a
Module PathandCallable Namefor a shipped callable — no code to write. Mark itreadand allow it in preview so the sandbox runs it live.Fill inName lookup_orderModule Path caliber.workflows.demo_toolsCallable Name lookup_orderSide effect level readAllow in preview enabled (run the real callable in sandbox) Input / Output schema paste the input_schema(requiresorder_id) andoutput_schemafrom lookup-order.tool.json into the Schema step's raw-JSON fieldsClick Register ToolYou'll see: The toollookup_orderis registered and opens in its workspace with stage tabs (Spec, Sandbox, Fixtures, Test Runs, Hardening, Publish).Screen snapshotcaliber · Library › Tools › Register ToolRegister Tool - Register the refund write tool the same way. Because
side_effect_leveliswrite, it is automatically mocked in the sandbox and becomes approval-gated when used in a workflow — exactly the safety behavior this lab wants. Leave Allow-in-preview off (writes are mocked regardless).Fill inName initiate_refundModule Path caliber.workflows.demo_toolsCallable Name initiate_refundSide effect level writeInput / Output schema paste the input_schema(requiresorder_id) andoutput_schemafrom initiate-refund.tool.jsonClick Register ToolYou'll see: The toolinitiate_refundappears in the Tools list and opens in its own workspace.Screen snapshotcaliber · Library › Tools › Register ToolRegister Tool Library › Tools › lookup_order › SandboxRun the read tool live in the Sandbox to confirm it imports and returns real output. Because it is areadtool with preview allowed, you get the genuine lookup result.Fill inInput JSON {"order_id": "ord_1120"}Click Test RunYou'll see: A real response object (e.g.{order_id, items, status}) with nomockedflag — proving the live read works.Screen snapshotcaliber · Library › Tools › lookup_order › SandboxTest RunLibrary › Tools › initiate_refund › SandboxRun the write tool in the Sandbox to confirm it is safely mocked. Write/external-action tools never execute for real in preview, so you can test the contract without issuing an actual refund.Fill inInput JSON {"order_id": "ord_1120", "amount": 49.0}Click Test RunYou'll see: A response carryingmocked: true, confirming the refund did not really fire in the sandbox.Screen snapshotcaliber · Library › Tools › initiate_refund › SandboxTest RunLibrary › Tools › lookup_order › Fixturescreates · tool fixtures: lookup_order suite refund-fixtures.jsonl ↓Save deterministic fixtures (named cases with assertions) so you can score the tool repeatably. Use Add case once per fixture; each case pairs an input JSON with an assertion.lookup_ordertakes onlyorder_id, so each case's input JSON must be scoped to just that key — passing the full multi-key fixture row would raise a TypeError and fail the case. The available assertion types areno_error,output_contains, andequals— useoutput_containswith a value likestatusto assert the lookup output.Fill inCases for each row in refund-fixtures.jsonl, add a case whose input JSON is scoped to just the order id (e.g. {"order_id": "ord_1120"}) paired with the assertionoutput_contains=statusClick Save casesYou'll see: The saved fixture cases appear in the panel, ready to run as a suite.Screen snapshotcaliber · Library › Tools › lookup_order › FixturesSave casesLibrary › Tools › lookup_order › Hardeningcreates · hardening run: lookup_order pass rateRun the saved fixture suite. The deterministic Hardening lane scores the assertions inline and reports a pass rate you can gate on (target ≥ 0.97). Save the cases first if you edited them, then run.Click Run calibrationYou'll see: An immediate result line: Pass rate NN% — passed/total — recorded as a run with per-case pass/fail. No waiting for a background job.Screen snapshotcaliber · Library › Tools › lookup_order › HardeningRun calibrationLibrary › Tools › lookup_order › Test Runscreates · pinned baseline: lookup_order hardeningPin the passing hardening run as the tool's baseline so future fixture/schema edits are measured against this known-good result. Open the run in the Test Runs history and pin it.Click Set as baselineYou'll see: The run gets a Baseline marker; the Test Runs tab now shows deltas against it. (Optional: edit a fixture or schema and re-run to watch a regression delta appear.)Screen snapshotcaliber · Library › Tools › lookup_order › Test RunsSet as baselineCompose › Workflows › New Workflowcreates · workflow: refund-decision-flow (hitl_review)Create the workflow from the Human Review template (thehitl_reviewkind) — it ships the human_approval node you need for the approval gate. Name it, then click the template tile to create it. Starting from this template means the gate is already wired for you.Fill inName refund-decision-flowTemplate the Human Review tile (Agent → PII redact → human approval → output) Click Human ReviewYou'll see: The workflow opens in the editor with a starting graph that already contains ahuman_approvalnode.Screen snapshotcaliber · Compose › Workflows › New WorkflowHuman ReviewCompose › Workflows › refund-decision-flow › Editorcreates · python_code node: decide_refund decide_refund.py ↓Add the deterministic decision as a Python Code node (drag it from the palette, or use the quick-add + menu, then paste the body in the node inspector). Custom decision logic lives in a python_code node (not a registered tool), so it versions with the workflow and needs no registration. This node sits first after START and reads the run input directly: a python_code node receives the run input as an unparsed string, so the pastedrun_python_node(...)entrypoint parses it as JSON and readsorder_state,risk_flags,amount, anddays_since_orderfrom it.Fill inPython Code node body paste the body of decide_refund.py (it defines decide_refund(...)and therun_python_node(...)entrypoint that parses the JSON run input and returns{decision, reason_code, requires_approval})Click 💾 SaveYou'll see: A Python Code node for the refund decision is on the canvas with the pasted body saved to the draft.Screen snapshotcaliber · Compose › Workflows › refund-decision-flow › Editor💾 SaveCompose › Workflows › refund-decision-flow › Editorcreates · workflow graph: decide → approve → refundWire the safety chain by adding the nodes and connecting them in order:START→decide_refund(python_code, which parses the JSON run input) →human_approval→initiate_refund. Add theinitiate_refundTool node from the palette, drag port-to-port to connect, and route through the existing human_approval gate so the write only fires after approval. The human_approval node has no condition field: when runtime approvals are enabled on the run, it pauses every run that reaches it (it does not readrequires_approval). Therequires_approvalflag from decide_refund is informational only — surfaced for the reviewer, not consumed by the gate. (Keeplookup_orderas the Tools-sandbox demo from the earlier steps; it is not part of this decision chain — decide_refund reads the decision fields straight from the run input.)Fill inNodes to add a Tool node for initiate_refund(decide_refund and human_approval are already on the canvas)Edges START → decide_refund → human_approval → initiate_refundClick 💾 SaveYou'll see: The saved draft graph shows START → decide_refund → human_approval → initiate_refund in sequence, with the approval gate sitting before the refund write.Screen snapshotcaliber · Compose › Workflows › refund-decision-flow › Editor💾 SaveCompose › Workflows › refund-decision-flow › Run Monitorcreates · run evidence: high-risk blocked at approval refund-fixtures.jsonl ↓Prove the gate holds. Runtime approvals are a deployment-level setting, not a per-run checkbox: an admin enables them in the environment by settingCALIBER_WORKFLOW_RUN_RUNTIME_APPROVALS_ENABLED=truetogether with checkpointing (CALIBER_WORKFLOW_RUN_CHECKPOINTING_ENABLED=true) and the run queue (CALIBER_WORKFLOW_RUN_QUEUE_ENABLED=true) in.env, then restarting — the approve/resume routes reject requests unless all three are on. (Their current values are shown read-only under Settings › Workflow runs; only LLM credentials are live-editable in the UI, so these flags come from the deployment env, not a Settings toggle.) With those enabled, open the Run Monitor panel in the editor, paste a case as the run input, and execute. The human_approval node then pauses the run at the gate instead of letting the write fire — it pauses every run that reaches it, regardless of the decision'srequires_approvalvalue. Use a high-risk case so the paused run is also one a reviewer would actually hold.Fill inRun input a case from refund-fixtures.jsonl, e.g. the high-risk F08 {"order_id":"ord_1166","order_state":"delivered","risk_flags":["fraud_suspected"],"amount":49.0,"days_since_order":4}Click RunYou'll see: The run advances to the gate and stops at status waiting_approval (shown as "Awaiting approval") — the refund has NOT fired, and Approve / Reject buttons appear.Screen snapshotcaliber · Compose › Workflows › refund-decision-flow › Run MonitorRunCompose › Workflows › refund-decision-flow › Run Monitorcreates · run evidence: approve marks approved, resume fires refundClear the paused run in two steps. First click Approve: this only records the approval decision (the approval row flips toapproved) — the run stays atwaiting_approvaland the write does NOT fire yet. Then click Resume, a separate action, which advances the paused run from the checkpoint; only now doesinitiate_refundexecute. (Resume refuses to advance until an approved decision exists, so the order matters.)Click Approve, then ResumeYou'll see: After Approve the status is still waiting_approval with the approval marked approved; after Resume the run advances and completes, and the timeline showsinitiate_refundrunning only AFTER the resume — the write never fires while the run is paused at the gate.Screen snapshotcaliber · Compose › Workflows › refund-decision-flow › Run MonitorApprove, then ResumeCompose › Workflows › refund-decision-flow › Run Monitorcreates · run evidence: rejected → write never firesNow prove the gate also stops the write entirely when you reject. Start another run (runtime approvals are still on at the deployment level) so it pauses at the gate, then click Reject instead of Approve. Rejection ends the run as failed — there is no Resume after a rejection — and the refund never fires.Fill inRun input any case from refund-fixtures.jsonl, e.g. F08 {"order_id":"ord_1166","order_state":"delivered","risk_flags":["fraud_suspected"],"amount":49.0,"days_since_order":4}Click RejectYou'll see: The run ends at status failed (errorapproval_rejected) —initiate_refundnever ran and the run cannot be resumed, confirming the write is hard-gated behind the approval decision.Screen snapshotcaliber · Compose › Workflows › refund-decision-flow › Run MonitorRejectLibrary › Prompts › New promptcreates · prompt: refund-explanation refund-explanation.md ↓ policy-reason-normalizer.md ↓ customer-safe-refund-language.md ↓Add the optional explanation layer that phrases the decision for the customer without changing it. Use the Write / paste on-ramp, name it, and paste the body (the lines below the YAML frontmatter).Fill inPrompt name refund-explanationPrompt text paste the body of refund-explanation.md (below the frontmatter; variables decision,reason_code,order_state,customer_name)Commit message v1 explanation layer — explains, never overrides, the deterministic decisionClick CreateYou'll see: The promptrefund-explanationis created at v1 and opens in its workspace, ready to test. (Optionally also create the two support skills below to govern its wording.)Screen snapshotcaliber · Library › Prompts › New promptCreateLibrary › Prompts › refund-explanation › Test Setscreates · generated cases for refund-explanation refund-fixtures.jsonl ↓Build a test set for the explanation prompt from the fixtures' decision contexts. Generate cases for the open prompt (each input carries adecision,reason_code, andorder_state), review them against the fixtures, then save them as the prompt's pinned test set.Fill inNumber of Test Cases pick a count covering approve / deny / manual_review reason codes, mirroring the decision contexts in refund-fixtures.jsonl Click Generate Test CasesYou'll see: Generated cases appear, each pairing a decision context with the expected behavior that the explanation must restate the decision faithfully.Screen snapshotcaliber · Library › Prompts › refund-explanation › Test SetsGenerate Test CasesLibrary › Prompts › refund-explanation › Test Setscreates · test set: refund-explanation faithfulness cases (pinned)Save the cases so the Runs stage can score the explanation against them. The expected behavior encodes faithfulness: the explanation must never upgrade, downgrade, or invent commitments beyond the fixed decision.Click Save to Test SetsYou'll see: A "Saved to Test Sets" confirmation; the set is pinned to the refund-explanation prompt.Screen snapshotcaliber · Library › Prompts › refund-explanation › Test SetsSave to Test SetsLibrary › Prompts › refund-explanation › Runscreates · run: explanation faithfulnessRun the explanation prompt against its pinned test set. The Runs stage executes the prompt and grades each case with the built-in pass/partial/fail judge, scoring whether the explanation stays faithful to the decision (the gate is ≥ 0.92). Run only after the deterministic refund lane above is green.Click Run testsYou'll see: A scorecard with a high overall pass rate (target ≥ 0.92), confirming the explanation never contradicts the deterministic decision — completing the deterministic-first, explanation-last gate.Screen snapshotcaliber · Library › Prompts › refund-explanation › RunsRun tests
Assets (copy-paste)
The exact files this cookbook uses — copy each into the matching field. Source: docs-site/cookbooks/03-tool-hardening-contract-lab/assets/.
{
"name": "lookup_order",
"version": "1",
"module_path": "caliber.workflows.demo_tools",
"callable_name": "lookup_order",
"side_effect_level": "read",
"allow_in_preview": true,
"input_schema": {
"type": "object",
"required": ["order_id"],
"properties": {
"order_id": {
"type": "string",
"description": "Order identifier to look up, e.g. \"ord_1120\"."
}
}
},
"output_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"items": {"type": "array", "items": {"type": "string"}},
"status": {"type": "string"}
}
},
"notes": "Read-only lookup; runs live in the sandbox (allow_in_preview). The shipped callable returns {order_id, items, status} and takes ONLY order_id (scope fixture inputs to {\"order_id\": ...} or it raises a TypeError). Used here purely as the Tools-sandbox demo; it is NOT the source of decide_refund's inputs — the decide_refund python_code node parses order_state/risk_flags/amount/days_since_order from the JSON run input."
}
{
"name": "initiate_refund",
"version": "1",
"module_path": "caliber.workflows.demo_tools",
"callable_name": "initiate_refund",
"side_effect_level": "write",
"input_schema": {
"type": "object",
"required": ["order_id"],
"properties": {
"order_id": {
"type": "string",
"description": "Order to refund."
},
"amount": {
"type": "number",
"description": "Refund amount in USD. Optional; the shipped callable ignores extras and returns its own amount_usd. Declared here so the workflow can pass the decided amount through the contract."
}
}
},
"output_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"refund_status": {"type": "string"},
"amount_usd": {"type": "number"}
}
},
"notes": "side_effect_level=write -> in the Tools SANDBOX the response is mocked and the envelope carries a top-level mocked:true (it never really fires). In a normal WORKFLOW run there is no top-level mocked flag: the tool runs the real callable, but only AFTER the human_approval gate clears. Wire it AFTER the human_approval node so the refund only fires once the run is approved."
}
{"id": "F01", "tags": ["golden", "approve"], "inputs": {"order_id": "ord_1120", "account_id": "acct_2101", "order_state": "delivered", "risk_flags": [], "amount": 49.0, "days_since_order": 5}, "expectations": {"decision": "approve", "reason_code": "ELIGIBLE_AUTO_APPROVE", "requires_approval": false}}
{"id": "F02", "tags": ["golden", "approve"], "inputs": {"order_id": "ord_1133", "account_id": "acct_2101", "order_state": "shipped", "risk_flags": [], "amount": 25.0, "days_since_order": 12}, "expectations": {"decision": "approve", "reason_code": "ELIGIBLE_AUTO_APPROVE", "requires_approval": false}}
{"id": "F03", "tags": ["golden", "deny"], "inputs": {"order_id": "ord_1002", "account_id": "acct_1432", "order_state": "cancelled", "risk_flags": [], "amount": 80.0, "days_since_order": 3}, "expectations": {"decision": "deny", "reason_code": "NOT_REFUNDABLE_STATE", "requires_approval": false}}
{"id": "F04", "tags": ["golden", "deny"], "inputs": {"order_id": "ord_0915", "account_id": "acct_1432", "order_state": "delivered", "risk_flags": [], "amount": 49.0, "days_since_order": 95}, "expectations": {"decision": "deny", "reason_code": "OUTSIDE_WINDOW", "requires_approval": false}}
{"id": "F05", "tags": ["edge", "boundary", "approve"], "inputs": {"order_id": "ord_1140", "account_id": "acct_3300", "order_state": "delivered", "risk_flags": [], "amount": 200.0, "days_since_order": 30}, "expectations": {"decision": "approve", "reason_code": "ELIGIBLE_AUTO_APPROVE", "requires_approval": false}}
{"id": "F06", "tags": ["edge", "boundary", "manual_review"], "inputs": {"order_id": "ord_1141", "account_id": "acct_3300", "order_state": "delivered", "risk_flags": [], "amount": 250.0, "days_since_order": 10}, "expectations": {"decision": "manual_review", "reason_code": "AMOUNT_OVER_THRESHOLD", "requires_approval": true}}
{"id": "F07", "tags": ["edge", "boundary", "deny"], "inputs": {"order_id": "ord_1150", "account_id": "acct_3300", "order_state": "delivered", "risk_flags": [], "amount": 49.0, "days_since_order": 31}, "expectations": {"decision": "deny", "reason_code": "OUTSIDE_WINDOW", "requires_approval": false}}
{"id": "F08", "tags": ["negative", "fraud", "manual_review"], "inputs": {"order_id": "ord_1166", "account_id": "acct_9001", "order_state": "delivered", "risk_flags": ["fraud_suspected"], "amount": 49.0, "days_since_order": 4}, "expectations": {"decision": "manual_review", "reason_code": "RISK_FLAG_PRESENT", "requires_approval": true}}
{"id": "F09", "tags": ["negative", "missing_data", "fail_closed", "manual_review"], "inputs": {"order_id": "ord_1177", "account_id": "acct_9001", "order_state": "delivered", "risk_flags": null, "amount": 49.0, "days_since_order": 4}, "expectations": {"decision": "manual_review", "reason_code": "MISSING_RISK_DATA", "requires_approval": true}}
"""Deterministic refund-eligibility decision for a workflow ``python_code`` node.
This is NOT a registered tool. Paste the body of this module into a CALIBER
**Python Code** node (``Compose → Workflows`` → drag a *Python Code* node). It
needs no registration, versions with the workflow, and uses **stdlib only**.
Wire it as the FIRST node after START: ``START -> decide_refund``. A python_code
node receives the run input as an UNPARSED string in ``run_input``, so this
entrypoint parses it as JSON (when it looks like a JSON object) and reads the
decision fields from that dict. (``json`` is pre-injected into the node sandbox
-- do NOT add a module-level ``import json``.)
Run-input fields (parsed from the run input JSON object):
order_state str -- order lifecycle, e.g. "delivered" | "shipped" |
"cancelled" | "returned" | "" (unknown).
risk_flags list -- fraud/risk signals, e.g. ["fraud_suspected"],
["chargeback_history"], or [] (none). May be
``None`` when the risk lookup failed -> we
FAIL CLOSED to manual_review (see RULES below).
amount float -- refund amount in USD (>= 0).
days_since_order int -- whole days between the order date and now.
Node outputs:
decision str -- one of: "approve" | "deny" | "manual_review".
reason_code str -- stable machine code (see REASON_CODES); pairs
with the policy-reason-normalizer skill.
requires_approval bool -- True when a human_approval gate must clear before
initiate_refund runs. Always True for
manual_review; never True for a clean approve.
Deterministic rules (evaluated top-to-bottom; first match wins):
1. FAIL CLOSED: risk data missing (risk_flags is None) OR order_state is
empty/unknown -> manual_review / requires_approval=True.
2. Any fraud/risk flag present -> manual_review / requires_approval=True.
3. Order not in a refundable state (only "delivered"/"shipped"/"returned"
are refundable) -> deny.
4. Outside the refund window (days_since_order > REFUND_WINDOW_DAYS) -> deny.
5. Negative/zero amount -> deny (nothing to refund).
6. Amount over the auto-approve threshold (amount > AUTO_APPROVE_MAX_USD)
-> manual_review / requires_approval=True.
7. Otherwise (in window, no risk, small amount) -> approve /
requires_approval=False.
The rule_checks in verification.yaml
(``deterministic_decision_preserved`` / ``approval_required_for_high_risk``)
are enforced HERE plus by the downstream human_approval gate -- not by a judge.
"""
# --- Policy constants (the only knobs; keep them visible at the top) ---------
REFUND_WINDOW_DAYS = 30 # matches demo_tools.lookup_policy
AUTO_APPROVE_MAX_USD = 200.0 # over this -> human approval required
REFUNDABLE_STATES = {"delivered", "shipped", "returned"}
# Stable reason codes (machine-facing; the normalizer skill maps these to copy)
REASON_CODES = {
"MISSING_RISK_DATA": "Risk signals unavailable; failing closed to review.",
"UNKNOWN_ORDER_STATE": "Order state unknown; failing closed to review.",
"RISK_FLAG_PRESENT": "Risk/fraud flag on the account; routing to review.",
"NOT_REFUNDABLE_STATE": "Order is not in a refundable state.",
"OUTSIDE_WINDOW": "Past the refund window.",
"INVALID_AMOUNT": "Refund amount is zero or negative.",
"AMOUNT_OVER_THRESHOLD": "Amount exceeds the auto-approve limit; needs review.",
"ELIGIBLE_AUTO_APPROVE": "In window, no risk, within auto-approve limit.",
}
def decide_refund(
order_state: str,
risk_flags,
amount,
days_since_order,
) -> dict:
"""Pure, deterministic refund decision. Returns the node-output dict."""
def out(decision: str, reason_code: str, requires_approval: bool) -> dict:
return {
"decision": decision,
"reason_code": reason_code,
"requires_approval": bool(requires_approval),
}
# Rule 1 -- fail closed when inputs are missing/unusable.
if risk_flags is None:
return out("manual_review", "MISSING_RISK_DATA", True)
state = (order_state or "").strip().lower()
if not state:
return out("manual_review", "UNKNOWN_ORDER_STATE", True)
# Coerce numerics defensively (still fail closed on garbage).
try:
amount_val = float(amount)
days = int(days_since_order)
except (TypeError, ValueError):
return out("manual_review", "MISSING_RISK_DATA", True)
# Rule 2 -- any risk flag -> review + approval.
if any(str(flag).strip() for flag in risk_flags):
return out("manual_review", "RISK_FLAG_PRESENT", True)
# Rule 3 -- non-refundable lifecycle state.
if state not in REFUNDABLE_STATES:
return out("deny", "NOT_REFUNDABLE_STATE", False)
# Rule 4 -- outside the refund window.
if days > REFUND_WINDOW_DAYS:
return out("deny", "OUTSIDE_WINDOW", False)
# Rule 5 -- nothing to refund.
if amount_val <= 0:
return out("deny", "INVALID_AMOUNT", False)
# Rule 6 -- large refund needs a human.
if amount_val > AUTO_APPROVE_MAX_USD:
return out("manual_review", "AMOUNT_OVER_THRESHOLD", True)
# Rule 7 -- clean auto-approve.
return out("approve", "ELIGIBLE_AUTO_APPROVE", False)
# --- python_code node entrypoint --------------------------------------------
# A CALIBER Python Code node calls ``run_python_node(...)`` and uses its RETURN
# value as the node output. A module-level ``result = ...`` is DISCARDED (the
# runtime wraps a body lacking this def in a function with no return), so define
# the entrypoint explicitly and return both output ports.
#
# ``json`` is PRE-INJECTED into the node sandbox; do NOT ``import json`` here.
# Because decide_refund is the first node after START, its inputs arrive as the
# run input -- an UNPARSED string in ``run_input``. Parse it as JSON when it
# looks like a JSON object, then read the decision fields from that dict.
def run_python_node(input=None, context=None, inputs=None, run_input=""):
if isinstance(run_input, str) and run_input.strip().startswith("{"):
data = json.loads(run_input)
else:
data = run_input or {}
if not isinstance(data, dict):
data = {}
result = decide_refund(
order_state=data.get("order_state", ""),
risk_flags=data.get("risk_flags"), # absent -> None -> fail closed
amount=data.get("amount", 0),
days_since_order=data.get("days_since_order", 0),
)
return {"text": json.dumps(result), "result": result}
---
name: refund-explanation
model_hint: a small/cheap instruct model is fine (explanation only, no decision)
variables: [decision, reason_code, order_state, customer_name]
commit_message: "v1 explanation layer — explains, never overrides, the deterministic decision"
---
You write a short, customer-facing explanation of a refund decision that has
ALREADY been made by a deterministic policy engine. You do NOT make or change
the decision. Your only job is to put the given decision into plain, respectful
language.
The decision is fixed and authoritative:
- decision: {{ decision }} (one of: approve, deny, manual_review)
- reason_code: {{ reason_code }} (the machine reason for that decision)
- order_state: {{ order_state }}
- customer_name: {{ customer_name }}
Hard rules:
- NEVER contradict, soften, or upgrade/downgrade {{ decision }}. If it is
"deny", do not imply the refund might still happen. If it is "manual_review",
say it is under review and a person will follow up — do NOT promise an outcome.
If it is "approve", confirm the refund is being processed.
- Make NO new commitments: no amounts, no dates, no timelines, no exceptions,
no policy promises that are not implied by {{ decision }} and {{ reason_code }}.
- Do not invent facts about the order beyond {{ order_state }} and the reason.
- Address {{ customer_name }} once, warmly and briefly. 2–4 sentences. Plain
text, no JSON, no markdown.
Write the explanation now.
---
name: policy-reason-normalizer
summary: Map a machine refund reason_code to one stable, human-readable reason phrase — without changing the decision.
---
# Policy Reason Normalizer
Use this skill whenever a refund decision carries a machine `reason_code` that
needs to be stated to a person (customer message, review note, audit log). It
normalizes the code to ONE canonical sentence. It never alters the `decision`
and never adds commitments.
## Canonical mapping
| reason_code | Canonical reason phrase |
| --- | --- |
| `ELIGIBLE_AUTO_APPROVE` | The order is within the refund window with no risk flags, so the refund was approved automatically. |
| `AMOUNT_OVER_THRESHOLD` | The refund amount is above the automatic-approval limit, so it was sent for human review. |
| `RISK_FLAG_PRESENT` | A risk or fraud signal is associated with the account, so the refund was sent for human review. |
| `MISSING_RISK_DATA` | Required risk information was unavailable, so the refund was sent for human review to stay safe. |
| `UNKNOWN_ORDER_STATE` | The order's status could not be confirmed, so the refund was sent for human review. |
| `NOT_REFUNDABLE_STATE` | The order is not in a state that can be refunded, so the refund was declined. |
| `OUTSIDE_WINDOW` | The order is past the refund window, so the refund was declined. |
| `INVALID_AMOUNT` | There is no positive amount to refund, so the refund was declined. |
## Rules
- Output exactly one phrase from the table for the given `reason_code`.
- If the `reason_code` is unrecognized, output: "This decision was made by
policy; please consult the review queue for details." — do not guess.
- Do not state or imply an outcome that conflicts with the `decision`
(`approve` / `deny` / `manual_review`).
- Add no amounts, dates, or timelines. The phrase is a reason, not a promise.
---
name: customer-safe-refund-language
summary: Guardrails for phrasing a refund decision to a customer safely — respectful tone, no new promises, never override the decision.
---
# Customer-Safe Refund Language
Apply this skill when turning a refund `decision` into customer-facing wording.
It governs TONE and SAFETY only; the `decision` and `reason_code` are inputs
you must preserve exactly.
## Allowed phrasing by decision
- **approve** — Confirm warmly that the refund is being processed. You may say
it has been approved. Do not invent the exact arrival date or amount unless
it was explicitly provided.
- **deny** — State plainly and kindly that the refund cannot be processed, and
give the canonical reason. Do not imply it might still happen, and do not
invite an appeal that policy does not offer.
- **manual_review** — Say the request is being reviewed and a team member will
follow up. Do NOT predict the outcome (no "you'll likely get it"), no ETA.
## Never do
- Never change, hedge, or reverse the `decision`.
- Never promise amounts, dates, timelines, credits, or exceptions that are not
already implied by `decision` + `reason_code`.
- Never blame the customer or disclose internal risk/fraud signals; for
risk-driven reviews say only that "additional review" is required.
- Never output JSON or internal codes to the customer — use plain language.
## Style
Address the customer by name once, keep it to 2–4 sentences, be courteous and
concrete. When in doubt, say less rather than promise more.
Evaluation & quality gates
| Quality gate | Target |
|---|---|
| Deterministic fixture pass rate | ≥ 0.97; decision mismatch = 0 |
| Approval enforced on high-risk | visible in the run timeline |
| Explanation faithfulness | ≥ 0.92 |
Developer notes & gotchas
- A registered tool needs an importable
module_path+callable_name; the shippeddemo_toolscallables let you do this from the UI with no code. - Field is
side_effect_level(read/write/external_action); read tools needallow_in_preview: trueto run live in the sandbox. - The decision table is structured workflow source: versioned, inspectable, and executable without custom Python.