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

  1. Use only type: "callback" (with actionId) or type: "link" (with url) when building card actions
  2. Validate untyped input against the SafeExternalChatCardAction discriminated union (e.g. with a zod schema) before projecting
  3. 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
  4. 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

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


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)