Two connected areas. The work board (/{slug}/workboard) is the coach’s task list. Automations (/{slug}/automations) define which events create those tasks and what else happens. In the API a task is an inbox item (InboxItem), and its type is InboxItemType.

Work board

Code lives in [organizationSlug]/workboard/:

Server load

workboard/page.tsx loads everything the board needs up front:
  • All open and snoozed tasks: /inbox with status OPEN and SNOOZED. readEveryPage in inbox-pages.ts walks the pages (INBOX_PAGE_SIZE 500, at most INBOX_MAX_PAGES 20) and mergeInboxItems deduplicates by id.
  • /coaches (100) and /clients (500).
  • The SmartSend chat embed config and the studio record. Both are optional. The studio feeds the portion unit label on plan stats and the studio name in the push preview.
?task= in the URL preselects a task. The page renders inside PageShell with the full height board layout. Filtering is done in the browser. task-filtering.ts exports deriveTaskView(...), matchesScope and matchesType. The scope filter has two sentinels: ALL_COACHES ("ALL") and MY_TASKS.

Task types

task-meta.ts maps every InboxItemType to a translation key under workboard.taskType.*, an icon and a colour in TASK_META. Tasks are grouped by priority into PRIORITY_GROUPS: urgent, today, later.

Layouts

A task’s panel is not hand written per type. task-layouts.ts describes it as data:
  • TASK_LAYOUTS holds a layout for 18 types: FORM_FILLED, FORM_SENT, FORM_RATING_BELOW, FORM_RESPONSE, INACTIVE, NO_WORKOUT, PLAN_EXPIRING, TECHNIQUE_VIDEO, NEW_CLIENT_IN_PLAN, SUBSCRIPTION_ASSIGNED, MILESTONE, CALORIE_UNDER, CALORIE_OVER, AEROBIC_UNDER, PAUSED_DURATION, TRAINING_PLAN_STALE, NUTRITION_PLAN_STALE and MESSAGE.
  • LAYOUT_ALIASES points legacy types at a live layout: FORM_FILLED_OVERDUE and FORM_FEEDBACK_SENT use FORM_FILLED, CHURN_RISK and MISSED_CHECK_IN use INACTIVE, MISSED_STREAK uses NO_WORKOUT.
  • layoutFor(type) resolves alias, then layout, then FALLBACK_LAYOUT. A type the web app has never heard of still renders.
  • visibleBlocks(layout, args) filters by each block’s when. A when that throws counts as false, so one bad condition cannot break the panel.
LayoutArgs gives every selector the task, its loaded TaskContext, the current time and the trainee’s other open tasks (siblings). Quick actions are one of push, smartsendTab, editProgram, reviewCheckin, cancelForm, sendForm and renew.

Blocks

detail/blocks/ has one block per kind of evidence: AerobicBlock, EngagementBlock, FormResponseBlock, FormsBlock, MessageBlock, MilestoneBlock, NutritionBlock, PlansBlock, SubscriptionsBlock, TechniqueVideoBlock and WorkoutsBlock. Each follows the same split: BlockRenderer.tsx picks the component for a BlockInstance. BlockSection.tsx and BlockState.tsx are the shared frame and the loading, empty and forbidden states. use-client-resource.ts is the hook a block uses to read a trainee resource.

How the panel loads data

The panel reads through GET route handlers, not server actions. Next.js runs a page’s actions one at a time, and a slow read was blocking the next task’s context and the “mark done” write. The reasoning and the endpoints are in Route handlers. On selection:
  1. staticPrefetch(layout) lists the resources whose range is fixed. They start loading at once.
  2. The task context loads from /api/workboard/{slug}/tasks/{taskId}/context.
  3. contextPrefetch(layout, args) lists the resources whose range depends on the context. For example a stale nutrition plan reads 30 days, or 3 months once the plan is older than that (nutritionStaleRange).
Trainee resources come from /api/workboard/{slug}/clients/{clientId}/{kind}. loadClientResource returns { status: "forbidden" } when the API answers 404, which happens when a coach may not see that trainee. The block renders its forbidden state instead of an error. Each request carries an AbortSignal. Moving to another task cancels the reads of the previous one.

Writes

In workboard/actions.ts: /{slug}/workboard/automations is an old URL. It redirects to /{slug}/automations. The button in the board’s header still links to the old path and relies on that redirect.

Automations

Code lives in [organizationSlug]/automations/: This is a canvas route. The sidebar collapses when the coach opens it.

The flow schema

One flow exists per trigger type. TRIGGER_ORDER lists the 18 triggers in display order. They are the same names as the task types above, from FORM_FILLED to NUTRITION_PLAN_STALE. FlowTrigger carries the trigger’s parameters: time and unit, pct, an optional score, programs, and for form triggers formTypes (INTAKE, CHECK_IN, ONE_TIME, PDF_SIGNATURE) and formIds. triggerFormScope(trigger) returns all form types when none are set. A step is a FlowNode, a union keyed on kind: A FlowRule compares a field with gt, lt or eq. Nested steps are addressed with a FlowLoc, a list of [stepIndex, branchIndex] hops. mkNode(kind), mkBranch(label, fallback) and mkRule() build defaults. freshId(prefix) generates ids.

Saving

automations/actions.ts: The name token inserted into messages is NAME_TOKEN in trigger-meta.ts. trigger-meta.ts also exports WA_TEMPLATES, CONTENT_ITEMS and DEMO_TRAINEES. Whether those constants still feed the live UI or only the test run preview was not confirmed while writing this page.

Home screen tasks

The dashboard shows a short task list too. dashboard-task-href.ts builds the link from a dashboard task into the board, and markInboxStatus and bulkInbox in the root actions.ts update status from there.