{"record":{"id":"e0ba3f29fc793e86","repo":"dapr/dapr","slug":"failed-to-purge-child-workflow-q-w","errorCode":null,"errorMessage":"failed to purge child workflow %q: %w","messagePattern":"failed to purge child workflow %q: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/actors/targets/workflow/orchestrator/recursivepurge.go","lineNumber":87,"sourceCode":"\tdeleted := int32(0)\n\n\tfor _, child := range collectChildren(state.History) {\n\t\tactorType := o.actorType\n\t\tif child.targetAppID != \"\" && child.targetAppID != o.appID {\n\t\t\tactorType = o.actorTypeBuilder.Workflow(child.targetAppID)\n\t\t}\n\n\t\tvar count int32\n\t\tcount, err = o.invokeRecursivePurge(ctx, actorType, child.instanceID, force)\n\t\tdeleted += count\n\t\tif err != nil {\n\t\t\t// Actor invocation surfaces api.ErrInstanceNotFound as a wire string;\n\t\t\t// match by suffix to keep recursive purge idempotent against children\n\t\t\t// that were already purged out-of-band.\n\t\t\tif strings.HasSuffix(err.Error(), api.ErrInstanceNotFound.Error()) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"failed to purge child workflow %q: %w\", child.instanceID, err)\n\t\t}\n\t}\n\n\tif err = o.cleanupWorkflowStateInternal(ctx, state, true); err != nil {\n\t\treturn nil, err\n\t}\n\tdeleted++\n\n\tresp, err := proto.Marshal(&protos.PurgeInstancesResponse{DeletedInstanceCount: deleted})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to encode recursive purge response: %w\", err)\n\t}\n\treturn resp, nil\n}\n\n// invokeRecursivePurge invokes RecursivePurgeWorkflowStateMethod on the actor\n// at (actorType, instanceID) and decodes the count from its response.\nfunc (o *orchestrator) invokeRecursivePurge(ctx context.Context, actorType, instanceID string, force bool) (int32, error) {","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/dapr/dapr/blob/74ad41702745709bb15fe2114ff693b8c59bc3cc/pkg/actors/targets/workflow/orchestrator/recursivepurge.go#L69-L105","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n_, err := client.PurgeInstances(ctx, instanceID, api.WithRecursivePurge())\n// child still RUNNING -> ErrNotCompleted wrapped here\n\n// after\n_, err := client.PurgeInstances(ctx, instanceID,\n    api.WithRecursivePurge(), api.WithPurgeForce())","handlingStrategy":"retry","validationCode":"// Before a non-force recursive purge, confirm every descendant is terminal.\nfor _, child := range collectChildInstances(parent) {\n    st, err := client.GetWorkflowMetadata(ctx, child)\n    if err != nil { return err }\n    if !st.IsComplete() {\n        return fmt.Errorf(\"child %s not completed; terminate it or use force purge\", child)\n    }\n}","typeGuard":null,"tryCatchPattern":"deleted, err := client.PurgeInstances(ctx, id, api.WithRecursivePurge())\nif err != nil {\n    if strings.Contains(err.Error(), api.ErrNotCompleted.Error()) ||\n       strings.Contains(err.Error(), api.ErrStalled.Error()) {\n        // children still active: terminate them first, or pass force\n    }\n    if isTransientActorCallErr(err) { // network/placement blips\n        return retryWithBackoff()\n    }\n    return err\n}","preventionTips":["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"],"tags":["workflow","purge","lifecycle","multi-app","dapr"],"backgroundTag":null,"analyzedSha":"74ad41702745709bb15fe2114ff693b8c59bc3cc","analyzedAt":"2026-08-16T04:22:26.543Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}