Skip to main content

Segment audience — Filter audience

Overview​

Filter audience creates a target group from a single condition, chosen from 5 data sources — campaign, Rich menu, activity tag, Auto Response, or a form — instead of uploading a list yourself. Groups created this way (and via Multi-source filter) are the only ones in the system that support scheduled member recalculation (auto refresh) and on-demand recomputation.

The sharpest difference from Upload audience: creating a group from conditions is fully synchronous within one request — no queue is involved at all. The backend computes the list, creates the row, wires up members, writes the CSV to object storage, and closes the status as completed before responding. A successfully created group is usable immediately (see Key details).

Who uses it — marketing teams segmenting customers by real behavior, e.g. people who clicked a campaign, tapped a menu, or answered a form.

User journey​

1. Review the condition-based groups​

Open Segment audience > Filter audience. This list reuses the same container as Upload audience but pins dataSource=filter, so single-source and multi-source groups appear together in one table. The filter bar offers search by name and a status select.

The columns that differ from the Upload page are Auto-refresh and Last synced:

  • The table header carries a badge for the interval configured at the LINE OA level — "Auto-refresh: {minutes} min" or "Auto-refresh: Off", read from GET /line-oa/{id}/audience-refresh-settings.
  • Each row has a switch controlling whether that group participates in the automatic cycle (PATCH /audiences/{id}/auto-refresh) — external-system groups show - instead.
  • Last synced shows Never if never computed, a spinning "Syncing..." tag while refreshStatus = processing, or the last computed timestamp.
  • A circular-arrow "Refresh now" button (POST /audiences/{id}/refresh) is disabled while a computation is in flight.

Other row actions — Copy (opens the Multi-source filter form with action=copy) and Delete, which only appears for groups already in completed status.

2. Create a group from a single condition​

The Create segment button in the table header goes to the Multi-source filter form by default. The single-source form described here is reachable from a link on the form-response page in Form builder, or by typing the URL /audience/filter/form?id=create directly.

  1. Enter an audience name (must be unique within the system).
  2. Pick one of 5 condition types — switching type clears every previously selected value.
    • Campaign — pick a campaign from Campaign, then check the tracking spots you want. There's a "Reach (View)" checkbox for visibility and a per-bubble "Click" checkbox group; tappable-image messages expand into "Spot A/B/…", Flex messages become a table you tick row by row, and plain-text messages are disabled (clicks aren't trackable).
    • Rich Menu — pick the menu type first (guest / member / custom / switch), then the menu itself from Rich menu, then check its spots — only Link, Text, and Switch action types are selectable (the others record no clicks).
    • Activity tag — check any of 4 activity windows: active-today, active-in-week (last 7 days, excluding today), active-over-week (over 7 days), inactive-over-30-day (inactive for over 30 days).
    • Auto Response — add rows selecting rules from Auto-response message, up to 5 rows (or the number of rules that actually exist, if fewer).
    • Form — pick a form from Form builder (active forms only), then build conditions in the "Advance filter" panel — up to 5 groups of 5 conditions each, with a join inside each group and a top-level join between groups.
  3. Set the top-level AND/OR join (defaults to AND) — for the campaign type, switching to OR while Reach is checked clears the selection for you.
  4. Preview the matching list — the first four types recompute automatically on every condition change (no debounce), but only actually fire once a minimum bar is met: a campaign needs Reach or at least one spot, a rich menu needs both a type and a spot, and so on. The form type requires clicking Search yourself, and the preview returns at most 10 rows (a cap enforced in the DTO).
  5. Click create — the form type posts to POST /audiences-filter/create-form-filter, every other type to POST /audiences-filter/create-filter. On success the system returns to the list and the group is ready to use immediately.

3. Let the group keep itself up to date​

  1. Turn on the Auto-refresh switch on that group's row (requires a plan that supports it — it's a premium feature).
  2. The recalculation interval is configured at the LINE OA level, not per group — the row switch only declares whether this group joins that cycle.
  3. The background job recomputes each group as its interval comes due, writes a fresh CSV, updates the member count, and logs who joined and who left, forwarding that delta to fire any triggers wired to the group.
  4. To recompute right now, use Refresh now at any time — both paths land in the same computation logic, differing only in the triggerType recorded in the log (manual vs auto_refresh).

Key details verified against the source​

Confirmed from internal/modules/audiencefilter/{controller,service,dto,multi_source_query_builder}.go and internal/modules/audience/scheduler.go in line-management-cms-api-go:

  • Creating a condition-based group is entirely synchronous, with no queue involved — completely unlike Upload mode. CreateAudienceFilter does every step within the one request: find the users from tracking → insert the audience row as processing → write audience_ids onto every matched user → build a one-column CSV (Line UserID) and upload it to object storage → update the status to completed with its stats → then respond. If the request succeeds, the group is usable straight away with no cycle to wait for. (The "queued for computation" caveat applies only to Upload mode.)
  • You cannot create a group that matches nobody (on these two routes) — if the computation returns zero users, create-filter responds 400 Not found trackingLineUsersTrackingKey and create-form-filter responds 400 No matching users found, leaving no row behind. Multi-source filter, by contrast, will happily create an empty group (see that page).
  • The automatic recalculation is a per-minute cron guarded by a Redis lock — scheduler.go registers cron * * * * *, and each run first claims a lock via SetNX on the key audience:refresh:scheduler:lock with a TTL of 55 seconds, deliberately shorter than the 1-minute interval so the lock expires on its own before the next tick even if the process dies mid-run. Failing to claim the lock exits immediately — which is what makes it safe to run many pods, with exactly one doing the work per tick.
  • Five conditions decide which groups are due — the cron reads LINE OAs where audience_refresh_enabled = true and audience_refresh_interval is set, then within each OA selects groups where auto_refresh_enabled = true and data_source = 'filter' and not soft-deleted and refresh_status != 'processing' and (last_refresh_date IS NULL or older than NOW() - INTERVAL 'N minutes'). It then stamps processing before publishing the job, so the next tick can't pick the same group up again.
  • A stuck processing status stops a group from ever recomputing — both the cron and the Refresh now button test the same condition (the button returns 400 Audience refresh is already in progress). If the worker dies mid-computation, that group is pinned at processing and nothing clears it automatically. A failed status, conversely, does not stick — the cron filters only on != 'processing', so the next tick retries it on its own.
  • The worker calls back in through an internal route guarded by an API key, not JWT — POST /api/audiences-filter/internal/refresh is registered on the public group and wrapped in a guard comparing the x-internal-key header against the INTERNAL_API_KEY env var. If that env var isn't set, the guard rejects everything (the check includes expected == ""), meaning a deploy that forgets the variable silences auto refresh system-wide with nothing surfacing in the UI.
  • The recomputation order was deliberately changed from the original — a source comment explains that the NestJS original removed the members first and only then re-evaluated the conditions, which opened a window where the segment was empty, and a crash during it made that emptiness permanent until the next cycle. The Go port inverts it: evaluate the conditions to get the new list first, then swap the whole membership in a single transaction.
  • Every computation logs the membership change and fires automations off the delta — the system diffs the old and new lists into added and removed, writes them to the audience_member_log table (action add/remove, with triggerType manual or auto_refresh), then publishes an audience-membership-trigger job only when there is an actual delta. A recomputation that changes nothing fires no automation.
  • Both manual refresh and enabling auto refresh are restricted to dataSource = 'filter' — checked in the service on both paths, returning 400 Only filter-based audiences can be auto-refreshed / Only filter-based audiences can be refreshed. Enabling auto refresh must additionally clear AssertAutoRefreshAllowed, a plan-level feature gate.
  • The Rich menu options carry two fixes to the original, each with the reasoning stated in a comment — (1) the menu title in the dropdown now always falls back to the menu's own title, because the original only set a title inside the tracking-data loop, so menus without tracking data came back with no title and the UI displayed raw ids (a QA defect); and (2) spot keys for custom menus were changed to the 3-part form {richMenuId}:0:{actionIndex}, because rich-menu clicks are always stored in a 3-part form — the original's 2-part key never matched any stored click, so filtering by a custom rich menu used to silently return nobody.
  • Not every question in a form is offered as a filter — GET /audiences-filter/form-questions/{formId} strips out the header1, header2, divider, and terms types before responding, since those are decorative elements with no answer to compare against.
  • Form-type membership writes are batched at 5,000 ids — unlike the other types, which write in one shot, since form-based groups can produce very large lists at once.
  • A duplicate name comes back with the field detail attached — every create route in this module responds 400 Title is already exist along with data: [{field: "title", message, value}], so the frontend can bind the message directly to the name field.
  • Campaign — the data source for the campaign condition type, pulled via GET /audiences-filter/list-dropdown-campaign, which accepts a statusCampaign parameter (sent / scheduled / cancel / draft) and returns every tracking spot of the attached Rich message for you to tick.
  • Rich menu — the data source for the Rich Menu condition type, via GET /audiences-filter/list-dropdown-richmenu; the guest/member types read from the menu archive while custom/switch read from the current menus.
  • Auto-response message — the data source for the Auto Response condition type, via GET /audiences-filter/list-dropdown-auto-response.
  • Form builder — the data source for the form condition type, via GET /audiences-filter/list-dropdown-form-builder and GET /audiences-filter/form-questions/{formId}; it is also the only in-app entry point that reaches the single-source form.
  • Multi-source filter — the option when you need to combine multiple conditions. It shares this page's list and detail views entirely, and is where the Create segment button goes by default.
  • Upload audience — the sibling mode using a file instead of conditions; the same table with dataSource = 'csv', and no auto-refresh support.

Things to watch out for​

  • A stuck processing status is a dead end — if a group sits on "Syncing..." abnormally long, both the automatic cycle and the Refresh now button will refuse it forever; the platform team has to reset refresh_status by hand.
  • The auto-refresh interval is one LINE OA-wide value for every group — you cannot have group A recompute every 15 minutes and group B every 6 hours; the only per-group choice is join or don't join.
  • Every recomputation fires automations off the membership delta — if the group is wired to a message-sending trigger, a short auto-refresh interval can re-message customers who cycle in and out. Check the attached triggers before enabling it.
  • The Copy button in the list always goes to the Multi-source form, and that form only hydrates from multi-source groups — copying a single-source group lands you on an empty form with no explanation.
  • The single-source form is hard to reach — no button in the list leads there (every create button goes to Multi-source). Get to it via the link from Form builder or by typing the URL.
  • The create button on the single-source form is enabled more broadly than it should be — it's disabled only once the preview has actually returned an empty array, so before previewing at all, or for the form type (which uses a different query), the button stays clickable and you meet the backend's 400 instead.

Access control (verified against the source)​

This is the sharpest divergence between the two backend modules that make up this menu — verified, and they are not equivalent.

  • The audience module (routes /api/audiences/*) carries a real, enforced ModuleGate — internal/modules/audience/controller.go applies modulegate.ModuleGate(d, "audiences") to the whole group, so every audience route (including the PATCH /:id/auto-refresh and POST /:id/refresh this page uses) returns 403 for customer-type users in an org whose module is closed.
  • The audiencefilter module (routes /api/audiences-filter/*) has no ModuleGate at all — internal/modules/audiencefilter/controller.go registers every route directly on the authed group with nothing but the no-op auth.CheckPolicies(...); there is no modulegate.ModuleGate call anywhere in the file. The consequence: even when a platform admin has closed the audiences module for an organization, the routes that create condition-based groups, list the campaign/menu/form dropdowns, and preview matching users all remain callable with any valid JWT. Closing the module blocks reading, editing, and deleting groups — not creating new ones.
  • The one route in audiencefilter with real enforcement is POST /audiences-filter/internal/refresh, which sits on the public group behind a guard comparing the x-internal-key header to the INTERNAL_API_KEY env var (no JWT involved at all — it's a worker-only entry point). With an empty env var it rejects everything with 401 Invalid internal API key.
  • No audience page on the frontend has a PermissionGuard — the only control is hiding the side-menu entry via ability.can('view', 'audience'); typing the URL directly isn't blocked.
  • Policy annotations on the audiencefilter routes are declared as create/read on the Audiences subject across the board, but run through the same no-op middleware as everywhere else — so there is no action-level permission enforcement anywhere in this module.
  • Every statement is still data-scoped by line_oa_id from the CLS context, so nothing leaks across OAs — what's missing is the module-level gate, not the tenant boundary.

Source file reference​

Frontend (cms-web-2026)

  • src/app/audience/filter/page.tsx, src/app/audience/filter/form/page.tsx — the list page and the single-source form
  • src/components/audience/filter/filter-form.container.tsx, filter-form.tsx, filter-form.util.ts — the 5 condition types, the pre-preview bar (processCheckBeforeSubmit), payload assembly
  • src/components/audience/filter/components/ — section-campaign.tsx, section-rich-menu.tsx, section-activity-tag.tsx, section-auto-response.tsx, section-form-builder.tsx, form-builder-filter-group.tsx, form-builder-condition-row.tsx, common-condition.tsx
  • src/components/audience/csv/csv-table.container.tsx — the table shared with the Upload page, the auto-refresh switch, the Refresh now button
  • src/services/audience-filter.service.ts, src/services/audience.service.ts
  • src/components/audience/constants/audience.constant.ts — TRACKABLE_CAMPAIGN_TYPES, TRACKABLE_RICH_MENU_TYPES

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

  • internal/modules/audiencefilter/controller.go — all 12 routes, no ModuleGate, internalApiKeyGuard
  • internal/modules/audiencefilter/service.go — the synchronous create, uploadAndComplete, RefreshAudience and its reordered logic
  • internal/modules/audiencefilter/dto.go — validation rules, the preview limit cap of 10
  • internal/modules/audiencefilter/tracking_line_users_repository.go, list_repositories.go, form_filter_query_builder.go — every user-finding query and dropdown
  • internal/modules/audience/scheduler.go — the per-minute cron, the 55-second Redis lock, the due-group conditions
  • internal/modules/audience/service.go — UpdateAutoRefresh, TriggerManualRefresh, GetMemberLogs

See the technical reference at Audience Management