Skip to main content

Settings — User Attribute Setup

Overview​

User Attribute Setup (the backend calls it attribute-master) is where you declare the central schema for the custom attributes attached to each OA's LINE users, up front — what fields you store, what data type each one is, and which ones can be used to filter an audience or shown in a report.

There are two levels worth keeping straight: an attribute set (attribute master) has a name, description, and status; inside it live the fields (attributes), which can form a tree. The system references a field by its key (auto-generated, never shown, never editable), and humans read it by its label.

Who uses it — administrators designing the customer data structure before real data collection starts (this module carries a real, enforced ModuleGate on the backend — see Access control below).

User journey​

1. Review the attribute sets​

Open Settings > User Attribute Setup. There are two filters — search by name, and a status Select (All / Active / Inactive), with the filter state remembered in sessionStorage. The table shows name, description, field count (counting top-level fields only, not children), status, and created date. Click the name to open it read-only, or the pencil icon to edit.

2. Create by hand​

  1. Click Create.
  2. Enter the set's name (required, max 255 characters), a description (optional), and status (defaults to active).
  3. Add fields in the tree schema editor — set the label, choose the data type (string / number / date / boolean / array / object), and flip the filterable and displayInReport switches as needed.
    • array/object fields cannot have either switch on (both are disabled).
    • A newly added field defaults to filterable = on, displayInReport = on.
    • The add-field button only adds at the top level — this editor has no way to create a child field at all. Tree-shaped fields can only arrive via JSON import or from existing data.
  4. Save. The frontend checks two things before submitting: every field must have a non-empty label, and labels must be unique across the whole tree (compared case-insensitively).

3. Import from JSON​

This path is currently switched off by a feature flag (ATTRIBUTE_JSON_IMPORT_ENABLED = false) — the Import from JSON button never renders, and hitting the URL directly redirects back to the list. The code is fully intact; when enabled the flow is:

  1. Step 1 — enter the name, description, and a root path (optional, for when the attributes you want sit inside a nested object, e.g. RetrieveAgentDetailsResponse.RCMSAgent), then paste a sample JSON.
  2. Click Parse & Continue — the frontend JSON.parses it for a basic syntax check and then hands it to the backend, which does all of the schema conversion. The frontend infers nothing itself.
  3. Step 2 — review and adjust the labels / data types / switches the system inferred, then Save.

4. Use it in other menus​

  1. Open Segment audience > Multi-source filter and pick a custom-attribute source — the system calls GET /attribute-master/filterable, which returns 10 built-in system attributes first (Display Name, First Name, Last Name, Email, Mobile No, Language, Follow Status, User Type, Registered Date, Last Activity Date), then the filterable custom attributes, labeled [Set name] label with the path Set name.path.
  2. Open All friends — attributes with displayInReport on become extra table columns automatically (via GET /attribute-master/reportable, which returns only custom attributes, no built-ins). The friend detail modal reads the full list instead, and additionally computes "orphaned attributes" for you — keys that still have data on the user but no longer exist in any schema.
  3. Open Workflow — the node config panel fetches the full list filtered to active sets only (the only consumer that filters by status), and can create/edit attributes right in the panel without coming back here.
  4. Open Friends Track — fetches the full list (capped at the first 100 sets) and builds options from top-level fields only, as Set name.key.

Key details verified against the source​

Confirmed from internal/modules/attributemaster/{controller,service,dto,parser}.go and internal/modules/planlimits/service.go in line-management-cms-api-go:

  • An attribute's key is immutable for life, and the backend genuinely enforces it — the frontend editor's own comment says the key is an internal id that is "deliberately NOT editable or shown," and the backend re-checks by diffing the incoming schema against the stored one: if it finds a field whose old label survives but whose key changed, it answers 400 with Attribute keys cannot be changed. "<label>" already exists — edit its label or delete it instead of changing its key.
  • Label and key uniqueness is checked across attribute sets for the whole OA, not just within one set — and this is exactly where the frontend check falls short: the UI only validates within the set being edited, while the backend walks every active set in the same OA and rejects with Attribute key "<key>" already exists in attribute master "<other set>" or Label "<label>" already exists in attribute master "<other set>". So a form can pass every frontend check and still 400 on save.
  • Set names must be unique too — checked on both create and rename, answering Attribute master with this name already exists.
  • The plan quota is real, and it counts every field at every level — AssertAttributeQuota runs on both create and schema update, counting fields in flattened form (i.e. every child at every depth is counted, not just the top level as the list table's number suggests), adds that to what already exists, and on overflow answers 400 with Plan limit reached: this would use N of M attributes. Please upgrade your plan. The quota is scoped per LINE OA when the token carries a lineOaId (the normal case) and per organization otherwise; -1 means unlimited. The frontend never displays the quota number anywhere, so the first time you see this message is when a save is refused.
  • object/array types are stripped from the filterable/reportable results server-side too — it doesn't rely on the frontend disabling the switches. So a field that had a switch on while it was a string and was later changed to object keeps filterable: true sitting in the schema, but is never handed out to other menus.
  • The switch defaults disagree across the 3 places you can create an attribute — this editor sets filterable: true, displayInReport: true; the backend's JSON parser sets filterable: true, displayInReport: false; and the Workflow node config panel sets both to false. Always re-check the switches after creating an attribute from anywhere other than this page.
  • The JSON parser infers the data type from the sample value and the label from the key name — a string starting with the YYYY-MM-DD pattern (with or without a trailing time) is judged date, numbers are number, booleans are boolean, and array/object values are walked to build child fields (an array builds its children from its first element and appends [] to the path). Labels come from Title-Casing the key name (splitting on camelCase and _), so agentName becomes "Agent Name".
  • The pre-delete impact check uses text search over whole JSON blobs, so false positives are easy — GET /attribute-master/:id/check-attribute-usage counts 4 things: users carrying the key in custom_attribute (a jsonb existence check, so accurate), trigger rules (comparing source_config->>'key' to custom.<key> exactly, so accurate) — but audiences and rich messages use LIKE '%<key>%' over the entire JSON text. A short or generic key (id, name, type) will match nearly every audience and rich message, so the warning dialog shows a long list of unrelated items.
  • The pre-delete warning dialog never blocks anything — the confirm button reads "Delete Anyway" and is never disabled under any condition. Worse, if the impact check itself fails, the frontend swallows the error and fabricates an all-zero result — "the check failed" is visually identical to "nothing uses it." And when the attribute set isn't saved yet (or you're in the JSON import flow), the field is deleted immediately with no check and no dialog at all.
  • Deleting an attribute set is a soft delete with no impact check whatsoever — DELETE /attribute-master/:id only verifies the row exists, then soft-deletes. It never calls checkAttributeUsage (that check exists only for deleting an individual field in the editor).
  • Passing an unknown column name as orderBy returns 500 on purpose — the code reproduces TypeORM's EntityPropertyNotFoundError behavior rather than splicing the string into an ORDER BY, with the security reason stated in the comment.
  • Saving preserves the incoming JSON's key order — the controller decodes the body order-preservingly so the field order in the stored jsonb schema matches what the client sent (matching the TS original's JSON.stringify behavior). The order you see in the editor is therefore the order those fields appear as report columns.
  • POST /attribute-master/import-json has no caller in the web app at all — the actual UI import path uses POST /attribute-master/parse-json for the preview and then submits the edited schema through the ordinary POST /attribute-master. A side effect: the import path never runs the frontend's empty/duplicate label validation (that check lives only in the manual create form's onFinish) — it relies entirely on the backend check.
  • Segment audience — reads GET /attribute-master/filterable when a custom-attribute source is picked, and derives its operator list from the selected attribute's data type.
  • All friends (report) — reads GET /attribute-master/reportable to build extra columns automatically; the friend detail modal reads the full list so values can be edited (except object/array types).
  • Workflow — reads the full list filtered to active sets, and can create/edit attributes from within the node panel.
  • Friends Track — reads the full list (first 100 sets) and builds options from top-level fields.
  • Trigger rule — verified to not read attribute-master directly; it resolves a condition's data type from merge tags (GET /template-message/merge-tags) instead. This page's pre-delete check does, however, count trigger rules whose source_type = attribute_change and whose key matches custom.<key>.
  • Form builder (Field Mapping) and BigQuery Sync (field mappings) — both use the same custom. namespace but neither reads its definitions from this menu: Form builder reads merge tags, and BigQuery Sync is a free-text input with a custom. prefix attached — no picker, and no validation that the key actually exists.
  • Import Data (import-mapping) — verified to not read attribute-master; it gets its target field list from its own GET /import-mapping/fields.

Things to watch out for​

  • Deleting a field on a saved record shows you the impact (users holding a value, trigger rules, audiences, rich messages that reference it), but it is decision support only — it never blocks the delete. And the audience / rich-message numbers have a high false-positive rate when the key is short or a common word.
  • The list table's "field count" only counts top level, while the plan quota counts every level — a set imported from deeply nested JSON always consumes more quota than the table implies.
  • Labels must be unique across every attribute set in the OA, not just within one set. If you hit a 400 the frontend never warned you about, check whether another set already uses that label.
  • Keys are immutable for life; the only way out is deleting the field and creating a new one, which orphans the data of every user tied to the old key (the warning dialog uses the word "orphaned" outright).
  • This editor cannot create a child field at all. Tree structures require the JSON import path — which is currently switched off by a feature flag.
  • Changing a field's type to object/array doesn't clear the old filterable/displayInReport switch values, but the backend stops handing that field to other menus. If an attribute vanished from the audience filter options, check its data type first.
  • This page and its form have no frontend PermissionGuard at all, and the sidebar entry isn't permission-filtered either (unlike OTP Verification, which is). The only real protection is the server-side ModuleGate.

Access control (verified against the source)​

  • Frontend — there is no PermissionGuard on either /attribute-setup or /attribute-setup/form; both files were verified to export the component raw, and the sidebar entry is an unconditional member of settingsChildren. A mapping 'attribute-master' → [MODULE_URL.ATTRIBUTE_SETUP] exists but nothing consumes it for this route.
  • Backend — every route is wrapped in a real, enforced ModuleGate("attribute-master"), applied via g.Use(...) at the group level so all 10 routes are covered without question. If a platform admin closes this module for an organization, customer-type users get a 403 immediately even calling the API directly (onemoby-type callers bypass, and the gate fails open on a lookup error).
  • Action-level policy annotations are declared on all 10 routes (readAll, read, create, update, delete on the AttributeMaster subject) — more than Form builder's 2 — but every one of them runs through the same no-op middleware documented in internal/modules/auth/policy.go. The real enforcement is the module-level ModuleGate, not the action-level policy.
  • Actual data scoping happens at the repository layer — every query filters by the caller's organization_id and line_oa_id, with soft deletes filtered automatically. checkAttributeUsage scopes by line_oa_id alone.
  • No route is @Public(); all sit behind the global JwtAuthGuard, and none requires Super Admin.

Source file reference​

Frontend (cms-web-2026)

  • src/app/attribute-setup/page.tsx, src/app/attribute-setup/form/page.tsx — entry points (no PermissionGuard) and the JSON-import route guard
  • src/components/attribute-setup/attribute-setup.flags.ts — the ATTRIBUTE_JSON_IMPORT_ENABLED feature flag
  • src/components/attribute-setup/list/attribute-setup.container.tsx — table/filters and the top-level field count
  • src/components/attribute-setup/form/attribute-setup-form.container.tsx — label validation and the 2-step JSON import flow
  • src/components/attribute-setup/form/components/attribute-editor.tsx — the tree editor, switch-disable rules, new-field defaults
  • src/components/attribute-setup/form/components/attribute-delete-warning-modal.tsx — the pre-delete impact dialog
  • src/services/attribute-master.service.ts — every endpoint the frontend calls

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

  • internal/modules/attributemaster/controller.go — all 10 routes, ModuleGate("attribute-master"), order-preserving body decoding
  • internal/modules/attributemaster/service.go — cross-set key/label checks, the quota call, checkAttributeUsage, /filterable and /reportable
  • internal/modules/attributemaster/parser.go — data-type inference, key→label conversion, child-field traversal
  • internal/modules/attributemaster/dto.go — inbound validation rules
  • internal/modules/planlimits/service.go — AssertAttributeQuota and the flattened field count

See the documentation team's earlier technical write-up at Attribute Setup