modules/index.ts mounts it. It is a library called by a worker and by two scripts.
Source: backend/apps/core-api/src/modules/demo-activity/.
Which studios are demo studios
A studio is in demo mode whenStudio.settings.demoMode is exactly true. loadDemoStudios queries with a JSON path filter on settings.demoMode and re-checks each row with isDemoStudio.
There is no endpoint that sets the flag. The script apps/core-api/src/scripts/setup-demo-studio.ts writes it as the last step of preparing the demo studio.
Callers
The setup helpers under
scripts/demo-setup/ (assign.ts, copy-source.ts) also import readTrainingPlan, demoId and DEMO_ID_PREFIX from this module.
No route handler and no other domain module calls runDemoActivity. The module itself calls into two task modules:
createFormFilledTaskfrommodules/tasks/form-filled.ts, after it fills a form.AUTOMATION_DEFAULTSfrommodules/task-automations/task-automations.schema.tsandCONTENTfrommodules/tasks/task-content.ts, to build showcase tasks.
The scheduler
startDemoActivityScheduler(ctx) is called from server.ts and closed on shutdown. It uses the BullMQ queue PerformQueue.DEMO_ACTIVITY (demo-activity).
- A repeatable job named
hourlywith job iddemo-activity-hourlyruns everyDEMO_ACTIVITY_EVERY_MS(60 minutes). - A one-off job named
bootis added at every start, so a deploy does not wait an hour for fresh data. - The worker runs with concurrency 1, a 10 minute lock, a 5 minute stalled check and
maxStalledCount: 1. - Completed jobs are removed. The last 50 failures are kept.
- The run summary is logged only when at least one demo studio exists.
{ "studios": 0, "results": [] }.
What a run does
backfillDays defaults to 2 and has a minimum of 1. For each demo studio, runForStudio:
- Loads the eligible clients.
- Loads the names of the exercises in their training plans.
- Builds day contexts for today and the previous
backfillDaysdays, in the studio’s timezone. - Builds rows for every client and day with
buildDemoDay. - Writes the rows with
writeDemoDays. - Fills and handles check-in forms with
runDemoForms. - Tops up showcase tasks with
topUpShowcaseTasks.
Eligible clients
loadDemoClients takes clients of the studio that are ACTIVE, not deleted, and have at least one subscription with status ACTIVE. For each it reads:
stepsGoal,waterGoalMl,weightKg,lastCheckInAt.- The newest active
TRAININGprogram and the newest activeNUTRITIONprogram. File plans (pdfUrlset) are skipped. - A weight baseline: the latest real
WeightEntry(one whose id does not start with the demo prefix), or elseClient.weightKg, or else 75 kg. - The latest real body measurements from
DailyMetric.measurements, limited towaist,chest,arm,hipsandthigh.
Deterministic output
Nothing usesMath.random. Every decision comes from hash(seed) in demo-random.ts, an FNV-style hash of a string such as <clientId>:<date>:train. The same client and day always produce the same rows, which is what makes a rerun safe.
Personas
personaOf(clientId, dayIndex) gives each trainee a persona for a 28 day block. The block start is offset per client, and the persona is re-rolled each block.
platformOf(clientId) fixes each trainee to ios (78%) or android.
Today is generated up to the current time
Past days are generated in full. For today,clockMinutes holds the current minute of the day in the studio’s timezone, and hasPassed drops anything scheduled later. A workout that would finish at 18:40 appears only in a run after 18:40. This is why the job runs hourly.
Rows written per trainee and day
buildDemoDay first decides whether the trainee opened the app that day, at a time between 06:30 and 09:30. If not, or if that time has not passed yet, nothing is written for that trainee and day.
Details worth knowing:
- Training days.
trainingWeekdaysspreads the plan’sworkoutsPerWeekover fixed weekdays. Plan days rotate week by week. - Weights. Each exercise gets a base weight from 10 kg in 2.5 kg steps, plus one step every four weeks in a 16 week cycle. From the third set on, a set has a 30% chance of one missed rep.
- Weight trend.
weightOndrifts from the baseline at a per-trainee rate between -0.45 and +0.15 kg a week, flattening over a 12 week horizon, with up to 0.3 kg of daily noise either way. - Steps. Between 55% and 130% of the goal (plan
stepsDaily, thenClient.stepsGoal, then 8000), scaled by how much of the day has passed. - Water. 500 ml cups at up to six fixed times, sized against the goal (
Client.waterGoalMl, then the plan’s water target, then 2500 ml). - Nutrition plan. Only the first day of the nutrition plan is read.
Client.lastCheckInAt and Client.lastCheckInPlatform are moved forward to the latest generated activity, and Client.weightKg is set to the latest generated weigh-in.
Forms and coach feedback
runDemoForms works on check-in forms only.
Filling. fillPending looks at FormAssignment rows with status PENDING, a CHECK_IN form template, and a creation time at least 2 hours ago. For each:
- The persona’s
fillsFormrate decides whether the trainee fills it. - If not, and the assignment is older than 6 days, it is set to
CANCELLED. - If yes, a submit time 2 to 22 hours after the assignment is chosen. Once that time has passed, the job creates a
FormResponsewith generated answers and sets the assignment toCOMPLETED. - It then calls
createFormFilledTask, the same hook a real submission triggers, so the studio’s form-filled task automations create their tasks. The created task’screatedAtis moved back to the submit time.
buildAnswers, by field type: range and rating follow the persona’s mood, number is 5 to 9, weight is the trainee’s current weight, text picks a Hebrew note that matches the mood, dropdown picks an option, and checkbox_confirmation is true. Other field types are left unanswered.
Handling. handleOlder takes up to 200 fabricated responses, submitted between 30 hours and 14 days ago, that are not handled yet. For each it picks an active coach of the studio and a handling time: 4 to 28 hours after the submit for 75% of responses, 60 to 110 hours for the rest. Once that time has passed, in one transaction it:
- Creates a
CheckinReviewwith a note, a message andchannels: ["push"]. - Stamps
handledById,handledByName,handledAtandhandledNoteon theFormResponse. - Marks the matching open form-filled tasks
DONE. - Creates a
ClientActivityof typeCHECKIN_FEEDBACK_SENT.
Showcase tasks
topUpShowcaseTasks keeps the tasks board populated. For every task automation type in AUTOMATION_DEFAULTS, plus MANUAL, it counts the studio’s open InboxItem rows of that type and creates enough to reach two. New rows use the type’s title, detail and priority from CONTENT, have source: "manual", an empty coachIds, and are attached to a trainee. Form related types are linked to one of the studio’s latest form responses or assignments through metadata.
How fabricated rows are marked
Rows created by the day generator and the form filler have a deterministic id with the prefixdm_ (DEMO_ID_PREFIX), built by demoId(kind, ...parts).
isDemoId(id) tests for the prefix. The fixed ids are also what makes reruns idempotent: inserts use createMany with skipDuplicates: true.
Some writes are not marked with the prefix:
CheckinReview,ClientActivityand showcaseInboxItemrows get ordinary generated ids.- Tasks created through
createFormFilledTaskare ordinary tasks. - Updates to existing rows (
Client,FormAssignment,FormResponsehandled fields,InboxItemstatus) carry no marker.
Live days and settled days
The writer treats the last two generated days as live. For those,DailyMetric and NutritionDayLog are upserted on clientId and date, so steps, water and eaten meals grow through the day as the hourly job reruns. Older days are settled and only inserted when missing.
Run summary
runDemoActivity returns one result per demo studio.
appDays, workouts, cardio, weighIns and meals count rows actually inserted. metrics and nutritionDays add the settled inserts to the number of live-day upserts, so they are never zero on an active studio.
Running it by hand
scripts/run-demo-activity.ts runs one pass against the configured database and prints the summary as JSON. Pass --days=N to backfill more history.