Job categories employers can actually find
The initiative in one sentence: replace the 89 generic categories behind a broken dropdown (employers could only ever see 10 of them, all tech roles) with ~18 categories grounded in who our clients are and what Singapore job portals already do — shown as one fully visible list, so no employer ever has to guess that a search box exists.
This is the compiled research and the proposal for the job-category redesign, researched on 2026-07-09. It exists so that:
- The category list can be defended with real data, not opinion.
- The rules for adding, naming, and merging categories outlive this one decision.
- The work can be broken into small PRs, each tracing back to a finding on this page.
Status: proposal. The 18-category set and the three judgment calls below are pending ratification.
Where the data comes from
| Source | Scope | Window |
|---|---|---|
jodgig_2026_clean MySQL dump — jod_jobs (447,037 rows), job_templates, job_types | All JodGig gig postings | up to 2026-03-10 (dump end) |
JodApp production export: Listings::Job per Taxonomy::Category (848 listings) | jodapp.com live listings | as of 2026-07-09 |
| JodApp production export: employer sign-ups per week | Identities::User with an Org::Membership | 2025-08 → 2026-07 |
| Portal verification: MyCareersFuture live API + production JS bundles; FastJobs via Wayback (2026-02-03, consistent with Dec-25/Jan-26); JobStreet via Wayback; Snagajob, Indeed Flex, Workstream live | Category schemes of incumbent portals | verified 2026-07-09 |
| Code audit: jodapp-web + jodapp-api, main branches | The picker, the API endpoint, the form | 2026-07-09 |
The problem
Two employers could not pick a category
Two support cases (example: Changi Cove) where an employer posting a job did not find a fitting category. They did not realize the dropdown hides a search box, and the visible options had nothing to do with their business. Our reply was "click the search field and type" — that is us explaining a workaround, not a fix.
Root cause — employers literally see only 10 of 89 categories
The dropdown is broken in three stacked ways:
- The frontend never sends a page size, so the API returns its default of 10 items. The dropdown has no pagination, no "load more" — it fetches page 1 and stops.
- The frontend never sends an ordering, so rows come back in seed order. The seed file starts with the tech block.
- The search input only appears inside the opened menu; nothing tells the user the list is incomplete.
So a restaurant or hotel employer opens "Job Category" and sees exactly this, with no hint there is more: Software Engineering, Data Science, DevOps & Infrastructure, Mobile Development, Web Development, QA & Testing, IT Support, Cybersecurity, Hardware Engineering, AI & Machine Learning.
The 89 categories themselves were seeded as a generic list before we knew our clientele — heavy on white-collar roles our employers never post.
Verified in code, 2026-07-09:
- Component fetches with no
page_size/order_by:jodapp-web/app/domains/taxonomy-category/taxonomy-category-select-field.jsx. - API default page size 10:
BaseIndexRequest::DEFAULT_PAGE_SIZE; ordering skipped whenorder_byabsent:jodapp-api/app/domains/taxonomy/categories/shared_index_manager.rb. - Seed order:
jodapp-api/db/seeds/development/taxonomy_category_seed.rb(tech block first). - Search is substring-only (
name ILIKE %q%), results also capped at 10.
What our data says
One key distinction first
JodGig is daily temp work — businesses topping up manpower shift by shift. A JodApp Careers::Job is a company hiring full-time/part-time staff. So JodGig volume tells us who our clients are (their industries), not which roles they will post full-time. A supermarket buying 50,000 temp cashier shifts may post few full-time cashier jobs — but will post store supervisors and department leads.
The client mix (447k JodGig jobs, normalized to 2,411 distinct titles)
- F&B is the widest client base: 59 companies active in the last 12 months, 141 all-time. Titles: waiter/waitress, service crew, food runner, bartender, kitchen roles.
- Retail & supermarket is the biggest volume (52.5% of the last 12 months) but concentrated: ~17 recent companies, dominated by one supermarket chain's cashier/retail-assistant titles.
- Then: warehouse/logistics & packing (3.4%), healthcare support (2.5%, essentially 3 recent companies), admin & customer service (1.2%), events & promotions (0.7%, but 39+ companies all-time), cleaning (thin volume, 24 companies all-time).
- JodGig's own
job_typeslabels are unreliable (clerical jobs filed under "Events", F&B under "Hotel", "Others" was 88% defunct COVID contact-tracing). Titles are the real signal. - A 12-category set drawn from these titles covers 99.93% of the last 12 months of volume.
Cross-check against JodApp production
834 of the 848 live listings (98.3%) map cleanly onto the proposed categories. The unmapped 1.7% are white-collar one-offs (Web Development, QA & Testing, Corporate Finance…) that belong in "Others". Nothing our employers actually post is lost.
Analysis of jodgig_2026_clean, 2026-07-09. Dump ends 2026-03-10, so "last 12 months" = 2025-03-11 → 2026-03-10 (102,386 jobs). Volume ranked by distinct posting companies, not raw job count, because one chain's shift volume would otherwise drown the signal. Category classification was rule-based over normalized titles; 0.04% of recent volume resisted classification.
Because JodApp is full-time hiring, the market research carries more weight
Full-time hiring pulls in roles that barely exist as daily gigs: cooks and chefs (kitchen brigades), security officers (classic full-time agency employment in Singapore), technicians, outlet managers. Our gig data is structurally blind to these. Where portals consistently carry such a category, that justifies it even with thin gig data.
What the market does
| Portal | Count | Structure | Ordering | Worth copying / avoiding |
|---|---|---|---|---|
| FastJobs (SG blue-collar) | 25 + Others | Flat | Alphabetical | The closest comparable. Note: their Oct-2025 taxonomy revamp was rolled back within ~2 months — renaming live categories is risky. Their "Others" absorbs ~3–4% of listings. |
| MyCareersFuture (SG govt) | 43 (42 + Others) | Flat | Alphabetical, Others pinned last in code | Even the government shows employers a friendly custom list — SSOC codes live in a separate backend field. |
| JobStreet SG (SEEK) | 30 top-level | 2-level | Alphabetical | White-collar heavy; 2 levels force vague parent names. Not our model. |
| Snagajob (US hourly) | 27 | Flat | Alphabetical + curated "top industries" layer | Widens buckets with "&" pairings instead of adding categories. |
| Indeed Flex (temp app) | ~5 industries → 17 roles | 2-level curated | Curated | Deliberately tiny list works at their scope. |
| Workclass (SG blue-collar) | 14 (2023) | Flat | Popularity subset first | Defunct — domain now redirects to an unrelated site. Their unpoliced "Other Jobs" filled up with misfiled jobs: the anti-pattern. |
| Indeed | 0 employer-facing | ML from free-text title | n/a | Only viable at Indeed scale. |
Seven of seven portals carry F&B, Retail, Hospitality, Warehouse/Logistics, and Admin. Every SG portal that forces a single pick carries an "Others".
The proposal: 18 options (17 named + Others)
Single-select, all options visible at once — never paginated. Ordering is deliberate: most-used block first (our client industries), then alphabetical, Others always last.
Block A — client-mix order:
- Food & Beverage
- Retail & Supermarket
- Hotels & Hospitality
- Cleaning & Housekeeping
- Warehouse & Logistics
- Drivers & Delivery
Block B — alphabetical:
- Accounting & Finance
- Admin & Office
- Beauty & Wellness
- Construction & Renovation
- Customer Service
- Events & Promotions
- Healthcare
- Manufacturing & Production
- Repair & Maintenance
- Sales & Marketing
- Security
Block C — pinned last:
- Others — picking it reveals one optional free-text field: "What kind of job is this?" That text feeds our category-request inbox.
Deliberately deferred (first add-on-request candidates): Education & Training, Human Resources, IT & Tech, Insurance, Real Estate.
The three judgment calls (pending ratification)
- One "Food & Beverage", not a front-of-house/kitchen split. All 7 portals run a single F&B category. A restaurant hiring full-time thinks "hiring for my restaurant" whether it is a server or a cook. The split in our gig data is a gig-marketplace artifact.
- Six categories included on market consensus despite thin gig data: Accounting & Finance, Beauty & Wellness, Construction & Renovation, Manufacturing & Production, Repair & Maintenance, Sales & Marketing. This is the temp-vs-full-time distinction cashing out (Security is the clearest case: near-zero gig volume, yet standalone on FastJobs, MCF, and Snagajob). Cutting these six gives a stricter 12-category set; we recommend keeping them because the failure we are fixing is "employer cannot find a category".
- Events & Promotions stays in even though it is thin as full-time hiring — 39+ existing JodGig client companies are events businesses, and they are in the migration cohort.
Why 18 works (the principles, in plain words)
- People recognize, they don't recall. A visible list of 18 is a reading task, not a memory task. The "max 7 choices" rule is about memory — it does not apply here.
- The whole list must be visible at once. If you cannot see all options, you cannot know whether the right one exists. That is exactly what broke. Never paginate a picker.
- Order by what is likely, not A-to-Z. First-time employers do not know our category names, so alphabetical ordering does not help them. Most-used first does.
- Use the employer's words. "Drivers & Delivery", not "Transportation". Copy names from where SG employers already are (FastJobs, MCF).
- Flat beats nested at this size. Two levels force vague parent names that confuse more than they help.
- Always give an exit, and put it last. Without "Others", people forced to pick will pick something wrong — and wrong data looks valid forever.
The picker design
Pattern: an always-visible grid of selectable category cards (radio-card grid) embedded in the form. The dropdown and its search box are removed for this field.
- The incident was caused by hidden options; any dropdown still hides options behind a click. A grid shows all 18 on page load — nothing to discover, nothing to type.
- Two columns, cards min-height 44px. On mobile this means large tap targets and no keyboard popping over a menu. Radio semantics (one tab stop, arrow keys).
- No "Popular" label and no divider between Block A and Block B — the ordering works invisibly; a label is one more thing for non-native English speakers to parse. Exactly one divider: before Others.
- Others selected → the optional "What kind of job is this?" field appears indented under it, with helper text "This helps us add new categories in the future."
- Search survives only in the shared select component, for genuinely large datasets (companies, geo areas).
Mockups: Pencil frames category-picker/01-default, 02-selected, 03-others-expanded, 04-error, 05-mobile-360.
Fixes that belong to the shared select component (separate work, 11 other pickers benefit)
Raise the 200px menu height; show "Showing first 10 — type to narrow down" when results are capped; per-picker search placeholders; don't autofocus the search input on touch devices; add combobox semantics and arrow-key navigation.
Keeping the taxonomy healthy
Escape-hatch policy. "Others" always exists, always last, never sorted inline. At our volume (tens of postings a month), ops reviews every "Others" posting weekly: recategorize misfits, log the rest as demand. Health metric: "Others" above ~5% of postings for a quarter means the taxonomy has a gap (FastJobs runs ~3–4%).
A request becomes a category when any one holds:
- 3+ distinct employers asked for the same concept, or
- 5+ "Others" postings of the same kind accumulated, or
- a strategic client's onboarding needs it (logged judgment call).
These thresholds are deliberately small because our volume is small; revisit at 10x volume.
Naming checklist (all must pass):
- Industry/function level — never a seniority ("Managers") or a schedule ("Part-time/Temp"); employment type is a separate field.
- Employer words, matching FastJobs/MCF naming where they agree.
- Widen before adding: can an existing category absorb it with an "&" pairing? Add only if two reasonable employers would disagree today on where the job goes.
- Must hold crew-to-manager for its industry ("Food & Beverage" fits both service crew and restaurant manager).
Anti-sprawl guardrails: soft cap 20 (past that, type-to-filter becomes mandatory), hard cap 25 (adding requires merging or retiring). One owner approves every add/rename/merge. Renames and splits need a migration plan for existing jobs first — FastJobs shipped a rename revamp and rolled it back within two months.
Implementation sketch
API (jodapp-api):
- Keep
taxonomy_categories; adddisplay_orderand anis_activeflag. The index endpoint serves active categories indisplay_order, no pagination for this consumer. - Seed the 18, remap existing
careers_jobsreferences (98.3% map cleanly; the rest go to Others), deactivate the old rows — foreign keys never break. - The unused hierarchy columns (
parent_id,path) stay unused.
Web (jodapp-web):
- New
taxonomy-category-radio-grid-fieldreplacing the category dropdown; renders in API order, never re-sorts. - Shared
SelectSearchFieldfixes as a separate PR (blast radius: 11 pickers).
Found during the form review, tracked separately:
- The job-title regex rejects
&and/— blocking "F&B Service Crew" and "Waiter/Waitress". The characters were removed because titles feed job-detail URLs; whether slug generation already normalizes them is under investigation. - Four form defects (error messages contradicting rules, dead react-hook-form v6 API references, first-keystroke validation,
|| trueedit-mode defaults) — planned as their own GitHub issue.
Open decisions
- Ratify the 18-category set and the three judgment calls.
- Who owns the weekly "Others" triage.
- On edit, does the API return the remapped category so the grid pre-selects correctly?
- Localization: check the longest Chinese/Malay/Tamil label lengths before locking the 2-column grid.
From this page to GitHub
Each work item above becomes a small issue (Context → Problem → Direction), linking back to this page: (1) API — categories data + endpoint, (2) Web — radio-grid picker, (3) Web — shared select fixes, (4) Web+API — title character rules pending the slug investigation, (5) Web — form defect fixes.