Appearance
Refunds and returns
A customer asks for a refund or a replacement through a short form, and rules, a model and a support agent decide together in one durable workflow: a rule a person wrote declines abuse before any model is asked, a model reads the customer's reason against the order and says whether the request fits it and which resolution it supports, six rules weigh the order, the amount, repeat refunds and the model's answers, and an agent takes whatever a rule or the model doubts. Every answer, rule result and decision stays on the record. Pack: examples/policies/refunds-and-returns/.
A pattern for a returns decision, not consumer-law advice: the thresholds, questions and fields are placeholders chosen to make the example run; a real policy comes from the business's own rules. Its model step calls an install named chatgpt (The chatgpt install).
The decision it automates
Whether to grant the refund or the replacement the customer asked for (approve), send the request to a support agent (manual_review), or decline it (reject), when the form is sent. A request is granted without a person only when every rule passes, the model's two answers among them. Only the abuse rule, which a person wrote, declines a request on its own: the model's doubt sends a request to an agent, never to a refusal.
Data expected
- The customer as a subject (kind
user) with the order they are returning from underorder(id,items,total,deliveredDaysAgo), which your store writes before it starts the run. Contact details such asemail,phoneandaddressmay sit beside it; the model never reads them. - The form's answers under
submission:orderId,item,wants(refundorreplacement),amountin USD, thereasonin the customer's own words, and optionalphotos. - A
refund.requestedevent for every request, this one included, posted throughPOST /events, so the velocity rules see the history.
Steps and rules
| Step | Type | Why |
|---|---|---|
collect | wait_for_collection | The refund-request flow: the request, optional photos, a confirmation. |
screen | evaluate_rules | refund_abuse alone (ruleKeys), under screen: the one rule that declines on its own runs before any model is asked. |
gate | branch | screen.outcome == reject goes to decline; a customer with an order (subject.order.id) goes on to assess; one without goes to rules, without the model. |
assess | call_app | chatgpt's decide: the two questions below, answered under assessment. |
rules | evaluate_rules | The refunds-and-returns rule set under max_severity, the model's answers among the data it weighs. |
route | branch | rules.outcome == manual_review opens a case. |
review | create_case | refund_review, medium priority, 24-hour SLA. |
decide | emit_decision | From the rules, or the agent's decision. |
decline | emit_decision | From screen: an automated reject. |
The rule set refunds-and-returns (useCase: custom) uses max_severity: a failed block rule rejects, a failed warn rule sends the run to review, and the weights add up to the score. The two rules over the model's answers are warn, so a model can send a request to a person but never decline it. refund_abuse runs again in the set, so the decision records every rule.
| Rule | Type | Severity, weight | Fails when |
|---|---|---|---|
refund_order_match | expression | warn, 20 | the customer has no order, orderId is not its id, or amount is above its total |
refund_amount_cap | score threshold | warn, 30 | amount is above $100 |
refund_repeat | velocity | warn, 30 | more than two refund.requested events in 90 days |
refund_abuse | velocity | block, 100 | more than five refund.requested events in 30 days |
refund_consistent | comparison | warn, 30 | assessment.answers.consistent.value is not true: the model is less than 80% sure, or was not asked |
refund_resolution | expression | warn, 20 | assessment.answers.resolution.value is not wants: another resolution, a request for evidence, or no answer |
The model step
assess asks two questions about one state, the form's orderId, item, wants, amount and reason and the subject's order, which its input mapping puts in the step's variables: a decision without a state of its own is about its variables, as one JSON object (Asking for decisions).
| Question | Answer | Asks | Read by |
|---|---|---|---|
consistent | yes or no, threshold 0.8 | Does the request fit the order: the item on it, the amount within its total, the reason possible in the days since delivery? | refund_consistent |
resolution | ask_for_evidence, refund or replacement | Which resolution does the reason support, beside the one the customer asked for (wants)? | refund_resolution |
The model is asked only when its answer can change something and it has the order to read: gate skips it for a request screen declines, and for a customer without an order, whose two model rules then fail for want of an answer and send the request to an agent. The answers reach the decision through the rules that read them, and the evaluation snapshots keep them as the rules saw them, so a backtest of a new cap replays them without asking a model again (Rules).
What the vendor sees
Through the install's connection, the step's two questions and the six values above: not the customer's name, contact details or address, and never the photos, since models read text and JSON only (Data protection). The customer writes the reason and may write instructions into it. The model reads it as data, and the most an answer it was talked into can do is let through a request that every other rule passes: at most $100, within the order's total, from a customer with no more than two requests in 90 days (Prompt injection).
The chatgpt install
A pack never carries an install: this one declares the install its step calls, "installs": { "chatgpt": "chatgpt" }, an install of the platform's ChatGPT (OpenAI API) app. The import reports chatgpt missing until the workspace has one (--publish publishes all the same), and a run that reaches assess without it fails with unknown_install. The admin console's import result installs it in one click, with a preset configuration that reaches OpenAI's Responses API with storing off, for decisions too, and a sample key (Installs at import and export).
OpenAI refuses the sample key. Until an administrator stores the workspace's own OpenAI key on the install's page (Connect › Apps, the install chatgpt, Configuration › Keys › openaiApiKey), every run that reaches assess fails there: it gets no decision and opens no case, and OpenAI's answer shows in the invocation record and under the run's app calls. The same holds whenever the call fails for good, through an outage that outlasts its retries, an exhausted quota or a refusal (Time, retries and failures). A request that screen declines, or one from a customer without an order, never reaches the model, so the sample key does not stop it. Once the key is stored, replay a failed run from assess (Operate › Runs, Replay from step: Admin console: Runs): the customer is not asked again. Every model app takes the same input and answers in the same shape, so the step can call another app's install instead, declared under its own name, or fall back on one (Falling back to another install).
What a reviewer sees
A refund_review case with the form, the photos when attached, each rule's result, the model's answers under assessment in the run's context, and its call under Vendor checks. Request info sends the form back to the customer with a message, such as a request for a photo or the carrier's tracking number; when they send it, the run repeats screen, assess and rules on the new answers and the case comes back with the rest of its SLA (Admin console: the case page). The agent's decision and note become the decision and its reasons; decision.created tells your store to refund, ship the replacement or write back (Outbound webhooks).
How to adapt it
Look the order up instead of trusting the subject: install the platform's http-api for your order system's API, add a call_app step after collect that maps submission.orderId into the request's variables with outputKey: "order", and point gate, the model's variables.order and the order rule at its answer (Adding a vendor step). Add a return window as a comparison on subject.order.deliveredDaysAgo, move the cap and the windows to your own, and backtest a change before publishing it. Write the questions' criteria for your own catalog, and move the 0.8 with the question's threshold.
Run it
bash
pnpm policy:import examples/policies/refunds-and-returns --publishpnpm policies:check runs three fixtures against a model stub (scripts/ai-stub.mjs), whose answers a cue word in the reason picks:
expect.json: a $34.99 refund for headphones the customer says never arrived, on an order marked delivered four days earlier, with one earlier request 40 days back. Cued low, the stub putsconsistentat 0.05 and asks for evidence, so both model rules fail: arefund_reviewcase opens, the check approves it and the run completes with a manualapprove.expect-auto-approve.json: a $59.99 replacement for earbuds that stopped charging. Cued high, the stub putsconsistentat 0.95 and supports a replacement, every rule passes, and the run completes with an automatedapprove, without a case.expect-abuse.json: a sixth request in 30 days.screendeclines it, and the run completes with an automatedrejectwithout asking the model.