Auto-response message
Overview
Auto-response message is a "keyword → Rich message" rule. When a user types something in chat that matches a configured keyword, the system automatically sends back the attached Rich message with no admin involved — ideal for repeat questions like opening hours, branch locations, or how to order.
The key thing to understand up front: this menu never answers messages itself. What saving actually does is write a keyword table into Redis for the entire OA, rebuilt from scratch; a background service then reads that table when a message arrives and does the sending. The consequence is that a rule which looks perfectly fine on this screen may not exist in the table that actually answers, if some condition isn't met (see Key details).
Who uses it — the team running the LINE OA's chat (this module carries a real, enforced ModuleGate on the backend — see Access control).
User journey
1. Review the rule list
Open the Auto-response message menu. The first thing the system does is read the LINE OA's settings and show a yellow warning banner above the table if message-handling priority is set to ai_classifier_only. The banner says outright that "Auto-response is currently disabled," because that mode hands every message to the chatbot workflow instead and no rule here will ever be triggered — and it tells you where to fix it (LINE OA Settings → Message Handling).
The table has columns No / Title / Rich message / Action, plus a status switch you can flip straight from the list. The Rich message column shows the attached message's title along with a status tag — green while it's still usable, and "Deleted" once that Rich message is no longer in an active state.
The filter bar has 2 fields — search (which covers both the rule name and the keywords themselves, as the placeholder "Title or keyword" says) and a status picker.
2. Create a new rule
- Click Create response to open the two-column form.
- Enter a rule title — max 100 characters, and it must not duplicate another rule. Below it, a Data Entry Conditions box lists the supported characters and symbols, plus the restriction that it can't start with a symbol.
- Enter keywords as tags — type and press Enter, or separate with commas to add several per rule. The hint box beside the field states three rules: it only responds to the keywords you set (with several set, it responds to any one of them); each keyword can be up to 30 characters; and a keyword already in use can't be used by another rule.
- Duplicates are checked the moment you add a tag (debounced by half a second). On a collision the system removes that tag for you, shows an error under the field, and pops a top-right notification with a button that opens the rule already using that keyword in a new tab.
- Pick the Rich message to reply with from those available in the Rich message menu.
- Check the preview in the right column, rendered as a real chat screen.
- Set the status switch (defaults to active on a new rule) and save.
3. Edit an existing rule
Open the edit form and the system compares the saved Rich message against the currently selectable list — if that Rich message is no longer in the list (it was deleted), the system clears the field and shows an error, forcing you to pick a new one before you can save. (The list page, by contrast, only shows the "Deleted" tag.)
Flipping the status switch on the list calls a separate route (PUT /auto-response/:id/status) without submitting the whole form, and every save or status flip rewrites the whole Redis keyword table.
Key details verified against the source
Confirmed from internal/modules/autoresponse/{controller,service,dto}.go in line-management-cms-api-go, plus internal/autoresponse/service.go and internal/cronscheduler/auto_response_sync.go in line-management-worker-go:
- Every save rewrites the whole OA's keyword table, not just the rule you touched — the mechanism deletes the Redis key
AUTO_RESPONSE:LINE_OA_ID:<id>entirely, then rebuilds it by reading every rule with active status in that organization + OA. The result is a Redis hash mappingkeyword → {autoResponseId, richMessageId}. This happens identically on create, update, status flip, delete, and hard delete — and if the computed table comes out empty, the system deletes the key and writes nothing back at all. - A rule attached to an unusable Rich message is silently dropped from the table that actually answers — while building the table, the system first pulls the list of Rich messages with
status = activefor that OA, then skips every keyword of any rule pointing at a Rich message not on that list. So the rule stays in the database, still shows in the table, its switch is still on, but it isn't in the answering table — which is exactly what the list's "Deleted" tag is telling you. - When the same keyword appears on more than one rule, the one read later overwrites the earlier — the table is assembled as a plain map in read order, so only one rule can answer that keyword. The duplicate check in the form is therefore the only protection there is — nothing at the database or API level guards against it.
- Keyword matching at runtime is an exact, whole-string match — the background service uses
HGETto read the field by keyword name directly. It's not a substring search and not fuzzy. One extra character in what the user typed and it won't match. POST /check-keyword-existsreturns a 500 when the keyword is free — this is a bug from the original system, deliberately preserved. The comment states it directly: the TS code dereferencesres[0].idon an empty array, producing a TypeError, so the endpoint answers 500 carrying the verbatim JavaScript messageCannot read properties of undefined (reading 'id')instead of{exists: false}. And the frontend copes by treating "the request failed" as "the keyword is available" — the keyword-is-free path is literally itscatchblock, which only writes a console log. Separately, the response field namedrichMessageIdactually carries the auto-response's id, not the Rich message's (the comment notes it was mislabeled in the original) — but that's precisely the value the UI needs for its "see the rule using this keyword" deep link, so the behavior is right despite the name.- The status-flip route performs no validation whatsoever —
PUT /auto-response/:id/statustakes thestatusvalue from the body and writes it straight into the column unchecked, because in the original system the body's type is a type alias, which makes the ValidationPipe skip it entirely (the comment explains this in full). Calling the API directly with any value stores it — and anything other thanactiveimplicitly drops the rule out of the answering table. - The status error message doesn't match what the system actually accepts — the message reads "status must be one of the auto-response status values: active, inactive," but the list of values that actually passes validation is all 9 CommonStatus values (active, inactive, delete, process, processing, completed, expired, draft, failed). Sending
draftorfailedto the create/update routes passes, despite the message claiming only two are allowed. - A non-string
statustriggers a 500 before any other field is validated — the system preserves class-transformer's behavior: every transform runs before all validation. So sendingstatusas a number or an object returns a 500 with the verbatim JavaScript messagetext.toLowerCase is not a function, masking every other field error in the payload. keywordsandrichMessageIdvalidation is far looser than the UI enforces —keywordshas only a "must not be empty" rule and no "must be an array" rule, so an empty array[]passes (as do a number or an object).richMessageIdis coerced by JavaScript's rules, so an empty string becomes the number 0, which passes both "not empty" and "must be a number" — and nothing ever checks that the Rich message exists or belongs to the caller's OA (unlike the Rich message menu, which validates quick reply ownership strictly). A rule pointing at a nonexistent id then gets silently dropped from the Redis table.- Three different things write the keyword table, and the third was added to fix the "only answers the second time" symptom — besides saves from the UI, there's a cron every 5 minutes that syncs every OA, and a cache-aside lazy warm on a cold key. The comment describes the original symptom precisely: the table was written by only the first two, so after a restart or a cache eviction the first message missed even with a valid config. The lazy warm therefore distinguishes "the key doesn't exist at all" (warm from the database and retry) from "the key exists but has no such keyword" (leave it alone), so ordinary chatter doesn't trigger a re-warm on every message.
- The cron used to stop partway through when one OA failed — the comment records that the old code used
returninstead ofcontinue, so a single broken or oversized config meant every OA after it in that pass never synced ("a prime cause of auto-response only working the 2nd time"). It now skips the failure and carries on. - If nothing matches and AI fallback is enabled, the message is handed off to the AI queue — the background service publishes it onto the trigger queue rather than dropping it. Without that flag, it just logs that no rule covers the keyword.
- Merge tags in the content are resolved at send time, not at save time — just before sending, the system checks whether the Rich message's content contains merge tags and, if so, looks up that LINE user's data to substitute them. And if the LINE OA isn't in an active state at that moment, the job is marked permanently failed with no retry.
- Deleting a Rich message also cleans out the keyword table — the Rich message module walks this menu's Redis hash and removes every field pointing at the deleted Rich message, so attached rules stop replying immediately rather than waiting for the next cron pass.
- Deleting a rule is a two-step soft delete, and the hard delete isn't OA-scoped — a normal delete first flips the status to
delete, then stampsdeleted_datein a second statement.DELETE /:id/hard, meanwhile, deletes on theidpredicate alone with noline_oa_idfilter, so it can remove any OA's rule by id (gated only by requiring Super Admin). Withoutconfirm=trueit's rejected with a 400 whose message is the raw stringErrorResponse.AUS_10— which the comment calls out explicitly as an original-system bug kept verbatim. - The search box really does search keywords, as the placeholder claims — the SQL runs a case-insensitive LIKE against the name column and walks every member of the keywords array (via
jsonb_array_elements_text). The special mode used by the Segment audience menu additionally restricts results to rules with active or inactive status that were created within the last 3 months.
Related menus in this journey
- Rich message — the content this rule sends. If the attached Rich message is deleted, the rule drops out of the answering table immediately; the list shows a "Deleted" tag and the edit form forces you to pick a new one.
- Quick reply — the keywords of active rules here surface as suggestions in that menu's "message" button text field, because text a chip sends into the chat really can trigger a rule here.
- Template message — likewise, active keywords surface as suggestions in the "send a message" button's text field.
- LINE OA settings — the message-handling priority decides whether rules here run at all. Set to
ai_classifier_only, none of them are triggered (the list page shows a warning banner). - Segment audience — can use rules from this menu as an audience condition, seeing only rules created within the last 3 months.
Things to watch out for
- If the bot still replies with the old content after an edit, wait a moment — the actual reply happens in a background service reading the Redis table. That table is rewritten immediately on save, but if the timing is unlucky, the 5-minute cron is there as a second safety net.
- A rule attached to a deleted Rich message goes completely silent, with no error anywhere — the switch is still on and the rule is still listed, but nothing answers. Check the tag in the Rich message column first whenever someone reports "the bot isn't replying."
- Keywords must match in full — a user typing "opening hours please" won't match the keyword "opening hours." To cover several phrasings, add multiple keywords to one rule (it answers on any one of them).
- If the LINE OA's message-handling priority is
ai_classifier_only, no rule in this menu works at all — the banner on the list page is the only signal, and it's dismissible; once dismissed, nothing warns you again. - The 30-character keyword cap is frontend-only — the backend never re-validates it.
- The status switch route performs no validation — normal use through the UI is safe, but calling that API yourself risks writing a value the system doesn't recognize into the database, which silences the rule while the screen may show something else.
- A duplicate rule title is rejected per-field — the response is a 400 pointing at the
namefield with "Title is already exist." - Nothing verifies the selected Rich message belongs to your own OA — in practice the UI only offers your own OA's list, but there's no guard at the API level.
Access control (verified against the source)
- Frontend — both the list and form pages are wrapped in a
PermissionGuardchecking theVIEWaction on theauto-responsesubject. - Backend — all 8 routes in this module are wrapped in a real, enforced
ModuleGate(d, "auto-response")(like the Rich message, Template message, and Campaign modules, and unlike Quick reply, which has no ModuleGate at all). The code comment states the behavior precisely: a customer-type caller whose org has the "auto-response" module closed by the platform admin gets a 403, while internal (onemoby) callers bypass it. - The header comment in
controller.gorecords that the controller-level@Public()in the original system is commented out, so every route runs through the JWT check — there are no public routes in this module. - Action-level policy annotations (
create/read/readAll/update/delete) are all declared but run through the same no-op middleware as most other modules — the real enforcement is the module-level ModuleGate. DELETE /api/auto-response/:id/hardrequires Super Admin plusconfirm=true(a value that isn't literallytrue/falseis rejected with "Validation failed (boolean string is expected)") — and as noted above, this hard delete is not restricted to the caller's own OA.- Cross-tenant isolation is enforced on every other route — reads, lists, updates, status flips, and soft deletes all pin the caller's
line_oa_id. Requesting an id that isn't yours returns a 400APP_007; a delete that finds no target returns a 400AUS_102.
Source file reference
Frontend (cms-web-2026)
src/app/auto-response/page.tsx— list entry point,PermissionGuard, and the LINE OA Message Handling read that drives the warning bannersrc/app/auto-response/form/page.tsx— form entry point (create/edit/duplicate/view)src/components/auto-response/list/auto-response.container.tsx— table, status switch, the Rich message status tagsrc/components/auto-response/form/auto-response-form.tsx— the tag-style keyword field, the debounced duplicate check, the notification linking to the colliding rulesrc/components/auto-response/form/auto-response-form.container.tsx— clearing a deleted Rich message when the edit form openssrc/services/auto-response.service.ts— every endpoint the frontend callsmessages/th/auto-response.json,messages/en/auto-response.json— all UI copy
Backend (line-management-cms-api-go)
internal/modules/autoresponse/controller.go— all 8 routes,ModuleGate("auto-response"), the hard-deleteSuperAdminGuard, the unvalidated status routeinternal/modules/autoresponse/service.go— the wholesale Redis keyword-table rebuild, dropping rules with unusable Rich messages,IsKeywordExistsinternal/modules/autoresponse/dto.go— inbound validation rules and the JavaScript-style coercion behaviorinternal/modules/autoresponse/query.go— list parameter binding and cache-key constructioninternal/modules/richmessage/service.go— the keyword-table purge when a Rich message is deleted
Worker (line-management-worker-go)
internal/autoresponse/service.go— reading the keyword table at runtime, the cache-aside lazy warm, the AI hand-off, merge tag substitutioninternal/cronscheduler/auto_response_sync.go— the 5-minute cron that syncs every OA's keywords
See the documentation team's earlier technical write-up at Auto Response