Support Ticket Reply
Draft a customer support response using CRM context — while blocking outbound email, payment access, and admin actions. The agent can draft but never send.
Contract
aex
task support_ticket v0
goal "Draft a customer support reply using CRM context."
use crm.lookup, ticket.read, email.draft
deny email.send, payment.*, admin.*, secrets.read
need customer_id: str
need ticket_id: str
do crm.lookup(id=customer_id) -> customer
do ticket.read(id=ticket_id) -> ticket
make reply: markdown from customer, ticket with:
- summarize the customer's issue
- acknowledge prior interactions
- propose the next step
- mark any uncertainty
check reply does not include customer.internal_notes
do email.draft(to=customer.email, body=reply) -> draft
return draftInputs
json
{
"customer_id": "cus_1234",
"ticket_id": "tkt_5678"
}Policy
Use an .aex policy file for the ambient security boundary:
aex
policy support v0
goal "CRM access boundary for support workflows."
allow crm.lookup, ticket.read, email.draft
deny email.send, payment.*, admin.*, secrets.read
confirm before email.draft
budget calls=30Or the equivalent JSON policy:
json
{
"allow": [
"crm.lookup",
"ticket.read",
"email.draft"
],
"deny": [
"email.send",
"payment.*",
"admin.*",
"secrets.read"
],
"require_confirmation": [
"email.draft"
]
}Run It
bash
aex run examples/support-ticket/task.aex \
--inputs examples/support-ticket/inputs.json \
--policy examples/support-ticket/policy.jsonWithout --auto-confirm, the CLI pauses at email.draft and waits for human approval — the policy's require_confirmation gate.
Expected Output
The agent returns the draft email object:
json
{
"to": "alice@example.com",
"body": "Hi Alice, thanks for reaching out about the billing discrepancy...",
"status": "draft"
}Blocked Actions
The contract denies email.send — the agent can prepare a draft but cannot send it. If it tries:
json
{"event":"tool.denied","tool":"email.send","reason":"denied by contract: email.send"}It also denies payment.* and admin.*, preventing the agent from issuing refunds, modifying accounts, or escalating privileges.
Audit Log
json
{"event":"run.started","agent":"support_ticket","version":"v0"}
{"event":"tool.allowed","tool":"crm.lookup","step":1}
{"event":"tool.result","tool":"crm.lookup","bind":"customer"}
{"event":"tool.allowed","tool":"ticket.read","step":2}
{"event":"tool.result","tool":"ticket.read","bind":"ticket"}
{"event":"make.result","bind":"reply","type":"markdown"}
{"event":"check.passed","condition":"reply does not include customer.internal_notes"}
{"event":"confirm.required","tool":"email.draft"}
{"event":"confirm.approved","tool":"email.draft"}
{"event":"tool.allowed","tool":"email.draft","step":3}
{"event":"tool.result","tool":"email.draft","bind":"draft"}
{"event":"run.finished","status":"success"}What This Proves
- Draft-only pattern:
deny email.sendlets the agent compose but never deliver — a human reviews before sending - Data isolation:
check reply does not include customer.internal_notesensures internal CRM notes never leak into customer-facing replies - Blast radius limits:
deny payment.*anddeny admin.*prevent the agent from taking irreversible financial or administrative actions - Confirmation gates: both the contract and the policy require approval before
email.draft, giving humans two independent checkpoints