TaskAutomation.flow
The node flow the automation builder edits. Validated by flowBody in apps/core-api/src/modules/task-automations/flow.schema.ts.
Limits
validateFlow runs as a superRefine on save: size, depth, unique node ids, and one trailing fallback per condition. A bad canvas state is refused, never stored.
trigger
Node kinds
Every node has anid (up to 40 characters) and a kind.
task
assign is one of none, coach, specific, manager, all. FLOW_ASSIGN_TO_MODE maps it to a TaskAssigneeMode:
wa
webhook
url must be https on a public host. The check refuses localhost, loopback, private ranges, link-local addresses, bracketed IPv6 literals, and .local and .internal names. An empty URL is allowed, so an unconfigured node can be saved, but it cannot run. The payload is described in Outbound webhooks.
paths
Each branch has id, label, fallback, rule and steps. The rule:
Legacy conversion
Whenflow is null the automation was never opened in the builder. Readers build an equivalent flow from the TaskAutomationTask rows, so no coach loses a configured name, assignee or delay.
AutomationFlowRun.position
The executor’s work stack: an array of frames. A new run starts with one frame at the root:
loc is the chain of branch ids from the root down to the step list this frame walks. An empty array means the flow’s own top-level steps. index is the next node to run in that list. Branch ids are unique within a flow, so a coach reordering nodes cannot point a resumed run at a different branch.
A frame is resolved against the automation’s current flow (stepsAt). If the coach deleted that branch while the run was waiting, the frame resolves to nothing. A malformed position restarts from the top, which is safe because the FlowNodeExecution ledger skips nodes that already ran.
The run is DONE when the stack is empty. When a node throws, the run becomes FAILED with the position preserved.
AutomationFlowRun.context
Facts frozen when the trigger fired, written in flow-runtime.ts:
TaskAutomation.thresholdByFrequency
Only for the NO_WORKOUT trigger. Days without a logged workout before a task is created, per weekly-frequency band:
WORKOUT_FREQUENCY_BANDS: 3, 4, 5, 6, 7. The value shown is DEFAULT_NO_WORKOUT_DAYS. Each value is clamped to 1 through 60. bandForWorkoutsPerWeek maps a trainee’s meta.workoutsPerWeek to a band: 3 or fewer is band 3, 7 or more is band 7. normalizeFrequencyThresholds fills missing or invalid bands from the defaults.
InboxItem.metadata
A small string map. What it holds depends on the task type:
Treat it as optional context.
videoIdOf shows the defensive read pattern: check that the value is an object, then that the key is a non-empty string.
SavedTraineeFilter.rules and TraineeFilterState.state
Both store the trainee board’s advanced filter rules.
The API stores rules as an array of free-form objects (up to 50 in a saved state), so an older client’s rule shape still saves. When rules are applied to the clients query they are parsed with filterRule in apps/core-api/src/modules/clients/clients.schema.ts:
TraineeFilterState.state is the whole filter bar, validated by traineeFilterStateBody:
The value above is
EMPTY_TRAINEE_FILTER_STATE. Both tables are per staff member: userId is the auth user id, and TraineeFilterState is unique on (studioId, userId).