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
- Use only the supported kinds: 'status', 'question', or 'confirmation'
- Map your domain category to a supported kind before publication (e.g. 'approval' -> 'confirmation')
- Fix casing/typos so the kind is an exact lowercase match to the allowed set
- 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
- Type card inputs as SafeExternalChatCardKind from @paperclipai/shared so this fails at compile time
- Map domain categories (approval, info) onto supported kinds before publication
- Validate config-driven card definitions with the shared validator at load time
- When adding a new kind, update SafeExternalChatCardKind and CARD_KINDS together and add a test
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
- ACPX identity permission mode is invalid
- eval-session acpxAgent must be codex or claude
- Expected ACPX permission mode is invalid
- External chat action style is invalid
- External chat interaction id is invalid
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)