Skip to main content

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.

What this document is

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

SourceScopeWindow
jodgig_2026_clean MySQL dump — jod_jobs (447,037 rows), job_templates, job_typesAll JodGig gig postingsup to 2026-03-10 (dump end)
JodApp production export: Listings::Job per Taxonomy::Category (848 listings)jodapp.com live listingsas of 2026-07-09
JodApp production export: employer sign-ups per weekIdentities::User with an Org::Membership2025-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 liveCategory schemes of incumbent portalsverified 2026-07-09
Code audit: jodapp-web + jodapp-api, main branchesThe picker, the API endpoint, the form2026-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:

  1. 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.
  2. The frontend never sends an ordering, so rows come back in seed order. The seed file starts with the tech block.
  3. 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.

Evidence: Strong

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 when order_by absent: 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_types labels 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.

Evidence: Strong

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

PortalCountStructureOrderingWorth copying / avoiding
FastJobs (SG blue-collar)25 + OthersFlatAlphabeticalThe 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)FlatAlphabetical, Others pinned last in codeEven the government shows employers a friendly custom list — SSOC codes live in a separate backend field.
JobStreet SG (SEEK)30 top-level2-levelAlphabeticalWhite-collar heavy; 2 levels force vague parent names. Not our model.
Snagajob (US hourly)27FlatAlphabetical + curated "top industries" layerWidens buckets with "&" pairings instead of adding categories.
Indeed Flex (temp app)~5 industries → 17 roles2-level curatedCuratedDeliberately tiny list works at their scope.
Workclass (SG blue-collar)14 (2023)FlatPopularity subset firstDefunct — domain now redirects to an unrelated site. Their unpoliced "Other Jobs" filled up with misfiled jobs: the anti-pattern.
Indeed0 employer-facingML from free-text titlen/aOnly 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:

  1. Food & Beverage
  2. Retail & Supermarket
  3. Hotels & Hospitality
  4. Cleaning & Housekeeping
  5. Warehouse & Logistics
  6. Drivers & Delivery

Block B — alphabetical:

  1. Accounting & Finance
  2. Admin & Office
  3. Beauty & Wellness
  4. Construction & Renovation
  5. Customer Service
  6. Events & Promotions
  7. Healthcare
  8. Manufacturing & Production
  9. Repair & Maintenance
  10. Sales & Marketing
  11. Security

Block C — pinned last:

  1. 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)

  1. 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.
  2. 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".
  3. 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)

  1. 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.
  2. 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.
  3. 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.
  4. Use the employer's words. "Drivers & Delivery", not "Transportation". Copy names from where SG employers already are (FastJobs, MCF).
  5. Flat beats nested at this size. Two levels force vague parent names that confuse more than they help.
  6. 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):

  1. Industry/function level — never a seniority ("Managers") or a schedule ("Part-time/Temp"); employment type is a separate field.
  2. Employer words, matching FastJobs/MCF naming where they agree.
  3. 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.
  4. 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; add display_order and an is_active flag. The index endpoint serves active categories in display_order, no pagination for this consumer.
  • Seed the 18, remap existing careers_jobs references (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-field replacing the category dropdown; renders in API order, never re-sorts.
  • Shared SelectSearchField fixes 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, || true edit-mode defaults) — planned as their own GitHub issue.

Open decisions

  1. Ratify the 18-category set and the three judgment calls.
  2. Who owns the weekly "Others" triage.
  3. On edit, does the API return the remapped category so the grid pre-selects correctly?
  4. 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.