dapr/dapr · error
failed to purge child workflow %q: %w
Error message
failed to purge child workflow %q: %w
What it means
Recursive purge walks every ChildWorkflowInstanceCreated event in history and calls RecursivePurgeWorkflowStateMethod on each child's actor (same-app or cross-app via the per-app workflow actor type). Only api.ErrInstanceNotFound is tolerated (matched by string suffix because actor invocation flattens it to a wire string) so already-purged children stay idempotent; every other child failure aborts the whole subtree purge with this wrap.
Source
Thrown at pkg/actors/targets/workflow/orchestrator/recursivepurge.go:87
deleted := int32(0)
for _, child := range collectChildren(state.History) {
actorType := o.actorType
if child.targetAppID != "" && child.targetAppID != o.appID {
actorType = o.actorTypeBuilder.Workflow(child.targetAppID)
}
var count int32
count, err = o.invokeRecursivePurge(ctx, actorType, child.instanceID, force)
deleted += count
if err != nil {
// Actor invocation surfaces api.ErrInstanceNotFound as a wire string;
// match by suffix to keep recursive purge idempotent against children
// that were already purged out-of-band.
if strings.HasSuffix(err.Error(), api.ErrInstanceNotFound.Error()) {
continue
}
return nil, fmt.Errorf("failed to purge child workflow %q: %w", child.instanceID, err)
}
}
if err = o.cleanupWorkflowStateInternal(ctx, state, true); err != nil {
return nil, err
}
deleted++
resp, err := proto.Marshal(&protos.PurgeInstancesResponse{DeletedInstanceCount: deleted})
if err != nil {
return nil, fmt.Errorf("failed to encode recursive purge response: %w", err)
}
return resp, nil
}
// invokeRecursivePurge invokes RecursivePurgeWorkflowStateMethod on the actor
// at (actorType, instanceID) and decodes the count from its response.
func (o *orchestrator) invokeRecursivePurge(ctx context.Context, actorType, instanceID string, force bool) (int32, error) {View on GitHub (pinned to 74ad417027)
Solutions
- Wait for or terminate child workflows first, then re-run the recursive purge
- If business semantics allow deleting running instances, invoke purge with the force option (MetadataPurgeForce=true) which skips the completed/stalled checks
- Verify the child app's health and actor placement (cross-app hop) and retry after the transient fault clears
Example fix
// before
_, err := client.PurgeInstances(ctx, instanceID, api.WithRecursivePurge())
// child still RUNNING -> ErrNotCompleted wrapped here
// after
_, err := client.PurgeInstances(ctx, instanceID,
api.WithRecursivePurge(), api.WithPurgeForce()) Defensive patterns
Strategy: retry
Validate before calling
// Before a non-force recursive purge, confirm every descendant is terminal.
for _, child := range collectChildInstances(parent) {
st, err := client.GetWorkflowMetadata(ctx, child)
if err != nil { return err }
if !st.IsComplete() {
return fmt.Errorf("child %s not completed; terminate it or use force purge", child)
}
} Try / catch
deleted, err := client.PurgeInstances(ctx, id, api.WithRecursivePurge())
if err != nil {
if strings.Contains(err.Error(), api.ErrNotCompleted.Error()) ||
strings.Contains(err.Error(), api.ErrStalled.Error()) {
// children still active: terminate them first, or pass force
}
if isTransientActorCallErr(err) { // network/placement blips
return retryWithBackoff()
}
return err
} Prevention
- Terminate or await child workflows before recursive purge of the parent
- Use force purge only when deleting running sub-trees is acceptable
- Keep child apps healthy (placement/network) before multi-app purges
When it happens
Trigger: PurgeInstances(recursive) while a child workflow is still RUNNING (api.ErrNotCompleted) or stalled (api.ErrStalled) in non-force mode; cross-app actor invocation failing on network/partition or placement errors; a child whose state was tombstoned as tampered.
Common situations: Operators purging a completed parent without realizing a sub-orchestration is still executing; cross-app workflow deployments where a child app is down; retrying a purge after a partial failure.
Related errors
- failed to decode recursive purge response from %s: %w
- ERR_PURGE_WORKFLOW
- closed
- cross-app purge does not support force without recursive
- failed to unmarshal activity HistoryEvent: %s
AI-assisted analysis of dapr/dapr@74ad417027 (2026-08-16).
Data as JSON: /api/errors/e0ba3f29fc793e86.
Report an issue: GitHub.