twentyhq/twenty · error · Error

on-opportunity-partner-assigned: ${failed} person clear(s) f

Error message

on-opportunity-partner-assigned: ${failed} person clear(s) failed — retrying

What it means

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.

Source

Thrown at packages/twenty-apps/internal/twenty-partners/src/modules/opportunity/matching/services/propagate-partner-user.service.ts:109

      removedMemberId,
    });
    if ((stillInUse.opportunities?.edges?.length ?? 0) > 0) {
      return { cascaded: true, cleared: true, companyKept: true };
    }

    // Clear the company (only if it belongs to this member) and every person stamped for them.
    const companyResult = await getCompanyPartnerUser(client, companyId);
    if (companyResult.company?.partnerUserId === removedMemberId) {
      await updateCompanyPartnerUser(client, companyId, null);
    }

    const peopleIds = await collectPeopleIds(client, {
      companyId: { eq: companyId },
      partnerUserId: { eq: removedMemberId },
    });
    const failed = await setPeoplePartnerUser(client, peopleIds, null);
    if (failed > 0) {
      throw new Error(
        `on-opportunity-partner-assigned: ${failed} person clear(s) failed — retrying`,
      );
    }
    return { cascaded: true, cleared: true, companyCleared: true };
  }

  // ── Assign / reassign ────────────────────────────────────────────────────────
  const partnerResult = await getPartnerOwner(client, partnerId);
  const partnerUserId = partnerResult.partner?.partnerUserId;
  if (!partnerUserId) return { cascaded: false, reason: 'partner_has_no_user' };

  await updateOpportunityPartnerUser(client, opportunityId, partnerUserId);

  const companyId = after?.companyId;
  if (!companyId) return { cascaded: true, partnerUserId };

  // Don't clobber a company already owned by a DIFFERENT partner member. The single
  // partnerUser column on Company/Person models one owner per company, so reassigning it

View on GitHub (pinned to 1f5dd2bbd2)

Solutions

  1. Retry the operation — the function is idempotent (filters by partnerUserId === removedMemberId, so already-cleared rows drop out).
  2. If it persists, inspect which personIds rejected (add per-promise logging) to find the offending record.
  3. Check app-server logs for 5xx or RLS denials on updatePerson within the retry window.
  4. Confirm the workflow engine calling this logic-function has retry-with-backoff configured for thrown errors.

Example fix

// before
const failed = await setPeoplePartnerUser(client, peopleIds, null);
if (failed > 0) {
  throw new Error(`on-opportunity-partner-assigned: ${failed} person clear(s) failed — retrying`);
}

// after — keep the throw (it drives idempotent retry), but log WHICH failed for diagnosis
const failedIds = await setPeoplePartnerUserDetailed(client, peopleIds, null);
if (failedIds.length > 0) {
  console.error(`person clear failures for ${opportunityId}:`, failedIds);
  throw new Error(`on-opportunity-partner-assigned: ${failedIds.length} person clear(s) failed — retrying`);
}
Defensive patterns

Strategy: retry

Validate before calling

// The cascade is inherently non-atomic; the best pre-check is to ensure the
// workflow engine has retry-with-backoff enabled for thrown logic-function errors.
// No client-side pre-validation can prevent a transient mid-cascade rejection.

Try / catch

// propagatePartnerUser is a logic-function entrypoint; let the workflow engine
// retry on throw. If calling directly, wrap with bounded retry:
for (let attempt = 0; attempt < 3; attempt++) {
  try {
    return await propagatePartnerUser(client, params);
  } catch (err) {
    if (attempt === 2) throw err;
    await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of twentyhq/twenty@1f5dd2bbd2 (2026-08-12). Data as JSON: /api/errors/8b4305d0d57f8f10. Report an issue: GitHub.