{"record":{"id":"8b4305d0d57f8f10","repo":"twentyhq/twenty","slug":"on-opportunity-partner-assigned-failed-person","errorCode":null,"errorMessage":"on-opportunity-partner-assigned: ${failed} person clear(s) failed — retrying","messagePattern":"on-opportunity-partner-assigned: (.+?) person clear\\(s\\) failed — retrying","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/twenty-apps/internal/twenty-partners/src/modules/opportunity/matching/services/propagate-partner-user.service.ts","lineNumber":109,"sourceCode":"      removedMemberId,\n    });\n    if ((stillInUse.opportunities?.edges?.length ?? 0) > 0) {\n      return { cascaded: true, cleared: true, companyKept: true };\n    }\n\n    // Clear the company (only if it belongs to this member) and every person stamped for them.\n    const companyResult = await getCompanyPartnerUser(client, companyId);\n    if (companyResult.company?.partnerUserId === removedMemberId) {\n      await updateCompanyPartnerUser(client, companyId, null);\n    }\n\n    const peopleIds = await collectPeopleIds(client, {\n      companyId: { eq: companyId },\n      partnerUserId: { eq: removedMemberId },\n    });\n    const failed = await setPeoplePartnerUser(client, peopleIds, null);\n    if (failed > 0) {\n      throw new Error(\n        `on-opportunity-partner-assigned: ${failed} person clear(s) failed — retrying`,\n      );\n    }\n    return { cascaded: true, cleared: true, companyCleared: true };\n  }\n\n  // ── Assign / reassign ────────────────────────────────────────────────────────\n  const partnerResult = await getPartnerOwner(client, partnerId);\n  const partnerUserId = partnerResult.partner?.partnerUserId;\n  if (!partnerUserId) return { cascaded: false, reason: 'partner_has_no_user' };\n\n  await updateOpportunityPartnerUser(client, opportunityId, partnerUserId);\n\n  const companyId = after?.companyId;\n  if (!companyId) return { cascaded: true, partnerUserId };\n\n  // Don't clobber a company already owned by a DIFFERENT partner member. The single\n  // partnerUser column on Company/Person models one owner per company, so reassigning it","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/twentyhq/twenty/blob/1f5dd2bbd2a8da3419c8cfd52dd545c0024df1a6/packages/twenty-apps/internal/twenty-partners/src/modules/opportunity/matching/services/propagate-partner-user.service.ts#L91-L127","documentation":"propagatePartnerUser's unassign path runs all Person partnerUser-clears via Promise.allSettled and throws if any rejected. The throw is intentional: the partner stamp/clear is designed idempotently so a re-invocation redoes exactly the remaining work. It prevents a partially cleared cascade from being reported as success, which would leave stale partnerUser stamps that leak RLS visibility.","triggerScenarios":"One or more updatePersonPartnerUser(client, id, null) promises reject during the unassign cascade. Causes: transient network/5xx on a single update; a Person record deleted between collectPeopleIds and the update; an RLS field-lock kicking in mid-cascade; concurrent reassignment mutating the same rows.","commonSituations":"Brief flapping under load (assign/unassign churn); a Person removed by another workflow while the cascade runs; the app-server restarted mid-batch; large company with many people where one update hits a timeout.","solutions":["Retry the operation — the function is idempotent (filters by partnerUserId === removedMemberId, so already-cleared rows drop out).","If it persists, inspect which personIds rejected (add per-promise logging) to find the offending record.","Check app-server logs for 5xx or RLS denials on updatePerson within the retry window.","Confirm the workflow engine calling this logic-function has retry-with-backoff configured for thrown errors."],"exampleFix":"// before\nconst failed = await setPeoplePartnerUser(client, peopleIds, null);\nif (failed > 0) {\n  throw new Error(`on-opportunity-partner-assigned: ${failed} person clear(s) failed — retrying`);\n}\n\n// after — keep the throw (it drives idempotent retry), but log WHICH failed for diagnosis\nconst failedIds = await setPeoplePartnerUserDetailed(client, peopleIds, null);\nif (failedIds.length > 0) {\n  console.error(`person clear failures for ${opportunityId}:`, failedIds);\n  throw new Error(`on-opportunity-partner-assigned: ${failedIds.length} person clear(s) failed — retrying`);\n}","handlingStrategy":"retry","validationCode":"// The cascade is inherently non-atomic; the best pre-check is to ensure the\n// workflow engine has retry-with-backoff enabled for thrown logic-function errors.\n// No client-side pre-validation can prevent a transient mid-cascade rejection.","typeGuard":null,"tryCatchPattern":"// propagatePartnerUser is a logic-function entrypoint; let the workflow engine\n// retry on throw. If calling directly, wrap with bounded retry:\nfor (let attempt = 0; attempt < 3; attempt++) {\n  try {\n    return await propagatePartnerUser(client, params);\n  } catch (err) {\n    if (attempt === 2) throw err;\n    await new Promise((r) => setTimeout(r, 2 ** attempt * 500));\n  }\n}","preventionTips":["Rely on the function's idempotent design — re-running clears only remaining rows.","Ensure the workflow engine invoking this logic-function retries thrown errors with backoff.","Monitor app-server logs for RLS denials or 5xx on updatePerson during cascades.","Avoid concurrent assign/unassign churn on the same opportunity where possible."],"tags":["cascade","retry","rls","concurrency","partners"],"backgroundTag":null,"analyzedSha":"1f5dd2bbd2a8da3419c8cfd52dd545c0024df1a6","analyzedAt":"2026-08-12T15:37:27.593Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}