paperclipai/paperclip · error · Error

Issue belongs to another company or does not exist: ${issueI

Error message

Issue belongs to another company or does not exist: ${issueId}

What it means

Thrown during profile validation when ctx.issues.get(issueId, companyId) returns nullish for an id in a 'root_issues' scope. The lookup is company-scoped, so null means the issue is missing, deleted, belongs to another company, or is not a root issue retrievable by id. Protects ingestion from cross-company or stale references.

Source

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

    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;
  profile: unknown;
}): Promise<PaperclipIngestionProfileRead> {
  const wikiId = normalizeWikiId(input.wikiId);
  const space = await resolveSpace(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug });
  const current = await profileForSpace(ctx, input.companyId, space);
  const profile = normalizePaperclipIngestionProfile(input.profile, { space, legacySettings: space.slug === DEFAULT_SPACE_SLUG ? await getEventIngestionSettings(ctx, input.companyId) : null });
  await validatePaperclipIngestionProfile(ctx, { companyId: input.companyId, space, profile });
  await updateSpace(ctx, {
    companyId: input.companyId,

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Call listPaperclipIngestionCandidates and only keep rootIssues ids it returns for the current company.
  2. Drop invalid ids before submission or prompt the user to re-pick.
  3. Confirm the companyId in the request owns every issueId.
  4. If issues were just created, ensure they are readable before referencing them.

Example fix

// before
profile.sourceScopes = [{ kind: "root_issues", issueIds: rawIssueIds }];

// after
const candidates = await listPaperclipIngestionCandidates(ctx, { companyId, wikiId });
const valid = new Set(candidates.rootIssues.map((i) => i.id));
profile.sourceScopes = [{
  kind: "root_issues",
  issueIds: rawIssueIds.filter((id) => valid.has(id)),
}];
Defensive patterns

Strategy: validation

Validate before calling

async function filterExistingRootIssues(ctx, companyId, issueIds) {
  const candidates = await listPaperclipIngestionCandidates(ctx, { companyId, wikiId: DEFAULT_WIKI_ID });
  const valid = new Set(candidates.rootIssues.map((i) => i.id));
  return issueIds.filter((id) => valid.has(id));
}

Type guard

function isLikelyIssueId(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 (/Issue 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 === "root_issues" ? { ...s, issueIds: s.issueIds.filter((p) => p !== id) } : s);
    return updatePaperclipIngestionProfile(ctx, { companyId, spaceSlug, profile });
  }
  throw err;
}

Prevention

When it happens

Trigger: Submitting a 'root_issues' scope containing a deleted, mistyped, cross-company, or non-existent issue id through updatePaperclipIngestionProfile.

Common situations: Stale id from a previously deleted issue; ids copied from another tenant; transient state right after issue creation; non-root issue id used where root issues are expected.

Related errors


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