paperclipai/paperclip · error · UnsafeChatPublicationError

External chat card kind is invalid

Error message

External chat card kind is invalid

What it means

projectCard checks interaction.card.kind against CARD_KINDS, the closed set of allowed card kinds: 'status', 'question', and 'confirmation' (chat-publication-projection.ts:67-71). Any other kind string throws UnsafeChatPublicationError because the provider-agnostic card schema (paperclip.chat.card.v1) only supports these kinds. The TypeScript type SafeExternalChatCardKind should prevent this statically, so the error fires on untyped or dynamically constructed input.

Solutions

  1. Use only the supported kinds: 'status', 'question', or 'confirmation'
  2. Map your domain category to a supported kind before publication (e.g. 'approval' -> 'confirmation')
  3. Fix casing/typos so the kind is an exact lowercase match to the allowed set
  4. If a new kind is genuinely needed, extend SafeExternalChatCardKind in @paperclipai/shared AND the CARD_KINDS set in chat-publication-projection.ts together

Example fix

// before
interaction: { id, card: { kind: "approval", title, body } }
// after
interaction: { id, card: { kind: "confirmation", title, body } } // 'confirmation' is in CARD_KINDS
Defensive patterns

Strategy: type-guard

Validate before calling

const CARD_KINDS = new Set(["status", "question", "confirmation"] as const);
if (!CARD_KINDS.has(card.kind)) throw new TypeError(`Unsupported card kind: ${String(card.kind)}`);

Type guard

function isSafeCardKind(kind: unknown): kind is "status" | "question" | "confirmation" {
  return kind === "status" || kind === "question" || kind === "confirmation";
}

Try / catch

try {
  const payload = projectSafeChatPublication({ classification: "external", source, text, interaction });
} catch (err) {
  if (err instanceof UnsafeChatPublicationError && /card kind is invalid/.test(err.message)) {
    console.error("card.kind must be one of status|question|confirmation", { kind: interaction?.card?.kind });
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling projectSafeChatPublication from JavaScript or untyped code with card.kind values like 'approval', 'info', 'warning', 'task', 'poll', or 'form'; a typo such as 'confirm' or 'statuses'; casing mismatches like 'Status'; or data loaded from a config file/API response without validation.

Common situations: An integration adds a new card style without extending SafeExternalChatCardKind and CARD_KINDS; a legacy payload uses an older kind name; a config-driven workflow reads 'kind' from YAML/JSON where 'question' was mistyped; a generic renderer maps its own categories directly onto card.kind.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/a20bdcce0b0cac7f. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/chat-publication-projection.ts:309

    }
    if (!seen.has(normalized)) {
      seen.add(normalized);
      output.push(normalized);
    }
  }
  return output.length ? output : undefined;
}

function projectCard(
  input: NonNullable<ChatPublicationProjectionInput["interaction"]>,
): { interactionId: string; card: SafeExternalChatCard } {
  if (!SAFE_IDENTIFIER_RE.test(input.id)) {
    throw new UnsafeChatPublicationError(
      "External chat interaction id is invalid",
    );
  }
  if (!CARD_KINDS.has(input.card.kind)) {
    throw new UnsafeChatPublicationError("External chat card kind is invalid");
  }

  const title = truncateByCodePoint(
    projectSafeChatPublicationText(input.card.title),
    MAX_TITLE_LENGTH,
  );
  const body = input.card.body
    ? projectSafeChatPublicationText(input.card.body)
    : undefined;
  const rawActions = input.card.actions ?? [];
  if (rawActions.length > MAX_CARD_ACTIONS) {
    throw new UnsafeChatPublicationError(
      `External chat cards support at most ${MAX_CARD_ACTIONS} actions`,
    );
  }

  const actions: SafeExternalChatCardAction[] = [];
  for (const action of rawActions) {

View on GitHub (pinned to 3f1d897a7c)