paperclipai/paperclip · error · UnsafeChatPublicationError
External chat card action type is invalid
Error message
External chat card action type is invalid
What it means
Every card action must be either type "callback" or type "link". After the callback branch, projectCard checks whether the action is a "link"; if the type is anything else it throws UnsafeChatPublicationError rather than silently dropping the unknown action type. This guards the provider-bound payload against unrecognized or future/unmapped action kinds.
Solutions
- Use only type: "callback" (with actionId) or type: "link" (with url) when building card actions
- Validate untyped input against the SafeExternalChatCardAction discriminated union (e.g. with a zod schema) before projecting
- If a new action kind is genuinely needed, add it to the shared type and to projectCard's handling — do not pass it through untyped
- Normalize aliases (e.g. "url" -> "link") at the ingestion boundary
Example fix
// before
const actions = raw.map((a) => ({ type: a.kind, ...a })); // kind may be "url" | "button" | ...
// after
const actions = raw.map((a) => a.kind === "url"
? { type: "link", label: a.label, url: a.url }
: { type: "callback", actionId: a.id, label: a.label }); Defensive patterns
Strategy: type-guard
Validate before calling
function isValidActionType(t: unknown): boolean {
return t === "callback" || t === "link";
}
card.actions = (card.actions ?? []).filter((a) => isValidActionType(a.type)); Type guard
function isSafeCardAction(a: unknown): a is SafeExternalChatCardAction {
if (typeof a !== "object" || a === null) return false;
const x = a as Record<string, unknown>;
if (x.type === "callback") return typeof x.actionId === "string" && typeof x.label === "string";
return x.type === "link" && typeof x.url === "string" && typeof x.label === "string";
} Try / catch
try {
const payload = projectSafeChatPublication(input);
} catch (err) {
if (err instanceof UnsafeChatPublicationError && err.message === "External chat card action type is invalid") {
input.interaction.card.actions = input.interaction.card.actions.filter(isSafeCardAction);
return projectSafeChatPublication(input);
}
throw err;
} Prevention
- Parse untrusted card JSON with a discriminated-union schema (zod) before projection
- Never construct action objects with a computed type field from arbitrary strings
- Keep the SafeExternalChatCardAction union as the single source of truth for action types
When it happens
Trigger: Passing an action whose type is not "callback" or "link" — e.g. "button", "select", "url" (lowercase vs "link" is fine, but "url" is not), "submit", an empty string, or an unvalidated runtime value that bypassed the SafeExternalChatCardAction union type (e.g. parsed from JSON).
Common situations: Constructing actions from untyped JSON (API payload, DB row, plugin output) without validating the discriminated union; inventing a new action type and forgetting to add it to the projection; casing mismatches like "Callback".
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
- Chat publication source must be explicitly classified for…
- External chat action id is invalid
- External chat action style is invalid
- External chat cards support at most
- VALIDATION_ERROR
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/0d82ebf55441291c.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/chat-publication-projection.ts:353
"External chat action id is invalid",
);
}
if (action.style && !CARD_ACTION_STYLES.has(action.style)) {
throw new UnsafeChatPublicationError(
"External chat action style is invalid",
);
}
actions.push({
type: "callback",
actionId: action.actionId,
label,
...(action.style ? { style: action.style } : {}),
});
continue;
}
if (action.type !== "link") {
throw new UnsafeChatPublicationError(
"External chat card action type is invalid",
);
}
const url = sanitizeExternalChatUrl(action.url);
if (!url) continue;
actions.push({ type: "link", label, url });
}
return {
interactionId: input.id,
card: {
schema: "paperclip.chat.card.v1",
kind: input.card.kind,
title,
...(body ? { body } : {}),
...(actions.length ? { actions } : {}),
},View on GitHub (pinned to 3f1d897a7c)