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

  1. Add the missing (AbortReason, state) entry to reasonStateMatrix in service/history/workflow/update/abort_reason.go for the pair printed in the panic message
  2. 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)
  3. 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

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


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/c4cc393e153bdc2d. Report an issue: GitHub.