twentyhq/twenty · error · Error

upsertRowLevelPermissionPredicates returned fewer than 2 pre

Error message

upsertRowLevelPermissionPredicates returned fewer than 2 predicates for opportunity OR group

What it means

Thrown by the partner RLS configuration script after it calls the metadata `upsertRowLevelPermissionPredicates` mutation to create the opportunity OR-group (predicate 0: partnerUser IS me; predicate 1: isListed = true). The mutation is expected to return both persisted predicates; if it returns fewer than two, the script aborts because the row-level security rule would be incomplete (partners would not reliably see listed briefs). It is a hard stop in a one-shot provisioning script, not a runtime user-facing path.

Source

Thrown at packages/twenty-apps/internal/twenty-partners/src/scripts/configure-partner-rls.ts:536

            operand: 'IS',
            workspaceMemberFieldMetadataId: workspaceMemberIdFieldId,
            rowLevelPermissionPredicateGroupId: OPPORTUNITY_RLS_OR_GROUP_ID,
            positionInRowLevelPermissionPredicateGroup: 0,
          },
          {
            fieldMetadataId: opportunityIsListedFieldId,
            operand: 'IS',
            value: true,
            rowLevelPermissionPredicateGroupId: OPPORTUNITY_RLS_OR_GROUP_ID,
            positionInRowLevelPermissionPredicateGroup: 1,
          },
        ],
      } satisfies UpsertPredicatesInput,
      'opportunity',
    );

    if (oppPredicates.length < 2) {
      throw new Error(
        'upsertRowLevelPermissionPredicates returned fewer than 2 predicates for opportunity OR group',
      );
    }

    for (const predicate of oppPredicates) {
      results.push(predicate);
    }

    console.log(
      `[rls:configure] ✓ opportunity: OR group id=${OPPORTUNITY_RLS_OR_GROUP_ID} ` +
        `(${oppPredicates.length} predicates: partnerUser IS me OR isListed = true)`,
    );
  }

  // workspaceMember predicate: "id IS the current member", scoping the role's read to the
  // partner's own record. Other members (e.g. an opportunity's internal owner) resolve to null.
  {
    const wmData = await metadataFetch<{

View on GitHub (pinned to 1f5dd2bbd2)

Solutions

  1. Inspect the actual mutation response: log `oppPredicates` (and the full `upsertRowLevelPermissionPredicates` payload) right before the check to see exactly which predicate is missing and any partial error.
  2. Verify `opportunityPartnerUserFieldId` and `opportunityIsListedFieldId` resolve to live fields on the opportunity object in this workspace (query `fieldMetadata` by the universal identifiers the script uses).
  3. Confirm `partnerRole.id` and `opportunityObjectId` are correct for the target workspace and that RLS/row-level permission groups are enabled on the opportunity object.
  4. If a previous partial run left an OR group with one predicate, delete the existing `OPPORTUNITY_RLS_OR_GROUP_ID` group (or run the script's cleanup/teardown path) so the upsert is not deduping against stale state.
  5. Re-run the script against a clean workspace after `database:reset` so all metadata IDs match the seed.

Example fix

// before
if (oppPredicates.length < 2) {
  throw new Error(
    'upsertRowLevelPermissionPredicates returned fewer than 2 predicates for opportunity OR group',
  );
}

// after — surface which predicate is missing before aborting
const expectedFieldIds = new Set([
  opportunityPartnerUserFieldId,
  opportunityIsListedFieldId,
]);
const returnedFieldIds = new Set(oppPredicates.map((p) => p.fieldMetadataId));
const missing = [...expectedFieldIds].filter((id) => !returnedFieldIds.has(id));
if (oppPredicates.length < 2) {
  throw new Error(
    `upsertRowLevelPermissionPredicates returned ${oppPredicates.length} predicates for opportunity OR group; missing fieldMetadataIds: ${missing.join(', ')}`,
  );
}
Defensive patterns

Strategy: validation

Validate before calling

// Before calling upsertPredicates for the opportunity OR group, confirm both
// fields exist on the opportunity object in this workspace.
const fieldIds = await fetchFieldMetadataIds(metadataUrl, apiKey, opportunityObjectId, [
  opportunityPartnerUserFieldId,
  opportunityIsListedFieldId,
]);
if (fieldIds.size !== 2) {
  throw new Error(
    `Cannot build opportunity OR group: missing field metadata ids on opportunity object`,
  );
}
// (then proceed to the upsertPredicates call)

Type guard

const isCompletePredicateSet = (
  predicates: unknown,
  expectedFieldIds: ReadonlyArray<string>,
): predicates is { id: string; fieldMetadataId: string }[] =>
  Array.isArray(predicates) &&
  predicates.length >= expectedFieldIds.length &&
  expectedFieldIds.every((id) =>
    (predicates as Array<{ fieldMetadataId: string }>).some(
      (p) => p.fieldMetadataId === id,
    ),
  );

Prevention

When it happens

Trigger: Running `configure-partner-rls` against a workspace where (a) `opportunityPartnerUserFieldId` or `opportunityIsListedFieldId` resolves to a field that does not exist on the opportunity object, (b) the `partnerRole.id` or `opportunityObjectId` is wrong/stale, (c) RLS is not enabled on the opportunity object so the mutation silently drops predicates, or (d) the OR group UUID `OPPORTUNITY_RLS_OR_GROUP_ID` collides with an existing group and the upsert dedupes one predicate away.

Common situations: Re-running the script against a workspace whose metadata UUIDs were regenerated (e.g. after a `database:reset` or a fresh seed), pointing the script at the wrong environment, or a partially-applied earlier run that left an orphaned OR group with one predicate. Also seen after renaming the isListed/partnerUser fields without updating the ID constants the script reads.

Related errors


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