Skip to main content

Quick reply

Overview​

Quick reply is the set of buttons (chips) that appear above the text input on LINE when a user receives a message from the OA. Admins build "quick reply sets" here as a central library, and other menus pick them up by reference — no need to re-create the same buttons for every message.

The one thing to understand up front: the reference is live, not a copy. A message that attaches a quick reply set stores only that set's id — it never copies the buttons into itself. Editing the buttons here once therefore changes every message referencing this set immediately (exactly what the form's hint says: "Editing it here updates every message that uses it"). The flip side is that deactivating one set makes its chips disappear from every attached message at once.

The other thing to know: quick reply sets work with Rich message only — not with Template message, despite some UI copy that still mentions template messages (see Key details).

Who uses it — content teams and the staff who run the LINE OA's chat, preparing reply buttons for other messages to call on (this module has no ModuleGate on the backend, unlike Rich message and Auto-response message — see Access control).

User journey​

1. Review the quick reply list​

Open the Quick reply menu. The list is a single table (no tabs), newest-created first, with columns No / Name / Items (how many buttons in the set) / Status / Action. The filter bar has 2 fields — search by name, and a status picker. Filter values are remembered in sessionStorage.

The Status column is a switch you can flip straight from the list without opening the form — but switching off a set that's in use won't go through immediately (see step 4).

2. Create a new quick reply set​

  1. Click Create quick reply to open the two-column form (fields on the left, chip preview on the right).
  2. Enter a name — required, capped at 255 characters.
  3. Add buttons one at a time — up to 13 buttons (matching LINE's own ceiling). An n/13 counter sits at the right of the Items heading, and the Add item button is disabled once you hit 13.
  4. Pick each button's type from 6 options — Message, URI / Link, Date/time picker, Camera, Camera roll, Location.
    • Message — the text sent into the chat on the user's behalf when the chip is tapped. This field is an autocomplete fed by the keywords of active rules in the Auto-response message menu, because text a chip sends back into the chat really can trigger an auto-response rule. Suggestions only — you can type anything.
    • URI / Link — a single field, with a placeholder spelling out the accepted forms: https://... or tel:... or line://....
    • Date/time picker — enter the postback data, choose a mode (Date / Time / Date & time — default is Date & time), and optionally set initial / min / max values.
    • Camera / Camera roll / Location — no additional fields; the form says so directly ("LINE opens the camera/location picker directly when tapped").
  5. Every button needs a Label shown on the chip — max 20 characters (the input has a counter and hard-stops at 20).
  6. Reorder buttons with the up/down arrows on each card (up is disabled on the first item, down on the last). The order in the form is the order LINE users actually see.
  7. Watch the live chip preview in the right-hand "Chip preview" card, which is pinned in place (Affix) so it stays visible while you scroll and edit.
  8. Set the Status switch (defaults to active on a new set) and save. The system takes you back to the list.

Changing the type of a button you've already filled in clears every type-specific field and keeps only the label — this prevents the old type's fields from riding along into the new type, which the backend would reject (see Key details).

3. Use it from another menu​

  1. Open the Rich message form. It has a quick reply picker that loads only active sets (fetched once, 100 at a time, no pagination), with a "None" option at the top.
  2. Once a set is picked, the system shows every button's label as a tag right under the picker so you can confirm you chose the right set, and renders the chips in that message's chat preview too.
  3. If the message references a set that has since been deactivated, the system pulls it in as an extra option tagged "Inactive" and warns that "its chips will not be sent." If the set was deleted, it shows as "Unavailable quick reply" with a prompt to pick another — deliberately, so the UI never shows a bare uuid.

4. Deactivate or delete a set that's in use​

Both actions are guarded by the same backend mechanism, but the outcomes are completely different:

  • Deactivate — a dialog opens ("Deactivate this quick reply?") listing every Rich message that references it, and explaining that "these messages will keep sending, but without this quick reply's chips." Click Deactivate anyway to confirm (the system re-sends the request with a force flag attached automatically). Cancel, and the switch snaps back to whatever the server actually has stored — it never lingers on the value you clicked.
  • Delete — the dialog reads "This quick reply is still in use," listing the same referencing messages, but there's no confirm button to push through, only a close button. You have to edit the referencing Rich messages to stop using this set first.

The edit form (not the create form) also carries a "Used by" card in the right column, showing the referencing messages up front, before you try to deactivate or delete — pulled from a dedicated endpoint that returns the same set the guards act on.

Key details verified against the source​

Confirmed from internal/modules/quickreply/{controller,service,dto,items}.go in line-management-cms-api-go and internal/quickreply/attach.go in line-management-worker-go:

  • The postback type is deliberately disabled on the backend, and hidden from the UI too — items.go catches type: "postback" and rejects it with a 400, "postback is not currently supported". The code comment explains exactly why: there's no generic postback handler yet (webhook-go only has a couple of hardcoded special-case prefixes), so a postback chip would "fire and go nowhere." The postback validation is kept intact for the day it's re-enabled. The frontend drops postback from the type options for the same reason, but keeps the constant and the postback field UI to stay compatible with existing legacy items.
  • URI is an allowlist, and the frontend never re-checks it — the backend accepts only https://, tel: and line:// (case-insensitively). http:// is rejected outright, never silently upgraded to https. The frontend only checks that the URI field isn't empty and never looks at the scheme — so typing http://... passes every form validation and then fails with a 400 on save.
  • Item validation is a "closed shape" — stray fields are rejected, not ignored — each type allows only its own key set (beyond type and label, which every type allows). An off-list key returns a 400 pinpointing the index, e.g. property items[2].uri should not exist. That's exactly why the frontend has to wipe type-specific fields every time you change a button's type.
  • Several length caps are enforced on the backend only — label 1–20 characters (counted as runes, so Thai characters count correctly), a message type's text max 300, URI max 1000, a date/time picker's postback data max 300, and initial/min/max 100 each. Of these, the frontend actually enforces only label (via the input's maxLength) and the 255-character set name — the 300-character text cap gets no warning in the form at all.
  • The deactivate guard fires only on the active → inactive transition — the code checks three conditions together (was active + will be inactive + no force sent) and only hits the database when all three hold, so an ordinary name or button edit never pays for the scan. Every other change (inactive→inactive, →active, or a plain name/items edit) is unguarded. The force flag itself is a request-only field that is never persisted — the comment states outright that it never reaches the update statement.
  • The reference scan looks at the rich_message table only, and that's a deliberate decision — the comment in service.go explains that template_message has no quick_reply_id column at all, because a template has no standalone LINE send path of its own (its flex only ever ships as one item inside a Rich message), so a quick reply attached to a template would be inert anyway. Meanwhile the frontend message files still carry a "Template message" reference-source label — a value the backend never returns.
  • The worker treats a deactivated/deleted set exactly like "no set at all," never as an error — at send time it runs SELECT items FROM quick_reply WHERE id = ? AND status = 'active' AND deleted_date IS NULL; no row means it quietly returns nothing and sends the message without chips rather than failing the send. The comment marks this as intentional: switching a quick reply off must silently stop the chips and must never break delivery. And chips attach to the last bubble of the message set only — a Rich message with 5 items gets its chips after item 5.
  • Sorting the list by the "Name" column doesn't actually work — the frontend table sets sorter: true on the name column and sends order/orderBy with the request, but this module's List reads neither, hardcoding ORDER BY created_date DESC every time. Click the column header and the arrow changes but the row order doesn't.
  • The quick_reply table has no gorm soft-delete hook — the entity never declares gorm.DeletedAt, so every query in this module has to spell out deleted_date IS NULL by hand, and deleting means stamping deleted_date (a soft delete) — the row stays in the database.
  • Rich message — the only menu in the system that actually references quick reply sets, and the only table the deactivate/delete guards scan.
  • Auto-response message — the keywords of active rules in that menu surface as suggestions in the "Message" button's text field here, because text a chip sends back into the chat really can trigger an auto-response rule.
  • Campaign — never picks a quick reply set directly, but buttons attached to a Rich message ride along with the campaign automatically when it goes out.
  • Template message — does not support attaching a quick reply set (there's no column for it in the database). If you want buttons alongside a template, attach them at the level of the Rich message wrapping that template.

Things to watch out for​

  • Editing buttons here edits live messages immediately — there's no versioning or approval step in between. Every Rich message referencing this set sends the new buttons from its very next send. The "Used by" card in the edit form is the only place that tells you which messages you're about to affect.
  • Typing http:// in the URI field passes the form but fails the save — the form never checks the scheme, so the error only shows up as a 400 when you hit save.
  • A "Message" chip whose text exceeds 300 characters won't save, and the input gives no counter or advance warning.
  • Deactivating an in-use set makes chips vanish silently, with no error anywhere — the messages still send fine, the chips just don't come. There's no user-facing alert. If someone reports "the buttons disappeared," check the attached set's status first.
  • More than 13 buttons gets truncated at the worker as a second line of defense — both the form and the backend cap at 13, but if legacy data holds more, the worker takes the first 13 and sends anyway rather than rejecting the send.
  • Sorting by name has no effect — the list is always newest-created first. Use the name search field instead when you have a lot of sets.

Access control (verified against the source)​

  • Frontend — both the list and the form pages are wrapped in a PermissionGuard checking the VIEW action on the quick-reply subject.
  • Backend — this module has no ModuleGate. Unlike Rich message (ModuleGate(d, "rich-message")), Template message, and Auto-response message, which all carry a real enforced gate, all 6 quick reply routes are wired with auth.CheckPolicies(...) alone — and the header comment in controller.go says so outright: "auth.CheckPolicies is a no-op today — see internal/modules/auth/policy.go — but the metadata is kept for when enforcement is switched on." In practice, any authenticated user can call every quick reply route even if a platform admin never enabled a module for them.
  • Cross-tenant isolation is the only layer actually enforced — every query pins line_oa_id from the caller's context. Requesting another OA's id returns 404 "Quick reply not found", not a 403 (so it never leaks whether that id exists).
  • Action-level policy annotations are declared in full — readAll/create/read/update/delete — but run through the same no-op middleware as most other modules, kept as metadata for the day enforcement is switched on.
  • There is no hard-delete route and no Super-Admin-only route in this module.

Source file reference​

Frontend (cms-web-2026)

  • src/app/quick-reply/page.tsx, src/app/quick-reply/form/page.tsx — list and form entry points, with PermissionGuard
  • src/components/quick-reply/list/quick-reply.container.tsx — table, status switch, and the 409 handling for both deactivate and delete
  • src/components/quick-reply/form/quick-reply-form.tsx, quick-reply-form.container.tsx — frontend validation rules, the "Used by" card, and the force-flag retry
  • src/components/quick-reply/form/quick-reply-items-editor.tsx — the 13-button ceiling, field-clearing on type change, reordering
  • src/components/quick-reply/form/enums/quick-reply-item.enum.ts — the selectable button types (and why postback is excluded)
  • src/components/quick-reply/QuickReplyPicker.tsx — the picker other menus use, including its handling of deactivated/deleted sets
  • src/components/common/auto-response-keyword-autocomplete.tsx — keyword suggestions sourced from Auto-response message
  • src/services/quick-reply.service.ts — every endpoint the frontend calls
  • messages/th/quick-reply.json, messages/en/quick-reply.json — all UI copy

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

  • internal/modules/quickreply/controller.go — all 6 routes, and the comment stating the policy check is a no-op
  • internal/modules/quickreply/service.go — CRUD, line_oa_id scoping, the deactivate/delete guards, the rich_message scan
  • internal/modules/quickreply/dto.go — name / status / force-flag validation
  • internal/modules/quickreply/items.go — the closed shape per button type, the URI allowlist, the postback block

Worker (line-management-worker-go)

  • internal/quickreply/attach.go — loading active-only sets, mapping to LINE's wire shape, attaching chips to the last bubble

See the documentation team's earlier technical write-up at Quick Reply