temporalio/temporal · error

unknown transfer task

Error message

unknown transfer task

What it means

GetTransferTaskEventID maps a concrete transfer Task implementation to the workflow event ID that spawned it. The type switch covers all known transfer task types; when a new transfer task implementation is added to the tasks package but not added to this switch, the default branch panics with an internal service error. Tags() relies on this function to attach event-ID tags to task metrics/logs.

Source

Thrown at service/history/tasks/utils.go:84

		eventID = task.ScheduledEventID
	case *CloseExecutionTask:
		eventID = common.FirstEventID
	case *DeleteExecutionTask:
		return getChasmTaskEventID()
	case *CancelExecutionTask:
		eventID = task.InitiatedEventID
	case *SignalExecutionTask:
		eventID = task.InitiatedEventID
	case *StartChildExecutionTask:
		eventID = task.InitiatedEventID
	case *ResetWorkflowTask:
		eventID = common.FirstEventID
	case *ChasmTask:
		return getChasmTaskEventID()
	case *FakeTask:
		// no-op
	default:
		panic(serviceerror.NewInternal("unknown transfer task"))
	}
	return eventID, true
}

func GetTimerTaskEventID(
	timerTask Task,
) (int64, bool) {
	eventID := int64(0)

	switch task := timerTask.(type) {
	case *UserTimerTask:
		eventID = task.EventID
	case *ActivityTimeoutTask:
		eventID = task.EventID
	case *WorkflowTaskTimeoutTask:
		eventID = task.EventID
	case *WorkflowBackoffTimerTask:
		eventID = common.FirstEventID

View on GitHub (pinned to bde624efd1)

Solutions

  1. Add a case for the unhandled task type in GetTransferTaskEventID returning the correct event ID.
  2. Align service versions so all history nodes know the same set of transfer task types.
  3. Check whether the task was corrupted in persistence (wrong type field) and repair or drop the row.
  4. Add a compile-time exhaustiveness aid (e.g. a unit test enumerating all transfer task types) to catch future additions.

Example fix

// before
case *ChasmTask:
    return getChasmTaskEventID()
default:
    panic(serviceerror.NewInternal("unknown transfer task"))

// after
case *ChasmTask:
    return getChasmTaskEventID()
case *NewTransferTask:
    eventID = task.GetEventID()
    return eventID, true
default:
    panic(serviceerror.NewInternal("unknown transfer task"))
Defensive patterns

Strategy: type-guard

Validate before calling

switch t.(type) {
case *tasks.TransferWorkflowTask, *tasks.TransferActivityTask, *tasks.TransferChasmTask:
    // safe to call
default:
    return fmt.Errorf("transfer task type %T unsupported", t)
}

Type guard

func isKnownTransferTask(t tasks.Task) bool {
    switch t.(type) {
    case *tasks.TransferWorkflowTask, *tasks.TransferActivityTask,
        *tasks.TransferWorkflowTaskTimer, *tasks.TransferActivityTimeoutTask,
        *tasks.TransferCloseTask, *tasks.TransferCancelTask, *tasks.TransferSignalTask,
        *tasks.TransferDeleteExecutionTask, *tasks.TransferChasmTask, *tasks.FakeTask:
        return true
    }
    return false
}

Prevention

When it happens

Trigger: Passing an unhandled transfer Task concrete type (anything not covered by the switch, e.g. a new *SomeNewTask struct) into GetTransferTaskEventID — typically via Tags() on a task whose type was added upstream but not handled in this fork/version.

Common situations: Version skew: a newer Temporal release introduces a new transfer task type while an older node processes it; custom forks adding task types without updating utils.go; misdeserialized tasks yielding an unexpected concrete type.

Related errors


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