Try a governed refund agent
Run three synthetic refunds with the public Actspan 0.2.0 CLI and native OpenAI: $75 pays automatically, $250 waits for a local review, and $750 is refused. Check the tool receipts against the sandbox ledger after each case. No real money moves. Claude and LangChain coverage is listed below the walkthrough.
Choose your entry
- Live workflow: continue below with your OpenAI key to observe payment, review and refusal against synthetic ledgers.
- No model key: the receipt-check exercise uses a scripted loopback provider and checks four receipt cases with no paid model calls. It still needs the public CLI, extracted examples and host Python.
Before you begin
Use the public CLI and examples archive.
The current native download supports Apple Silicon macOS. Starting with that
archive, all three cases and the review commands are on this page. You also need
host Python 3.10+ (python3) for ledger inspection and a working OPENAI_API_KEY exported
privately in this terminal. Live runs incur OpenAI charges; setup does not.
This path uses the bundled OpenAI runtime and gpt-4.1-mini, without a separate
provider-plugin installation or Cloud account.
The native CLI runs its tools with bundled Python, but commands such as
python3 refund_service.py ledger and the receipt-check script use a separate
Python installed on your computer. The CLI does not install that host Python.
On a Mac without one configured, python3 may prompt you to install Apple's
Command Line Tools. Check python3 --version and prepare Python 3.10+ before
starting this walkthrough.
Approval scope in 0.2.0: the unchanged example can complete local $250 approval. Its explicit file entrypoints avoid the known package-resolution defect. Imported helpers remain outside automatic approval-change detection in that release. Keep the example files, identity and ledger fixed while paused; start a fresh run after changing them. The approval guide explains the affected configurations and the limits of this workaround.
Problem and flow
A customer asks for money back. A convincing reply can still hide an unauthorized payment or a fabricated ID. This example separates the service's refund limits, Actspan's review checkpoint, the completed tool receipt and the persisted ledger.
| Question | Evidence to inspect |
|---|---|
| Was payment allowed now? | Quote tier and selected workflow branch. |
| Did a review occur first? | The saved refund_review decision before the reviewed payment tool. |
| Did the action really happen? | Matching completed receipt plus the payment row in SQLite. |
Set up a fresh sandbox
Enter the OpenAI refund directory from the public archive. Adjust the first path if you extracted it elsewhere. Use one new ledger, leaving earlier ledgers intact:
cd "$HOME/actspan/actspan-examples/openai/refund_agent"
actspan --version
python3 --version
mkdir -p .demo
REFUND_TRIAL_DIR=$(mktemp -d "$PWD/.demo/walkthrough.XXXXXX")
export REFUND_LEDGER_DB="$REFUND_TRIAL_DIR/refunds.sqlite3"
export REFUND_CUSTOMER_ID=CUST-100
export ACTSPAN_PORTAL_DISABLED=1
actspan init workspace.yaml --no-env-file --no-portal
actspan validate workspace.yaml --strict-env
actspan preview workspace.yaml --strict-schema --no-portal
Expected: version 0.2.0, successful validation and a preview with no model call.
Export the customer identity before the first run; request text does not
authenticate a customer. Keep this terminal and working directory for continuation.
Understand the rules
The sandbox service reads these values from refund_policy.yaml (JSON is valid
YAML):
{
"auto_refund_max_usd": 100,
"human_approval_max_usd": 500,
"require_photo_evidence": true
}
For a qualifying damage claim, up to $100 is automatic; above $100 through $500
needs review; above $500 is blocked. Each supplied claim belongs to CUST-100,
has photos, and requests less than its order total.
| Layer | What enforces the decision |
|---|---|
| Sandbox service | Classifies the quote using monetary thresholds; checks customer, claim, quoted amount, policy hash and payment path before writing. |
| Actspan workflow and policy | Limits the assessor to read/quote tools, withholds the reviewed payment step until its approval checkpoint is approved, and restricts each agent's tools. |
| Actspan output evidence | Requires configured quote/payment claims to match completed tool receipts. |
| Ledger inspection | Lets you compare those claims with persisted sandbox rows independently. |
The service owns the monetary rule. It does not independently authenticate the reviewer; the local CLI decision controls entry to the reviewed workflow step.
Run the $75 automatic refund
Automation limit: public 0.2.0 can report exit 0/outer success for the separate direct-prompt receipt-failure path even when nested execution failed. Do not generalize a successful shell exit to a verified business write. Inspect nested status, the same-run recorded error and ledger; use the model-free checker exercise for its supported status checks. The source correction requires a newly qualified release.
actspan run workspace.yaml refund_request --json --no-portal --no-cache \
--input-text 'Claim CLM-1001 on order ORD-1001. Two mugs arrived cracked and I uploaded photos. I want a $75 refund.' \
> "$REFUND_TRIAL_DIR/75.json"
cat "$REFUND_TRIAL_DIR/75.json"
python3 refund_service.py ledger
Expected: outcome: refunded with a generated PAY-… payment ID and QTE-…
quote ID. The ledger has exactly one payment: CLM-1001, amount_cents: 7500,
path: auto. Both IDs must match the final output. Your IDs will differ.
Run the $250 human-review case
actspan run workspace.yaml refund_request --json --no-portal --no-cache \
--input-text 'Claim CLM-1002 on order ORD-1002. My $400 headphones arrived damaged and I uploaded photos. I want a $250 refund.' \
> "$REFUND_TRIAL_DIR/250.json"
cat "$REFUND_TRIAL_DIR/250.json"
RUN_250=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["result"]["run_id"])' "$REFUND_TRIAL_DIR/250.json")
THREAD_250=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["result"]["output"]["thread_id"])' "$REFUND_TRIAL_DIR/250.json")
actspan inspect "$RUN_250" --node refund_review --json
actspan inspect "$RUN_250" --trace --json
python3 refund_service.py ledger
Expected: pending_approval; inspection shows the saved claim, $250 amount and
quote at refund_review. The ledger contains an approval-tier quote for
CLM-1002, no payment for that claim, and only the earlier $75 payment.
The trace calls this checkpoint paused_approval.
Approve the saved request:
actspan continue --thread-id "$THREAD_250" --approve \
--actor demo-reviewer --note 'Reviewed the synthetic damage claim.' --json \
> "$REFUND_TRIAL_DIR/250-approved.json"
cat "$REFUND_TRIAL_DIR/250-approved.json"
python3 refund_service.py ledger
continue --approve supplies the decision and resumes execution; no second
resume command is needed. Expected: one $250 payment for CLM-1002 with
amount_cents: 25000, path: reviewed and matching quote/payment IDs. There are
now two payments in total. --actor is a caller-supplied audit label, not an
authenticated reviewer identity. Portal reporting stays disabled in this walkthrough.
Run the $750 blocked refund
actspan run workspace.yaml refund_request --json --no-portal --no-cache \
--input-text 'Claim CLM-1003 on order ORD-1003. The desk arrived damaged and I uploaded photos. I want a $750 refund.' \
> "$REFUND_TRIAL_DIR/750.json"
cat "$REFUND_TRIAL_DIR/750.json"
python3 refund_service.py ledger
Expected: a quote with tier: blocked, final outcome: blocked, an empty
payment ID and no payment for CLM-1003. The main walkthrough's total remains
two payments: the automatic $75 and the approved $250.
In the qualified run the service classified the quote as blocked, and the
settlement agent returned that outcome without calling a payment tool. This is
not an observed Actspan TOOL_DENIED dispatch. The service also independently
rejects a payment attempt against a blocked quote. The workflow itself can
finish with execution status succeeded and exit code 0 while its business
outcome is blocked.
Match the final output to the ledger
Normal successful runs store the final object as a JSON string at
result.output; successful approval continuation uses result.final_output.
The pending run instead has an object at result.output, including thread_id.
Print the final outcome after approval:
python3 - "$REFUND_TRIAL_DIR/250-approved.json" <<'PY'
import json
import sys
result = json.load(open(sys.argv[1]))["result"]
value = result.get("final_output", result.get("output"))
print(json.dumps(json.loads(value) if isinstance(value, str) else value, indent=2))
PY
python3 refund_service.py ledger
Use 75.json or 750.json instead to inspect another
completed case. For refunded or already_refunded, compare amount_usd * 100
with the payment row's amount_cents and compare both IDs literally. For
declined or blocked, verify there is no payment for the claim; a declined
output reports amount zero while its original $250 quote remains. A model
saying “refunded” does not establish payment.
Optional: try rejection in a separate sandbox
This exercise starts with no payments and does not rerun $75. Use a subshell so its ledger, files and thread variables do not replace the main walkthrough's:
(
set -eu
REJECTION_TRIAL_DIR=$(mktemp -d "$PWD/.demo/rejection.XXXXXX")
export REFUND_LEDGER_DB="$REJECTION_TRIAL_DIR/refunds.sqlite3"
export REFUND_CUSTOMER_ID=CUST-100
export ACTSPAN_PORTAL_DISABLED=1
actspan run workspace.yaml refund_request --json --no-portal --no-cache \
--input-text 'Claim CLM-1002 on order ORD-1002. My $400 headphones arrived damaged and I uploaded photos. I want a $250 refund.' \
> "$REJECTION_TRIAL_DIR/250.json"
REJECTION_THREAD=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["result"]["output"]["thread_id"])' "$REJECTION_TRIAL_DIR/250.json")
actspan continue --thread-id "$REJECTION_THREAD" --reject \
--actor demo-reviewer --note 'Declined the synthetic damage claim.' --json \
> "$REJECTION_TRIAL_DIR/250-rejected.json"
cat "$REJECTION_TRIAL_DIR/250-rejected.json"
python3 refund_service.py ledger
)
Expected: outcome: declined, amount zero, an empty payment ID, the original
$250 quote and zero payments in this separate ledger. Each pending request
receives one decision. The main walkthrough's 250-approved.json and ledger
remain available. If you also run $750 in this rejection sandbox, its payment
count still stays zero; a blocked quote creates no payment.
Optional: test receipt validation failures
The example's output_evidence rules require a quoted claim to match a
quote_refund completion. For refunded or already_refunded, this payment
rule requires all mapped fields from one successful tool receipt:
output_evidence:
- name: payment_receipt
tools: [issue_auto_refund, issue_reviewed_refund]
when:
/outcome: [refunded, already_refunded]
result_when:
/status: [sandbox_succeeded, already_refunded]
match:
/payment_id: /payment_id
/quote_id: /quote_id
/amount_usd: /amount_usd
These are explicit checks on configured output fields, not verification of every natural-language sentence.
Download receipt-check revision 2 and its checksum file. Save both files using their original names in your Downloads directory. This separate docs download is not inside the immutable examples archive. From the OpenAI example directory, in the same terminal used above:
(
set -eu
REFUND_EXAMPLE_DIR="$PWD"
cd "$HOME/Downloads"
shasum -a 256 -c refund-receipt-check-2_SHA256SUMS.txt
unzip refund-receipt-check-2.zip -d "$REFUND_TRIAL_DIR"
python3 "$REFUND_TRIAL_DIR/refund-receipt-check/run.py" \
--actspan "$(command -v actspan)" \
--example-dir "$REFUND_EXAMPLE_DIR" \
--output "$REFUND_TRIAL_DIR/receipt-check"
cat "$REFUND_TRIAL_DIR/receipt-check/qualification.json"
)
The fixture uses Python's standard library, a loopback scripted Responses endpoint, your actual 0.2.0 executable and copies of the public refund service and tools. It gives the child process a dummy key and separate state/ledgers; it does not call a paid model, modify the runtime or reuse your walkthrough's ledger. Its synthetic declaration exercises the payment-evidence mapping above, not the full live agent workflow or model behavior.
Expected results:
| Controlled response | Runtime result | Payments left in that case's sandbox ledger |
|---|---|---|
| Invented payment ID, no tool call | failed, OUTPUT_EVIDENCE_UNSUPPORTED_CLAIM recorded |
0 |
| Genuine successful tool receipt | succeeded, IDs and amount match |
1 |
| Incorrect reported payment ID after a real sandbox write | failed, unsupported claim recorded |
1 |
| Payment ID from one receipt mixed with quote/amount from another | failed, unsupported claim recorded |
2 |
Each case retains run.stdout, completed tool receipts, local execution records
and ledger-after.json for inspection. On this published direct-prompt path,
the shell can exit 0 and the outer JSON can say success: true even when
result.status is failed. The fixture checks the nested execution status and
the recorded error for that run; shell success alone is insufficient.
A failed output-evidence check rejects the reported result; it does not reverse a completed sandbox payment. Inspect the ledger before retrying. Tool callbacks show observed execution, while persisted rows show what the sandbox recorded. Neither establishes general exactly-once payments.
Provider and review coverage
| Path | Qualified behavior | Boundary |
|---|---|---|
| Public native CLI 0.2.0, bundled OpenAI | $75 automatic, $250 local CLI approval and $750 blocked; matching ledger IDs | The complete public walkthrough above. Local actor labels do not authenticate a reviewer. |
| Public native CLI 0.2.0, bundled Claude | One $75 automatic refund with a matching receipt | Public bundle $250/$750 review walkthrough has not been qualified. |
| Installed core 0.2.0, isolated Anthropic 0.1.0 | $75, $250 approve/reject, $750 and foreign-identity checks | Separate Python/plugin installation; does not extend the public-bundle claim. |
| LangChain 0.1.1 with core 0.2.0, OpenAI or Claude | $75 pays with receipts; $250 blocks before payment; $750 pays nothing | Local $250 denial is not human review. The separate Cloud approval workspace remains unqualified on these artifacts. |
| Native OpenAI and Claude, authenticated local portal | Pause, human review, changed-configuration refusal and reviewed payment | Historical core 0.1.2 check; the complete portal path was not rerun on 0.2.0. Production OIDC is separate. |
For a Claude alternative, keep the archive's sibling directories together and
use actspan-examples/anthropic/refund_agent with ANTHROPIC_API_KEY.
Its model is claude-haiku-4-5-20251001; the shared ledger command is
python3 ../../openai/refund_agent/refund_service.py ledger. Start with a new
ledger and the $75 request; do not infer the untested public cases from the
isolated-plugin results. See Anthropic setup.
Independent OpenAI 0.1.1/Anthropic 0.1.0 plugins and the partner-only LangChain examples need host Python and supplied package access. See provider requirements and LangChain tool approvals. Configuration-drift follow-on tests and portal history are documented in the approval guide; they are not additional completed steps of this public walkthrough.