Work Model & Information Architecture (Proposal)¶
Status: draft proposal. Not part of the canonical numbered set. If accepted: the information architecture and page specification fold into 10 Client Applications & UX (Part B ยง4 and ยง6); the schema into 04 Data Model; the change log, proposals and attention model into 03 Agent Architecture & Extensibility (ยง10 attention surfacing, replacing ยง18 agent views); space policies and patient masking into 07 Security & Governance; regional deployments into 15 Tenancy & Deployment Topology and 05 Persistence, Storage & Ingestion; the channel posture into 16 Chats, Channels & Identity. The decision records (ยง15) are appended to the decision log. Do not run the docs index/changelog maintainer against this file until it is promoted. ADR numbers are provisional (latest accepted is ADR-030; ADR-031 is drafted in the model router proposal).
Companion material. The walkthrough A Day in Thinklio (private Claude artifact, https://claude.ai/artifact/HykpDowjuV3Yvn9Up9eppr) shows this model as a working day for three people, with clickable screens. The research behind it is in Model expressiveness and UI strategy and Experimental AI interfaces survey. The adoption plan it serves is Thinklio as the Novansa/FALL daily work surface.
Precis¶
Thinklio's backend is in good shape, and its governance model (account โ team โ user, narrowing only, everything audited) is a real differentiator. The app built on top of it is organised around the machinery: agents, chats, jobs and knowledge facts. Users have to pick a mechanism before they can get to their work, and the result feels like "chats with agents in them". There is a governance hierarchy (who is allowed) but no work hierarchy (what this is about), nothing arrives without a prompt, and the accountability features users would trust most are only visible on admin pages.
This proposal reorganises the product around work objects and gives it an opinion: accountable delegation. Every piece of work has a home, an owner and a record. Work is handed to people or agents and comes back for review. Five building blocks carry this: spaces (ongoing areas of work), lists and records (anything you track, with your own fields, views and totals), tasks and projects (one generalised task type; projects are records whose tasks roll up), people and agents (one directory, one assignee picker), and an inbox that routines, mailboxes, handoffs and approvals fill. Messaging stays the engine's substrate (ADR-020); it stops being the user's mental model.
Underneath, every change to a work object goes through one write path that logs who or what made it, from what source, before and after, and whether it was applied or only proposed. History, undo, "what changed" and agent proposals all read from that log. This is the piece that is expensive to retrofit, so it is designed in now. The first delivery is a pilot for Novansa's own team: a small group of developers who use it for their own work every day until they love it. FALL and other accounts join after that.
1. What the current app gets wrong¶
- Messaging-first spread from the engine into the UI. ADR-020 says it "eliminates the distinction between 'chatting with an agent' and 'using the platform'". That is a strong engine decision (one audit trail, channels as windows) and the reason the app feels like a chat client. Job progress, approvals and artefacts arrive as cards inside a chat stream (
apps/web/src/components/chat/MessageStream.tsx), and there are two chat paradigms side by side (components/chatfor agent chat,components/chatsfor Messages). - Agents are presented as apps.
apps/web/src/components/views/AgentViewRenderer.tsxmaps Taskmaster to tasks, Rolodex to contacts, Scribe to notes, Keeper to the vault and Dispatch to tickets. To see your contacts you open an agent, and the roster answers "which agents need me?" rather than "what needs me?". The new/taskspage is the first place this pattern breaks. - There is a governance hierarchy but no work hierarchy.
chatshas no subject, tasks sit intask_lists, notes link to one object, and contacts and items float. "Context-specific chat" has nothing to attach to, and agents have no bounded working context beyond "this chat plus four knowledge layers". - Nothing arrives. The briefing is a dashboard of module cards ("Urgent Items" is high-priority rows from
items), and the notification panel renders a hard-coded empty array. On a normal day, getting value means writing a prompt, which the UI strategy research (ยง6.4) names as the failure to avoid. - The best feature is admin-only. The cascade, audit and accountability live on seven admin pages. Users never see them, so they add nothing to trust or to the working day.
2. The opinion: accountable delegation¶
Notion's opinion is "no opinion". Office's is the document, Slack's is the conversation, Linear's is the issue in a cycle. Thinklio's is:
Every piece of work has a home, an owner and a record. You hand work to people or agents, and it comes back to you for review.
This puts the governance model into the daily experience, and it is opinionated enough to stay clear of both failure modes the product must avoid.
Guardrails against becoming Notion or Office:
- Fixed object types. Spaces, lists and records, tasks and projects, docs, threads. No blocks, no page trees, no arbitrary nesting. Lists are the one flexible schema, and they are bounded to fields plus views.
- Templates carry the opinion. A space or list template brings its fields, views, task blueprints, agents and rules. Admins pick templates; they rarely build from scratch.
- Docs stay plain. A good editor, comments, revisions, agent suggestions and export to docx/pdf. Thinklio does not compete with Word or Google Docs.
- No canvas or graph as a primary surface, per the research's survey of what has and hasn't been adopted.
3. The model: five building blocks¶
Spaces and Lists are working names. They stay unless people stumble over them: first in the Novansa pilot, then with FALL staff when FALL joins, since they are the better test of plain naming (ยง13).
3.1 Spaces¶
A space is an ongoing area of work: Outreach, Orthotics, Clinic ops, Clinical knowledge for FALL; Product, Clients, Operations for Novansa. A space:
- has members, who are people and agents, each with a role (owner, member, viewer);
- holds lists, tasks, docs and a conversation (a fixed toolset, not a page tree);
- can add rules that narrow the account's and team's policies, so the cascade becomes account โ team โ space โ user;
- is the unit of agent context: an agent that is a member of a space works from that space's records, docs, tasks and conversation, which keeps its context bounded and explainable. Agents retrieve that content directly as documents and records; the four knowledge layers (agent, account, team, user) stay as they are, and a space layer is reconsidered after the pilot;
- never ends. Things that end (projects, trips) are records inside spaces.
Teams stay what they are today: org structure that decides who you are and what roles you hold. A space may belong to a team, in which case it inherits that team's policies; it can also include people from several teams. Teams do not get spaces automatically: onboarding suggests spaces from templates (Outreach, Orthotics, Client projects and so on), which keeps the sidebar to real areas of work.
3.2 Lists and records¶
A list (schema name collection) holds anything you track: trips, orthotic orders, expenses, stock, equipment, follow-ups, projects, clients. Each list has its own fields, saved views (table, board, calendar) and totals. Each record is a place, not just a row: fields, notes, a thread, a history with undo and, when it needs one, a checklist of tasks. Lists are covered in detail in ยง4.
3.3 Tasks and projects¶
Tasks are one built-in type shared by the whole system. A task lives in a space and can sit inside a project or any other record. Its assignee can be a person or an agent. Projects are records in a built-in Projects list, and their tasks roll up into progress. Any record (a trip, a client) can carry tasks the same way. Links connect tasks and records to anything else. Covered in detail in ยง5.
3.4 People and agents¶
One directory and one assignee picker. Agents can be assigned, @-mentioned, added to spaces and given standing duties (routines, ยง8.3). They work on records rather than owning them. Each person has an assistant; specialists join a space to do one job (Lab Watcher in Orthotics, Stock Watcher in Clinic ops). The domain agents in the catalogue that exist mainly to own a data type (Taskmaster, Rolodex, Keeper, Scribe, Dispatch) become capabilities of the assistant, which fits the skills proposal (agent = who, tool = what, skill = how). Agent-to-agent delegation (ADR-013) is unchanged underneath; users see it as a trail on the task (ยง5.4).
3.5 Inbox¶
A processing queue rather than a dashboard: Needs you, Today, Waiting on and What changed, with a capture bar on top. Routines, mailboxes, handoffs, mentions and approvals fill it. The start location is a user setting with account and team defaults. Covered in ยง7.
3.6 How they nest¶
Account (FALL) governance: account policies
โโ Team (Clinical) governance: team policies (who you are)
ยท owns, or not
Space (Outreach) work: members (people + agents), space rules
โโ Lists Trips ยท Expenses ยท Follow-ups
โ โโ Record Newman & Jigalong ยท Feb
โ โโ fields ยท notes ยท thread ยท history
โ โโ tasks (checklist created by the trip template)
โโ Tasks loose tasks and milestones
โโ Docs
โโ Conversation
Inbox (per person) needs you ยท today ยท waiting on ยท what changed
3.7 From today's navigation¶
| Today | Becomes |
|---|---|
| Briefing | Inbox, a processing queue (ยง7) |
| Tasks | My work, plus each space's Tasks tab |
| Messages | Threads on records and space conversations, plus a Chats entry for direct messages and general group chats (FALL Chat, Announcements, Front Desk); their mentions and requests appear in the Inbox |
| Agents | People and agents directory; agent configuration moves to Admin / Agent Studio |
| Activity | "Waiting on" in the Inbox, plus each record's history; the full audit stays in Admin |
| Knowledge | Library (docs and "ask the organisation"); the personal fact browser becomes a privacy page, "what Thinklio knows about me" |
AgentViewRenderer agent tabs |
Retired. Contacts, notes, items and the vault become lists or records in spaces |
Sidebar, top to bottom: capture and search (โK), Inbox, My work, Chats, Spaces (pinned first), People and agents, Library, then Admin for roles that have it.
4. Lists in detail¶
This section adopts the List Manager design in the adoption strategy (field definitions stored on the collection, record values keyed by stable field IDs, a notes field on every record, multiple named views) and extends it with what the walkthroughs showed was needed.
4.1 Field types¶
| Type | Notes |
|---|---|
text, longtext |
longtext is markdown |
number, currency |
precision, unit label, currency code |
select, multiselect, status |
choices with colours; status adds ordered stages used by boards and by "days in stage" totals |
date, datetime |
optional end (ranges, so Notion date ranges import cleanly); time zone on datetime |
checkbox |
|
person |
a user or an agent; single or multiple |
file |
stored in the account's regional bucket (ยง11) |
url, email, phone |
|
relation |
to records in another list; stored as links (ยง5.3) so both sides see it |
rollup |
count, sum, average, min, max, earliest, latest, percent checked, over a relation |
external |
a reference into another system (cliniko.patient, cliniko.appointment, xero.bill, google.mail.thread, drive.file), rendered live with an action menu |
created_at, updated_at, created_by |
system fields |
formula |
later |
Field IDs are stable and never reused, so renaming a field or changing a compatible type never touches records. Removing a field archives it.
4.2 Views and totals¶
- View types: table, board (group by a select or status field), calendar (by a date field). Gallery later.
- Configuration per view: visible fields, filters (AND/OR groups of field, operator, value), sorts, grouping, and totals. Views are saved per list and shared by default; a person can keep a private view.
- Totals appear in column headers (board), group headers and footers (table): count, sum, average, min, max, percent checked, earliest, latest, and days between two dates or since a status change.
- Views from questions. "What's our average turnaround by lab this quarter?" asked of a list returns an answer with Save as view. A saved view is a durable object that stays current, which is the "views become queries, then objects" step in the research doc (ยง7.2).
Implementation notes. Convex cannot index arbitrary keys inside a record's values, so filtering and sorting on custom fields happens in the query handler after an indexed range on collectionId. That is fine for the hundreds to low thousands of records these lists hold. For larger lists, add a small number of indexed slots per collection (for example one sortable date and one status) that the write path keeps in sync. Totals over large lists and rollups use the Aggregate component (@convex-dev/aggregate, 11 Convex Reference) rather than counting at read time.
4.3 The record page¶
Header with the title field and the space's rule chips, then fields in schema order (editable inline), then tabs: Checklist (tasks whose parent is this record), Thread (the record's conversation, ยง10.4), History (from the change log, with undo), Notes, and any relation tabs (a trip's Expenses). External fields show live data and actions: a Cliniko patient field offers Book appointment with the practitioner's open slots.
4.4 Templates¶
A list can define record templates. A template sets default field values and a task blueprint: tasks with titles, assignee rules and due dates relative to a date field on the record (start โ 56 days, end + 7 days). Creating a record from a template creates those tasks. When the date field moves, open tasks move with it. The Outreach trip template is the reference case: practitioners at Tโ10 weeks, flights at Tโ8, accommodation at Tโ6, clinic rooms at Tโ5, car at Tโ4, recall list at Tโ3, packing at Tโ3 days, reconciliation at end +1 week.
4.5 Rules¶
Simple "when this changes, do that" rules on a list: when a field changes or becomes a value, create a task, notify someone, or hand it to an agent (which produces a proposal, ยง6). Example: when an order's status becomes Received, create "Book fitting" for the order's practitioner. Rules are owned by space owners and admins, like routines.
4.6 Built-in list templates¶
Shipped as templates with guaranteed fields (users can add more):
- Projects: owner, status, health, start, target, progress (rollup of tasks), milestones. See ยง5.2.
- Expenses: date, vendor, amount, GST, category, paid by (company or person, for reimbursement), related record (usually a trip), photo, Xero status, Xero bill link. Agreed for receipts (ยง4.8).
- From the adoption strategy: remote visit roster, stock register, contact list, equipment register, meeting log.
4.7 Import¶
CSV and Google Sheets first, then Notion databases through the Notion API or the existing Notion MCP service in thinklio-services. Notion's property types map almost one to one:
| Notion | Thinklio |
|---|---|
| Title, Text | text (primary), text or longtext |
| Number | number or currency, by the property's format |
| Select, Status, Multi-select | select, status, multiselect |
| Date (incl. ranges) | date / datetime with optional end |
| Person | person (matched to account members) |
| Files & media | file (copied into the regional bucket) |
| Checkbox, URL, Email, Phone | same |
| Relation, Rollup | relation, rollup |
| Formula | imported as static values and flagged until formula exists |
| Created/edited time and by | system fields |
Each page's body imports into the record's notes. Saved Notion views are recreated where they map to table, board or calendar.
4.8 List or attachment?¶
A rule of thumb for deciding whether something is its own list: if you will ever total it, look at it across parents or track its own status, it is a list. If it is only evidence (a photo of a consent form, a scan), attach it to the record it belongs to.
Receipts are the worked example. As attachments they would need special code for totals, could not be viewed across trips ("everything not yet in Xero", "fuel this quarter"), could not carry their own Xero or approval status, and would have nowhere to go when they are not about a trip. As an Expenses list related to trips, the trip's Receipts tab is that list filtered to the trip and its spend is a rollup, while the same records support reimbursement, Xero sync and non-trip spending. On the trip page the two options look identical.
5. Tasks, projects and links¶
5.1 Tasks¶
The existing tasks table becomes the single task type for the whole system:
- Home: every task belongs to a space. It may also have a parent: a project, any other record (a trip, an order, a client) or another task (sub-tasks).
- Assignee: a person or an agent. Assigning a task to an agent is dispatching a run (ยง5.4).
- Status:
todo,in_progress,in_review(work handed back for checking),done,cancelled. - Due date, priority, recurrence (RRULE, already built and spawning on completion).
- Milestone: today's
task_listsbecome milestones and sections inside a project or space, so existing tasks carry over. - Thread, history and attachments like any record.
- Custom fields per space using the same field definitions as lists, built after the pilot and designed in now so they need no migration.
POST /v1/tasks (ADR-029) gains spaceId, parent and links, so n8n, Telegram and other systems can file tasks in the right place.
5.2 Projects¶
A project is a record in a built-in Projects list. Any space can have one.
- Guaranteed fields: owner, status, health, start, target date, progress; plus any fields the space adds (client, budget).
- Milestones group its tasks; progress rolls up from them.
- Project templates create tasks with dates relative to the start or target, exactly like trip templates.
- Portfolio is a view over Projects lists, for one space or across every space a person can see.
This settles an open question from the IA review: spaces are ongoing and never end; projects and trips are the things inside them that do.
5.3 Links¶
One typed link table connects work objects:
| Kind | Meaning | Example |
|---|---|---|
about |
the task or doc concerns this object | "Chase Kestrel" is about order O-1180 |
blocks |
one task holds up another | "Date time zones" blocks "Calendar view" |
relates_to |
anything else worth showing | a client record relates to a contract doc |
duplicates |
marks a duplicate | |
field |
the value of a relation field |
a receipt's related trip |
Containment (task in project, sub-task in task) is the task's parent, not a link, because it drives queries and rollups.
- Backlinks everywhere. Every page shows what links to it: an order shows "2 open tasks about this order"; a client shows its projects.
- Links cross spaces. "Courier 2 rolls of felt to Onslow" lives in Clinic ops and is about both the Onslow trip (Outreach) and the felt stock record (Clinic ops).
- A link never grants access. If you cannot see the other end, you see that a linked item exists and nothing more.
- Agents create links with provenance, through the same write path as everything else (ยง6).
This follows the polymorphic patterns already in the schema (entity_tags, notes.linkedType) and the research doc's rule (ยง5): give strict types to relationships that drive behaviour (containment, dependency) and leave the rest as relates_to.
5.4 Handoffs¶
Assigning a task to an agent starts a run under the harness (doc 03), with the task's brief, its parent record and its links as context. The task's status follows the run. When the agent finishes, the result comes back as a proposal attached to the task (ยง6.2) and the task moves to in_review in the assigner's Inbox. Agent-to-agent delegation inside the run appears on the task as a trail ("Research asked the Fact Checker to verify 4 claims") with its cost, which replaces the Activity page for users.
5.5 My work¶
Every task assigned to you, wherever it lives, grouped by due date (overdue, today, this week, later), with the parent shown as a breadcrumb. Next to it, Waiting on lists tasks and proposals you handed to someone else or to an agent.
6. One write path: changes, proposals and effects¶
6.1 Why¶
The research doc's central argument (ยง2, ยง8) is that history, provenance, reversibility and proposals are cheap to record at write time and expensive to retrofit. Thinklio does not need the full assertion store and bitemporal model on day one, but every write to a work object must go through one path that records enough to support them later.
6.2 The change record¶
Every create, update, archive, link and unlink on a record, task, link or doc writes the new state and a changes row in the same Convex transaction:
- what: object type and ID, operation, and a field-level diff (before and after);
- who: actor type (
user,agent,integration,system) and ID; - basis:
expressed(a person stated it),inferred(the system concluded it),confirmed(inferred, then ratified), with an optional confidence; - source: the message, email, Cliniko record, API call or import it came from, with a short excerpt;
- status:
applied,proposed,rejected,undoneorsuperseded; - reversibility: whether undo is possible;
- validity: optional
validFrom/validTofor facts that are true over a period (the research doc's second clock), unused at first but present so it never needs a migration.
The history tab, undo, "What changed", the audit view and the agent's own record of what it did all read from this table.
6.3 Proposals and effects¶
A proposal groups proposed changes with effects: actions that leave Thinklio and cannot be undone from inside it (send an email, create a Xero bill, book a Cliniko appointment). The stock reorder in the walkthrough is one proposal: one email effect, three record changes and one new task. A reviewer can accept all of it, accept part of it, edit it or reject it.
Act or propose:
- Apply directly when the change is reversible, the space's policy lets that actor act, and confidence is above the space's threshold. It still appears in "What changed" with undo.
- Propose when any of those is false, and always for effects unless a policy explicitly allows the actor to perform them.
- Ask (a "Choose" item) when the agent cannot resolve identity, such as an email that matches two orders.
Who reviews. A proposal goes to:
- the reviewer named on the routine that produced it, set when the routine is set up and defaulting to whoever set it up;
- whoever handed over the work, when a task was assigned to an agent;
- whoever the space's policy names, for approvals such as spending (FALL's Xero bills over $500 go to Priya).
The proposal carries the rule that required review, which the UI shows as the chip ("Clinic ops: agents draft supplier emails, people send them"). Today's pending_approvals (tool-call approval gates in chats) are a kind of proposal; they appear in the Inbox from the start and merge into proposals once the write path exists.
6.4 Growth¶
changes is append-only and grows with use. Keep 18 months readily available, or longer where the account's audit retention requires it (doc 07). After that, summarise each record's older history into snapshots that keep the audit-relevant fields, offer a full export first, and show an "older history summarised" marker on the record rather than letting history disappear quietly.
7. The Inbox and attention¶
7.1 Sections¶
| Section | What appears | Stored as |
|---|---|---|
| Needs you | proposals awaiting your review; approvals; requests and @-mentions in threads; tasks returned to you in_review; "Choose" questions from agents |
inbox_items, with state (open, done, snoozed, dismissed) |
| Today | tasks due today; calendar events; dated records in your spaces (fittings, trip days) | live query |
| Waiting on | tasks and proposals you handed to others or to agents; approvals you requested | live query |
| What changed | changes in your spaces since you last looked, excluding your own, summarised in domain language, with undo where safe | live query over changes and a last-seen marker |
The capture bar takes anything ("remind me to call Kestrel Thursday", a pasted email, a photo) and routes it through smart input triage (doc 03 ยง9) into a task, handoff, note or thread message. The attention principles in doc 03 ยง10 carry over unchanged: quiet means good, nothing cries wolf, FYI items decay, and an empty Needs you says "You're clear."
7.2 Start location and defaults¶
Where a person lands is a preference: Inbox (default), My work, a specific space, or a chat. Preferences follow the cascade: the account sets a default, a team can set its own (field staff to Today, operations to the Inbox), and each default says whether people may override it. The same mechanism carries other defaults, starting with the morning brief.
7.3 Morning brief¶
On by default for field staff (decided), set at account or team level and switchable per person. Delivered by push (Flutter), email or Telegram: today's list, flagged items and anything in Needs you.
8. Governance in the work model¶
8.1 The cascade with spaces¶
Account โ team (when the space belongs to one) โ space โ user, narrowing only. Space policies reuse the categories of team_policies (action permission, cost limit, data boundary, approval gate and so on), so the policy engine evaluates one more layer rather than a new mechanism.
8.2 Rules where people act¶
Rules show up at the point of action as chips on spaces, records and proposals, and every proposal names the rule that required it. Approval routing uses the same rules: FALL's "Xero bills over $500 need the owner's approval" sends that proposal to Priya.
8.3 Routines¶
A routine is a standing duty an agent owns in a space: on a schedule (every Monday, check stock against reorder levels) or on an event (a new email in the orders mailbox). Routines produce changes and proposals through the write path. Space owners and admins set them up (decided); admins decide which kinds of routine and which integrations each space may use. scheduled_agent_runs and event_triggers gain a space, an owner and a reviewer, who receives the routine's proposals and defaults to whoever set it up.
9. Patient identity (Cliniko)¶
Decision: copy patient names only from Cliniko into Thinklio, no other health information. Cliniko stays the source of truth. Everything else (appointments, contact details, notes) is looked up on demand through the existing Cliniko MCP service, using the acting user's own Cliniko API key. Cliniko's permissions differ per user, and Thinklio must never widen them.
- Copy on link. A patient is copied (Cliniko ID, first and last name) only when a Thinklio record links to them, using the linking person's key. The name refreshes when someone with Cliniko access to that patient views the record and the copy is more than a day old, using their key. There is no background refresh, because there is no shared key. The copy is removed when the patient is archived in Cliniko or no longer linked.
- Per-user keys everywhere. Lookups and actions such as Book appointment run with the key of the person doing them. An agent uses the key of the person it acts for. A routine that acts for nobody in particular uses the key of the person who set it up, so it never sees more than they can. Each person adds their own Cliniko key once, as a person-level connection (ยง10.2).
- Names follow Thinklio's access; everything else follows Cliniko's. A copied name is visible to anyone who can see the record in Thinklio, while other patient data needs the viewer's own Cliniko access. Keep the Orthotics space's membership to people who may see those patients' names.
- Field type.
externalwith providercliniko.patientstores the Cliniko ID; the record shows the name frompatient_refsand offers live actions (open in Cliniko, book appointment). - Treat names as health information. A name next to an orthotic order still shows that someone is a FALL patient. Patient names are readable only through records in spaces the person can see, and views and exports of patient fields are logged.
- Keep names out of model prompts by default. When an agent works on a record, the harness replaces patient names with initials or the Cliniko ID before the LLM call and restores them for display. A space policy (category
data_boundary) can allow names when a task needs them. Storage residency does not cover prompts sent to model providers, so this default applies in every region. - When. None of this is in the Novansa pilot, which holds no patient data. It is built when FALL joins (ยง13).
- Residency. FALL holds patient names and no other patient data. Where FALL's account starts is in ยง11.
- Search. Patient names are searchable inside Thinklio, which is the main reason to copy them.
10. Integrations and channels¶
10.1 Provider-neutral capabilities¶
Integrations are built as capabilities (mail, calendar, files, contacts, identity) with one adapter per provider, so adding a provider never changes product code. Google Workspace first (Novansa and FALL use it), then Microsoft 365, then Apple: Sign in with Apple, iCloud calendar and contacts over CalDAV/CardDAV, iCloud mail over IMAP, and later Siri and Shortcuts through App Intents, which doc 16 (ยง3) already anticipates as an OS-level consumer.
10.2 Three roles¶
Every integration plays up to three roles:
- Source: brings things in. Lab and supplier emails update orders; Cliniko bookings attach to trips; calendar events fill Today.
- Link: appears on records as an
externalfield with live data. - Action: does something in the other system. Actions go through proposals and the rule cascade (ยง6.3); FALL can let agents create draft bills in Xero and never approve them.
Connections use the scopes doc 09 defines: account-level (Xero, a shared mailbox) set up by admins; person-level (own mailbox and calendar) where the account allows it. Cliniko is person-level, not account-level as doc 09 lists it today: each person connects their own Cliniko API key, because Cliniko's permissions differ per user (ยง9). Credentials attach to principals per ADR-025.
Order: Google Workspace (mail, calendar, Drive), Notion import, Cliniko and Xero (both already exist as MCP services in thinklio-services, ADR-030); Telegram is in place. Then Microsoft 365, Slack and Teams as channels, Zapier and Make alongside the Platform API, and Apple.
10.3 Channels¶
The posture in ADR-027 stands: the native apps mirror conversations in full; other channels work in relay or injection mode. Within that:
- Write the channel adapter contract (doc 16 ยง8.3, currently deferred) against the cross-product Interaction Protocol (
dev/docs/interaction-protocol.md, outside this repo), so every channel plugs into threads the same way. - Telegram relay first: morning briefs, quick capture (a receipt photo is read and filed against the current trip), and approvals with inline buttons. A field worker's message lands in the right record's thread; replies come back to them.
- Tavus video, ported from Twikka (
twikka/docs/twikka-video-coaching-spec.md, outside this repo). Twikka already treats video as "another channel into the same conversation": call markers in the thread, a summary and the full transcript after the call, and actions extracted from the transcript for confirmation (spec ยง5.4, ยง5.6). In Thinklio a call started from a record leaves its summary and transcript in that record's thread and its actions as a proposal.
10.4 Threads on records¶
chats gains an optional subject (a record, task or doc) and a space. Each work object has at most one subject chat, created when someone first comments or a channel message is routed to it. Membership comes from the space; chat_members rows record followers and notification preferences. Channels bind to these chats exactly as they bind to any chat today, so Telegram relay and Tavus calls need no new concepts.
11. Regional deployments and storage¶
Decision (cross-product, applies to every Novansa product on Convex):
- Add a second Convex account in Sydney (
aws-ap-southeast-2) alongside the existing Ireland deployment (aws-eu-west-1). Convex now offers Sydney; any non-US region costs 1.3ร the standard rate (Ireland already does), and a deployment cannot change region, so moving means a new deployment and an export and import (Convex regions). - At signup, accounts based in the EU go to Ireland with no choice (proposed). Every other account chooses its data location, pre-selected from a location-based suggestion (
dev/docs/regional-deployments.mdยง3). - Object storage follows the region: Cloudflare R2 for the EU, an S3-compatible service hosted in Australia for AU.
- Development and the Novansa pilot run on the Ireland deployment, where Novansa stays. The pilot holds no patient data.
- FALL joins after the pilot. If the Sydney account exists by then, FALL starts there and no tenant move is needed. If FALL starts in Ireland, it moves to Sydney before production as a one-off tenant move (export, import with ID remapping, then delete; doc 15 ยง6), so FALL's data must stay cleanly scoped to its account.
Consequences:
- Region lives on the Clerk organisation (public metadata), set at signup before any data is written. Clients read it at sign-in and connect to the matching Convex URL; the account switcher changes deployment when a person belongs to accounts in both regions (Andrew in Novansa and FALL, for example). This is the minimal form of the doc 15 control plane (ยง5.2), which is now needed earlier than doc 15 planned.
- Clerk webhooks reach both deployments; each ignores organisations whose region is not its own.
- Code stays deployment-agnostic (doc 15 ยง4.1): no hardcoded Convex URLs or buckets; the agent catalogue and platform defaults come from seed functions.
- Storage:
storage_bucketsalready models providers and jurisdictions (au,eu). It gains ans3_compatibleprovider for the Australian service, and each regional deployment registers its own platform bucket. - Identity data in Clerk covers staff only. Clerk is hosted in the US with no regional option; EU data goes there under the Data Privacy Framework, and privacy policies disclose it to Australian users. Patient names never go to Clerk. All Convex products may move to Convex Auth v2 once it covers our needs (
dev/docs/regional-deployments.mdยง11), which would keep sign-in data in the region too. Clerk should only sign users in. Today Thinklio also takes account membership and the admin/member role from Clerk organisations (the auth middleware readso.idando.rolfrom the token, andmembers.tsnotes that membership "comes from Clerk"). Moving those into Thinklio's own tables (ยง14) is part of the foundation, and new code reads the account and role from the auth middleware (lib/middleware.ts), never from Clerk directly. - Prompts leave the region whichever deployment the data is in, so the masking default in ยง9 matters in both.
- Doc 15 changes emphasis: region becomes a signup choice for every tenant, while dedicated (T2) and customer-owned (T3) deployments remain for isolation and procurement reasons.
- Cross-product write-up. Every Novansa product on Convex (Twikka, CalmerFlow, Clindice and Thinklio) follows the same pattern, which is written up in
dev/docs/regional-deployments.md(outside this repo): region choice at signup, routing through Clerk metadata, webhook handling, storage, legal notes and rollout. This section is Thinklio's instance of it.
12. Clients¶
- Flutter is the primary app on phones and tablets (
convex_flutterand the Clerk Dart SDK, per doc 02). The web app is responsive as a fallback for people who do not have the Flutter app installed, and is the primary client on desktop. - Shared definitions, per-platform renderers. Field types, view types, proposal and effect types, and Inbox item kinds are data defined once in Convex. React and Flutter each implement a renderer per type, which is the split the Interaction Protocol already makes (one shared contract, renderers per environment).
- Flutter priorities: Inbox, My work, record pages, capture (camera for receipts and documents), push notifications, then list views (board and table become stacked lists on phones).
- Web changes: the navigation in ยง3.7; the mobile bottom bar becomes Inbox, My work, Chats and Spaces, with capture one tap away; Briefing, Activity, the agent-owned views and the standalone Knowledge fact browser are retired as described.
13. Delivery plan¶
Each step produces something FALL or Novansa staff can use.
- This proposal. Review, then promote: ADRs to the decision log, schema to doc 04, IA to doc 10, residency to doc 15. The cross-product regional pattern is drafted in
dev/docs/regional-deployments.md. - Foundation in Convex. Account membership and roles in Thinklio's own tables, so Clerk only signs users in; spaces, space members and space policies; collections, records, views and rules; the write path with
changesandproposals;links; the task extensions; subject chats. Task changes use widen, migrate, narrow with the Migrations component (assignedToโassigneeType/assigneeId; backfillspaceId). - Novansa pilot. Novansa's own team runs its own work in Thinklio, in the Ireland deployment with no patient data: its spaces, its lists (imported from Notion, CSV first), projects and tasks, record pages with thread and history. Web first and responsive; Flutter starts on Inbox, My work and record pages in parallel. The team are developers, so rough edges are acceptable and the pilot ships in small pieces, fixing what daily use shows. Other accounts are invited only once the team loves it. Success: the team opens Thinklio first for its own work, and the Notion lists it replaces stop being updated without anyone being told to stop.
- Inbox and Google Workspace. The Inbox (ยง7) with start location and defaults; the first routine on a real Novansa workflow; Calendar into Today; sending mail as an effect; the morning brief.
- FALL joins. The Orthotics space with its orders list imported from Notion, the Pipeline board and table with totals; Cliniko patient names copied on link (
patient_refs, thecliniko.patientfield) with each user's own key, masking in prompts and Book appointment (ยง9); the shared orders mailbox feeding Lab Watcher. Then Outreach: trips with templates and relative-date checklists, the Expenses list with photo capture (Flutter camera and Telegram relay), Xero draft bills with approval routing. Check the names Spaces and Lists with FALL staff. Region per ยง11. - Channels. The adapter contract, Telegram relay, Tavus video from Twikka.
- Later. Microsoft 365 and Apple adapters; the rest of the Notion migration; per-space task fields; a space knowledge layer if the pilot shows a need; formulas and charts; the region choice in every product's signup.
Success indicators for each account follow the adoption strategy: staff open Thinklio first for a work task, new SOPs and records are created in Thinklio, and Notion activity drops without anyone being told to stop.
14. Schema sketch¶
Illustrative, not final. New tables follow the index naming in convex/_generated/ai/guidelines.md (by_field1_and_field2); existing tables keep their names until touched. accountId is the Clerk organisation ID, as elsewhere.
const fieldDefinition = v.object({
id: v.string(), // stable, never reused
name: v.string(),
type: v.string(), // ยง4.1; a literal union in the real schema
options: v.optional(v.any()), // per type: choices, currency, relation target, rollup spec, external provider
required: v.optional(v.boolean()),
archived: v.optional(v.boolean()),
});
// Inside defineSchema({ โฆ }):
// โโ Spaces โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
spaces: defineTable({
accountId: v.string(),
teamId: v.optional(v.id("teams")), // inherits team policies when set
name: v.string(),
slug: v.string(),
description: v.optional(v.string()),
icon: v.optional(v.string()),
colour: v.optional(v.string()),
templateKey: v.optional(v.string()), // "outreach", "orthotics", "client_projects", โฆ
archived: v.boolean(),
createdBy: v.string(),
})
.index("by_accountId_and_archived", ["accountId", "archived"])
.index("by_accountId_and_slug", ["accountId", "slug"]),
space_members: defineTable({
accountId: v.string(),
spaceId: v.id("spaces"),
memberType: v.union(v.literal("user"), v.literal("agent")),
memberId: v.string(), // user ID or agent ID, as in chat_members
role: v.union(v.literal("owner"), v.literal("member"), v.literal("viewer")),
joinedAt: v.number(),
})
.index("by_spaceId", ["spaceId"])
.index("by_memberType_and_memberId", ["memberType", "memberId"])
.index("by_spaceId_and_memberType_and_memberId", ["spaceId", "memberType", "memberId"]),
// space_policies: same shape as team_policies, keyed by spaceId
// โโ Lists โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
collections: defineTable({
accountId: v.string(),
spaceId: v.id("spaces"),
name: v.string(),
description: v.optional(v.string()),
icon: v.optional(v.string()),
kind: v.union(v.literal("custom"), v.literal("projects"), v.literal("expenses")),
fields: v.array(fieldDefinition), // bounded (cap ~200 fields)
primaryFieldId: v.string(),
templates: v.optional(v.array(v.any())), // record templates with task blueprints (ยง4.4); bounded
archived: v.boolean(),
createdBy: v.string(),
})
.index("by_spaceId_and_archived", ["spaceId", "archived"])
.index("by_accountId_and_kind", ["accountId", "kind"]),
collection_records: defineTable({
accountId: v.string(),
spaceId: v.id("spaces"),
collectionId: v.id("collections"),
title: v.string(), // denormalised primary field for search and sort
values: v.record(v.string(), v.any()), // keyed by field ID; relations live in `links`
notes: v.optional(v.string()), // markdown
position: v.optional(v.number()), // manual board order
archivedAt: v.optional(v.number()),
createdBy: v.string(),
})
.index("by_collectionId_and_archivedAt", ["collectionId", "archivedAt"])
.searchIndex("search_title", { searchField: "title", filterFields: ["collectionId", "spaceId"] }),
collection_views: defineTable({
collectionId: v.id("collections"),
name: v.string(),
type: v.union(v.literal("table"), v.literal("board"), v.literal("calendar")),
config: v.any(), // visible fields, filters, sorts, group, date field, totals
ownerId: v.optional(v.string()), // set for a private view
savedFromQuestion: v.optional(v.string()), // the question it came from, if any
position: v.number(),
}).index("by_collectionId_and_position", ["collectionId", "position"]),
collection_rules: defineTable({
collectionId: v.id("collections"),
name: v.string(),
trigger: v.any(), // { fieldId, op: "changes" | "becomes", value? }
actions: v.array(v.any()), // create_task | notify | hand_to_agent; bounded
enabled: v.boolean(),
createdBy: v.string(),
}).index("by_collectionId", ["collectionId"]),
// โโ Links โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
links: defineTable({
accountId: v.string(),
fromType: v.string(), // "task" | "record" | "note" | "contact" | "file" | โฆ
fromId: v.string(),
toType: v.string(),
toId: v.string(),
kind: v.union(
v.literal("about"), v.literal("blocks"), v.literal("relates_to"),
v.literal("duplicates"), v.literal("field"),
),
fieldId: v.optional(v.string()), // set when kind = "field"
changeId: v.optional(v.id("changes")), // provenance
})
.index("by_fromType_and_fromId_and_kind", ["fromType", "fromId", "kind"])
.index("by_toType_and_toId_and_kind", ["toType", "toId", "kind"]),
// โโ The write path โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
changes: defineTable({
accountId: v.string(),
spaceId: v.optional(v.id("spaces")),
objectType: v.string(),
objectId: v.string(),
op: v.union(
v.literal("create"), v.literal("update"), v.literal("archive"),
v.literal("restore"), v.literal("link"), v.literal("unlink"),
),
diff: v.array(v.object({ // one object's fields: bounded
field: v.string(),
before: v.optional(v.any()),
after: v.optional(v.any()),
})),
actorType: v.union(v.literal("user"), v.literal("agent"), v.literal("integration"), v.literal("system")),
actorId: v.string(),
basis: v.union(v.literal("expressed"), v.literal("inferred"), v.literal("confirmed")),
confidence: v.optional(v.number()),
source: v.optional(v.object({ kind: v.string(), ref: v.optional(v.string()), excerpt: v.optional(v.string()) })),
status: v.union(
v.literal("applied"), v.literal("proposed"), v.literal("rejected"),
v.literal("undone"), v.literal("superseded"),
),
proposalId: v.optional(v.id("proposals")),
reversible: v.boolean(),
revertsChangeId: v.optional(v.id("changes")),
validFrom: v.optional(v.number()), // second clock, optional for now
validTo: v.optional(v.number()),
decidedBy: v.optional(v.string()),
decidedAt: v.optional(v.number()),
})
.index("by_objectType_and_objectId", ["objectType", "objectId"])
.index("by_spaceId", ["spaceId"]) // "What changed": range on _creationTime
.index("by_proposalId", ["proposalId"]),
proposals: defineTable({
accountId: v.string(),
spaceId: v.optional(v.id("spaces")),
title: v.string(),
why: v.optional(v.string()),
proposedByType: v.union(v.literal("agent"), v.literal("user"), v.literal("integration")),
proposedById: v.string(),
reviewerId: v.string(), // routine reviewer, whoever handed over the work, or the policy's approver (ยง6.3)
rule: v.optional(v.string()), // the policy that required review (the chip)
effects: v.array(v.object({ // bounded per proposal
kind: v.string(), // "mail.send" | "xero.bill.create" | "cliniko.appointment.create" | โฆ
payload: v.any(),
status: v.union(v.literal("pending"), v.literal("done"), v.literal("failed"), v.literal("skipped")),
})),
status: v.union(
v.literal("open"), v.literal("accepted"), v.literal("partly_accepted"),
v.literal("rejected"), v.literal("expired"),
),
decidedAt: v.optional(v.number()),
})
.index("by_reviewerId_and_status", ["reviewerId", "status"])
.index("by_spaceId_and_status", ["spaceId", "status"]),
// โโ Inbox โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
inbox_items: defineTable({
accountId: v.string(),
userId: v.string(),
kind: v.union(
v.literal("review"), v.literal("approval"), v.literal("request"),
v.literal("mention"), v.literal("choose"), v.literal("returned"),
),
objectType: v.string(),
objectId: v.string(),
spaceId: v.optional(v.id("spaces")),
summary: v.string(),
state: v.union(v.literal("open"), v.literal("done"), v.literal("snoozed"), v.literal("dismissed")),
snoozedUntil: v.optional(v.number()),
}).index("by_userId_and_state", ["userId", "state"]),
space_reads: defineTable({ // high-churn, kept apart from profiles
userId: v.string(),
spaceId: v.id("spaces"),
lastSeenAt: v.number(),
}).index("by_userId_and_spaceId", ["userId", "spaceId"]),
// โโ Preferences and defaults (the cascade applied to settings) โโโโโโ
preference_defaults: defineTable({
accountId: v.string(),
teamId: v.optional(v.id("teams")), // absent = account-wide
key: v.string(), // "startLocation" | "morningBrief" | โฆ
value: v.any(),
userOverridable: v.boolean(),
}).index("by_accountId_and_teamId_and_key", ["accountId", "teamId", "key"]),
user_preferences: defineTable({
accountId: v.string(),
userId: v.string(),
key: v.string(),
value: v.any(),
}).index("by_accountId_and_userId_and_key", ["accountId", "userId", "key"]),
// โโ Patients (names only, copied on link) โโโโโโโโโโโโโโโโโโโโโโโโโโ
patient_refs: defineTable({
accountId: v.string(),
provider: v.literal("cliniko"),
externalId: v.string(),
firstName: v.string(),
lastName: v.string(),
displayName: v.string(),
status: v.union(v.literal("active"), v.literal("archived")),
linkCount: v.number(), // removed at zero
syncedAt: v.number(),
})
.index("by_accountId_and_provider_and_externalId", ["accountId", "provider", "externalId"])
.searchIndex("search_displayName", { searchField: "displayName", filterFields: ["accountId"] }),
Changes to existing tables:
- New:
account_members(account, user, role: owner, admin or member) andaccount_invitations, replacing Clerk organisation membership and roles. The auth middleware reads the role fromaccount_membersinstead of token claims, and the account switcher lists the user'saccount_membersrows. tasks: addspaceId,parentType(record|task) andparentId,milestoneId(โtask_lists),assigneeType(user|agent) andassigneeIdreplacingassignedTo, statusin_review,customValues, and a reference to the agent run when the assignee is an agent. Indexes on space and status, on parent, and on assignee and status.task_lists: become milestones and sections; addspaceId, an optional parent record and a target date.chats: addspaceId,subjectTypeandsubjectId, asubjectchat type, and an index on the subject.scheduled_agent_runs,event_triggers: addspaceId,ownerIdandreviewerId; outputs go through the write path.storage_buckets: add providers3_compatible.notes.linkedType/linkedIdandentity_tags: migrate tolinksover time.
15. Draft decision records¶
Draft ADR-032: The UI Is Organised Around Work Objects; Messaging-First Remains the Engine Abstraction¶
Date: 2026-10-05 Status: Proposed (amends ADR-020)
Context: ADR-020 made messaging the core abstraction and explicitly removed the distinction between chatting with an agent and using the platform. In use, the app feels like "chats with agents in them": results live in transcripts, agents act as apps, and nothing arrives without a prompt.
Decision: Messaging remains the engine abstraction: every interaction is a message in a canonical chat, channels are windows, and the audit trail is unified. The user-facing model is work objects (spaces, lists and records, tasks and projects) with conversation attached to them as threads. The home is an Inbox (needs you, today, waiting on, what changed); where each person starts is a preference with account and team defaults.
Reasoning: Engine and UI abstractions have different jobs. Messaging is the right substrate for channels, governance and audit; it is the wrong home for results, which need to be durable, attributable and reviewable. Every widely adopted work tool has a "where" container and a daily loop; Thinklio had neither.
Draft ADR-033: Spaces Are the Unit of Work, Agent Context and Policy¶
Date: 2026-10-05 Status: Proposed
Context: Teams describe who people are. Nothing describes what the work is about, so chats float, records have no home and agents have no bounded context.
Decision: Introduce spaces: ongoing areas of work with members (people and agents), a fixed toolset (lists, tasks, docs, conversation) and their own rules. The policy cascade becomes account โ team โ space โ user, narrowing only. An agent's working context is the spaces it belongs to. Routines (scheduled or event-driven agent duties) belong to a space and are set up by space owners and admins; each names a reviewer for its proposals, defaulting to whoever set it up. Teams do not get spaces automatically; onboarding suggests them from templates.
Reasoning: One object does three jobs: the place people work, the context agents work from, and the scope rules apply to. Making the space a policy layer reuses the existing engine instead of adding a mechanism.
Draft ADR-034: Lists Are a Core Primitive; Projects Are Records; Tasks Are Generalised and Linked¶
Date: 2026-10-05 Status: Proposed
Context: Every organisation needs lists that are not task lists (FALL's outreach trips and orthotic orders). Project and task management is essential. The adoption strategy planned a List Manager as a later pillar.
Decision: Build lists (collections) now, with custom fields, saved views, totals, record pages, templates with relative-date task blueprints, simple field-change rules and Notion import. Projects are records in a built-in Projects list whose tasks roll up. Tasks remain one built-in type with a space, an optional parent (any record or task), person-or-agent assignees and an in_review status. A typed links table (about, blocks, relates_to, duplicates, field) connects work objects across spaces, without granting access. Receipts are an Expenses list related to trips.
Reasoning: One list engine serves trips, orders, expenses and projects, and gives portfolio views for free. Keeping tasks as a concrete table keeps their queries indexed. Typed links give backlinks and dependencies while keeping strict typing to relationships that drive behaviour.
Draft ADR-035: One Write Path; Changes Are Logged with Provenance; Proposals Carry Changes and Effects¶
Date: 2026-10-05 Status: Proposed
Context: History, undo, "what changed", agent attribution and review of proposed changes all depend on recording enough at write time. Retrofitting them later means migrating data that was never captured.
Decision: Every write to a work object goes through one path that records a changes row in the same transaction: actor, basis (expressed, inferred, confirmed), confidence, source, field-level diff, status (applied, proposed, rejected, undone, superseded), reversibility and optional validity dates. A proposal groups proposed changes with effects (actions that leave Thinklio). Changes that are reversible, permitted and confident apply directly with undo; everything else is proposed; effects are proposed unless a policy allows them. Proposals go to the routine's reviewer, to whoever handed over the work, or to the approver a space policy names. pending_approvals merges into proposals. Change history stays readily available for 18 months (longer where audit retention requires), then is summarised per record after an export is offered.
Reasoning: This is the minimum of the research doc's assertion model that must exist from day one. The review-of-change pattern is the one proven way to supervise agents, and it needs this structure underneath.
Draft ADR-036: Patient Names Are Copied from Cliniko on Link; Names Are Masked in Model Prompts by Default¶
Date: 2026-10-05 Status: Proposed (agreed in discussion, 2026-10-05)
Context: Orthotic orders, trips and follow-ups refer to patients. Looking names up on demand complicates display, search and agent matching; copying more than names would duplicate the clinical record.
Decision: Copy names only (Cliniko ID, first and last name) into patient_refs when a record first links a patient; refresh on view; remove when the patient is archived or unlinked. Cliniko remains the source of truth. All other patient data is looked up on demand through the Cliniko MCP service with the acting user's own API key (agents use the key of the person they act for, routines their owner's), so Thinklio never widens Cliniko's per-user permissions. Patient names are readable only through records in spaces the person can see, and their views and exports are logged. The harness replaces names with initials or IDs in model prompts unless a space data_boundary policy allows them.
Reasoning: Names make the product usable (display, search, email matching) at low risk, provided they are treated as health information in context. Masking prompts by default addresses the disclosure that storage residency cannot.
Draft ADR-037: Regional Deployments in Ireland and Sydney; Region Is Chosen at Signup; Storage Follows Region¶
Date: 2026-10-05 Status: Proposed (decided; applies to all Novansa products on Convex)
Context: Thinklio runs in one Convex deployment in Ireland. Convex now offers Sydney (aws-ap-southeast-2) at the same 1.3ร non-US rate. A deployment cannot change region. FALL and other Australian customers will hold health-related data.
Decision: Add a second Convex account in Sydney. Accounts based in the EU go to Ireland with no choice (proposed); every other account chooses its data location at signup, pre-selected from a location-based suggestion. The region is recorded on the Clerk organisation before any data is written. Clients connect to the deployment for the account's region. Object storage follows the region: R2 for the EU, an S3-compatible service in Australia. Development and the Novansa pilot run on Ireland, where Novansa stays. FALL joins after the pilot: in Sydney if that account exists by then, otherwise in Ireland with a move to Sydney before production. The shared pattern is dev/docs/regional-deployments.md (outside this repo).
Reasoning: Choosing region at signup avoids migrating tenants later (the export and import with ID remapping that doc 15 calls the bulk of the work). Clerk metadata is enough of a control plane until external customers need more. The decision covers every Novansa product on Convex (Twikka, CalmerFlow and Clindice as well as Thinklio); FALL's PocketBase systems and Couple Tools on Supabase are outside it.
Draft ADR-038: Integrations Are Provider-Neutral Capabilities; Google Workspace First¶
Date: 2026-10-05 Status: Proposed (decided)
Context: Customers will use Google Workspace, Microsoft 365 or Apple services. Novansa and FALL use Google Workspace.
Decision: Build mail, calendar, files, contacts and identity as capabilities with one adapter per provider: Google Workspace first, then Microsoft 365, then Apple. Each integration acts as a source, a link (an external field) or an action; actions go through proposals and the rule cascade. Connection scopes follow doc 09 and credentials attach to principals (ADR-025).
Reasoning: Building a "Gmail integration" would mean rebuilding it for each provider. Capabilities keep product code stable as providers are added.
Draft ADR-039: Flutter Is the Primary Phone and Tablet Client; the Web App Is Responsive¶
Date: 2026-10-05 Status: Proposed (decided)
Context: Field staff need the full product on phones and tablets, and channels like Telegram cannot carry all of it. People without the native app still need a usable phone experience.
Decision: The Flutter app is the primary client on phones and tablets, with feature parity. The web app is responsive as a fallback for people without the Flutter app and remains the primary desktop client. Field types, view types, proposal and effect types and Inbox item kinds are defined once in Convex; each client implements its own renderers. Other channels (Telegram, email, Tavus video) remain additive under ADR-027.
Reasoning: One set of definitions keeps the two clients in step, which is the Interaction Protocol's split of a shared contract and per-environment renderers.
16. Questions resolved in review¶
Resolved with Andrew on 2026-10-05. Each answer is reflected in the section noted.
- Names. Keep Spaces and Lists. Check them in the pilot and with FALL staff when FALL joins, and rename only if people stumble over them (ยง3, ยง13).
- Spaces and teams. Teams do not get spaces automatically. Onboarding suggests spaces from templates, and any space can belong to a team (ยง3.1).
- Direct messages. A Chats entry below Inbox and My work holds direct messages and general group chats; their mentions and requests appear in the Inbox (ยง3.7, ยง12).
- Knowledge layers. No space layer yet. Agents retrieve a space's documents and records directly, the four existing layers stay as they are, and a space layer is reconsidered after the pilot (ยง3.1, ยง13).
- Per-space task fields. Built after the pilot, designed in now (ยง5.1, ยง13).
- Proposal routing. Routines name a reviewer when set up, defaulting to whoever set them up; work handed to an agent returns to whoever handed it over; approvals such as spending go to whoever the space's policy names (ยง6.3, ยง8.3).
- Change log retention. 18 months readily available, or longer where audit retention requires it; then per-record summaries, with a full export offered first and a visible marker on the record (ยง6.4).
- Existing accounts. Novansa stays in Ireland. The FALL pilot also runs in Ireland, and FALL moves to Sydney closer to production (ยง11; revised later the same day, see below).
- Shared docs. The regional pattern is written up in the cross-product docs as
dev/docs/regional-deployments.md, next to the Interaction Protocol (ยง11, ยง13).
Also agreed in the same review: keep patient names out of model prompts by default (ยง9, ADR-036). Revised later the same day: the FALL pilot stays in Ireland and moves to Sydney closer to production, and Cliniko lookups use each user's own API key because Cliniko's permissions differ per user (ยง9, ยง10.2, ยง11, ยง13).
Revised again on 2026-10-05: the pilot is Novansa's, not FALL's. FALL's staff are busy and slow to take on new products, while Novansa is a small team of developers who can use Thinklio until they love it. FALL and other accounts join after that. Cliniko patient names move out of the pilot into the FALL step (ยง9, ยง11, ยง13).
New questions raised during promotion or the pilot go here.
- Novansa pilot scope. Which spaces and lists Novansa runs in the pilot, and what they replace (Notion, GitHub issues, something else).
- FALL's starting region. Start FALL directly in Sydney (needs the Sydney account and Australian storage first) or in Ireland with a later move (ยง11).
Cross-references¶
- 01 Product & Strategy: principles, personas, pricing
- 03 Agent Architecture & Extensibility: ยง9 smart input triage, ยง10 attention surfacing, ยง18 agent views (to be retired)
- 04 Data Model: current tables
- 07 Security & Governance: policy categories and the cascade
- 09 External API & Tool Integration: Platform API, connection scopes, MCP services
- 10 Client Applications & UX: current IA and page specification
- 15 Tenancy & Deployment Topology: tiers, control plane, tenant migration
- 16 Chats, Channels & Identity: chats, binding modes, ยง8.3 adapter contract
- Decision log: ADR-013, ADR-020, ADR-025, ADR-027, ADR-029, ADR-030
- Skills as a behaviour primitive (proposal)
- Model router & tiered inference (proposal)
- Adoption strategy: the three pillars and the List Manager design
- Model expressiveness and UI strategy and Experimental AI interfaces survey
- Interaction Protocol (
dev/docs/interaction-protocol.md, outside this repo) and Twikka video coaching spec (twikka/docs/twikka-video-coaching-spec.md, outside this repo)