different-ai/openwork · error

member_not_found (Error; invalid desktop policy member IDs)

Error message

member_not_found (Error; invalid desktop policy member IDs)

What it means

resolveMemberIds validates that every member ID supplied for a desktop policy exists in the given organization and has not been removed. It queries MemberTable filtered by organizationId, the supplied IDs, and removedAt IS NULL; if the matched row count differs from the input count, it throws Error("member_not_found").

Source

Thrown at ee/apps/den-api/src/routes/org/desktop-policies.ts:87

function resolveRoles(values: DesktopPolicyAssignmentRole[]) {
  return [...new Set(values)]
}

async function resolveMemberIds(input: {
  organizationId: typeof DesktopPolicyTable.$inferSelect.organizationId
  values: string[]
}) {
  const memberIds = [...new Set(input.values)].map(parseMemberId)
  if (memberIds.length === 0) return [] as MemberId[]

  const rows = await db
    .select({ id: MemberTable.id })
    .from(MemberTable)
    .where(and(eq(MemberTable.organizationId, input.organizationId), inArray(MemberTable.id, memberIds), isNull(MemberTable.removedAt)))

  if (rows.length !== memberIds.length) {
    throw new Error("member_not_found")
  }

  return memberIds
}

async function resolveTeamIds(input: {
  organizationId: typeof DesktopPolicyTable.$inferSelect.organizationId
  values: string[]
}) {
  const teamIds = [...new Set(input.values)].map(parseTeamId)
  if (teamIds.length === 0) return [] as TeamId[]

  const rows = await db
    .select({ id: TeamTable.id })
    .from(TeamTable)
    .where(and(eq(TeamTable.organizationId, input.organizationId), inArray(TeamTable.id, teamIds)))

  if (rows.length !== teamIds.length) {

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Before saving, verify each member ID via GET org members and drop IDs that no longer resolve.
  2. Refresh the member picker data so removed members are excluded from the submission.
  3. If a member was re-added, use their NEW member ID — the old MemberTable row is tombstoned with removedAt.
  4. Confirm the IDs belong to the same organizationId as the policy being written.

Example fix

// before
memberIds: ["mem_1", "mem_removed"] // throws member_not_found
// after
const valid = await listOrgMembers(orgId)
memberIds: ["mem_1", ...].filter(id => valid.some(m => m.id === id && !m.removedAt))
Defensive patterns

Strategy: validation

Validate before calling

const ids = payload.memberIds
const rows = await db.select({ id: MemberTable.id }).from(MemberTable)
  .where(and(eq(MemberTable.organizationId, orgId), inArray(MemberTable.id, ids), isNull(MemberTable.removedAt)))
if (rows.length !== ids.length) {
  throw new Error(`Unknown/removed member IDs: ${ids.filter(i => !rows.some(r => r.id === i)).join(", ")}`)
}

Type guard

function allMembersActive(ids: string[], members: Array<{ id: string; removedAt: Date | null }>): boolean {
  return ids.every(id => members.some(m => m.id === id && m.removedAt === null))
}

Try / catch

try {
  await saveDesktopPolicy(orgId, { memberIds, ...rest })
} catch (e) {
  if (e instanceof Error && e.message === "member_not_found") {
    const fresh = await listOrgMembers(orgId)
    return saveDesktopPolicy(orgId, { memberIds: memberIds.filter(id => fresh.some(m => m.id === id)), ...rest })
  }
  throw e
}

Prevention

When it happens

Trigger: Creating/updating an org desktop policy with memberIds containing an ID from another organization, a deleted/nonexistent member, or a member whose removedAt is set (removed from the org).

Common situations: Client UI caching member IDs after a member was removed; copying policy JSON between orgs; a member deactivated between fetching the form and submitting; typos or stale test fixtures in API calls.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/6ab130bf0a2bf79b. Report an issue: GitHub.