{"record":{"id":"83d02051f53ca4a4","repo":"twentyhq/twenty","slug":"on-opportunity-partner-assigned-failed-person-83d020","errorCode":null,"errorMessage":"on-opportunity-partner-assigned: ${failed} person stamp(s) failed — retrying","messagePattern":"on-opportunity-partner-assigned: (.+?) person stamp\\(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":141,"sourceCode":"  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\n  // here would steal the company (and its contacts) from the other partner and expose their\n  // data. Stamp only the opportunity in that case and leave the company/people alone.\n  const companyResult = await getCompanyPartnerUser(client, companyId);\n  const companyOwner = companyResult.company?.partnerUserId;\n  if (companyOwner && companyOwner !== partnerUserId) {\n    return { cascaded: true, partnerUserId, companyShared: true };\n  }\n\n  await updateCompanyPartnerUser(client, companyId, partnerUserId);\n\n  const peopleIds = await collectPeopleIds(client, { companyId: { eq: companyId } });\n  const failed = await setPeoplePartnerUser(client, peopleIds, partnerUserId);\n  if (failed > 0) {\n    throw new Error(\n      `on-opportunity-partner-assigned: ${failed} person stamp(s) failed — retrying`,\n    );\n  }\n\n  return { cascaded: true, partnerUserId };\n}\n","sourceCodeStart":123,"sourceCodeEnd":148,"githubUrl":"https://github.com/twentyhq/twenty/blob/1f5dd2bbd2a8da3419c8cfd52dd545c0024df1a6/packages/twenty-apps/internal/twenty-partners/src/modules/opportunity/matching/services/propagate-partner-user.service.ts#L123-L148","documentation":"propagatePartnerUser's assign/reassign path runs all Person partnerUser-stamps via Promise.allSettled and throws if any rejected. Same idempotent-retry design as the clear path: re-running re-collects people for the company and re-stamps only those still missing the partnerUser, so a thrown error safely drives a retry rather than leaving a partial stamp.","triggerScenarios":"One or more updatePersonPartnerUser(client, id, partnerUserId) promises reject during the assign cascade. Causes: transient network/5xx; Person deleted mid-cascade; RLS field-lock; concurrent reassignment racing on the same company's people.","commonSituations":"Assigning a partner to an opportunity on a company with many contacts; a contact concurrently edited/deleted; app-server restart mid-batch; field-lock misconfiguration rejecting the stamp.","solutions":["Retry — the function is idempotent (assign path re-stamps all people for the company; already-stamped rows are a no-op).","If persistent, log which personIds rejected to isolate the record.","Check app-server logs for 5xx or RLS denials on updatePerson.","Verify the partnerUser field on Person is not field-locked for the app identity running the cascade."],"exampleFix":"// before\nconst failed = await setPeoplePartnerUser(client, peopleIds, partnerUserId);\nif (failed > 0) {\n  throw new Error(`on-opportunity-partner-assigned: ${failed} person stamp(s) failed — retrying`);\n}\n\n// after — keep the throw for retry; log failures for diagnosis\nconst failedIds = await setPeoplePartnerUserDetailed(client, peopleIds, partnerUserId);\nif (failedIds.length > 0) {\n  console.error(`person stamp failures for ${opportunityId}/${partnerId}:`, failedIds);\n  throw new Error(`on-opportunity-partner-assigned: ${failedIds.length} person stamp(s) failed — retrying`);\n}","handlingStrategy":"retry","validationCode":"// No client-side pre-validation prevents transient mid-cascade rejections.\n// Ensure the workflow engine retries thrown logic-function errors.","typeGuard":null,"tryCatchPattern":"// propagatePartnerUser is idempotent on the assign path too (re-stamps all\n// people for the company; already-stamped rows are a no-op). Retry on throw:\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 idempotent re-runs — already-stamped people are not duplicated.","Ensure the workflow engine retries thrown errors with backoff.","Confirm the partnerUser field on Person is not field-locked for the app identity.","Monitor app-server logs for RLS denials/5xx on updatePerson during cascades."],"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"}