There is a loop in the product that watches for answers the assistant gets wrong often enough to be worth fixing. When it finds one, a merchant can ask it to draft an FAQ entry: a question, a proposed answer, and a row that sits in review until somebody publishes it.
Drafting the same entry twice should not create two rows, so the opportunity remembers which row it made and reuses it. That reuse is where a live answer went off the air.
A pointer that outlived the state it assumed
The sequence is ordinary. The loop drafts an entry. The merchant reads it, approves it, and it is published — it is now the answer the store gives. The opportunity still points at that row, because the row is the same row; approving it did not move it.
Later the same question comes back, the opportunity reopens, and the drafting step runs again. It follows its pointer, finds the row, updates the answer and stamps the status back to draft.
Only published rows reach a shopper, and only from sources the merchant still has switched on. So the store's live answer to that question quietly stopped being live, at the moment somebody asked for a draft of it.
Nobody chose that behaviour. It is the gap between "the row this opportunity made" and "the row this opportunity made, which is still a draft". For as long as those two were the same line of code, asking for a draft was also an unpublish.
The row is the same row throughout. Only the status moves, and one write moved it the wrong way.
The fix is one clause in the lookup:
status: "draft"
If the standing row has since been published, it is not found, and a fresh draft is inserted alongside it. The live answer is untouched. A live answer stops being live when a person says so, never as a side effect of something else.
Two more decisions in the same few lines
The same function carries two other rules for related reasons. Both look like things you could simplify away, and both would take something with them.
It never adopts a draft by matching the question text. That would be the obvious way to avoid duplicates without keeping a pointer at all: look for a draft that asks the same thing. But two opportunities can carry the same words about different products — "is this in stock?" is one question string and any number of separate problems. One row shared between them means approving it resolves one opportunity and leaves the other sitting in review for ever, pointing at a row that is no longer a draft. Matching on identity rather than on text is what keeps them separate.
A merchant's own typed answer outranks anything we can recompute. If the standing draft already holds an answer, it stays, even though the drafting step has a note and a scan it could use instead. Both of those are earlier and less deliberate than a person typing into the editor. Recomputing would overwrite their work silently, inside a step they asked for expecting it to help.
There is a smaller point folded into that one. The label saying where the answer came from is only changed to "yours" when the standing answer actually differs from what a source would have produced. If the strings are identical, the source is still where the answer came from, and relabelling it would erase provenance a merchant checks before publishing something their shoppers will read.
A rule we now apply everywhere
Updating a row's text is an update. Updating a row's text and its status is a publish or an unpublish, whatever the function happens to be called, and whether it is safe depends entirely on what the status was beforehand.
That was the thing the code never checked. The pointer was correct. The row existed. The write succeeded. Nothing in the system had the slightest reason to complain. We have written up another bug with exactly that shape, and the family resemblance is the useful part: a query that is individually valid and wrong about which row it is entitled to.
So any write that sets a field controlling visibility now has to say which prior states it will move from, and refuse the rest. In this case that statement is two words in a filter, status: "draft", sitting in the only place that can enforce it.