The Question State Machine
How pending, answered, expired, and cancelled states keep the Question flow safe and understandable.
The Question primitive only works if its state machine is strict.
Humans will tap stale notifications. Agents will retry requests. Mobile devices will deliver duplicate actions. Expiry sweeps may run near the same time as an answer. A room-scope question might receive multiple taps in the same second.
The protocol needs to turn that messy reality into one clear outcome.
The Four States
The full state machine is:
pending -> answered
pending -> expired
pending -> cancelled
The terminal states are:
answered
expired
cancelled
There is no transition out of a terminal state. No endpoint should resurrect a question, change an answer, or convert an expired question into an answered one after the deadline.

Pending
pending means the question is open.
The system has persisted it, enqueued or delivered the Ping, and is waiting for a valid answer, cancellation, or expiry. During pending, the app can show answer buttons, the notification can show actions, and the asker can long-poll the wait endpoint.
Pending is the only state where an answer is accepted.
Answered
answered means one valid responder chose one valid option.
For direct scope, the responder must be the target user. For room scope, the responder must be an eligible room member. For v1, the first valid room answer wins.
The server must record the chosen option, responder identity, and timestamp atomically. The responder identity must come from authentication, never from the request body.
Expired
expired means the deadline passed with no valid answer.
This is a real result, not an absence of result. The asker needs to distinguish "human chose no" from "human did not answer." Those outcomes drive different workflows.
An expiry sweep should safely flip overdue pending rows to expired. It should be idempotent and safe to run repeatedly.
Cancelled
cancelled means the asker withdrew the question before it was answered or expired.
Cancellation is useful when the workflow changes. Maybe the deploy was cancelled from another system. Maybe the agent found a safer path. Maybe the question was sent to the wrong room.
After cancellation, notification actions become no-ops and waiters receive a cancelled result.
Race Handling
The hard part is not the diagram. It is concurrency.
The answer operation should be an atomic conditional update:
update questions
set state = 'answered', answer = ...
where id = ? and state = 'pending'
Only one caller can win. Everyone else receives the already-resolved state.
Expiry and cancellation use the same pattern. They only transition rows that are still pending. If an answer committed first, the sweep skips it. If cancellation committed first, the answer path returns cancelled.
Human Experience
Race handling should not punish the human.
If a person taps a stale notification, they should see that the question was already answered, expired, or cancelled. That is not an error in the human sense. It is just the state of the workflow.
The app should update the in-app cell and, where possible, the notification state so the stale action does not keep inviting taps.
Agent Experience
The agent should get the same terminal envelope from every result path.
Long-poll wait, fetch, list, and outgoing-webhook events should all represent the same final state. That consistency lets SDKs stay simple.
The Rule
Every Question has exactly one terminal story.
It was answered by this responder with this option, expired without an answer, or was cancelled by the asker. The protocol should make that story impossible to confuse.
Mahdi Salmanzade
The Ping that cuts through.


