Training

Training programs, quiz questions, group assignments, per-period completion tracking, and the weekly reminder digest.

Training is where you build the courses your team has to complete, assign them to groups of employees, and track who has finished. A training is a named program with rich content and an optional quiz. You assign it to employee groups, and Carbon tracks completion per employee for the current period, chases the stragglers with a weekly email, and lets a manager mark someone complete by hand. It lives in the resources area and is governed by the resources permission.

A quick example: you create a "Lockout/Tagout" training, set its type to "Mandatory" and frequency to "Annual", write the material, add a few quiz questions, then assign it to the "Machinists" group. Every machinist now shows as "Pending" for the current year until they complete it — and every Sunday night anyone still outstanding gets a reminder.

The training catalog

Each training is one row in the training table (packages/database/supabase/migrations/20251205021915_training.sql:18). You create one from the training list with just a Name — the create form is deliberately minimal (apps/erp/app/modules/resources/ui/Training/TrainingForm.tsx:76). Everything else is edited afterward from the training's Properties panel (apps/erp/app/modules/resources/ui/Training/TrainingProperties.tsx), which saves each field on change through a bulk-update action.

FieldType
The training's title.
Whether completion is required. Drives the badge on the list and, for mandatory trainings, the sense of "you must finish this."
How often the training must be re-taken. "Once" means complete it a single time, forever. "Quarterly" and "Annual" are recurring — completion is tracked per period and resets when a new period begins.
Lifecycle of the training itself. See below.
The training material, stored as rich JSON (content). This is the body an employee reads.
A free-text estimate of how long the training takes.
Free-form tags for filtering the catalog.
The person responsible for the training.

The status enum is a simple linear lifecycle on the training record — not to be confused with an employee's completion status further down.

Off-track
Active
Published and in use. Assignments and reminders operate against active trainings.

Quiz questions

A training can carry a quiz. Each question is a row in trainingQuestion (20251205021915_training.sql:229), owned by one training, ordered by sortOrder, and marked required or not. Carbon supports five question types (apps/erp/app/modules/resources/resources.models.ts:343):

FieldType
Pick exactly one correct answer from a list of options.
Pick one or more correct answers from a list.
A boolean question with a correctBoolean answer.
Match left items to right items, stored as JSON pairs.
Enter a number; graded against a correctNumber within a tolerance.

Questions are authored on the training detail page and can be reordered. The stored answer key columns differ by type: options and correctAnswers for the choice types, correctBoolean for true/false, matchingPairs for pairs, and correctNumber / tolerance for numerical (20251205021915_training.sql:229).

Assigning training to groups

You don't assign a training to individuals one at a time — you assign it to groups. A trainingAssignment (20251205021915_training.sql:93) links one trainingId to an array of groupIds. Every employee who belongs to one of those groups is on the hook for the training.

NOTE

Assignments target groups, then expand to people

The assignment stores groupIds, not employee ids. Carbon expands the groups to their members when it computes who owes the training, so adding a person to a group automatically enrolls them — you don't re-assign.

Open an existing assignment and switch to the Status tab to see the per-employee roster: each member with their current status, a completion date if they've finished, and a "Mark Complete" button (apps/erp/app/modules/resources/ui/Training/TrainingAssignmentForm.tsx). Assigning also fires a notification to the newly-covered employees (see notifications).

Completion and status

Completion is recorded in trainingCompletion (20251205021915_training.sql:156) — one row per employee per assignment per period, with a completedAt timestamp and completedBy. For recurring trainings the row also carries a period string, and a unique constraint stops double-recording the same employee for the same period.

The period is the heart of the recurring logic (20251206000000_training_assignments.sql). Carbon derives the current period from the training's frequency:

FieldType
No period (NULL). Complete it once and you're done for good.
A string like Q1-2026. Completion in one quarter doesn't carry into the next.
A string like 2026. Resets at the start of each year.

An employee's status is computed, not stored — Carbon evaluates it from whether a completion exists for the current period and when the employee started. There are four possible values (apps/erp/app/modules/resources/resources.models.ts:318):

Off-track
Completed
A completion row exists for this employee and period.
HEADS UP

New hires aren't retroactively overdue

Recurring trainings check the employee's start date against the period. Someone who joined after a past period closed shows "Not Required" for that period, not "Overdue" — so onboarding a new hire doesn't instantly paint their record red.

A manager marks completion from the assignment's Status tab; the action posts to markTrainingComplete with the employee and the current period (apps/erp/app/modules/resources/ui/Training/TrainingAssignmentForm.tsx). An employee already "Completed" or "Not Required" has no button — there's nothing to mark.

The weekly reminder

Every Sunday at 21:00 UTC, a scheduled job sweeps outstanding training and emails the stragglers (packages/jobs/src/inngest/functions/scheduled/weekly.ts, cron 0 21 * * 0). For each company it pulls the assignment status, keeps only the employees whose status is a reminder-worthy one ("Pending" or "Overdue"), groups them by person, and sends one digest per employee.

The notification is the TrainingReminder event (training-reminder) under the Training topic. Its email heading is "Training reminder" and the call-to-action button reads "View training" (packages/notifications/src/index.ts). It's the only recurring notification event in Carbon — so it needs a guard against nagging forever.

NOTE

Reminders stop after five sends per period

A recurring reminder is capped: once an outstanding training has had five successful reminder emails delivered for the current period, Carbon stops re-sending it. For quarterly and annual trainings the count is keyed to the period, so the cap resets when a new period begins. If a reminder "stopped coming," this cap is usually why.

Two related one-shot events fire when work is handed to people rather than chased: "Training assigned to you" (training-assignment) and "New training available" (resource-training-assignment). See notifications for how these are delivered and how a person tunes them.

Abilities are a separate system

Carbon also tracks employee abilities — a skill or certification with a learning curve — under the people side of the app (apps/erp/app/modules/people/ui/Person/PersonAbilities.tsx). An ability defines a starting proficiency, the weeks to reach full proficiency, and a shadowing period; each employee's progress against it is tracked as NotStarted, InProgress, or Complete (apps/erp/app/modules/resources/types.ts).

HEADS UP

Abilities and trainings are not linked

Despite the shared vocabulary, abilities and trainings are two independent systems today. There is no active link between a training and an ability, and work centers do not require a specific ability — the fields that would connect them (requiredAbilityId on a work center, an abilityId on a training) exist only as commented-out placeholders in the code (apps/erp/app/modules/resources/resources.models.ts:495). Completing a training does not advance an ability, and vice versa.

Because they're separate, plan around it: use training for course-and-quiz compliance with reminders, and abilities for skill-curve tracking on a person's profile. Don't expect one to update the other.