{"record":{"id":"98edbfc9855613db","repo":"paperclipai/paperclip","slug":"external-chat-cards-support-at-most-max-card-actions-actions","errorCode":null,"errorMessage":"External chat cards support at most ${MAX_CARD_ACTIONS} actions","messagePattern":"External chat cards support at most (.+?) actions","errorType":"validation","errorClass":"UnsafeChatPublicationError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-publication-projection.ts","lineNumber":321,"sourceCode":"  if (!SAFE_IDENTIFIER_RE.test(input.id)) {\n    throw new UnsafeChatPublicationError(\n      \"External chat interaction id is invalid\",\n    );\n  }\n  if (!CARD_KINDS.has(input.card.kind)) {\n    throw new UnsafeChatPublicationError(\"External chat card kind is invalid\");\n  }\n\n  const title = truncateByCodePoint(\n    projectSafeChatPublicationText(input.card.title),\n    MAX_TITLE_LENGTH,\n  );\n  const body = input.card.body\n    ? projectSafeChatPublicationText(input.card.body)\n    : undefined;\n  const rawActions = input.card.actions ?? [];\n  if (rawActions.length > MAX_CARD_ACTIONS) {\n    throw new UnsafeChatPublicationError(\n      `External chat cards support at most ${MAX_CARD_ACTIONS} actions`,\n    );\n  }\n\n  const actions: SafeExternalChatCardAction[] = [];\n  for (const action of rawActions) {\n    const label = truncateByCodePoint(\n      projectSafeChatPublicationText(action.label),\n      MAX_ACTION_LABEL_LENGTH,\n    );\n    if (action.type === \"callback\") {\n      if (!SAFE_IDENTIFIER_RE.test(action.actionId)) {\n        throw new UnsafeChatPublicationError(\n          \"External chat action id is invalid\",\n        );\n      }\n      if (action.style && !CARD_ACTION_STYLES.has(action.style)) {\n        throw new UnsafeChatPublicationError(","sourceCodeStart":303,"sourceCodeEnd":339,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/chat-publication-projection.ts#L303-L339","documentation":"projectCard enforces a hard cap of MAX_CARD_ACTIONS (12) actions on interaction cards projected for external chat delivery (Slack etc.). If input.card.actions contains more than 12 entries, it throws UnsafeChatPublicationError instead of silently truncating. The cap bounds the size and complexity of what gets posted to third-party providers.","triggerScenarios":"Calling projectSafeChatPublication (or the interaction projection path that calls projectCard) with input.interaction.card.actions containing 13 or more action objects (callback or link actions combined).","commonSituations":"Building a rich confirmation or question card that programmatically appends an action per option/task/label and ends up with more than 12 buttons; concatenating several card action lists; a UI feature that generates one action per issue link without limiting the count.","solutions":["Reduce card.actions to at most 12 entries before calling projectSafeChatPublication (keep the most important actions, or drop links first)","Split the content across multiple publications or move the overflow actions into the card body text as plain links","Check the actions length at card-construction time and fail early where the list is built, rather than at projection time"],"exampleFix":"// before\nconst actions = allOptions.map((o) => ({ type: \"link\", label: o.label, url: o.url }));\n// after\nconst actions = allOptions.slice(0, 12).map((o) => ({ type: \"link\", label: o.label, url: o.url }));","handlingStrategy":"validation","validationCode":"const MAX_CARD_ACTIONS = 12;\nif ((card.actions ?? []).length > MAX_CARD_ACTIONS) {\n  card.actions = card.actions.slice(0, MAX_CARD_ACTIONS); // or throw early\n}","typeGuard":"function hasAcceptableActionCount(card: { actions?: unknown[] }): boolean {\n  return (card.actions?.length ?? 0) <= 12;\n}","tryCatchPattern":"try {\n  const payload = projectSafeChatPublication(input);\n} catch (err) {\n  if (err instanceof UnsafeChatPublicationError && /at most \\d+ actions/.test(err.message)) {\n    input.interaction.card.actions = input.interaction.card.actions.slice(0, 12);\n    return projectSafeChatPublication(input);\n  }\n  throw err;\n}","preventionTips":["Cap action lists where the card is constructed, not at projection time","Prefer links in the body text when the action set is dynamic and potentially large","Write a unit test asserting cards never exceed 12 actions"],"tags":["validation","chat-publication","limit-exceeded"],"backgroundTag":"value-out-of-range","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}