CopilotKit/CopilotKit · error · Error

WhatsApp control value too large to round-trip: encoded id i

Error message

WhatsApp control value too large to round-trip: encoded id is ${encoded.length} chars (max ${WA_LIMITS.controlId}). Use a smaller value or a short key the handler maps.

What it means

WhatsApp interactive buttons/rows carry the action id in a payload with a hard character limit (WA_LIMITS.controlId). CopilotKit encodes `actionId::JSON.stringify(value)` into that field; when the encoded string exceeds the limit the renderer refuses to emit a message that could never round-trip back through decodeInteraction.

Source

Thrown at packages/channels-whatsapp/src/render/message.ts:269

  if (handler && typeof handler === "object" && "id" in handler) {
    const id = (handler as { id?: unknown }).id;
    if (typeof id === "string") return id;
  }
  return undefined;
}

/**
 * Build a reply-control id. A bare minted id is short and safe to clamp. When a
 * value must round-trip (WhatsApp replies carry only an id, no value field) we
 * encode `${id}::${JSON.stringify(value)}`; if that exceeds WhatsApp's 256-char
 * id limit it CANNOT round-trip, so we fail loudly rather than truncate (which
 * would make decodeInteraction silently parse garbage).
 */
function buildControlId(actionId: string, value: unknown): string {
  if (value === undefined) return truncateText(actionId, WA_LIMITS.controlId);
  const encoded = `${actionId}::${JSON.stringify(value)}`;
  if (encoded.length > WA_LIMITS.controlId) {
    throw new Error(
      `WhatsApp control value too large to round-trip: encoded id is ${encoded.length} chars ` +
        `(max ${WA_LIMITS.controlId}). Use a smaller value or a short key the handler maps.`,
    );
  }
  return encoded;
}

/**
 * Flatten a `BotChildren` tree to a single text string. Handles both plain
 * string children and `renderToIR`-lowered `{ type: "text", props: { value } }`
 * leaf nodes.
 */
function textOf(children: unknown): string {
  if (children == null || children === false || children === true) return "";
  if (typeof children === "string") return children;
  if (typeof children === "number") return String(children);
  if (Array.isArray(children)) return children.map(textOf).join("");
  const node = children as ChannelNode;

View on GitHub (pinned to 68fbe97d87)

Solutions

  1. Pass a short key (id/index) as value and map it back to the full data inside your handler
  2. Pre-serialize/truncate large values and store them server-side, sending only an id
  3. If value is undefined, only actionId is used — prefer omitting value entirely

Example fix

// before
{ type: "list", value: bigProductObject }

// after
{ type: "list", value: bigProductObject.sku } // handler looks up by sku
Defensive patterns

Strategy: validation

Validate before calling

const encoded = `${actionId}::${JSON.stringify(value)}`;
if (encoded.length > 256 /* conservative stand-in for WA_LIMITS.controlId */) value = shortKeyFor(value);

Type guard

function isControlValueSafe(actionId: string, value: unknown): boolean {
  return value === undefined || `${actionId}::${JSON.stringify(value)}`.length <= 200;
}

Try / catch

null

Prevention

When it happens

Trigger: Rendering a control (list item, button, etc.) with a large `value` prop — e.g. a whole object, long string, or base64 blob — so actionId+JSON exceeds the limit.

Common situations: Passing full row data (records, URLs, base64 images) as the value instead of a key; generating dynamic values that grow unbounded.

Related errors


AI-assisted analysis of CopilotKit/CopilotKit@68fbe97d87 (2026-08-27). Data as JSON: /api/errors/ff3b11dee8e3050c. Report an issue: GitHub.