paperclipai/paperclip · error · Error

Project belongs to another company or does not exist: ${proj

Error message

Project belongs to another company or does not exist: ${projectId}

What it means

Thrown during profile validation when ctx.projects.get(projectId, companyId) returns nullish for a project id listed in a 'selected_projects' scope. The projects.get helper is company-scoped, so a null result means the id is unknown or belongs to a different company. This guards ingestion from binding to projects outside the company boundary.

Source

Thrown at packages/plugins/plugin-llm-wiki/src/wiki/core.ts:1007

  });
  if (!policy.allowed) throw new Error(policy.message);
  if (input.profile.enabled && input.profile.sourceScopes.length === 0) {
    throw new Error("Paperclip ingestion profile must include at least one source scope before it can be enabled.");
  }
  if (input.profile.sourceScopes.length > MAX_PAPERCLIP_INGESTION_PROFILE_SOURCE_COUNT) {
    throw new Error(`Paperclip ingestion profile sources exceed the hard cap of ${MAX_PAPERCLIP_INGESTION_PROFILE_SOURCE_COUNT}.`);
  }
  for (const scope of input.profile.sourceScopes) {
    if (scope.kind === "company_all" && input.space.slug !== DEFAULT_SPACE_SLUG) {
      throw new Error("Everything in the company is only available on the default wiki space.");
    }
    if (scope.kind === "selected_projects") {
      if (scope.projectIds.length > MAX_PAPERCLIP_PROFILE_SELECTED_PROJECTS) {
        throw new Error(`selected_projects exceeds the hard cap of ${MAX_PAPERCLIP_PROFILE_SELECTED_PROJECTS}.`);
      }
      for (const projectId of scope.projectIds) {
        const project = await ctx.projects.get(projectId, input.companyId);
        if (!project) throw new Error(`Project belongs to another company or does not exist: ${projectId}`);
      }
    }
    if (scope.kind === "root_issues") {
      if (scope.issueIds.length > MAX_PAPERCLIP_PROFILE_ROOT_ISSUES) {
        throw new Error(`root_issues exceeds the hard cap of ${MAX_PAPERCLIP_PROFILE_ROOT_ISSUES}.`);
      }
      for (const issueId of scope.issueIds) {
        const issue = await ctx.issues.get(issueId, input.companyId);
        if (!issue) throw new Error(`Issue belongs to another company or does not exist: ${issueId}`);
      }
    }
  }
}

export async function updatePaperclipIngestionProfile(ctx: PluginContext, input: {
  companyId: string;
  wikiId?: string | null;
  spaceSlug?: string | null;

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Call listPaperclipIngestionCandidates first and restrict the picker to ids it returns for the current company.
  2. Strip project ids that no longer exist before submitting, or surface them to the user for re-selection.
  3. Verify the companyId passed into the profile update matches the company that owns every projectId.
  4. If a project was just created, wait for it to be readable before adding it to the scope.

Example fix

// before
profile.sourceScopes = [{ kind: "selected_projects", projectIds: rawProjectIds }];

// after
const candidates = await listPaperclipIngestionCandidates(ctx, { companyId, wikiId });
const valid = new Set(candidates.projects.map((p) => p.id));
profile.sourceScopes = [{
  kind: "selected_projects",
  projectIds: rawProjectIds.filter((id) => valid.has(id)),
}];
Defensive patterns

Strategy: validation

Validate before calling

async function filterExistingProjects(ctx, companyId, projectIds) {
  const candidates = await listPaperclipIngestionCandidates(ctx, { companyId, wikiId: DEFAULT_WIKI_ID });
  const valid = new Set(candidates.projects.map((p) => p.id));
  return projectIds.filter((id) => valid.has(id));
}

Type guard

function isLikelyProjectId(id) {
  return typeof id === "string" && id.length > 0 && /^[a-zA-Z0-9_-]+$/.test(id);
}

Try / catch

try {
  await updatePaperclipIngestionProfile(ctx, { companyId, spaceSlug, profile });
} catch (err) {
  if (/Project belongs to another company or does not exist/.test(err.message)) {
    const id = err.message.split(": ").pop();
    profile.sourceScopes = profile.sourceScopes.map((s) =>
      s.kind === "selected_projects" ? { ...s, projectIds: s.projectIds.filter((p) => p !== id) } : s);
    return updatePaperclipIngestionProfile(ctx, { companyId, spaceSlug, profile });
  }
  throw err;
}

Prevention

When it happens

Trigger: Submitting a 'selected_projects' scope whose projectIds includes an id that was deleted, mis-typed, copied from another company, or pending creation. The lookup happens inside validatePaperclipIngestionProfile, reached via updatePaperclipIngestionProfile.

Common situations: Stale project id cached in a UI form after the project was archived/deleted; cross-company copy-paste of ids; test fixtures referencing a project from a different tenant; race between project creation and profile save.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/e1bebe1f570201f4. Report an issue: GitHub.