Training

Training programs, written, PDF or YouTube video content, quiz questions, group assignments, external training, per-period completion tracking, and the weekly reminder digest.

Training manages required courses, employee-group assignments, and completion records. A training program contains written content, a PDF or a YouTube video, and a quiz. Training taken outside Carbon is an external assignment with only a name, which an admin marks complete. Carbon tracks completion for each employee and period, sends weekly reminders for outstanding assignments, and allows managers to record manual completion. Training belongs to the resources area and uses the resources permission.

For example, create a "Lockout/Tagout" training with type "Mandatory" and frequency "Annual", then assign it to the "Machinists" group. Each group member remains "Pending" for the current year until completion and receives a Sunday reminder while outstanding.

The training catalog

Each training is a row in the training table (packages/database/supabase/migrations/20251205021915_training.sql:18). The create form requires only a Name (apps/erp/app/modules/resources/ui/Training/TrainingForm.tsx:76). Configure the remaining fields in the Properties panel (apps/erp/app/modules/resources/ui/Training/TrainingProperties.tsx), which saves each change through a bulk-update action.

Questions on the left, the content in the middle, properties on the right.
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, changed from the page header. See below.
The written training material, stored as rich JSON (content). Employees read it before the questions.
The uploaded PDF. A training with a PDF shows the PDF to employees instead of the written content.
The YouTube video, stored as its watch link. A training with a video shows the video to employees instead of the written content.
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.

Change the status from the page header: a Draft training shows Publish, an Active one shows Archive, and Reopen in the ⋯ menu moves it back to Draft. Each asks for confirmation.

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

Only a Draft training can change

The content, the PDF, the video, and the questions are editable only while the training is "Draft"; the server refuses a PDF or video change on any other status (apps/erp/app/routes/x+/training+/update.tsx). Employees can open and complete only an "Active" training. To change a published training, Reopen it first.

Written content, a PDF or a video

A training's content is one of three kinds, chosen with the Write | Upload PDF | YouTube Video toggle on the Content card:

  • Write: type the material in the editor.
  • Upload PDF: drop one PDF, for shops that already have their training material as a document. Replace swaps it (apps/erp/app/modules/resources/ui/Training/TrainingGuide.tsx).
  • YouTube Video: paste a link to one YouTube video (a youtube.com/watch, youtu.be, Shorts or live link). Carbon stores it as the video's watch link; Change swaps it (apps/erp/app/modules/resources/ui/Training/TrainingVideo.tsx). Carbon does not host video files.

Employees see the PDF in a viewer, or the video in YouTube's player, when the training has one, and the written content when it has neither. To switch to another kind, choose it on the toggle; Carbon asks before it removes the PDF or the video. All three change only while the training is Draft.

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

Trainings are assigned to groups, not individual employees. A trainingAssignment (20251205021915_training.sql:93) links one trainingId to an array of groupIds. Every employee in those groups receives the assignment.

A new assignment is Internal or External. An internal assignment picks a training from the catalog. An external assignment has only a Name, as below.

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.

Click an assignment in the list to open its roster: each member with their current status, a completion date if they've finished, and a "Mark Complete" button (apps/erp/app/routes/x+/resources+/assignments.$assignmentId.tsx). Edit Assignment in the row menu changes its name or groups. Assigning an internal training also fires a notification to the newly-covered employees (see notifications).

External training

Some training happens outside Carbon, such as a forklift course from an outside provider. Create an External assignment with the course's Name and its groups. Carbon stores no training for it (trainingAssignment.name, 20261014164752_external-training-assignments.sql), so it has no content, no quiz and no frequency. It is one-time: each employee is "Pending" or "Completed".

  • No employee page and no notifications. Employees are not notified or reminded, and they cannot open it.
  • Admin completes it: on the roster, Mark Complete can attach the employee's certificate (one PDF, PNG or JPG of 10 MB or less). The certificate is optional. A phone photo in HEIC format is converted to JPEG on upload.
  • Proof: a completed row with a certificate shows View Certificate on the roster and on the person's Trainings card.

The assignments list shows an External badge on these rows.

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 roster with "Mark Complete"; Carbon works out the current period on the server (recordTrainingCompletion, apps/erp/app/modules/resources/resources.server.ts). An employee already "Completed" or "Not Required" has no button — there's nothing to mark.

To undo a wrong completion, "Remove Completion" on the roster deletes it and the employee goes back to "Pending" (or "Overdue"). It needs the resources_delete permission, and it also deletes the attached certificate file. Any ability the training granted stays with the employee; remove it on their Abilities page if needed.

The weekly reminder

Every Sunday at 21:00 UTC, a scheduled job finds outstanding training (packages/jobs/src/inngest/functions/scheduled/weekly.ts, cron 0 21 * * 0). It selects "Pending" and "Overdue" assignments and sends one digest to each 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 one-time events announce assignments: "Training assigned to you" (training-assignment) and "New training available" (resource-training-assignment). See notifications for delivery and preferences.

Training can grant an ability

An ability is an employee's qualification for a process. A training can grant one: set Grants Ability in Properties, and completing the training adds that ability to the employee (grant_ability_on_training_completion, packages/database/supabase/migrations/20260720121629_capacity-planning.sql:264). The scheduler and the shop floor use abilities to decide who may run the process.

Training is one way to grant an ability; a manager can also add it by hand on the person's Abilities page. Removing a training completion does not remove the ability.