Skip to content

feishu: card V2 schema rejects deprecated action container — exec/plugin approval cards fail with code 230099 #79824

Description

@xiabee

Summary

The bundled @openclaw/feishu plugin (verified on 2026.5.7) emits Feishu cards that wrap action buttons in a top-level { tag: "action", actions: [...] } container. Feishu's V2 card schema has dropped this container, so every button-bearing card is rejected by https://open.feishu.cn/open-apis/im/v1/messages with:

http_status: 400
feishu_code: 230099
feishu_msg : Failed to create card content, ext=ErrCode: 200861;
             ErrPath : ROOT -> body -> elements -> [1](tag: action);
             ErrMsg  : cards of schema V2 no longer support this capability;
             ErrorValue: unsupported tag action

Because the cards never reach Feishu, no exec or plugin approval can be delivered to a Feishu approver — every approval is silently lost on the chat side. The gateway then keeps retrying via gateway/delivery-recovery, flooding error logs.

This is distinct from the previously-fixed code 230099 bugs (#53310, #53771) which were about ErrCode 200621 / parse card json err in markdown cards. The new sub-error is ErrCode 200861 / unsupported tag action, surfaced after Feishu tightened V2 schema validation.

Repro

  1. OpenClaw gateway 2026.5.7 with @openclaw/feishu 2026.5.7 enabled, Feishu DM channel configured.
  2. Run anything that triggers an exec approval, e.g. git status under mainSecurity: fullAsk.
  3. Approval card fails to send; gateway/exec-approvals logs the 230099 error above.

The same code path is used for /quick-actions cards and any buildFeishuCardElementForBlock("buttons") block — they all fail.

Root cause

In @openclaw/feishu (visible in dist/channel.runtime-*.js and dist/monitor.account-*.js, four sites total), button rows are constructed as

{ tag: "action", actions: [ /* button objects */ ] }

Feishu V2 requires multi-button rows to be expressed via column_set (or as top-level button body elements). The action tag was a V1 grouping element and is no longer accepted.

Suggested fix

Replace the action container with a column_set, one column per button:

{
  tag: "column_set",
  flex_mode: "none",
  horizontal_spacing: "default",
  columns: actions.map(btn => ({
    tag: "column",
    width: "auto",
    vertical_align: "center",
    elements: [btn],
  })),
}

I'm running this as a local hot-patch — a recursive rewriter wrapped around every JSON.stringify(card) in dist/send-*.js:

function __ocMigrateAction(node) {
  if (!node || typeof node !== 'object') return node;
  if (Array.isArray(node)) return node.map(__ocMigrateAction);
  if (node.tag === 'action' && Array.isArray(node.actions)) {
    return {
      tag: 'column_set',
      flex_mode: 'none',
      horizontal_spacing: 'default',
      columns: node.actions.filter(Boolean).map(btn => ({
        tag: 'column', width: 'auto', vertical_align: 'center',
        elements: [__ocMigrateAction(btn)],
      })),
    };
  }
  const out = {};
  for (const k in node) if (Object.prototype.hasOwnProperty.call(node, k)) out[k] = __ocMigrateAction(node[k]);
  return out;
}

After applying, exec-approval cards render with working Confirm / Cancel / Allow Once / Allow Always / Deny buttons end-to-end on Feishu Lark (Win desktop, May 2026 build).

Happy to send a PR against extensions/feishu (or wherever the source lives in the monorepo) — left it as an issue first since the four call sites would need to flow through the proper sanitizer functions, and the maintainers might prefer a different shape (e.g. a shared cardActionRowToColumnSet helper).

Side note (separate issue worth filing?)

While debugging, I noticed pending approvals are cleared after expiry or restart (per the APPROVAL_NOT_FOUND_DETAILS.remediation string in approval-shared). For long-running agents that block on approvals this means a routine gateway restart silently invalidates every outstanding approval card the user can still see in their Feishu chat — clicking those buttons returns unknown or expired approval id. Persisting pending approvals so they survive restarts would significantly improve UX. I can file that as a separate issue if maintainers agree it's in scope.

Versions

  • openclaw: 2026.5.7 (eeef486)
  • @openclaw/feishu: 2026.5.7
  • node: 24.15.0
  • OS: Kylin Linux V10 SP1 (gateway), Win 10 (Feishu desktop client, May 2026 build)

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority user-facing bug, regression, or broken workflow.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions