Skip to main content
← Blog

Letting record changes flow back into the template

VauDium ·

A template is a shape for future records, but as you make more records from it, the template falls behind. Today's work was building the path that learning takes back — moving it from push to pull.

Letting record changes flow back into the template

In Fecit, a task template is a shape — “next time I do this, start here.” Records are made from a template and hold what actually happened that day.

After making a few records from the same template, you notice the template falling behind. The “workout” template’s description says “30 min bodyweight,” but the last five records all add ”+ 50 pushups.” The user keeps making the same adjustment at the record level, and the template keeps its old face.

That learning needs a way back into the template. Today’s work was building that way.

The first attempt

Originally a modal opened the moment you saved a record: “Apply this change to the template too?” When a record diverged from its template, we asked right there.

It was uncomfortable to use. You’d finish a workout, hit save, and a dialog pops up asking you to make a template-level decision. You weren’t in decision mode; you were closing out the day. And once dismissed, the signal was gone. The same ”+ 50 pushups” change could repeat five times — asked five times, ignored five times, all forgotten.

The bigger issue was that signals didn’t accumulate. There was no way to tell a one-off deviation from a systematic update repeated five times.

Server accumulates, template surfaces

So I flipped it. Not push (ask at save time) but pull (see what’s piled up when you visit the template).

The server detects where a record diverges from its template and stores pending_improve_suggestion entries. Equivalent changes dedupe. The template document ends up carrying its PENDING suggestions embedded inline.

When the client loads a template, it groups them by type and shows an [improve] icon next to the affected field’s label. A duration suggestion goes beside the duration picker label, a description-edit suggestion beside the description label. The icon only appears where the user’s eye is already going.

const pendingSuggestionsByType = useMemo(() => {
    const map = new Map<string, TaskTemplateImproveSuggestionModel[]>();
    for (const s of taskTemplate?.pendingImproveSuggestions ?? []) {
        if (s.status !== "pending" || !s.type) continue;
        map.set(s.type, [...(map.get(s.type) ?? []), s]);
    }
    return map;
}, [taskTemplate?.pendingImproveSuggestions]);

I deliberately didn’t make a separate inbox screen. An inbox is a pass-through space — items there get dismissed quickly. Attaching suggestions to the template itself means you only decide on them when you’re already thinking about that template.

A page, not a modal

Tapping [improve] opens a review surface. I first tried a bottom sheet — light, swipe-down to dismiss.

What bothered me was that the sheet’s default close action reads as dismiss. A modal naturally says “look and close.” But improve suggestions are “look and decide.” Anything floating on top of the current screen, dismissible by tapping outside, makes the decision feel cheap.

So they became full pages — improve-task-template-duration, improve-task-template-description, both registered in the stack. Entering one signals: stay here for a moment and choose.

A page per type

Duration and description suggestions feel different. Duration is a single value — “60min → 45min” — and one card is enough: current vs suggested, two buttons.

Description is line-level. Edits usually come in several hunks. Accepting the whole thing wholesale is rare; cherry-picking is the more common need. So I went with GitHub PR style — a red card (before) and a green card (after) in monospace, with apply/dismiss decided per hunk.

- 30 min bodyweight
+ 30 min bodyweight + 50 pushups

DANGER50 / SUCCESS50 backgrounds, −/+ markers in front of each line, accept one chunk, dismiss the next. That flow turned out natural.

When a new signal type shows up, I add another page with the same pattern: improve-task-template-???.tsx. I didn’t fold everything into a mega-modal with branches because the screens for different signal types actually don’t share much.

Ripping out the old flow

With the new flow in place, the old record→template apply modal could have stayed. “Two systems coexist, user picks.” Easy enough to leave.

I tore it out. Handling the same intent in two different places is a tax on the user and the code. The acceptImprove toggle, the improveSuggestion atom, the hasDifference check after record save, the applyToTemplate action — all gone. About 440 lines came out.

Looking back

It’s interesting how the same feature feels different depending on push vs pull. The information flow is almost the same, but the distance between “being interrupted” and “going to look” is large.

A single model — pending_improve_suggestion — creates that distance. When a record is made, the signal doesn’t reach the user right away. It sits on the server for a while, waits for siblings (other records carrying the same signal), and only after enough collect does it surface quietly at the template. The signal’s origin and its reception time are split.

Next time I add another signal type — maybe “the template’s default time-of-day” — it’s the same shape: one more page in the same slot. The road is laid.