Skip to main content

Switch rich menu

Overview​

Switch rich menu is a submenu attached to a parent menu (Rich menu default or Rich menu custom), serving as the destination when a user taps a "switch rich menu" cell on the parent. It suits multi-level or tab-style menus. The thing to understand up front: the link between two menus is a deterministic hash, not a database lookup. So if the destination menu has never actually been published to LINE, tapping the button does nothing at all, with no warning anywhere.

Who uses it — admins building a multi-level menu on top of an existing Rich menu default/custom (this module carries a real, enforced ModuleGate on the backend — see Access control below).

User journey​

1. Review the submenus​

Open Rich menu > Switch rich menu. This page has a 3-field filter — Search (by title), Parent rich menu (a dropdown grouped into "Rich menu default" and "Rich menu custom", each option showing a small thumbnail next to the name), and Updated on (a date range labeled "(BKK Time)").

The table shows No. / Image / Title / Parent rich menu / Updated on / Action — note that this page has no Status column (unlike the other two), because a submenu has no user-settable status.

2. Create a submenu​

  1. A parent menu must already exist, since the form requires you to declare which menu this is a submenu of.
  2. Click create, opening the form at /rich-menu/form?type=switch — or take the shortcut straight from the parent menu's form: in a "switch rich menu" action cell there's a "Create new" button that opens this form in a new tab with parentId already passed through (that button is disabled unless the parent is in Draft or Inactive status).
  3. Pick the parent rich menu — the dropdown is grouped into default and custom menus and is searchable by name. Changing the parent clears every tap area and action you've configured, and on the edit page this field is permanently disabled — a submenu cannot be moved under a different parent.
  4. Pick a layout / upload an image / configure per-cell actions — identical to the other two types (15 layouts across 3 tabs; JPG/PNG up to 1 MB at the allowed resolutions; the 5 action types).
  5. This form has no status field at all, on create or on edit — it's hidden and set to Active automatically. As a result the submenu is pushed to LINE the moment it's saved, and because the status is Active from the outset, every field in the action-setting section is required (there is no draft mode for parking unfinished work).
  6. One of the action options is "Switch rich menu", pointing back at the parent or across to a sibling submenu. That dropdown has two groups — "Parent rich menu" and "Sub-rich menu" — shows a thumbnail preview of the selected destination, and carries the note "You can only select rich menus of the same Parent rich menu."
  7. Save, confirm, and the system calls POST /api/rich-menu.

3. Review stats and manage an existing submenu​

  • Click the eye icon for read-only mode, with a menu preview, the per-cell click-stats table, and a Re-sync button to request a recalculation.
  • Editable only while the parent is in Draft or Inactive status (for custom parents), and never once the submenu itself is Completed, Expired, or Deleted — the button's tooltip states the reason outright (e.g. "Cannot edit: Parent rich menu must be Draft or Inactive status").
  • Not deletable while either the submenu itself or its parent is "Completed" — every other status can be deleted (with confirmation).

Key details verified against the source​

Confirmed from internal/modules/richmenu/{service,update,create_update}.go, internal/modules/linemessageapi/richmenu.go, and internal/core/httpx/codes.go in line-management-cms-api-go:

  • switch is the only type whose status rules the backend actually enforces — ValidateStatusTransition / ValidateStatusForAction / ValidateStatusForDelete all open with if type == switch and pass straight through for anything else. The rules that really bite: Draft can only become Active; Active can only become Active/Inactive/Delete; Inactive can only become Delete; and reverting from Inactive or Delete is forbidden. On top of that, a submenu in Inactive / Delete / Completed / Expired status cannot be modified at all (400 "Cannot modify rich menu in current status"), and a Completed submenu cannot be deleted.
  • The two rules above combine into a real consequence: a deactivated submenu can never be edited through the UI again — because the form hides the status field and always sends status = active, while the stored row is inactive (usually cascaded down from the parent). The save is rejected by the "cannot revert from Inactive" rule, and rejected a second time by the "cannot modify in current status" rule. The only way forward is to create a new submenu.
  • The switch linkage is a fixed hash with no database lookup — when building a switch-type tap area, the system computes richMenuAliasId as richmenu-alias- + the first 16 chars of md5("{organizationId}_{lineOaId}_{destinationRichMenuId}") and sets data to richmenu-changed-to- + that same hash, while each menu registers its own alias on LINE using exactly the same formula. The two sides meet without any relationship being stored anywhere. The upside: line_rich_menu_id can change on every save without touching a single switch button. The downside: if the destination has never been published to LINE, that alias doesn't exist there, and the tap does nothing.
  • Nothing checks whether the destination is actually live before saving — the error code RMN_003 ("Rich menu alias is not active") is declared in the error-code table, and the frontend has handling for both RMN_003 and RMN_004 with explanatory copy, but the Go rich-menu module never emits either (RMN_004 isn't even declared in the code table). So pointing a switch button at a menu that isn't ready "saves successfully" and then fails silently when a user taps it in the LINE app.
  • The destination dropdown in the Switch form filters by status differently than the other forms — inside a parent menu's form (default/custom) the submenu options are filtered to status = active, but inside the Switch form itself no status condition is sent at all, so you see sibling submenus in every status, including ones that were never activated. That is the main path by which a switch button ends up pointing at an unready menu. Both cases filter on the same parentId and always exclude the menu being edited from its own options.
  • Every save creates a new submenu on LINE and deletes the old one — same as the other two types — and the alias is updated first, falling back to create if that fails, so the parent's switch button needs no adjustment.
  • Deleting a submenu does nothing to its parent — the delete path only gathers a "submenus to cascade" list when the menu being deleted is of type custom. Deleting a submenu is therefore just a soft delete of itself (status = delete plus a deleted_date stamp) followed by a best-effort delete on LINE — the parent's switch tap area still points at a dead alias until you open the parent and save it again.
  • A submenu's stats don't use the default menu's "compare the two most recent archives" mechanism — that live-recompute only applies to guest/member types (and rich_menu_archive rows are only written for those two). A submenu just reads whatever the worker wrote into template_tracking.
  • A switch button's clicks are recorded through the webhook, not a redirect link — a switch tap area becomes a LINE richmenuswitch action with metadata attached in its data field (contentType: rich_menu, type: richmenu_switch, organizationId, lineOaId, richMenuId, richMenuActionIndex, richMenuArchiveId). And because that event never passes through the redirect service, it never sets the RICH_MENU_STAT_DIRTY flag — so the hourly cron never picks that menu up for recalculation and you have to use the Re-sync button (details on the Rich menu default page).
  • Rich menu default — can be a parent (guest or member type) and is where the user taps to reach this submenu. Inside a default menu's form, the submenu options offered are filtered to active only.
  • Rich menu custom — can also be a parent, and it's the type that affects submenus most: a submenu's status is cascaded when the custom menu expires or is deleted (with the reason written into a reason column naming the parent), and whether the submenu can be edited or deleted at all depends on that custom menu's status.
  • Segment audience — not involved in submenus directly, but it determines who sees a custom parent menu (and therefore who can tap through to this submenu).
  • Workflow / Trigger rule — can switch a specific user's rich menu directly through the line_change_richmenu queue as a setRichMenuByTriggerRule job, without waiting for the user to tap anything.

Things to watch out for​

  • Make sure the destination menu has actually been published to LINE before pointing a switch button at it — nothing validates this, there is no warning, and the end user gets a button that does nothing.
  • A deactivated submenu can no longer be edited — you have to create a new one (see above).
  • Picking the wrong parent means starting over — the "Parent rich menu" field is permanently disabled on the edit page; a submenu can't be moved under a different parent.
  • Changing the parent on the create page clears every tap area and action you configured — settle on the parent before you start configuring actions.
  • After deleting a submenu, go edit the parent too — otherwise the parent's switch button points at a destination that no longer exists.
  • This form has no draft mode — because the status is forced to Active, every field is required and unfinished work can't be parked.
  • Avoid layout 15, same as the other two types — the tap areas you draw won't work in the real LINE app.

Access control (verified against the source)​

  • Frontend — this page is wrapped in a PermissionGuard checking the VIEW action on the rich-menu-switch subject, a separate subject from rich-menu-default and rich-menu-custom.
  • Backend — every route is wrapped in a real, enforced ModuleGate("rich-menu"), and it is a single gate covering all three types at once (Default / Custom / Switch share the rich_menu table, distinguished only by a type column) — closing the rich-menu module for an organization 403s all three pages together; there's no way to enable or disable just submenus at the backend level.
  • This is the only type with real server-side "which action is allowed in which status" enforcement — unlike Default/Custom, whose status rules live entirely on the frontend, calling the API directly to edit an Inactive/Completed/Expired submenu really is rejected with a 400.
  • Cross-tenant (IDOR) protection — findById (which both PUT and DELETE call as their first step) always binds the caller's line_oa_id (a miss returns 400 APP_007), recalculate-stat verifies ownership before queueing (a miss returns 404), and every soft-delete statement is scoped by line_oa_id.
  • Action-level policy annotations are all declared but run through no-op middleware — module-level enforcement is the ModuleGate; status-level enforcement lives in the service logic.
  • DELETE /api/rich-menu/:id is a soft delete; DELETE /api/rich-menu/:id/hard requires Super Admin plus a confirm=true query parameter.

Source file reference​

Frontend (cms-web-2026)

  • src/app/rich-menu/switch/page.tsx — list entry point (PermissionGuard subject rich-menu-switch)
  • src/components/rich-menu/list/rich-menu-switch-filter.tsx — the switch-type-specific 3-field filter (search / parent rich menu / updated on)
  • src/components/rich-menu/list/rich-menu.container.tsx — the parent and updated-on columns, the hidden status column, edit-and-delete disable rules driven by parent status
  • src/components/rich-menu/form/action-setting/action-setting-render-type.tsx — RenderTypeAlias: the grouped dropdown, the "Create new" button that passes parentId, the destination thumbnail preview
  • src/components/rich-menu/form/default/rich-menu-default-form.container.tsx — destination filtering by parentId (and the status condition that differs by form type), reading parentId off the URL
  • src/components/rich-menu/form/default/components/RichMenuStatusRadioGroup.tsx — hiding the status field entirely for the switch type
  • src/services/rich-menu.service.ts — every endpoint the frontend calls

Backend (line-management-cms-api-go)

  • internal/modules/richmenu/service.go — ValidateStatusTransition / ValidateStatusForAction / ValidateStatusForDelete / GetAvailableStatuses (all switch-only) and the delete path
  • internal/modules/richmenu/update.go — status-rule checks before an edit, the cascade from a parent menu, the update-then-fall-back-to-create alias handling
  • internal/modules/richmenu/create_update.go — generateRichMenuAliasId and the LINE call sequence on create
  • internal/modules/richmenu/util.go — the alias hash formula and its validation rules (non-empty, ≤255 chars, only a-z A-Z 0-9 - _)
  • internal/modules/linemessageapi/richmenu.go — converting a switch tap area into a LINE richmenuswitch action and the metadata attached to it
  • internal/core/httpx/codes.go — the RMN_003 code that is declared but never emitted by any module

See the documentation team's technical write-up at Rich Menu (Default / Custom / Switch)