---
title: "Plan inputs for fitness apps | Snowplow Signals"
description: "Snowplow Signals tells your plan builder what each user started, finished and abandoned this week, so the next session fits the week they had."
source: https://signals.snowplow.io/solutions/health-fitness/plans
---

[Health and fitness](https://signals.snowplow.io/solutions/health-fitness) / Plan inputs 

# A plan that fits the week the user just had. 

Signals tells your plan builder what each user started, finished and stopped partway through this week, and when, so the next session is built for the week they had rather than the one the program assumed.

[Start free](https://signals.snowplow.io/start) [Talk to us](https://signals.snowplow.io/start?intent=talk) 

Higher workout completion

## Get more workouts finished by building the next session from this week. 

Workout completion moves when the next session fits what the user did in the last few. Signals gives your plan builder what each user started, finished and stopped partway through this week, how long each session ran, which days they trained and which types they chose. Your plan builder decides what the next session is.

Four ways fitness apps use it

01 Users who stop partway through Three starts this week, no finishes. Shorten the next one. 02 Users who finish with time to spare Every session finished, all of them under the planned time. Offer the next level. 03 Users who train on the same three days Monday, Wednesday and Friday for a month, on a five-day plan. Build it on those days. 04 Users who skip one type of session Every run started, no strength session touched all month. Swap the type out. 

[Read the docs →](https://docs.snowplow.io/docs/signals/introduction/) 

### Shorten the next session for a user who keeps stopping early

A user has started three sessions this week and finished none of them, each one stopped around the middle. The plan has a fourth session queued at the same length, and the report that would show the pattern arrives after the week is over. By then the fourth session has been served and stopped too. The right move is a shorter session, ready the next time this user opens the app.

What Signals knows

`sessions_started_7d` 

Sessions started in the last seven days

`sessions_finished_7d` 

Sessions finished in the last seven days

`days_since_finish` 

Days since they last finished a session

What your plan builder does

Shortens the next session when three of this week's starts have no finish, and returns to the planned length once a finish comes through. Which exercises make up the shorter session stays with the plan builder.

Define it · PythonUse it · TypeScript

plan-builder.ts · 13 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, Event, StreamAttributeGroup, user_id

workout_started = Event(name="workout_started", vendor="com.example", version="1-0-0")
workout_completed = Event(name="workout_completed", vendor="com.example", version="1-0-0")

sessions_started_7d = Attribute(
    name="sessions_started_7d",
    type="int32",
    events=[workout_started],
    aggregation="counter",
    period=timedelta(days=7),
    default_value=0,
)

sessions_finished_7d = Attribute(
    name="sessions_finished_7d",
    type="int32",
    events=[workout_completed],
    aggregation="counter",
    period=timedelta(days=7),
    default_value=0,
)

days_since_finish = Attribute(
    name="days_since_finish",
    type="double",
    events=[workout_completed],
    aggregation="time_since_last",
    time_unit="d",
)

# Users are logged in to train, so the group keys on the user id.
fit_plans_abandoned = StreamAttributeGroup(
    name="fit_plans_abandoned",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[sessions_started_7d, sessions_finished_7d, days_since_finish],
)
```

```
// In the service that builds the user's next session
const user = await signals.getServiceAttributes({
  name: "fit_plans_abandoned",
  attribute_key: "user_id",
  identifier: userId,
});
// { sessions_started_7d: 3, sessions_finished_7d: 0, days_since_finish: 9.4 }

const unfinished = user.sessions_started_7d - user.sessions_finished_7d;

if (unfinished >= 3) {
  planBuilder.next({ userId, length: "short" });
}
```

### Offer the next level to a user who finishes every session early

A user has finished every session they started this month, and each one took less time than the plan allowed for it. The program moves users up a level on a fixed week, so a user who is ready in week two waits until week four. Two more weeks of sessions that feel easy give them less reason to open the app. The time to offer the next level is while they are finishing early.

What Signals knows

`sessions_started_30d` 

Sessions started in the last 30 days

`sessions_finished_30d` 

Sessions finished in the last 30 days

`finish_seconds_mean_30d` 

Average length of a finished session in the last 30 days, in seconds

What your plan builder does

Offers the next level once every start this month has a finish and the average finished session runs under the early-finish mark the plan builder sets for that level. The user takes it or stays where they are.

Define it · PythonUse it · TypeScript

plan-builder.ts · 14 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, Event, EventProperty, StreamAttributeGroup, user_id

workout_started = Event(name="workout_started", vendor="com.example", version="1-0-0")
workout_completed = Event(name="workout_completed", vendor="com.example", version="1-0-0")

sessions_started_30d = Attribute(
    name="sessions_started_30d",
    type="int32",
    events=[workout_started],
    aggregation="counter",
    period=timedelta(days=30),
    default_value=0,
)

sessions_finished_30d = Attribute(
    name="sessions_finished_30d",
    type="int32",
    events=[workout_completed],
    aggregation="counter",
    period=timedelta(days=30),
    default_value=0,
)

finish_seconds_mean_30d = Attribute(
    name="finish_seconds_mean_30d",
    type="double",
    events=[workout_completed],
    aggregation="mean",
    property=EventProperty(vendor="com.example", name="workout_completed", major_version=1, path="duration_seconds"),
    period=timedelta(days=30),
)

fit_plans_ahead = StreamAttributeGroup(
    name="fit_plans_ahead",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[sessions_started_30d, sessions_finished_30d, finish_seconds_mean_30d],
)
```

```
// In the service that builds the user's next session
const user = await signals.getServiceAttributes({
  name: "fit_plans_ahead",
  attribute_key: "user_id",
  identifier: userId,
});
// { sessions_started_30d: 11, sessions_finished_30d: 11, finish_seconds_mean_30d: 1420 }

const allFinished = user.sessions_started_30d > 0 && user.sessions_finished_30d >= user.sessions_started_30d;
const early = user.finish_seconds_mean_30d <= plan.earlyFinishSeconds;

if (allFinished && early) {
  planBuilder.offer({ userId, level: plan.nextLevel });
}
```

### Fit the plan to the days they train

A user has trained on Monday, Wednesday and Friday every week this month, and the plan they are on schedules five sessions a week. Two of those are missed every week, and each one shows up as a skip. The schedule was set at signup from the days the user said they would train, and it changes when the user goes back into settings, which lags behind the habit by weeks. The days they train are in the sessions they start.

What Signals knows

`top_training_day_30d` 

The weekday they started the most sessions on in the last 30 days

`sessions_by_day_30d` 

Sessions started on each weekday in the last 30 days

What your plan builder does

Rebuilds the week around the days with a session started in most weeks this month, and moves the sessions scheduled on the other days onto them or drops them. The user sees a plan that matches the days they train, and the two skips a week stop appearing.

Define it · PythonUse it · TypeScript

plan-builder.ts · 15 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, Event, EventProperty, StreamAttributeGroup, user_id

workout_started = Event(name="workout_started", vendor="com.example", version="1-0-0")
day_of_week = EventProperty(vendor="com.example", name="workout_started", major_version=1, path="day_of_week")

top_training_day_30d = Attribute(
    name="top_training_day_30d",
    type="string",
    events=[workout_started],
    aggregation="most_frequent",
    property=day_of_week,
    period=timedelta(days=30),
)

sessions_by_day_30d = Attribute(
    name="sessions_by_day_30d",
    type="dict",
    events=[workout_started],
    aggregation="category_count",
    property=day_of_week,
    period=timedelta(days=30),
)

fit_plans_days = StreamAttributeGroup(
    name="fit_plans_days",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[top_training_day_30d, sessions_by_day_30d],
)
```

```
// In the service that lays out the user's week
const user = await signals.getServiceAttributes({
  name: "fit_plans_days",
  attribute_key: "user_id",
  identifier: userId,
});
// { top_training_day_30d: "monday", sessions_by_day_30d: { monday: 4, wednesday: 4, friday: 3 } }

const trainingDays = Object.entries(user.sessions_by_day_30d)
  .filter(([, starts]) => starts >= 3)
  .map(([day]) => day);

if (trainingDays.length > 0 && trainingDays.length < plan.daysPerWeek) {
  planBuilder.rebuild({ userId, days: trainingDays });
}
```

### Swap out the type of session a user keeps skipping

A user's plan mixes runs, strength and mobility, and this month they have started every run and no strength session. The plan keeps scheduling strength, each one is logged as a skip, and the review that would change the mix comes at the end of the program block. Until then the user opens the app to a session they are going to skip. The sessions they do start say which type to keep.

What Signals knows

`types_started_30d` 

Sessions started of each type in the last 30 days

`top_type_finished_30d` 

The type of session they finished most in the last 30 days

What your plan builder does

Replaces a type with no start this month with more of the type the user finishes most, and keeps the swap until a session of the dropped type is started. Whether and when the type returns is the plan builder's rule.

Define it · PythonUse it · TypeScript

plan-builder.ts · 13 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, Event, EventProperty, StreamAttributeGroup, user_id

workout_started = Event(name="workout_started", vendor="com.example", version="1-0-0")
workout_completed = Event(name="workout_completed", vendor="com.example", version="1-0-0")

types_started_30d = Attribute(
    name="types_started_30d",
    type="dict",
    events=[workout_started],
    aggregation="category_count",
    property=EventProperty(vendor="com.example", name="workout_started", major_version=1, path="workout_type"),
    period=timedelta(days=30),
)

top_type_finished_30d = Attribute(
    name="top_type_finished_30d",
    type="string",
    events=[workout_completed],
    aggregation="most_frequent",
    property=EventProperty(vendor="com.example", name="workout_completed", major_version=1, path="workout_type"),
    period=timedelta(days=30),
)

fit_plans_types = StreamAttributeGroup(
    name="fit_plans_types",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[types_started_30d, top_type_finished_30d],
)
```

```
// In the service that builds the user's next session
const user = await signals.getServiceAttributes({
  name: "fit_plans_types",
  attribute_key: "user_id",
  identifier: userId,
});
// { types_started_30d: { run: 9, mobility: 4 }, top_type_finished_30d: "run" }

const skipped = plan.types.filter((type) => !(type in user.types_started_30d));

if (skipped.length > 0 && user.top_type_finished_30d) {
  planBuilder.swap({ userId, from: skipped, to: user.top_type_finished_30d });
}
```

## Try it on your own site. 

Add the SDK, define one thing you want to know about a visitor, and watch it update while you click around. The free tier runs on the real engine: no card, no sales call.

[Start free](https://signals.snowplow.io/start) [Talk to an engineer](https://signals.snowplow.io/start?intent=engineer)
