Build Reclaim.ai

KINDAreplaces $12/mosaves $144/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A scheduling assistant whose core is a constraint solver over your calendar: fit tasks and recurring habits into free time, respect deadlines and working hours, and rearrange when a meeting lands on top of something without producing a different plan every time it runs.

estimated effort multi-daythe files for this build are in the project pack

RuntimeTypeScript with Fastify and PostgreSQLSolverA deterministic greedy placement with a stability preferenceCalendarTwo-way sync with events the tool created marked as its ownTimeInstants in UTC with the user's zone and working hours declaredReconciliationDetecting external changes before every solve

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the server, the solver and the sync workers.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree locally

    Why Placements, solves and busy time with correct interval queries.

    Get it Docker or your package manager. open ↗

    Verify psql connects and a range overlap query returns correctly

  3. free

    Why Phase 3 writes real events into a real calendar, and the failure modes only appear against a live account with existing meetings.

    Get it A test calendar with a month of realistic meetings, including recurring series with exceptions and some declined invitations.

    Verify Events can be created and the tool's own events are identifiable by stored id

  4. have readyfree

    Why Solver behaviour is entirely determined by how fragmented a real calendar is, and a test calendar with three meetings will make any solver look excellent.

    Get it Use your actual calendar, or reconstruct a genuinely busy week including back-to-back meetings and a day with almost no free time.

    Verify A week exists with at least fifteen meetings and one nearly full day

  5. decidefree

    Why Declined, tentative, all-day and out-of-office events each need an answer before the solver exists, and changing the answer later silently changes every plan.

    Get it Write down whether each status blocks time, and what working hours actually mean for you including whether evenings count.

    Verify A written note gives a rule for every status your calendar produces

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- tasks(id, title, duration_minutes, due_at, min_chunk_minutes, max_chunk_minutes, priority, completed_at)
- habits(id, title, cadence, duration_minutes, window_start, window_end, days_json, priority)
- placements(id, source_kind, source_id, calendar_event_id, starts_at, ends_at, solve_id, pinned, user_moved)
- solves(id, at, reason, placements_changed, inputs_hash) · so you can explain why the plan changed
- busy(id, calendar_id, starts_at, ends_at, external_event_id, is_ours) · the calendar as the solver sees it
- working_hours(day_of_week, start_local, end_local, zone)
- A placement records whether the user moved it, because a block someone dragged somewhere is a decision the solver must respect rather than undo.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://localhost:5432/schedulePlacements, tasks and a copy of your calendar's busy time.
CALENDAR_CLIENT_IDsecretrequireda provider client idCalendar access. Request the narrowest scope that allows creating and updating your own events.
USER_TIMEZONErequiredEurope/LondonWorking hours and habit windows are local. Everything stored is in UTC.
MIN_CHUNK_MINUTESrequired45The smallest useful block. Below this a placement is an interruption rather than work.
SOLVE_STABILITY_BIASrequiredhighHow strongly to prefer keeping existing placements over a marginally better plan.
TOUCH_FOREIGN_EVENTSrequiredneverPresent as a constant so the answer is visible in configuration. There is no other value.

The build, in order

  1. Knowing when you are actually free

    A correct picture of free time, including the awkward cases.

    1. The series minus its cancellations. Assuming the series is the common bug.

      Files src/recurrence.tssrc/recurrence.test.ts

    2. Declined, tentative, all-day, out of office. Each needs an explicit answer.

      Files src/busy.ts

    3. Not the server's, and not a fixed offset.

      Files src/hours.ts

    done when · tick each as it passes
    watch out
    • Treating a declined invitation or an all-day event as busy time removes most of the week from consideration, and treating tentative events as free schedules work on top of meetings people are probably attending. Decide the rule for each status explicitly, and expand recurrence with exceptions rather than assuming the series.
  2. Placing work into the gaps

    Tasks and habits get placed sensibly and the same inputs give the same plan.

    1. Same inputs, same plan, asserted in the suite. This is what makes it usable.

      Files src/solve.tssrc/solve.test.ts

    2. Twenty minutes of deep work is not useful. The minimum exists for a reason.

      Files src/chunk.ts

    3. Silently dropping a task before its deadline is the worst possible behaviour.

      Files src/report.ts

    done when · tick each as it passes
    watch out
    • A solver that finds a slightly better arrangement each run will move your Tuesday afternoon every time it executes, and a plan that changes constantly is one nobody can work to. Prefer keeping existing placements over marginal improvements, and make identical inputs produce an identical plan.
  3. Writing to a calendar you did not create

    Placed blocks appear in your real calendar and nothing else is ever touched.

    1. Never by title or colour. A rename must not orphan a block.

      Files src/calendar.ts

    2. In the write path, with a test that attempts it and expects a refusal.

      Files src/calendar.tstests/foreign-events.test.ts

    3. Sync tokens expire. A resync that duplicates a week is very visible.

      Files src/sync.ts

    done when · tick each as it passes
    watch out
    • Identifying your own events by title or colour means a user renaming a block makes it invisible to the tool, which then creates a duplicate, or worse, a coincidence of naming makes the tool delete a real meeting. Store the created event identifier and treat anything not in that set as untouchable.
  4. When a meeting lands on your work

    A new meeting moves the affected block and nothing else.

    1. The displaced block and its dependents. Not the week.

      Files src/resolve.ts

    2. Someone dragging a block has expressed a preference. Undoing it is insulting.

      Files src/placements.ts

    3. A block that started is history, whether or not the work happened.

      Files src/resolve.ts

    done when · tick each as it passes
    watch out
    • Re-solving the whole week when one meeting appears rearranges work the user has already mentally committed to, and doing that a few times teaches them to ignore the calendar entirely. Move the displaced block and its dependents, leave everything else, and treat a block the user dragged as pinned.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more tasks & calendar.