There is a specific kind of argument that happens in almost every cannabis post-harvest room in Canada. A trimmer says they worked eight hours. The supervisor says the bag took six. Both are describing the same shift, and neither has a record that can settle it. TrimXTimer is an Android app built to produce that record — and, as of this writing, to be honest about the parts of it that aren't finished yet.
Start with the wage, because that is what the record is for
Be careful about the statistics here, because the obvious ones are misleading. Canada has no standalone statistical category for a cannabis trimmer. Under the federal National Occupational Classification, "cannabis trimmer" is an example job title inside NOC 85103 — nursery and greenhouse labourers, a group that also covers greenhouse labourers, plant propagators, and Christmas tree shearers.
So the figures below are the wage for that whole occupational group, not a cannabis-only number. They are the closest official measurement available, and they are what a trimmer is actually classified under — but they are not a pure cannabis figure, and anyone presenting them as one is overselling the precision. The Government of Canada Job Bank reports, sourced from the Statistics Canada Labour Force Survey, reference period 2023–2024:
| Canada | Low | Median | High |
|---|---|---|---|
| All Canada | $15.25/hr | $19.50/hr | $29.00/hr |
| Ontario | $17.60/hr | $20.50/hr | $33.33/hr |
| Quebec | $16.64/hr | $20.00/hr | $25.78/hr |
| British Columbia | $17.85/hr | $19.00/hr | $29.36/hr |
| Alberta | $15.00/hr | $19.50/hr | $25.00/hr |
| Saskatchewan | $15.35/hr | $17.00/hr | $25.00/hr |
| Nova Scotia | $16.50/hr | $18.50/hr | $24.00/hr |
Two things are worth noticing. First, the spread between low and high is nearly two to one — so the gap between an honest record and a guess can be the difference between a fair invoice and a contested one. Second, 62.7% of people in this occupation receive at least one non-wage benefit, which means a trimmer's effective compensation is not simply hours multiplied by a rate.
At the median of $19.50 an hour, one unrecorded break of forty-five minutes is worth about $14.63. Over a week that is real money, and it is the smallest unit of dispute in the entire industry.
What we could not find, and will not invent
Two gaps are worth stating plainly, because the alternative is filling them with numbers from elsewhere.
First, there is no published Canadian data on piece-rate cannabis trimming. The academic studies that document per-pound trimmer pay — including peer-reviewed work showing piece rates falling from roughly $150/lb to $100/lb, with eight-to-thirteen-hour days — are all American, mostly California, and describe undocumented cash labour. We have not found a Canadian equivalent, so we are not going to quote a California rate as though it described a British Columbia trim floor.
Second, there is no reliable Canadian productivity benchmark for trimming — no equivalent of a trustworthy "pounds per trimmer per shift" figure. The commonly cited numbers all trace to American industry surveys, several published by machine vendors with a commercial interest in the answer, or to marketing pages with no methodology at all. Several widely circulated claims, including that a strong hand-trimmer produces 2.5 to 3.5 pounds per hour, have no traceable source we can stand behind. We have left them out.
It is also worth knowing that Health Canada collects cannabis employment data but no longer surfaces a clean public headcount, and Statistics Canada does not track cannabis employment as a separate category — production workers are counted as agriculture, budtenders as retail. The labour-market context below is agriculture-wide for that reason.
The problem with a stopwatch is that it counts the wrong thing
A generic timer measures elapsed wall-clock time from start to finish. That number is not hours worked. It includes breaks, equipment failures, material handoffs, the ten minutes spent finding the next clean bin, and the meeting that interrupted the shift.
TrimXTimer separates three different quantities and keeps all of them:
- Shift time — clock-in to clock-out.
- Active trim time — the sum of running intervals only, with every pause excluded.
- Non-production time — the remainder, so it is visible rather than quietly absorbed.
Pausing is not optional and it is not free: the app requires a reason for every pause and stores the full interval with timestamps. A pause you cannot explain is a pause that does not get paid.
Getting the clock to survive Android
This turned out to be the hardest engineering problem in the app, and the solution is worth explaining because it affects whether any of the numbers are trustworthy.
Android will happily kill a WebView process while your phone is in a pocket. It throttles JavaScript in the background. Device clocks get corrected by NTP mid-shift, which can step time backwards. A naive timer reports garbage in all three cases.
The app runs on two clocks. While the page is alive it uses
performance.now(), a monotonic counter that is immune to system
clock adjustments. When the WebView restarts, that counter resets to zero —
so the app detects the reset and switches to a wall-clock calculation, which
correctly includes the period the app was dead. On boot it books that dead
time into the accumulated total and re-anchors both clocks.
There is a regression test for the nastiest edge case: a stale performance anchor left over from a previous page life, crossed mid-session. The QA audit caught it showing 8 seconds for a 49-second task. The fix re-anchors on restart; the retest showed 40 seconds against 41 seconds of real time. A separate 8-hour drift test holds error under 50 milliseconds.
The app also refuses to accept an action if the device clock has moved backwards relative to the last recorded event, rather than silently writing a negative interval.
Crews, and the unit that actually matters
One trimmer on a bag and six trimmers on a bag are not the same amount of work per hour. The app reports both, and keeps them distinct:
- Line hours — elapsed active time, regardless of crew size.
- Labour hours — active time multiplied by the number of people on the crew. Six people for two hours is twelve labour hours.
- Grams per labour hour — the productivity number that survives crew composition.
This distinction is not academic. The development history contains a real bug where every open shift was being attached to each new session, which put unrelated workers on the crew and inflated labour hours more than threefold, corrupting every downstream rate. The fix assigns only the operating worker's own open shift. It is the kind of error that would have quietly made the app's headline metric wrong, and it was found by testing rather than by reasoning.
Two stages, and a chain of custody for the material
Trimming is not one activity. The app models two stages:
- Bucking / de-stemming — breaking down the harvest.
- Conveyor + final trim — the finish pass.
The line stage must be linked to a completed bucking bin from the same batch, and that bin must already have an output container ID. A bin cannot be assigned to two final-trim runs, and the same bag ID cannot have two bucking records. The result is a traceable chain: harvest bag in, bucked bin out, final-trim run against that bin, weighed output at the end.
Mass balance: the app refuses to believe impossible numbers
This is the feature we would point a skeptical production manager at first. When a session is finished or weighed, the app checks that output, shake, and waste do not exceed the input weight, and rejects the save with a plain error if they do. It also computes the difference, so unaccounted grams are visible rather than assumed to be zero.
It will not invent a weight to fill a field, either. An early version reported fabricated zeros on sessions where nobody had weighed anything, which made every grams-per-hour figure fictional. The current design separates the two roles: the floor stop records no weights at all, and the head trimmer records real weights afterwards through a separate weigh action. A missing weight stays null and is excluded from averages. We would rather show a gap than a plausible-looking lie.
The batch ledger, which is really a compliance record
Each batch carries a lot code, cultivar, facility, optional lineage and classification, THC, CBD, total terpenes, terpene profile, a certificate-of- analysis reference, material state, moisture percentage, and free-text notes. Duplicate lot codes are rejected.
That is not a strain-enthusiast feature. It is a record that lets a finished lot be traced back to what came in, who worked on it, how long it took, and what it weighed.
Strain intelligence — and an honest label on it
Over time, weighed sessions accumulate into per-cultivar history: grams per labour hour, broken out by stage, with the sample size displayed beside every number.
We want to be precise about what this is. The app labels these figures descriptive, not difficulty-adjusted, and states on the screen that batch and equipment differences remain in the results and that more comparable observations are needed before treating them as a benchmark. A dense, sticky, high-THC lot and a loose, easy one are not comparable, and the app does not pretend otherwise.
The view also reports its own status plainly: local records only, shared network not connected, AI model not trained. A network-wide benchmark is a future idea. A local history you can audit is something you have today.
Two roles — and an honest word about what enforces them
Trimmers get three tabs: their own timer, their own work, and their own stats. Head trimmers get seven, adding the dashboard, live floor, crew board, batch ledger, reports, and settings.
Here is the part worth being precise about. The database side is real: the
Supabase schema enables row-level security, with policies that scope a
trimmer's reads to their own rows and grant head-trimmer visibility, and a
migration now hardcodes every new signup to the trimmer role so
a client cannot nominate its own privileges.
But the app's local storage is a single shared database on the device, and the role check is a display gate in the interface. A trimmer working a head-trimmer's tablet can see everything in the local state. The app says so itself — it labels the team view a local profile view, not a private login. Anyone treating this as a multi-tenant system with real per-user isolation would be wrong, and we would rather say so than let the tab count imply otherwise.
Motivation without surveillance
Seven badges are implemented and actually award, each with a real condition: first cut; finding your rhythm (5 sessions); building a craft (20); devoted to the craft (50); master trimmer (100); reliable (10 completed shifts); and strain explorer (5 different cultivars). Daily streaks are computed from consecutive days with completed work, and reset if more than a day is missed.
Two more appear in the badge catalogue but are wired to nothing — "early bird" and "weekend warrior" are listed and rendered, yet no code path ever awards them. An eighth, "clean sweep," is meant to reward zero unaccounted grams across five sessions, but it tests a field that only exists as a derived value, never on a stored session, so it evaluates true. We are reporting these rather than quietly dropping them from the count.
The intent behind the working badges is worth stating: "strain explorer" rewards breadth and "reliable" rewards attendance, not raw output speed. A leaderboard built purely around grams would reward rushing and penalise careful work on difficult material.
What comes out of the app
- Production CSV — one row per completed session, with around forty fields: batch, cultivar, facility, potency data, stage, source and output IDs, crew, UTC timestamps, active and paused seconds, labour hours, all weights, yield percentage, grams per line hour, grams per labour hour, condition ratings, equipment, every pause, and who recorded and who assessed it.
- Attendance CSV — clock-in/out records with elapsed seconds per shift.
- Invoice CSV — formatted for QuickBooks import, one row per shift, hours to two decimals.
- JSON backup — the complete state, for archiving or moving it. Note there is no restore or import screen yet: a backup is something you keep, not something the app can read back.
- PDF production report — a formatted document with the session log, an hourly breakdown, totals, and a four-way time accounting summary.
Two details that matter more than they sound: every timestamp is UTC, and
every cell is protected against formula injection, so a note field containing
a leading = cannot execute when the file is opened in Excel.
Built to work with no signal at all
Production floors are concrete boxes in remote valleys. The app treats IndexedDB on the device as the source of truth and works fully offline — timer, ledger, exports, gamification. Cloud sync is a target, not a dependency. It also runs with no account at all: a local profile mode with no server, which survives a WebView restart by persisting to local storage.
Supabase, when configured, adds authentication, a PostgreSQL backend with row-level security, and a queued sync path. It is genuinely separate from the WeedWork job board — different project, different data.
What is not finished
We would rather list this than let you find it later. From the project's own QA audit:
- Cloud sync is not production-ready. The push queue is fundamentally broken and the pull path is disabled as a corruption guard. Offline mode is the shipping path. This is the largest open piece of work.
- QuickBooks is CSV only. The direct API integration is a placeholder that raises an explicit error until developer credentials exist. There is no live QuickBooks connection.
- Four views are built but not yet reachable from the navigation: strain intelligence, gamification, invoices/QuickBooks, and connections. They render when called directly but have no menu entry until we decide which ship, and for which role.
- No push notifications or background service. A foreground-service permission is declared but unused.
- No cross-device leaderboards, and no predictive AI estimates.
- Two badges never award, and one always does. "Early bird" and "weekend warrior" have no award logic; "clean sweep" tests a derived field that is never stored, so it always passes.
- Conveyor / final-trim cannot be started from the timer screen. The main START button hardcodes the bucking stage. The line stage is only reachable through the head trimmer's start dialog.
- Pause reasons are not really captured. The pause reason is recorded, but the floor screen sends a fixed "break" value rather than offering a choice, and the head-trimmer floor view has no pause control at all.
- Role separation is a display gate, not a security boundary. Row-level security is correct in the database, but the on-device store is shared, so anyone holding the device sees the full local state.
- The service worker is generated but never registered. Offline operation comes from the APK bundling its assets, not from service-worker caching.
- QA was run in a desktop browser. The Android WebView path — including the reload-recovery fix — has not yet been re-verified on a physical device.
The test suite is real, not aspirational: 22 unit tests covering pause/resume recovery, mass-balance rejection, duplicate clock-in and lot codes, blocked clock-out during an active task, invalid stage rejection, CSV formula protection, backwards clock rejection, the reanchor edge case, the line-stage start, and the supervisor weigh action. A separate functional audit exercised 21 flows in a live browser, and a 114-check script run passes clean.
Why build it at all
The Canadian cannabis labour market has a documented shortage problem. The Canadian Agricultural Human Resource Council reported more than 28,200 unfilled agriculture jobs at peak season in 2022, a 7.4% vacancy rate against a 5.9% national rate, and shortages tied to a 3.7% decline in sales that year — roughly $3.5 billion in lost agricultural revenue. Cannabis competes for the same labour pool.
One caveat on that framing: the Job Bank rates prospects for this occupation in British Columbia as limited, and there is no Canadian equivalent of the "cannabis labour shortage" story. The Canadian picture is more contraction and consolidation than a scramble for workers, which makes accurate records more useful, not less — when you are competing for a shrinking pool, the operators who can show a worker exactly how their time was recorded are the ones who keep them.
That is the whole argument: not speed, not gamification for its own sake, but a record both sides can believe.
Where it is now
TrimXTimer is an Android app built with Capacitor 8 and Vite 7, packaged as an APK, currently in active development under the WeedWork umbrella. It is not a finished product and we are not selling it as one. What exists is a timer that survives Android, weighs what actually happened, refuses to record what did not, and exports the result in a format your accountant can open.
If you run a post-harvest floor and want to see it against your own process, we would like to hear from you — including about the parts you would do differently.