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

  1. Wait for or terminate child workflows first, then re-run the recursive purge
  2. If business semantics allow deleting running instances, invoke purge with the force option (MetadataPurgeForce=true) which skips the completed/stalled checks
  3. 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

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


AI-assisted analysis of dapr/dapr@74ad417027 (2026-08-16). Data as JSON: /api/errors/e0ba3f29fc793e86. Report an issue: GitHub.