Skip to main content

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​

  1. Click Create response to open the two-column form.
  2. 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.
  3. 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.
  4. 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.
  5. Pick the Rich message to reply with from those available in the Rich message menu.
  6. Check the preview in the right column, rendered as a real chat screen.
  7. 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 mapping keyword → {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 = active for 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 HGET to 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-exists returns 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 dereferences res[0].id on an empty array, producing a TypeError, so the endpoint answers 500 carrying the verbatim JavaScript message Cannot 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 its catch block, which only writes a console log. Separately, the response field named richMessageId actually 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/status takes the status value 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 than active implicitly 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 draft or failed to the create/update routes passes, despite the message claiming only two are allowed.
  • A non-string status triggers a 500 before any other field is validated — the system preserves class-transformer's behavior: every transform runs before all validation. So sending status as a number or an object returns a 500 with the verbatim JavaScript message text.toLowerCase is not a function, masking every other field error in the payload.
  • keywords and richMessageId validation is far looser than the UI enforces — keywords has 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). richMessageId is 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 return instead of continue, 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 stamps deleted_date in a second statement. DELETE /:id/hard, meanwhile, deletes on the id predicate alone with no line_oa_id filter, so it can remove any OA's rule by id (gated only by requiring Super Admin). Without confirm=true it's rejected with a 400 whose message is the raw string ErrorResponse.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.
  • 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 name field 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 PermissionGuard checking the VIEW action on the auto-response subject.
  • 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.go records 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/hard requires Super Admin plus confirm=true (a value that isn't literally true/false is 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 400 APP_007; a delete that finds no target returns a 400 AUS_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 banner
  • src/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 tag
  • src/components/auto-response/form/auto-response-form.tsx — the tag-style keyword field, the debounced duplicate check, the notification linking to the colliding rule
  • src/components/auto-response/form/auto-response-form.container.tsx — clearing a deleted Rich message when the edit form opens
  • src/services/auto-response.service.ts — every endpoint the frontend calls
  • messages/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-delete SuperAdminGuard, the unvalidated status route
  • internal/modules/autoresponse/service.go — the wholesale Redis keyword-table rebuild, dropping rules with unusable Rich messages, IsKeywordExists
  • internal/modules/autoresponse/dto.go — inbound validation rules and the JavaScript-style coercion behavior
  • internal/modules/autoresponse/query.go — list parameter binding and cache-key construction
  • internal/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 substitution
  • internal/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