temporalio/temporal · critical
unknown workflow update abort reason %s or update state %s
Error message
unknown workflow update abort reason %s or update state %s
What it means
AbortReason.FailureError maps an (AbortReason, state) pair to the Failure/error placed on update futures during abort. The mapping lives in reasonStateMatrix; when the combination has no entry, the code assumes an internal invariant was violated and panics with this message. This is a programmer/protocol error inside the update subsystem, not user input validation.
Source
Thrown at service/history/workflow/update/abort_reason.go:111
// Updates which *have* been seen by the Workflow are aborted with non-retryable error.
// Failed WFT will be retried but Update must not. Otherwise, internal retries will exhaust and Unavailable error will be returned to the client.
reasonState{r: AbortReasonWorkflowTaskFailed, st: stateSent}: {f: nil, err: workflowTaskFailErr},
// Updates which passed Accepted state are not retried when the registry is cleared, so there is no need to abort them.
reasonState{r: AbortReasonWorkflowTaskFailed, st: stateProvisionallyAccepted}: {f: nil, err: nil},
reasonState{r: AbortReasonWorkflowTaskFailed, st: stateAccepted}: {f: nil, err: nil},
reasonState{r: AbortReasonWorkflowTaskFailed, st: stateProvisionallyCompleted}: {f: nil, err: nil},
reasonState{r: AbortReasonWorkflowTaskFailed, st: stateProvisionallyCompletedAfterAccepted}: {f: nil, err: nil},
reasonState{r: AbortReasonWorkflowTaskFailed, st: stateCompleted}: {f: nil, err: nil},
reasonState{r: AbortReasonWorkflowTaskFailed, st: stateProvisionallyAborted}: {f: nil, err: nil},
reasonState{r: AbortReasonWorkflowTaskFailed, st: stateAborted}: {f: nil, err: nil},
}
// FailureError returns failure or error which will be set on Update futures while aborting Update.
// Only one of the return values will be non-nil.
func (r AbortReason) FailureError(st state) (*failurepb.Failure, error) {
fe, ok := reasonStateMatrix[reasonState{r: r, st: st}]
if !ok {
panic(fmt.Sprintf("unknown workflow update abort reason %s or update state %s", r, st))
}
return fe.f, fe.err
}
func (r AbortReason) String() string {
switch r {
case AbortReasonRegistryCleared:
return "RegistryCleared"
case AbortReasonWorkflowCompleted:
return "WorkflowCompleted"
case AbortReasonWorkflowContinuing:
return "WorkflowContinuing"
case AbortReasonWorkflowTaskFailed:
return "WorkflowTaskFailed"
case lastAbortReason:
return fmt.Sprintf("invalid reason %d", r)
}
return fmt.Sprintf("unrecognized reason %d", r)View on GitHub (pinned to bde624efd1)
Solutions
- Add the missing (AbortReason, state) entry to reasonStateMatrix in service/history/workflow/update/abort_reason.go for the pair printed in the panic message
- Check the abort call path to ensure the update state passed to FailureError is legal for that reason (guard with AbortReason.AssertRunnable or similar state checks before aborting)
- If the values are computed (e.g. deserialized from persistence), validate them against known enums before calling FailureError
Example fix
// before (reason added without matrix entry)
const (
AbortReasonFoo AbortReason = iota + abortReasonSentinel
)
// after
const (
AbortReasonFoo AbortReason = iota + abortReasonSentinel
)
func init() {
reasonStateMatrix[reasonState{r: AbortReasonFoo, st: StateUnspecified}] = reasonStateInfo{}
reasonStateMatrix[reasonState{r: AbortReasonFoo, st: StateAdmitted}] = reasonStateInfo{}
} Defensive patterns
Strategy: validation
Validate before calling
// Validate (reason, state) is legal before aborting
if _, ok := update.ReasonStateMatrixLookup(reason, st); !ok {
return fmt.Errorf("illegal abort reason %v for update state %v", reason, st)
} Type guard
func knownAbortReasonState(r update.AbortReason, st update.State) bool {
_, ok := update.ReasonStateMatrixLookup(r, st)
return ok
} Try / catch
// Panics are not recoverable by convention here; prefer validating above.
// If wrapping a test harness:
func safeFailureError(r update.AbortReason, st update.State) (fe *failurepb.Failure, err error) {
defer func() {
if rec := recover(); rec != nil {
err = fmt.Errorf("abort mapping panic: %v", rec)
}
}()
return r.FailureError(st)
} Prevention
- Every time you add an AbortReason or update State, add entries for all legal pairs in reasonStateMatrix and run the matrix completeness test
- Add a unit test that iterates the full AbortReason × State cross-product and asserts which pairs must be present
- Validate deserialized reason/state enums from persistence before use
When it happens
Trigger: A new AbortReason value or update state was added without a corresponding entry in reasonStateMatrix, or abort() was called with a state that is not legal for that reason (e.g. aborting an already-completed update with a reason defined only for running states).
Common situations: Developers extending the workflow update state machine (adding an abort reason or state) in the Temporal server; backporting partial changes between versions; tests constructing AbortReason/state pairs directly and passing an illegal combination.
Related errors
- expect at least one reservation
- Found key with non-zero pending task count but has no corres
- ActivityTaskScheduledEventAttributes.ActivityID is not set
- Current cluster name is empty
- Version increment <= 0 or > 2147483647
AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01).
Data as JSON: /api/errors/c4cc393e153bdc2d.
Report an issue: GitHub.