Skip to content

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 under order (id, items, total, deliveredDaysAgo), which your store writes before it starts the run. Contact details such as email, phone and address may sit beside it; the model never reads them.
  • The form's answers under submission: orderId, item, wants (refund or replacement), amount in USD, the reason in the customer's own words, and optional photos.
  • A refund.requested event for every request, this one included, posted through POST /events, so the velocity rules see the history.

Steps and rules ​

StepTypeWhy
collectwait_for_collectionThe refund-request flow: the request, optional photos, a confirmation.
screenevaluate_rulesrefund_abuse alone (ruleKeys), under screen: the one rule that declines on its own runs before any model is asked.
gatebranchscreen.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.
assesscall_appchatgpt's decide: the two questions below, answered under assessment.
rulesevaluate_rulesThe refunds-and-returns rule set under max_severity, the model's answers among the data it weighs.
routebranchrules.outcome == manual_review opens a case.
reviewcreate_caserefund_review, medium priority, 24-hour SLA.
decideemit_decisionFrom the rules, or the agent's decision.
declineemit_decisionFrom 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.

RuleTypeSeverity, weightFails when
refund_order_matchexpressionwarn, 20the customer has no order, orderId is not its id, or amount is above its total
refund_amount_capscore thresholdwarn, 30amount is above $100
refund_repeatvelocitywarn, 30more than two refund.requested events in 90 days
refund_abusevelocityblock, 100more than five refund.requested events in 30 days
refund_consistentcomparisonwarn, 30assessment.answers.consistent.value is not true: the model is less than 80% sure, or was not asked
refund_resolutionexpressionwarn, 20assessment.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).

QuestionAnswerAsksRead by
consistentyes or no, threshold 0.8Does the request fit the order: the item on it, the amount within its total, the reason possible in the days since delivery?refund_consistent
resolutionask_for_evidence, refund or replacementWhich 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 --publish

pnpm 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 puts consistent at 0.05 and asks for evidence, so both model rules fail: a refund_review case opens, the check approves it and the run completes with a manual approve.
  • expect-auto-approve.json: a $59.99 replacement for earbuds that stopped charging. Cued high, the stub puts consistent at 0.95 and supports a replacement, every rule passes, and the run completes with an automated approve, without a case.
  • expect-abuse.json: a sixth request in 30 days. screen declines it, and the run completes with an automated reject without asking the model.

Released under the Apache-2.0 License.