Campaign
Overview
Campaign is where the marketing team sends messages out to friends on the LINE OA, either to everyone (Broadcast) or to a specific target group (Multicast). The one thing to understand up front: a campaign doesn't create message content itself — it must reference a Rich message that's already been created and enabled in the Rich message menu.
Who uses it — marketing/admin staff with permission to the Campaign module (this module carries a real, enforced ModuleGate on the backend — see Access control below).
User journey
1. Review the campaign list
Open the Campaign menu. The list always starts on the Sent tab, newest-send-date first. Switch tabs to see Scheduled (nearest date first), Cancelled, and Draft (both sorted by last-edited date). Each tab's date column changes meaning to match (send date / cancelled-on / last-edited), always labeled "(Bangkok time)".
The filter bar has 3 fields — search by name, pick a recipient (pulled from a dedicated endpoint that returns only target groups actually tied to an existing campaign — never an option with nothing behind it; if a real Broadcast campaign exists, "All friends on OA" always appears first), and a date range. Filter results are remembered in sessionStorage while you're on this page.
2. Create and send a campaign
- Click to create a new campaign, opening the form.
- Enter a campaign name — must not duplicate another campaign's name in the same system, capped at 100 characters (that length cap is a frontend-only restriction — the backend API doesn't validate name length at all, only that it isn't empty).
- Choose recipients — send to all friends on the OA (Broadcast), or pick a target group already prepared in Segment audience (Multicast).
- Choose content — pick an enabled Rich message from the list. The system shows a live preview in the chat frame right away, including any Quick Reply attached to it.
- If sending as Broadcast, any Rich message containing a merge tag (e.g.
{{name}}) still appears in the list but shows up disabled, labeled as unusable with "All friends on OA" — and if you switch from Multicast to Broadcast while a merge-tag Rich message is already selected, the system clears that selection automatically and explains why, rather than leaving the Save button mysteriously disabled with no visible cause. - If sending as Multicast and the selected Rich message has a merge tag, an extra checkbox appears: "Skip recipients whose merge tag data is incomplete."
- If sending as Broadcast, any Rich message containing a merge tag (e.g.
- Choose how to send — immediately, scheduled, or saved as a draft.
- Choosing Draft relaxes every other field to optional — only the campaign name is required to save.
- Choosing Schedule pre-fills the date/time as now + 35 minutes (a 5-minute buffer to fill out the rest of the form), but the actual rule is at least 30 minutes from now, checked twice (live as you type, and again on save). If today is selected, hours/minutes already in the past are disabled in the time picker.
- Save and confirm — the confirmation dialog's text and button change to match the chosen send method (Send now / Save campaign / Save as draft). Once confirmed, the system calls
POST /campaign(create/duplicate) orPUT /campaign/{id}(edit).
3. Review results of a sent campaign
- From the "Sent" tab, click a campaign's name (or the "total activity" number in the table) to open its results page — every field in the form is now read-only.
- Review the four summary cards — Sent, Reach, Unique Clicks, Total Clicks. The three after "Sent" are clickable, opening a side panel listing the actual LINE users who did that action, complete with profile picture, name, event count, first/last interaction time, and pagination.
- Drill into the per-message table (Message A, B, C…) to see which message or button got the most clicks — image/video messages show view-count columns; tappable-image/Flex messages show position, data, type, unique clicks, and total clicks (click a number to open the user panel scoped to that specific button/link).
- Plain-text messages state directly that "this message can't track clicks."
4. Manage an existing campaign
- Draft — editable (pencil icon), or deletable (with confirmation).
- Scheduled — cancellable (with confirmation; a cancelled campaign moves to the "Cancelled" tab immediately).
- Any status — always duplicable (opens the create form pre-filled from the original as a template).
Key details verified against the source
Confirmed from internal/modules/campaign/{controller,service,dto,tracking}.go in line-management-cms-api-go:
- Sending immediately is claim-based — clicking "Send now" does not set the status straight to "sent." The backend runs a conditional SQL statement that first flips the status to
sending(UPDATE ... WHERE status NOT IN ('sending','sent')). If no row is affected (e.g. a duplicate concurrent send request, or the job is already in flight), it responds 409 Conflict immediately without publishing a second job — a deliberate guard against double-clicking Send and firing the message twice. - The
sendingstatus doesn't exist anywhere on the frontend — the enum the frontend knows about is onlysent/scheduled/cancel/draft. That means for the brief window between clicking "send now" and the worker finishing and flipping the status tosent, that campaign shows up in none of the tabs (not Scheduled, not Draft, not Sent) — a temporary but worth-knowing quirk if a user wonders where their just-sent campaign went. - Merge-tag detection uses a strict regex — it must be a genuine
{{...}}closed by two actual closing braces ({{name}}is caught; text with mismatched braces isn't caught and gets sent out literally as typed), and the backend re-checks this on save independently, not trusting the frontend's check alone — content with a merge tag saved withcastType = broadcastgets rejected with a 400 "Merge tags are only supported with multicast campaigns" even calling the API directly, bypassing the UI. - The "Sent" figure on the results page isn't always a fixed stored value — if nothing has been recorded yet (
totalRecipient = 0), the backend computes it live: for Multicast, it readstotalSuccessfrom the target group's summary in Segment audience and writes it back onto the campaign row for next time; for Broadcast, it counts distinct users straight from the tracking log. This is why "Sent" can sit at 0 for a while right after sending, until the tracking system has data to work with. - Tracking data uses a hybrid of pre-aggregated and live numbers — click/view counts per message are periodically rolled up into a summary (with an "aggregated as of" cursor), then merged with tracking-log events that happened after that cursor, computed live every time the results page is opened — keeping the page fast even for a campaign with a huge number of recipients and clicks.
- The duplicate-name check has a spot where the backend deliberately behaves differently from the original TS logic — when no name is sent at all during an edit (e.g. cancelling a campaign by sending only
{status: "cancel"}), the system skips the duplicate-name check entirely, so an edit that never touches the title can't be wrongly rejected with "Title is already exist."
Related menus in this journey
- Rich message — must be created and enabled first before a campaign can be built.
- Segment audience — required when sending as Multicast, and also the source of the "totalSuccess" figure used to compute a Multicast campaign's "Sent" count. In the other direction, the "campaign" condition type used to build a target group in that menu only offers campaigns that are sent, and sent within the last 3 months (filtered via a dedicated query parameter the backend provides for exactly that purpose).
- Quick reply — reply buttons attached to the Rich message ride along with the campaign automatically.
- Campaign Planner — a calendar view of this same set of campaigns.
Things to watch out for
- A successful send means "queued," not "delivered" — cms-api never talks to LINE directly, it only publishes a job onto RabbitMQ (queue
line_broadcast_rich_messageorline_multicast_rich_messagedepending on type) for the worker to actually deliver. - The duplicate-send guard described above exists, but nothing on screen tells the user "this is currently sending" — if a campaign vanishes from every tab briefly right after an immediate send, wait a moment and refresh.
- The 100-character campaign-name cap is a frontend-only restriction; the backend never re-validates it.
Access control (verified against the source)
- Frontend — wrapped in a
PermissionGuardchecking theVIEWaction on thecampaign-managementsubject. - Backend — every route in this module is wrapped in a real, enforced
ModuleGate("campaign")(unlike the Dashboard module, whose policy check is a no-op) — if a platform admin has closed the campaign module for an organization, users in that org get a 403 immediately, whether they go through the UI or call the API directly. - Action-level policy annotations (
create/update/delete/read/readAll) are declared but run through the same no-op middleware as most other modules in the system — the real enforcement here happens at the module-levelModuleGate, not at the action-level policy. DELETE /api/campaign/:idis a soft delete (flips status tocancel, then stamps a deletion timestamp); a true hard delete (DELETE /api/campaign/:id/hard) requires Super Admin and aconfirm=truequery parameter, or it's rejected.
Source file reference
Frontend (cms-web-2026)
src/app/campaign-management/page.tsx,src/app/campaign-management/form/page.tsx— list and form entry pointssrc/components/campaign-management/list/—campaign-management.container.tsx(table/tabs/filter),campaign-management-filter.tsxsrc/components/campaign-management/form/—campaign-management-form.container.tsx,campaign-management-form.tsx(merge-tag rules, scheduling rules, results cards),tracking-users-drawer.tsxsrc/services/campaign-management.service.ts— every endpoint the frontend calls
Backend (line-management-cms-api-go)
internal/modules/campaign/controller.go— all 8 routes,ModuleGate, hard-delete'sSuperAdminGuardinternal/modules/campaign/service.go— create/update/delete logic, the pre-send claim mechanism, Redis cachinginternal/modules/campaign/dto.go— inbound validation rulesinternal/modules/campaign/tracking.go— the pre-aggregate + live-tail tracking merge
See the documentation team's earlier technical write-up at Broadcast Campaign Management