Done means confirmed
A resolution is done when the platform confirms it, not when the model says so
A support ticket that asked for a refund is not resolved when the model writes that the refund is on its way. It is not resolved when the system saves a row that says the refund was issued. It is resolved when the refund exists in Shopify, and the reply that says so exists in the brand's helpdesk.
The distinction sounds pedantic until the first time the three disagree. The model said it. The database recorded it. The store has no refund. The customer, who was told the money was coming, writes again four days later, and the second ticket is worse than the first because it now carries a broken promise.
A system that marks a ticket resolved before the external platform confirms the action is not reporting success. It is manufacturing it.
Three moments that are easy to confuse
Every action a support system takes passes through three moments, and each one produces a different kind of evidence.
The first is the proposal. The model, or a person, decides that a refund of a given amount is the right answer to this case. That is an intention. It proves what someone wanted to do.
The second is the internal write. The system records that it is about to act, or that it acted. That is a claim the system makes about itself. It proves what the system believes.
The third is the external confirmation. Shopify returns a refund object with its own identifier and a transaction status. The helpdesk returns the message it accepted for sending. That is a fact recorded by the system that actually holds the money and the conversation. It is the only one of the three that proves the action happened.
| Moment | Who asserts it | What it proves | What it does not prove |
|---|---|---|---|
| Proposal | the model or the person | what was intended | that it was allowed |
| Internal write | the support system | what the system believes | that anything left the building |
| External confirmation | Shopify, the helpdesk | that the action exists | nothing further is needed |
| Only the last row is evidence about the world. The first two are evidence about the system. | |||
The failure that matters is the collapse of the second moment into the third. A system marks the ticket resolved, the delivery sent, the refund issued, at the moment it writes its own row — and the call to the platform either never ran, ran and failed, or ran and returned nothing anyone read. Every dashboard downstream now counts a success that has no counterpart outside the system that reported it.
Razviel's design closes that collapse by rule. The success signals — delivered, resolved by the AI, the external reference — are written only with the echo of the identifier the external platform returned. A reply is delivered when the helpdesk has it, attributed to the sender that wrote it. A refund is done when Shopify has it. Nothing upstream of that may say done.
A timeout is neither a failure nor a success
The hard case is not the clean error. A validation error before execution is easy: the platform refused, nothing moved, the system corrects the request or escalates the case. The hard case is the timeout — the request left, and no answer came back. The refund may exist. It may not. The caller does not know.
Amazon's engineering library describes this exact ambiguity: after a failed call, it is not clear whether the work is running or not, and simply retrying can produce the same work twice. In support, the same work twice is a double refund.
The industry's answer is the idempotency key. Stripe's API reference
describes it plainly: the client attaches a unique key, and if a
connection error occurs the request can be repeated without creating
a second object or performing the update twice; Stripe saves the
first result for a given key and returns it to every retry. Shopify
applies the same contract to its own money mutations. Its refund
mutation accepts a key through an @idempotent directive,
and the reference states that from the 2026-04 version the key is
required.
A key only protects if it is derived from the right thing. In the design, it belongs to the server, never to the client, and it is derived from the workspace, the ticket, the action and its parameters. Two clicks on the same refund produce the same key, so the platform executes once and both clicks receive the same answer. When a person genuinely means to repeat an action, the interface says that it was already processed, and when. It never decides on its own.
A key also expires. Stripe allows keys to be removed once they are at least 24 hours old; after that, a reused key is a new request. The design takes the consequence literally: within the window, a timeout is retried with the same key. Past it, nothing is retried blind. The system reads the order and decides from what the store actually holds.
A refund that has not confirmed is not done. Not probably, and not with a note.
One more property makes this work under load. In the design, a money action is recorded as an intention before any external call is made, and the call happens off the path of the response. The record of the attempt exists before the attempt does. If the process dies between the write and the platform's answer, the intention is still there, with its key, waiting to be completed or read back. Queues and retries fix delay. Only reconciliation against the platform fixes absence.
The honest state is often the middle one
A support system with two states — open and resolved — has nowhere to put an action that is in flight. So it rounds. Usually up.
The platforms themselves do not round. Shopify's order transaction status has six values: success, pending, awaiting response, failure, error and unknown. Four of the six are not success, and two of those four are not failure either. The source of truth publishes the ambiguity as a first-class state. A support system that compresses it into a checkmark has deleted information the store went to the trouble of giving it.
| What the platform says | What the ticket records | What the customer is told |
|---|---|---|
| Success, with its identifier | done, with the external reference | the refund was issued |
| Pending, awaiting response | in progress, with the key | it is being processed |
| Failure, error | failed, with the reason | it did not go through, and what happens next |
| No answer, or unknown | in progress until the order is read | it is being processed |
| No row of this table turns into success without the first column saying so. | ||
The customer-facing consequence is a sentence that is easy to write and expensive to skip. Until the action comes back confirmed, the reply says it is being processed. Authorizing is not executing, and executing is not confirming. The AI never tells the customer, or the Human Node working the case, that an action is done before the external system has said so.
The same rule governs availability. When a dependency is down, the design stops touching money and credentials and sends the case to the brand's helpdesk with its context. It does not improvise a result. The honest state and the safe state turn out to be the same state, because both come from refusing to lie about where the action is.
One record, whoever closed the ticket
Confirmation only holds if it applies to every resolver. A system that demands an external echo for the AI's actions and accepts a person's word for their own has built two standards, and the weaker one is where unverifiable success accumulates.
In the design the ticket record is identical whoever resolved it: the AI alone, the AI with a Human Node, or the brand's own team. One field — the resolution attribution — distinguishes them, and it is the only field that does. The AI does not get a thinner record because it is cheaper to produce, and a Human Node does not get a looser one because a person is presumed to be careful.
The same holds for the path to the action. A Human Node working a case in the Console proposes an action, and that proposal runs through the same deterministic Policy Gate and the same confirmation as the AI's — four checks before execution, the platform's echo after it. The Human Node never holds the brand's credentials. What reaches Shopify is an authorized action executed on the brand's behalf, and what reaches the record is the platform's answer to it.
The record keeps two fields apart for the same reason: the path, which lists everyone who worked on the ticket in order, and the resolver, which is set only when the ticket closes. A resolution is attributed to whoever closed it. It is never attributed before it is closed.
What a resolution rate actually counts
The confusion between the three moments does not stay inside the database. It surfaces as a headline number.
A resolution rate can be defined on the conversation: the share of conversations that ended without a handoff to a person. Under that definition, a customer who was told a refund was coming and stopped writing counts as a resolution. So does a customer who gave up. So does a customer who went to the brand's email address instead. The metric measures the end of a conversation. It says nothing about whether anything happened in the store.
A resolution rate can also be defined on the action: the share of cases in which the requested outcome exists on the external platform — the refund in Shopify, the address change on the order, the reply in the helpdesk. That number is harder to produce, because it requires the confirmation to be captured and joined to the ticket. It is also the only one of the two that a finance team can reconcile.
The two definitions can diverge on the same traffic, and the gap runs in the direction that flatters the first: every silence counts toward it, confirmed or not. Any figure presented as an AI resolution rate deserves one question before it is compared to anything: resolved according to whom. If the answer is the system that also wrote the reply, the number is a measure of how often the system agrees with itself.
This describes the design. Razviel publishes no resolution rate, and the definitions above are the reason any future one will be stated with its numerator.
The takeaway
A resolution has three moments, and only the external confirmation is evidence that anything happened. Systems that mark success at the proposal or at the internal write are not optimistic; they are publishing a claim the store cannot back. The fix is not a better model. It is a state machine that refuses to round, a key that makes retries safe, and one record that holds every resolver to the same proof.
What to check, in your own operation or in any vendor's:
- Trace one refund end to end. Pick a ticket marked resolved with a refund, and find the refund in Shopify by the identifier the ticket stored. If the ticket stores no external identifier, it stores no proof.
- Ask what happens on a timeout. The right answer names a retry with the same idempotency key, and a read of the order once the key can no longer protect. "We retry" alone is how double refunds happen.
- Look for the middle state. A system with no pending or failed status for an action in flight is rounding, and it rounds up.
- Compare the record across resolvers. Open a ticket the AI closed and one a person closed. If the AI's record is thinner, the weaker standard is where errors go unseen.
- Read the numerator of any resolution rate. A rate built on closed conversations measures silence. A rate built on confirmed actions measures outcomes.
Done is not a word the system gets to say about itself. It is a word the store says back.
Sources
-
"Making retries safe with idempotent APIs"
https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/
SupportsThe sentence reporting that, after a failed call, it is not clear whether the work is running or not, and that simply retrying can produce the same work twice.
-
"Idempotent requests"
https://docs.stripe.com/api/idempotent_requests
SupportsThe sentences reporting that a request carrying an idempotency key can be repeated after a connection error without creating a second object or performing the update twice, that Stripe saves the first result for a key and returns it to retries, and that keys can be removed once they are at least 24 hours old, after which a reused key is a new request.
-
"Idempotent requests"
https://shopify.dev/docs/api/usage/idempotent-requests
SupportsThe sentence reporting that Shopify applies the idempotency contract to its own money mutations: repeated requests with the same key are executed only once.
-
"refundCreate" mutation
https://shopify.dev/docs/api/admin-graphql/latest/mutations/refundCreate
SupportsThe sentence reporting that the refund mutation accepts an idempotency key through an @idempotent directive and that, from the 2026-04 version, the key is required.
-
"OrderTransactionStatus" enum
https://shopify.dev/docs/api/admin-graphql/latest/enums/OrderTransactionStatus
SupportsThe sentence reporting that Shopify's order transaction status has six values: success, pending, awaiting response, failure, error and unknown.
The record every resolver writes, and the one field that tells them apart: Who resolved a ticket