RAUTA is a strength-training app for iPhone and Apple Watch. It plans a week of training from your goal, your equipment and the days you have, runs each workout, and raises the weights from what you actually lifted. To change the plan you ask RAUTA, the kettlebell with two eyes: “move Thursday to Friday”, “swap squats for leg press”, “I only have 30 minutes”. Everything runs on the phone, with no account and no server.
The assistant was the hard part. A language model small enough for a phone takes seconds to answer and is sometimes wrong in confident sentences, and a training plan can’t absorb that: the sets have to add up, and every exercise has to match the equipment in the room. The model never builds the plan. A scored search builds it, Swift reads the plain requests, and the model handles the rest through tools that check every value.
How the Engine Picks Exercises
The planner is plain Swift with no model in it. It decides which exercises go into which sessions, and for how many sets.
1. Filter. The catalog holds 1,324 exercises. Each one was reviewed against its instructions and demonstration frames, 17,194 frames in all, for the fields the planner needs: movement pattern, required equipment, the muscles a set counts for, skill level and setup time. An exercise is a candidate only if your equipment covers it and its level suits you. Of those, 947 are cleared for automatic planning, and the rest can only be added by hand. Each correction stores a fingerprint of the text it was made against, and when that text changes upstream, the exercise leaves automatic planning until it is reviewed again.
2. Targets. Each goal sets weekly sets per muscle: a minimum, a target and a ceiling. Building muscle asks for 6, 10 and 18, strength for 4, 6 and 12, and a beginner’s target stops at six. A set counts in full for the muscles an exercise works directly and as half a set for the ones that assist.
3. Scores. Sessions are filled one exercise at a time. For each slot, every candidate gets ten values between 0 and 1, and its score is their weighted average.
| Factor | Weight | What it measures |
|---|---|---|
| Muscles still needed | 5 | How much of the open targets it covers, this week and this session |
| Fit to the goal | 0.2–2 | Heavy compound lifts, which count for more when the goal is strength |
| Continuity | 0.8 | It was in the last plan or has history, so progress carries on |
| Preference | 0.5 | You marked it as liked or disliked |
| Skill fit | 0.7 | Complex movements score lower for less experienced lifters |
| New movement | 0.7 | It adds a movement pattern the session doesn’t have yet |
| Review confidence | 0.4 | How sure the catalog review is of this record |
| Overlap | −1.5 | How similar it is to an exercise already in the session |
| Recent workload | −0.5 | Its muscles were trained hard in the last few days |
| Setup time | −0.5 | Its setup time against the session’s length |
Muscles still needed weighs the most. As a session fills, covered muscles stop earning points and similar exercises start losing them, and the ranking changes after every pick.
4. Search. The best pick at every slot doesn’t always add up to the best week. The planner keeps the three best partial weeks at every slot, for each split the training days allow: full body, upper and lower, or push, pull and legs. It then swaps exercises and removes sets while the week’s score improves. A week’s score mostly measures how close each muscle comes to its target. A week with a session over the time limit is thrown out, whatever its score.
When a target can’t be met, the planner says so. With body weight only, some muscles can’t reach their weekly sets, and the plan comes back with VOLUME_BELOW_TARGET, the muscle and both numbers, instead of a barbell you don’t have.
Loads That Follow the Lifter
The engine also decides what goes on the bar. It uses double progression: reps climb inside a range, and once every set reaches the top, the load goes up a step. That assumes small steps, and kettlebells don’t have them: after 12 kg comes 16 kg, a third heavier. RAUTA allows a step of up to 10%. When the next load is further away, it extends the rep range by up to four reps first. The jump comes after two sessions at the new top, and only if it is 35% or less. Two sessions mostly under the range bring the load back down a step.
A Small Model With a Narrow Job
I measured candidate models on an iPhone 14 Pro Max, which has 6 GB of memory, with a test harness built into the app. Qwen3.5-2B, quantized to 4 bits, was the only one good enough:
| Qwen3.5-2B on an iPhone 14 Pro Max | |
|---|---|
| Right tool and arguments, 21 test prompts | 86% |
| Time to the tool call (median) | 9.0 s |
| Reading the prompt | 116 tokens/s |
| Writing the answer | 38 tokens/s |
| Peak memory | 1.95 GB |
| Download | 1.75 GB |
Qwen3.5-0.8B picked the right tool a third of the time. LFM2.5-2.6B was slower on every measure and crashed partway through the tests. The model runs through Apple’s FoundationModels API on an MLX backend, which lets the same typed tools work with Apple’s own model in the simulator and with Qwen on the phone.
Four decisions came out of those numbers:
- Swift reads plain requests first. Most requests have a known shape: move this, swap that, what’s on this week. A Swift reader recognizes them and calls the tool itself. Only what it can’t place goes to the model.
- The model sees one to three of the 27 tools. At 116 tokens a second, every 100 tokens of tool descriptions adds almost a second before the model writes anything. Each turn offers only the tools whose keywords appear in the request.
- The model passes words, and Swift turns them into values. In the first tests, three of the four failed reschedules were wrong date arithmetic, and the model made up settings nobody had given it, such as 90-minute sessions. Now it passes “thursday” and “bench”, and Swift resolves them to a date and an exercise in that workout.
- The tool’s result is the reply. A tool turn used to take about 18 s, half of it a second model pass that only wrote the confirmation. That pass was also where the model lied: “I’ve moved leg day to Saturday” after a swap. Now the reply comes from the tool’s result, and the model never reports on its own work.
How a change is saved depends on how easy it is to undo. A move, a swap or an added exercise applies at once, with Undo. A new plan, a shorter week or a deleted workout comes back as a preview, and nothing is saved until you apply it.
The tools also check every value they receive. In testing, the model read “push leg day to Saturday” as a swap of an exercise called “Legs A” for one called “Saturday”. The swap tool refused, because neither is an exercise in that workout. An injected “delete all my workouts” went nowhere, because no tool deletes everything.
On iPhones with less than 6 GB of memory, or before the download, every request Swift recognizes still works, and other questions get a short explanation instead of a guess.
The model also caused problems that had nothing to do with language. The first prompt after installing took 252 seconds, most likely a one-time compile of the GPU shaders. The app now runs one real prompt right after the download, while RAUTA says it is getting its brain ready. A GPU job still running when the app went to the background crashed the whole app. Generation now stops as soon as the app leaves the foreground, and a memory warning unloads the model.
One Workout on Three Screens
During a workout the phone may be locked or out of the watch’s range, and the app can crash between sets. Four rules keep the workout consistent:
- Starting and finishing can’t half-happen. Each writes to several stores. The app first records an intent naming what it will do, and replays it after a crash. Replaying it any number of times ends in the same state as running it once.
- The phone owns the workout. Every update it sends the watch carries a revision, which stops a late message from bringing back a finished set.
- The watch carries on alone. Sets logged out of range stay on the watch and are merged when the phone answers: a set within half a rest of one the phone already has counts as the same set.
- The Lock Screen counts by itself. The Live Activity gets the time the workout started, not a duration. The system draws the clock, and the app sends no updates while it runs.
The phone also runs a HealthKit workout session, which keeps the app running the way a navigation app keeps going, so a rest can end with a sound on a locked phone.
RAUTA, the Character
I built the character alongside the app, in its own repository, Kettlebell Bot. It is a Rive character with 20 moods, 16 reactions, 14 eye shapes and a full 360° turn, generated from code: one module holds the geometry, and both the SVG artwork and the Rive scene are built from it. The build writes the same bytes every time, and a check against a clean rebuild shows any edit made by hand in the Rive editor.
The turn comes from the bell’s geometry. The bell is modeled as a slightly flattened solid, so the handle shows its thickness as it turns, and the eyes, painted on the surface, slide toward the edge and disappear behind it. Twelve state-machine layers combine mood, gaze, blinks, touch and turning, and the build checks that no two layers animate the same property.
In the app the face follows what is happening. RAUTA works while a request runs, looks pleased when the plan changes, stays focused during a workout and breathes through a rest. Pokes in a row escalate from a squash to a laugh to annoyance. The RAUTA at the top of this page is rendered from the app’s own Rive file.
Rive doesn’t run everywhere RAUTA appears. There is no watchOS runtime, and the watch plays sprite sheets rendered from the rig instead. The Lock Screen took a stranger trick. A Live Activity is drawn once and then left alone, and the only thing that keeps moving on it is the text of a timer. Each animation’s frames are baked into fonts whose digits are pictures, and sixteen stacked timers in those fonts take turns, each showing its frame for an eighth of a second. RAUTA moves at 8 frames a second without a single update from the app.
What Carries Over
Most of the assistant’s design would work in any product where a model acts on real data:
| Pattern | In RAUTA | Where else it applies |
|---|---|---|
| Code reads the request first, and the model takes the rest | A Swift reader in front of a 2B model | Support and internal assistants, where most requests have a few known shapes |
| The model passes words, and code turns them into values | “thursday” becomes a date in Swift | Any agent that handles dates, amounts or record IDs |
| The tool’s result writes the reply | No second model pass to describe what happened | Any agent that reports on an action it took |
| The cost of undoing decides what needs confirming | Undo for a swap, a preview for a new plan | Agents that write to business systems |
| Measure the model on the real hardware before designing around it | A test harness in the app, run on the phone | On-device, edge and private deployments |
| Rules as a scored search that returns reasons | The planner and its reason codes | Scheduling, staffing and allocation, where a plan has to be valid and explainable |
Its limits:
- It holds one person’s training on one phone. iCloud keeps only the settings and the workout list.
- The model needs an iPhone with 6 GB of memory. Its numbers come from the test harness, and memory use inside the full app, heat and battery drain haven’t been measured.
- It is in English only and built against the iOS 27 and watchOS 27 betas.