Data model

The entities WireCat tools keep in their shared local store, how they connect, and how an agent narrows to the right context before it compares meaning.

Every WireCat tool — tg, max, memo, zm — keeps what it reads in one local SQLite file, wirecat.db. This page is for developers and agent authors who want to query that file or understand what an agent can find in it. It explains the entities first, then how they connect, then how a search finds context, and only at the end the columns worth querying by. How the store is opened, migrated and locked is in the local store.

It describes the schema as read on 10 October 2026, at a fixed commit of cli-messaging, the package that owns the file. Every column with its meaning is in the schema reference.

Key entities

How the main entities point at each other

In words: a person has identities; identities send messages and emails and take part in meetings. An organization holds accounts and projects. Each account brings its chats, emails, documents and meetings; chats hold messages, and messages group into conversations. A meeting belongs to an event. A project holds tasks and decisions. A memory and a tag can sit on any entity, so their dotted lines say "any entity"; the line to Account stands for all of them. Colours group the cards: people, chats and mail, the account they came through, documents and meetings, work, and the two that attach to anything.

People and identities

A person is one human. An identity is how one source knows a person: a Telegram user, a MAX user, an email address, a Zoom participant. One person has many identities; each identity belongs to at most one person, with the method and confidence of that link kept beside it. Earlier profiles are kept as revisions. A bot is an AI agent or script that works in the store: it can own projects, take tasks and write notes. A messenger's own bot account is an identity, not a bot.

Organizations and projects

An organization is a company, team, family or community. A project is a place for tasks with a short key (MEET), a type (work, client, personal, open source, other) and an optional organization. Work accounts and projects belong to an organization; a family is an organization too.

Accounts and chats

An account is one integration the owner connected: a messenger account, a mailbox, Zoom, a notes folder. A chat belongs to one account. Chats nest: a forum topic sits inside its group, and a channel's discussion group sits under the channel. A thread is different: it hangs from a root message inside one chat, as comments under a channel post do.

Messages and conversations

A message belongs to a chat and is sent by an identity. Edits keep the earlier text, and a message deleted at the source keeps its row without its text. A conversation is a group of messages of one chat that belong to the same discussion, rebuilt from replies, mentions and the sender continuing (how they are built).

Email

An email belongs to an email thread and to one account. Its sender and each recipient (to, cc, bcc) resolve to identities, so mail and messages from the same person meet at one person. Folders and labels are mailboxes; one email can sit in several.

Documents, notes and memories

A document stands on its own: a file in a notes folder, a page, a wiki page. A note is short text a person or a bot wrote about one thing — a person, a chat, a meeting, a task; comments on a task are notes. A memory is what an agent concluded: a summary, a daily digest, a fact, a preference. A memory can be wrong, so it always carries its evidence, a confidence and a status (proposed, confirmed, stale, superseded).

Events and meetings

An event is the owner's record of something that happened, with no provider fields. A meeting is one occurrence as a meeting app recorded it: its participants, transcripts, in-meeting chat and summaries. A meeting points at its event, so records of the same call from different sources meet at one event. Repeating events and repeating meetings each have a series.

Tasks and decisions

A task belongs to a project and is named by its key and number (MEET-12). Its type is bug, feature, chore, question, request, mention or promise: a question someone asked is a task of type question, and a promise someone made is a task of type promise. People and bots are assigned in a role (assignee, reviewer, watcher), and every change is a task event. A decision is a choice that holds until another replaces it, with its evidence linked.

Tags and topics

A tag is a free label anyone can add. A topic is a tag from a short list that only the owner creates; agents can propose one. A thing can carry many tags and topics, and one topic can be marked as its main one. A name is either a tag or a topic, never both.

Proposed and logged actions

A proposed action is something an agent wants done outside the store — a reply, a send, a ban, a new task — waiting for a person to approve or reject it. Nothing external happens without that approval. Separately, every tool an agent calls is logged with its access tier and target, without the text of its arguments.

How they connect

Four tables connect anything to anything, so none of the entities above needs a column for every other:

  • Links connect two things with a kind: a question task to the message that answered-by it, a decision or a memory to its evidence, a thing to what it was created-from, a task to the people, messages, emails and meetings it concerns.
  • Taggings put a tag or a topic on any thing.
  • Notes attach a person's text to any thing.
  • Aliases give any thing other names: a nickname, a contact name in one messenger, a short project name. Name matching and search read them.

Scope is personal or work. It is set on accounts, organizations and projects, and what they hold inherits it: a chat takes its account's scope unless it sets its own, and a memory takes the scope of its evidence. A question asked for work reads only work data, so it never pulls in a private chat.

How search finds context

Finding context for an agent is three steps, in this order.

  1. Narrow with tables. Person, project, account, time and scope are columns with indexes, so the first step is ordinary SQL. The involvement index is the shortcut: one row per person per message, email, meeting, task or document they took part in, with their role, the time and the scope. "Everything Alice Example did at work this month" is one indexed range.
  2. Match words and meaning over the candidates only. Each kind of text has its own word and stem index: messages, emails, attachments, documents, notes, memories and meetings. Vectors then rank by meaning, and every piece of text carries the scope, account, project and time of its parent, so the vector scan filters before it compares. How words, stems and vectors work is explained in how search works.
  3. Expand along links. From a hit, follow the reply chain, the conversation it belongs to, the thread root, the email thread, or the meeting a transcript row came from, and the links that point at it.
SELECT subject_type, subject_id, role, occurred_at
FROM involvements
WHERE person_id = :person AND scope = 'work' AND occurred_at >= :since
ORDER BY occurred_at DESC
LIMIT 50

Vectors belong to long texts: conversations, documents, emails, attachments' text, notes, memories, meeting transcripts and summaries, events and tasks. A single message never gets its own vector; it is found by words, or by meaning through its conversation. A text is cut into pieces, and each piece's vector is stored once per model and text hash, so the same text is never embedded twice.

The involvement index is derived: it is rebuilt from senders, recipients, participants, assignees and confirmed links, and holds nothing they do not.

Tables

Every table of the store, by area, with each column's type and meaning. Times are epoch milliseconds. The schema reference also lists every constraint and index.

Naming conventions

  • Tables are plural and snake_case. The primary key is id; a foreign key is <singular>_id.
  • external_id is the source's own id for a row; another thing's source id is <thing>_external_id.
  • created_at and updated_at say when the row was saved and changed in this file. Times in the world keep their own names: sent_at, joined_at, started_at, due_at.
  • deleted_at means gone at the source. Sync never removes a row; only an owner command does.
  • A pointer to a row of any table is a <name>_type text, the singular table name, and a <name>_id integer. Who made or owns something is a person or a bot.
  • What is searched has its own column; metadata is JSON for what is not.
  • Flags have no is_ prefix. Times are integers in epoch milliseconds.
  • Every foreign key and every _type/_id pair has an index.

Sources

Every integration is an account; each sync run is logged.

accounts

Every integration the owner connects: a messenger account, a mailbox, Zoom, a notes folder.

ColumnTypeMeaning
idinteger, PKPrimary key.
providertext, not nullWhich messenger the account belongs to, as a lowercase name such as telegram or max; open-ended so new adapters need no change.
external_idtext, not nullthe source's own id for it
nametextThe account's display name as the messenger reports it; NULL when not known.
created_atinteger, not nullwhen this row was saved here
settingstextJSON: per-integration settings, e.g. a folder's path and format
statustextactive, paused, failed
updated_atinteger, not nullwhen this row last changed here
scopetext, not nullpersonal or work; what a context query may read for a given purpose, so a work question never pulls private chats
organization_idinteger → organizations.idthe organization a work account belongs to

sync_cursors

Per account, what a sync remembers between runs: a delta marker, when a list was last complete.

ColumnTypeMeaning
account_idinteger, not null → accounts.idthe integration account it came through
keytext, not nullName of what a sync remembers for this account, e.g. chat_list_complete, history_start:&lt;chatId>, fetched:&lt;chatId>, or a provider delta marker.
valuetext, not nullThe remembered value as text (a delta marker, a message id, a JSON list); callers encode numbers themselves.
updated_atinteger, not nullwhen this row last changed here
created_atinteger, not nullwhen this row was saved here

sync_ranges

The stretches of each chat's history held completely, so a fetch knows what is missing.

ColumnTypeMeaning
chat_idinteger, not null → chats.idthe chat it belongs to
from_keyinteger, not nullLowest provider ordering key (Telegram's message id) of a stretch of the chat held completely; missing messages inside it were deleted.
to_keyinteger, not nullHighest provider ordering key of that complete stretch; messages outside every stretch were simply never fetched.
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

fetch_leases

Who is fetching a stretch of a chat right now, so two processes do not fetch the same pages.

ColumnTypeMeaning
chat_idinteger, not null → chats.idthe chat it belongs to
anchortext, not nullWhich stretch or job of the chat is being fetched, as a label such as gaps, so two processes never fetch the same pages.
holdertext, not nullRandom id (a UUID) of the process that currently holds the lease.
expires_atinteger, not nullWhen the lease runs out (epoch ms); after it another holder may take the stretch.

bot_updates

Every update a bot account received through the Bot API, kept as it came, so it can be inspected and replayed.

ColumnTypeMeaning
idinteger, PKPrimary key.
account_idinteger, not null → accounts.idthe bot account
external_idtext, not nullthe update id
kindtext, not nullmessage, callback, member change, …
payloadtext, not nullJSON, exactly as received
received_atinteger, not nullwhen the update arrived
handled_atintegernull until handled
errortextwhy handling failed
replayed_atintegerwhen it was last replayed
created_atinteger, not nullwhen this row was saved here

syncs

One run of an integration's sync: what it did and how it ended.

ColumnTypeMeaning
idinteger, PKPrimary key.
account_idinteger, not null → accounts.idthe integration account it came through
kindtext, not nullfull, incremental, import
started_atinteger, not nullwhen the run began
finished_atintegerwhen it ended; null while it runs
statustext, not nullrunning, succeeded, failed
countstextJSON: created, updated, deleted, skipped
errortextwhat stopped a failed run, as the importer reported it
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

People, organizations and bots

Humans, the per-source identities they have, the organizations they belong to, and the agents that work here.

persons

One human. Each person has identities in the sources; one person is the store's owner.

ColumnTypeMeaning
idinteger, PKPrimary key.
nametextDisplay name of the person (taken from their first identity's name); NULL if none known.
ownerinteger, not null1 for the person who owns the store; a new store creates that row, so it always exists.
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

identities

How one source knows a person: a messenger user, an email address, a meeting participant.

ColumnTypeMeaning
idinteger, PKPrimary key.
providertext, not nullMessenger the identity lives in (telegram, max, or any string such as email); one identity per provider, not per account.
external_idtext, not nullthe source's own id for it
usernametextThe person's public handle in that messenger, without @; NULL when they have none.
nametextThe person's current display name as the messenger last reported it.
botinteger1 if the messenger says this identity is a bot, 0 if it says not, NULL when unknown.
phone_hmactextKeyed hash (HMAC) of the person's phone number, for matching the same person across messengers; the number itself never reaches a log, fixture or document. No tool fills it yet.
metadatatextJSON: what the source sends that has no column of its own and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here
descriptiontextWhat they wrote about themselves.

Which person an identity belongs to, how that was decided and how sure it is.

ColumnTypeMeaning
identity_idinteger, PK → identities.idthe identity being linked; one link per identity
person_idinteger, not null → persons.idthe person it belongs to
methodtext, not nullHow the identity was assigned to its person: initial when first seen, or a caller's label such as manual or same-email.
confidencereal, not nullHow sure the link is, from 0 to 1; 1 for every link the store writes.
created_atinteger, not nullwhen this row was saved here
authortext, not nullWho decided the link: ingest for the automatic first link, otherwise owner or the name of a program.
sourcetextwhich integration or importer proposed it
updated_atinteger, not nullwhen this row last changed here

Every move of an identity from one person to another, so a wrong link can be undone.

ColumnTypeMeaning
idinteger, PKPrimary key.
identity_idinteger, not null → identities.idthe identity it belongs to
from_person_idinteger → persons.idthe person it belongs to
to_person_idinteger, not null → persons.idthe person it belongs to
methodtext, not nullHow this move of an identity between persons was decided (initial, manual, same-email…), kept so a bad link can be undone.
created_atinteger, not nullwhen this row was saved here
authortext, not nullWho made this move: ingest, owner, or a program's name.

identity_revisions

Each profile a person was seen with, a row when it differs from the one before; identities holds the latest.

ColumnTypeMeaning
idinteger, PKPrimary key.
identity_idinteger, not null → identities.idthe identity it belongs to
nametextThe display name the person had at this snapshot of their profile (a row is added only when the profile changes).
usernametextThe handle (without @) the person had at this snapshot.
descriptiontextThe person's self-written bio/about text at this snapshot.
markstextJSON: the messenger's marks — bot, scam, fake, deleted, has a photo — where it gave them.
created_atinteger, not nullwhen this row was saved here

account_identities

Which identities each account sees, and when it last talked to each of them one to one.

ColumnTypeMeaning
account_idinteger, not null → accounts.idthe integration account it came through
identity_idinteger, not null → identities.idthe identity it belongs to
created_atinteger, not nullwhen this row was saved here
last_messaged_atintegerTheir one-to-one chat's newest message, as refreshRecency last worked it out: the contact order.
updated_atinteger, not nullwhen this row last changed here

aliases

Other names for anything: a person's nickname, a contact's name in one messenger, a project's short name. Search and name matching read them.

ColumnTypeMeaning
idinteger, PKPrimary key.
aliasable_typetext, not nullwhat the alias names: identity, person, organization, chat, project, bot, …
aliasable_idinteger, not nullthe row in the aliasable_type table
account_idinteger → accounts.idset when the alias holds only as seen from one account, as a contact name in one messenger does; null when it holds everywhere
nametext, not nullthe alias as written: a nickname, a short name, a former name
name_foldedtext, not nullname folded for matching: lower case, accents removed
displayinteger, not null1 for the alias to show instead of the real name; at most one per thing and account
sourcetext, not nullowner, agent, an import
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

identities_fts

Full-text index over name, username.

organizations

A company, team, family or community the owner deals with. Projects, work accounts and people's identities belong to one.

ColumnTypeMeaning
idinteger, PKPrimary key.
kindtext, not nullcompany, team, family, community
nametext, not nullthe organization's name
scopetext, not nullpersonal or work; what a context query may read for a given purpose, so a work question never pulls private chats
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here
deleted_atintegergone at the source; sync never hard-deletes

bots

An AI agent or a script that works here: it registers itself, owns projects and tasks, is assigned work, writes notes. A bot of a messenger (a Telegram bot) is an identity, not this.

ColumnTypeMeaning
idinteger, PKPrimary key.
nametext, not null, uniquehandle, e.g. zm-puller
kindtext, not nullagent, script, integration
descriptiontextwhat the bot is for, in a sentence, written when it registers
owner_person_idinteger → persons.idwho runs it
modeltextthe model it runs on, when an agent
token_digesttexthash of its access token, for when bots authenticate
last_seen_atintegerthe last time the bot did anything here
disabled_atintegerset when the owner switches the bot off; it keeps what it owns
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

Chats and messages

What messengers bring.

chats

A place to talk in one account: a dialog, group, channel or saved messages. Chats can nest.

ColumnTypeMeaning
idinteger, PKPrimary key.
account_idinteger, not null → accounts.idthe integration account it came through
external_idtext, not nullthe source's own id for it
kindtext, not nullType of chat: dialog (one-to-one), group, channel, saved (notes-to-self) or unknown.
titletextThe chat's name as the messenger shows it; NULL when it has none.
unread_countintegerUnread messages the messenger reported; NULL means the messenger did not say, which is not zero.
last_message_atintegerTime of the chat's newest message (epoch ms); NULL when it never had one.
participants_countintegerMember count the messenger reported; NULL when not given.
metadatatextJSON: what the source sends that has no column of its own and is not searched
updated_atinteger, not nullwhen this row last changed here
usernametextThe chat's public handle (without @) where it has one, e.g. a public channel.
membership_statetextNULL is unknown. Searchable does not follow from it: a chat left keeps its messages.
searchableinteger, not null1 if the chat's messages appear in searches that do not name it, 0 if only a search naming the chat sees them.
message_countinteger, not nullKept by triggers, so a query can choose how a filter reaches the index.
members_tracked_atintegerWhen the owner asked serve to fetch its member list daily; NULL when not tracked.
descriptiontextthe chat's description, as the messenger gives it
details_fetched_atintegerwhen title, username and description were last read
created_atinteger, not nullwhen this row was saved here
parent_chat_idinteger → chats.idthe chat this one sits inside: a forum topic in its group, a channel's discussion group, a channel in a workspace
scopetextoverrides the account's scope for this chat; null takes the account's

chat_members

Who is in a chat, as the account last saw it: a list replaces the chat's membership whole.

ColumnTypeMeaning
chat_idinteger, not null → chats.idthe chat it belongs to
identity_idinteger, not null → identities.idthe identity it belongs to
created_atinteger, not nullwhen this row was saved here

member_stays

One stay of a person in a group, from member lists read whole or in part. A return after leaving is a new row. gone_at is set only from a list read whole: a cut list says nothing about who is missing.

ColumnTypeMeaning
idinteger, PKPrimary key.
chat_idinteger, not null → chats.idthe chat it belongs to
identity_idinteger, not null → identities.idthe identity it belongs to
first_seen_atinteger, not nullWhen a member-list read first showed the person in the group for this stay (epoch ms).
last_seen_atinteger, not nullWhen a member-list read last showed the person in the group (epoch ms).
joined_atintegerWhen the messenger says they joined; NULL where it does not.
invited_by_identity_idinteger → identities.idthe identity it belongs to
roletextThe person's role in the group at the last read: owner, admin or member; NULL when not given.
left_atintegerWhen a complete member list first lacked the person, closing this stay (epoch ms); NULL while they are still in it.
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

member_counts

A group's size once a day: the messenger's own count and how many members one read listed.

ColumnTypeMeaning
chat_idinteger, not null → chats.idthe chat it belongs to
datetext, not nullYYYY-MM-DD, UTC; SQLite has no date type
reported_countintegerthe count the messenger reports
listed_countinteger, not nullhow many members one read actually returned
complete_listinteger, not nullwhether that read returned the whole list
created_atinteger, not nullwhen this row was saved here

messages

One message in a chat, with its text, sender, reply and thread.

ColumnTypeMeaning
idinteger, PKPrimary key.
chat_idinteger, not null → chats.idthe chat it belongs to
account_idinteger, not null → accounts.idthe integration account it came through
external_idtext, not nullthe source's own id for it
thread_external_idtextThe messenger's id of the topic or thread inside the chat the message belongs to, where the messenger has threads.
sender_identity_idinteger → identities.idthe identity it belongs to
sender_chat_external_idtextThe messenger's id of the chat that authored the message (a channel post or a message sent as the group); NULL for a person.
sender_nametextThe sender's display name as carried on this message.
sent_atinteger, not nullWhen the message was sent (epoch ms).
edited_atintegerWhen the messenger last recorded an edit (epoch ms); NULL for a message never changed.
deleted_atintegerwhen it disappeared at the source; the row stays, sync never hard-deletes
texttext, not nullThe message text exactly as received (empty string when there is none or it was deleted).
reply_to_external_idtextThe messenger's id of the message this one replies to, even when no copy of it was sent.
reply_totextJSON copy of the replied-to message as the messenger sent it (id, sender, time, text, attachments); NULL when not sent.
forwardtextJSON copy of the original message this one forwards (same shape as reply_to); NULL when not a forward.
outgoinginteger1 if this account sent it, 0 if someone else did, NULL when the account's own identity is unknown.
reactionstextJSON of the reactions: counts per reaction, this account's own reaction and the total; NULL when never asked.
metadatatextJSON: what the source sends that has no column of its own and is not searched
created_atinteger, not nullwhen this row was saved here
sourcetext, not nullHow the message reached the store, e.g. history, send, backfill, update, context; diagnostic only.
normalized_texttextthe text folded for search: lower case, accents removed; filled by the indexer
normalizer_versionintegerVersion of the text-normalisation rules that produced the message's search text (currently 1); NULL until normalised.
mentionstextJSON: the people the text mentions by id, where the messenger says so; @handles are read from the text.
updated_atinteger, not nullwhen this row last changed here
thread_root_idinteger → messages.idthe message a thread hangs from: a Slack thread, comments under a channel post, replies in a topic; null outside a thread

message_revisions

Earlier texts of a message, one row per edit.

ColumnTypeMeaning
idinteger, PKPrimary key.
message_idinteger, not null → messages.idthe message it belongs to
texttext, not nullThe message text before an edit replaced it.
edited_atintegerThe edit time (epoch ms) the message carried while it had this older text; NULL if it was not edited before.
created_atinteger, not nullwhen this row was saved here

Each candidate for "the earlier message this one answers", and where it came from.

ColumnTypeMeaning
idinteger, PKPrimary key.
chat_idinteger, not null → chats.idThe message's chat, kept here so a rebuild finds and drops an old build without reading messages.
message_idinteger, not null → messages.idthe message it belongs to
parent_idinteger → messages.idNULL: the source says this message starts a conversation.
sourcetext, not nullWho proposed this parent for the message: provider (the messenger's own reply), rule, or agent.
kindtext, not nullWhy the message is linked to its parent: reply, mention, same_sender (rules/provider) or answer (an agent's choice).
confidencereal, not nullHow sure the source is that this is the right parent, from 0 to 1.
methodtext, not nullThe rule's name, or the agent's model.
versiontextFor provider and rule links, the linking-rules version that wrote it (as text); for agent links, the agent's skill version, if given.
batchtextWhich agent batch wrote it.
buildintegerThe rebuild that wrote a provider or rule link; NULL for an agent's, which outlive rebuilds.
created_atinteger, not nullwhen this row was saved here
stale_atintegerAn end of the link changed after it was written; never chosen until asked again.
updated_atinteger, not nullwhen this row last changed here

message_counter_observations

The latest value of a message's views, reactions or comments counter.

ColumnTypeMeaning
message_idinteger, not null → messages.idthe message it belongs to
countertext, not nullWhich counter was observed: views, reactions or comments.
valuereal, not nullThe counter's value at the observation (a non-negative number).
created_atinteger, not nullwhen this row was saved here
sourcetext, not nullHow it was observed: remote_fetch (the store asked) or remote_update (the messenger pushed a change).

message_transcripts

What a voice message said, keyed by chat and message id rather than a message row: a message can be heard before the store holds it. Derived — it can be heard again.

ColumnTypeMeaning
idinteger, PKPrimary key.
message_idinteger → messages.idset once the message is stored
chat_idinteger, not null → chats.idthe chat it belongs to
message_external_idtext, not nullThe messenger's id of the voice message transcribed; keyed this way because a message can be heard before it is stored.
texttext, not nullWhat the voice message said, as transcribed; never empty.
sourcetext, not nullThe model or the messenger that heard it.
heard_atinteger, not nullWhen the transcript was made (epoch ms).
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

chats_fts

Full-text index over title.

messages_fts

Full-text index over text.

message_words

Full-text index over normalized_text, scope.

message_stems

Full-text index over stems, scope.

message_stems_pending

Messages whose stems are stale. Triggers fill it, because SQL cannot stem; JS empties it.

ColumnTypeMeaning
idinteger, PKPrimary key.

member_observations

One read of a chat's member list: when, whether it was the whole list, and how many it reported and returned. Retention reads presence at a checkpoint from these.

ColumnTypeMeaning
idinteger, PKPrimary key.
chat_idinteger, not null → chats.idthe chat it belongs to
observed_atinteger, not nullwhen the member list was read
started_atintegerwhen the read began, for a list read in pages
completeinteger, not null1 when the read returned the whole list, so a member missing from it is gone
reported_countintegerthe count the messenger reports
listed_countinteger, not nullhow many members this read returned
sourcetext, not nullwhat read it: a sync, a command, an import
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

member_observation_members

Who one member-list read saw.

ColumnTypeMeaning
member_observation_idinteger, not null → member_observations.idthe member_observation it belongs to
identity_idinteger, not null → identities.idthe identity it belongs to
member_stay_idinteger, not null → member_stays.idthe stay the member was in when seen

Mail

Email has tables of its own: threads, subjects, recipients, mailboxes.

email_threads

A conversation by mail.

ColumnTypeMeaning
idinteger, PKPrimary key.
account_idinteger, not null → accounts.idthe integration account it came through
external_idtext, not nullGmail thread id, or derived from References
subjecttextthe subject of the thread's first email, without Re: and Fwd:
last_email_atintegerwhen the newest email of the thread was sent
emails_countinteger, not nullhow many emails of the thread are stored
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here
deleted_atintegergone at the source; sync never hard-deletes

emails

One email.

ColumnTypeMeaning
idinteger, PKPrimary key.
account_idinteger, not null → accounts.idthe integration account it came through
email_thread_idinteger, not null → email_threads.idthe email_thread it belongs to
external_idtext, not nullthe Message-ID header
subjecttextthe Subject header, decoded
from_identity_idinteger → identities.idthe identity it belongs to
from_addresstextthe sender's address, lower case
from_nametextthe sender's display name, as written in From
sent_atintegerthe Date header
received_atintegerwhen the owner's mailbox received it
in_reply_totextthe Message-ID this email answers
referencestextJSON: the References header
body_texttextthe plain-text body; derived from the HTML when the email has no text part
body_htmltextthe HTML body, as received
snippettextthe first words of the body, for lists
outgoinginteger1 when the owner sent it
readinteger1 when it is marked read in the mailbox
flaggedinteger1 when it is flagged or starred
draftinteger1 for a draft not yet sent
sizeintegerthe raw email's size, in bytes
headerstextJSON: every header, as received
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here
deleted_atintegergone at the source; sync never hard-deletes

email_recipients

One address on an email.

ColumnTypeMeaning
idinteger, PKPrimary key.
email_idinteger, not null → emails.idthe email it belongs to
identity_idinteger → identities.idthe identity it belongs to
addresstext, not nullthe address, lower case
nametextthe display name written beside it
roletext, not nullto, cc, bcc, reply_to
positioninteger, not nullorder within its parent, from 0
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

mailboxes

An IMAP folder or a Gmail label. The owner's own tags are taggings.

ColumnTypeMeaning
idinteger, PKPrimary key.
account_idinteger, not null → accounts.idthe integration account it came through
external_idtext, not nullthe source's own id for it
nametext, not nullthe folder or label name as the provider shows it
kindtextinbox, sent, archive, label, folder
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

email_mailboxes

Which mailboxes an email is in; Gmail puts one email in several.

ColumnTypeMeaning
email_idinteger, not null → emails.idthe email it belongs to
mailbox_idinteger, not null → mailboxes.idthe mailboxe it belongs to
created_atinteger, not nullwhen this row was saved here

email_words

Full-text index over normalized_text, scope.

email_stems

Full-text index over stems, scope.

email_index_pending

Rows of emails waiting to be indexed: triggers enqueue, JS normalizes, stems and writes the index.

ColumnTypeMeaning
idinteger, not nullThe row waiting to be indexed.
indexable_typetext, not nullwhich table the waiting row belongs to

Attachments

Files of a message, an email or a meeting, with their extracted text.

attachments

A file of a message, an email or a meeting, with its extracted text.

ColumnTypeMeaning
idinteger, PKPrimary key.
attachable_typetext, not nullmessage, email or meeting
attachable_idinteger, not nullthe row in the attachable_type table
positioninteger, not nullorder within its parent, from 0
kindtext, not nullLowercase type of the attachment, such as photo, video, file, voice, sticker, share.
mimetextThe file's MIME type, where the messenger gives one.
nametextThe file name, where the messenger gives one.
titletextTitle of a shared web page (for share attachments).
urltextA link that opens the bytes or the shared page, when the messenger hands one out.
sizeintegerFile size in bytes, where the messenger gives it.
widthintegerWidth in pixels for images and video.
heightintegerHeight in pixels for images and video.
durationrealLength in seconds for audio and video.
provider_reftextOpaque JSON the messenger adapter needs to fetch the bytes later; meaningless outside that adapter.
local_pathtextWhere messages download saved the file on this computer; NULL when it was never downloaded.
texttextthe text extracted from the file
normalized_texttextthe text folded for search: lower case, accents removed; filled by the indexer
extractiontexthow the text was got: text, ocr, agent, failed
extractortextWhat produced the attachment's text: plain, docx:[email protected], pdf:[email protected], an OCR engine, or an agent's own label.
extraction_errortextShort reason code why no text came out (e.g. no_text), never a line of the file; NULL on success.
content_sha256textHex SHA-256 of the file bytes that were read, so an unchanged file is not read again.
extracted_atintegerWhen the attachment's text (or its error) was written (epoch ms).
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

attachment_words

Full-text index over normalized_text.

Documents, notes and memories

Documents are files and pages that stand on their own; notes are what a person wrote about something; memories are what an agent concluded.

documents

A file or page that stands on its own: a file in a notes folder (md, pdf, docx, xlsx…), a Notion page, a Drive file, a memo wiki page.

ColumnTypeMeaning
idinteger, PKPrimary key.
account_idinteger, not null → accounts.idthe folder, Notion, Drive
external_idtext, not nullpath in the folder, page id, file id
kindtext, not nullfile, page, wiki
titletextfrom front matter, the first heading, or the file name, in that order
locationtextwhere it lives in its source: the folder path, the Drive folder, the Notion parent page
file_nametextwith its extension; null for a page that is not a file
extensiontextlower case, without the dot: md, pdf, docx
urltextwhere the source opens it
storagetextlocal, drive, notion, web — where the bytes are
local_pathtexta copy on this machine, when one is kept; for a local folder, the file itself
mimetextthe media type, e.g. application/pdf
sizeintegerthe file's size, in bytes
content_hashtexthash of the file's bytes; an unchanged file is not read again
front_mattertextJSON, as written in the file
bodytextMarkdown as is; extracted text for PDF and Office; OCR for scans
normalized_texttextthe text folded for search: lower case, accents removed; filled by the indexer
extractiontextnone, text, ocr, agent, failed
extraction_errortextwhy the text could not be extracted
languagetextdetected language of the body, as an ISO 639-1 code
revisioninteger, not nulledit counter, raised on every change; an edit names the revision it read, so two writers cannot overwrite each other
export_pathtextwhere memo exported it, for wiki pages
external_created_atintegerwhen the source says it was created
external_updated_atintegerwhen the source says it last changed
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here
deleted_atintegergone at the source; sync never hard-deletes

document_revisions

Earlier bodies of a document.

ColumnTypeMeaning
idinteger, PKPrimary key.
document_idinteger, not null → documents.idthe document it belongs to
bodytext, not nullthe body as it was before that revision
revisioninteger, not nulledit counter, raised on every change; an edit names the revision it read, so two writers cannot overwrite each other
created_atinteger, not nullwhen this row was saved here

document_words

Full-text index over normalized_text, scope.

document_stems

Full-text index over stems, scope.

document_index_pending

Rows of documents waiting to be indexed: triggers enqueue, JS normalizes, stems and writes the index.

ColumnTypeMeaning
idinteger, not nullThe row waiting to be indexed.
indexable_typetext, not nullwhich table the waiting row belongs to

notes

Short text about something: a person, a chat, an event, a document, a task. Comments on a task are notes too.

ColumnTypeMeaning
idinteger, PKPrimary key.
notable_typetext, not nullwhat the note is about: person, chat, event, document, task, …; a note about nothing in particular is about the owner's person
notable_idinteger, not nullthe row in the notable_type table
titletextoptional heading
bodytext, not nullthe note, in Markdown
author_typetextperson or bot
author_idintegerthe row in the author_type table
revisioninteger, not nulledit counter, raised on every change; an edit names the revision it read, so two writers cannot overwrite each other
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here
deleted_atintegergone at the source; sync never hard-deletes

note_revisions

Earlier bodies of a note.

ColumnTypeMeaning
idinteger, PKPrimary key.
note_idinteger, not null → notes.idthe note it belongs to
bodytext, not nullthe body as it was before that revision
revisioninteger, not nulledit counter, raised on every change; an edit names the revision it read, so two writers cannot overwrite each other
created_atinteger, not nullwhen this row was saved here

note_words

Full-text index over normalized_text, scope.

note_stems

Full-text index over stems, scope.

note_index_pending

Rows of notes waiting to be indexed: triggers enqueue, JS normalizes, stems and writes the index.

ColumnTypeMeaning
idinteger, not nullThe row waiting to be indexed.
indexable_typetext, not nullwhich table the waiting row belongs to

memories

What an agent concluded: a summary, a daily digest, a fact, a preference. Derived and fallible, so it carries its evidence (links of kind evidence), its confidence and its status; notes are what a person wrote.

ColumnTypeMeaning
idinteger, PKPrimary key.
kindtext, not nullsummary, digest, fact, preference
bodytext, not nullthe memory's text
subject_typetextwhat it is about: person, chat, meeting, project, …; null for a general fact
subject_idintegerthe row in the subject_type table
author_typetext, not nullperson or bot
author_idinteger, not nullthe row in the author_type table
modeltextthe model that wrote it
confidencereal0–1, as the author rated it
statustext, not nullproposed, confirmed, stale, superseded
last_verified_atintegerwhen its evidence was last checked
supersedes_idinteger → memories.idthe memory this one replaces
scopetext, not nullpersonal or work, from its evidence
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

memory_words

Full-text index over normalized_text, scope.

memory_stems

Full-text index over stems, scope.

memory_index_pending

Rows of memories waiting to be indexed: triggers enqueue, JS normalizes, stems and writes the index.

ColumnTypeMeaning
idinteger, not nullThe row waiting to be indexed.
indexable_typetext, not nullwhich table the waiting row belongs to

Events and meetings

The owner's events, and each meeting app's record of them.

event_series

A repeating event, above any one provider's recurrence.

ColumnTypeMeaning
idinteger, PKPrimary key.
titletextwhat the repeating event is called, e.g. the weekly sync
recurrencetextRRULE when known
origintext, not nullauto or owner
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

events

The owner's own record of something that happened, as a person is the owner's record of someone. Holds no provider fields: sources point at it.

ColumnTypeMeaning
idinteger, PKPrimary key.
event_series_idinteger → event_series.idthe event_sery it belongs to
titletextthe owner's title; filled from the first meeting or calendar entry linked
descriptiontextwhat the event is about
locationtextwhere it happens: an address, a room, or online
starts_atintegerwhen it starts
ends_atintegerwhen it ends
timezonetextIANA zone the event is held in, e.g. Europe/Madrid
origintext, not nullauto or owner
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here
deleted_atintegergone at the source; sync never hard-deletes

meeting_series

A provider's recurring or scheduled meeting.

ColumnTypeMeaning
idinteger, PKPrimary key.
account_idinteger, not null → accounts.idthe integration account it came through
external_idtext, not nullZoom meeting id
event_series_idinteger → event_series.idthe event_sery it belongs to
titletextthe meeting's topic at the provider
descriptiontextthe agenda, as the provider stores it
kindtextinstant, scheduled, recurring
recurrencetextJSON, the provider's rule
host_identity_idinteger → identities.idthe identity it belongs to
join_urltextthe link to join; also what later matches a calendar entry
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here
deleted_atintegergone at the source; sync never hard-deletes

meetings

One occurrence: it happened once, at one time.

ColumnTypeMeaning
idinteger, PKPrimary key.
account_idinteger, not null → accounts.idthe integration account it came through
meeting_series_idinteger → meeting_series.idthe meeting_sery it belongs to
event_idinteger → events.idset by event linking
external_idtext, not nullZoom occurrence UUID
titletextthis occurrence's topic
descriptiontextagenda; also what a calendar entry carries
locationtexta physical room, when the provider says
join_urltextthe link to join this occurrence
started_atintegerwhen the occurrence started
ended_atintegerwhen it ended
duration_msintegerhow long it lasted, in milliseconds
timezonetextIANA zone the provider gives for the meeting
host_identity_idinteger → identities.idthe identity it belongs to
participants_countintegerhow many took part, as the provider reports it
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here
deleted_atintegergone at the source; sync never hard-deletes

meeting_participants

One person in one meeting, as that meeting saw them; the name and email stay with this meeting.

ColumnTypeMeaning
idinteger, PKPrimary key.
meeting_idinteger, not null → meetings.idthe meeting it belongs to
identity_idinteger, not null → identities.idkeyed by the provider's user id, else email, else name@meeting
display_nametextthe name shown in this meeting; a later rename does not change it
emailtextthe address the provider gives, when it gives one
roletexthost, co-host, attendee, panelist, guest
joined_atintegerfirst join
left_atintegerlast leave
duration_msintegertime in the meeting, summed over all joins
sessionstextJSON: each join and leave
external_idtextthe source's own id for it
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

meeting_transcripts

One transcript of a meeting: Zoom's, a bot's, or a corrected version, which supersedes the old one.

ColumnTypeMeaning
idinteger, PKPrimary key.
meeting_idinteger, not null → meetings.idthe meeting it belongs to
sourcetext, not nullapi, file, fireflies
formattextvtt
languagetextthe transcript's language, as an ISO 639-1 code
content_hashtextthe same file imported twice is skipped
external_created_atintegerwhen the source says it was created
superseded_atintegerset when a newer transcript of the same meeting replaces this one; this one is kept
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here
deleted_atintegergone at the source; sync never hard-deletes

meeting_transcript_rows

One row of a transcript (a WebVTT cue): who spoke, when, and what they said.

ColumnTypeMeaning
idinteger, PKPrimary key.
meeting_transcript_idinteger, not null → meeting_transcripts.idthe meeting_transcript it belongs to
positioninteger, not nullorder within its parent, from 0
start_msinteger, not nullwhen the row starts, in milliseconds from the start of the recording
end_msinteger, not nullwhen it ends
speaker_participant_idinteger → meeting_participants.idthe participant whose name the cue gives, matched within the meeting; null when no single participant matches
speaker_nametextas written in the cue
texttext, not nullwhat was said, without the speaker prefix
normalized_texttextthe text folded for search: lower case, accents removed; filled by the indexer
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here

meeting_chat_messages

The in-meeting chat.

ColumnTypeMeaning
idinteger, PKPrimary key.
meeting_idinteger, not null → meetings.idthe meeting it belongs to
external_idtextthe source's own id for it
sent_atinteger, not nullwhen it was sent
sender_participant_idinteger → meeting_participants.idthe meeting_participant it belongs to
sender_nametextthe sender's name as the chat shows it
recipienttexteveryone, or a name for a private message
texttext, not nullthe message
normalized_texttextthe text folded for search: lower case, accents removed; filled by the indexer
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

meeting_summaries

A summary of a meeting, from the provider's AI, a bot, the owner or an LLM run here.

ColumnTypeMeaning
idinteger, PKPrimary key.
meeting_idinteger, not null → meetings.idthe meeting it belongs to
sourcetext, not nullzoom-ai, fireflies, owner, llm
titletextthe summary's own title
overviewtextthe short summary paragraph
sectionstextJSON: [{label, text}]
next_stepstextJSON: [text]
contenttextthe provider's full text
doc_urltexta link to the summary at the provider, when it has one
external_created_atintegerwhen the source says it was created
external_updated_atintegerwhen the source says it last changed
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

meeting_words

Full-text index over normalized_text, scope.

meeting_stems

Full-text index over stems, scope.

meeting_index_pending

Rows of transcript rows, meeting chat and summaries waiting to be indexed: triggers enqueue, JS normalizes, stems and writes the index.

ColumnTypeMeaning
idinteger, not nullThe row waiting to be indexed.
indexable_typetext, not nullwhich table the waiting row belongs to

Tasks

A task and ticket system for people and agents: projects with keys, typed and prioritised tasks, assignees that are people or bots, and the decisions made along the way. A promise is a task of type promise; a question is a task of type question whose answer is linked.

reminders

When to remind someone about a task, and whether the reminder was delivered.

ColumnTypeMeaning
idinteger, PKPrimary key.
task_idinteger, not null → tasks.idthe task it belongs to
account_idinteger, not null → accounts.idthe integration account it came through
due_atinteger, not nullWhen the reminder should fire (epoch ms).
timezonetext, not nullIANA time zone the reminder was scheduled in, e.g. Europe/Madrid.
statetext, not nullDelivery state: pending, leased (claimed by a deliverer), delivered or cancelled.
revisioninteger, not nulledit counter, raised on every change; an edit names the revision it read, so two writers cannot overwrite each other
lease_untilintegerUntil when the current deliverer holds the reminder (epoch ms); after it another may claim it. NULL unless leased.
receipttextId handed to the deliverer at claim time, which it must return to confirm delivery; NULL when not leased.
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

projects

A place for tasks, with the short key their ids start with (MEET-12).

ColumnTypeMeaning
idinteger, PKPrimary key.
keytext, not null, uniqueuppercase, e.g. MEET
nametext, not nullthe project's name
descriptiontextwhat the project is for
typetext, not nullwork, client, personal, oss, other; tags group projects beyond that
organization_idinteger → organizations.idthe organization it belongs to
account_idinteger → accounts.idthe account an inbox project collects tasks for; null for a project of the owner's own
scopetext, not nullpersonal or work; what a context query may read for a given purpose, so a work question never pulls private chats
owner_typetextperson or bot
owner_idintegerthe row in the owner_type table
tasks_countinteger, not nullthe last number given out; the next task takes +1 in the same transaction
statustextactive, archived
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here
deleted_atintegergone at the source; sync never hard-deletes

tasks

A task or ticket, for people and agents alike. Everything it concerns — people, messages, emails, documents, meetings — is a links row; its tags are taggings; its comments are notes.

ColumnTypeMeaning
idinteger, PKPrimary key.
project_idinteger, not null → projects.idthe project it belongs to
numberinteger, not nullper project
keytext, not null, unique&lt;project key>-&lt;number>, the id people and agents use
titletext, not nullone line saying what is to be done
descriptiontextthe detail, in Markdown
typetext, not nullbug, feature, chore, question, request, mention, promise — the last four are what the task package finds in messages
statustext, not nullopen, in_progress, blocked, done, dismissed
priorityinteger0 urgent … 4 low
parent_idinteger → tasks.ida subtask's parent
due_atintegerwhen it should be done
started_atintegerwhen someone started it (status in_progress)
closed_atintegerwhen it became done or dismissed
closed_by_typetextperson or bot
closed_by_idintegerthe row in the closed_by_type table
close_reasontextwhy it was closed, in a few words, e.g. no-reply-needed
author_typetext, not nullperson or bot
author_idinteger, not nullthe row in the author_type table
sourcetext, not nullowner, agent, rule, or an import
package_idtext, uniquethe task package's own id for the task; null for a task made here
source_locatortextwhat the task came from: a message locator, never its text
source_kindtextthe kind of thing source_locator names
source_grouptextthe group the task package files it under
resolutiontexthow it ended, in words; a question's answer. Where the answer came from is a links row of kind answered-by
verdicttextuseful or not_useful, as the owner judged a task an agent raised; null until judged
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here
deleted_atintegergone at the source; sync never hard-deletes

task_assignments

Who works on a task: a person or a bot, in a role.

ColumnTypeMeaning
idinteger, PKPrimary key.
task_idinteger, not null → tasks.idthe task it belongs to
assignee_typetext, not nullperson or bot
assignee_idinteger, not nullthe row in the assignee_type table
roletext, not nullassignee, reviewer, watcher
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

task_events

The history of a task: created, status changed, assigned, due date moved.

ColumnTypeMeaning
idinteger, PKPrimary key.
task_idinteger, not null → tasks.idthe task it belongs to
actor_typetextperson or bot
actor_idintegerthe row in the actor_type table
kindtext, not nullwhat happened: created, status, assigned, unassigned, due, priority, moved
changestextJSON: {field: [from, to]}
created_atinteger, not nullwhen this row was saved here

decisions

A choice that was made and holds until replaced: why we do something. Its evidence — the message, transcript row or document — is links of kind evidence; a decision an agent proposed links back to that memory with created-from.

ColumnTypeMeaning
idinteger, PKPrimary key.
project_idinteger → projects.idthe project it belongs to
statementtext, not nullthe decision in one sentence
statustext, not nullproposed, accepted, superseded, reversed
decided_atintegerwhen it was made, as the evidence shows
supersedes_idinteger → decisions.idthe decision this one replaces
confirmed_by_typetextwho accepted it; null while proposed
confirmed_by_idintegerwho accepted it; null while proposed
sourcetext, not nullowner, agent, an import
metadatatextJSON: what the source sends that has no column, and is not searched
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here
deleted_atintegergone at the source; sync never hard-deletes

Agents and actions

What agents propose and what they did: every external action waits for approval, every tool call is logged.

proposed_actions

Something an agent wants done outside the store — reply, send, delete, ban, mute, pin, invite, create a task — waiting for a person to approve it. Nothing external happens without approval.

ColumnTypeMeaning
idinteger, PKPrimary key.
kindtext, not nullreply, send, delete, ban, mute, pin, invite, create_task, …
account_idinteger → accounts.idthe account that would act
target_typetextwhat it acts on: message, chat, identity, task, …
target_idintegerthe row in the target_type table
payloadtextJSON: what to do, e.g. the reply text
reasontextwhy the agent proposes it
statustext, not nullproposed, approved, rejected, executed, failed
proposed_by_typetext, not nullperson or bot
proposed_by_idinteger, not nullthe row in the proposed_by_type table
decided_by_typetextperson or bot
decided_by_idintegerthe row in the decided_by_type table
decided_atintegerwhen it was approved or rejected
executed_atintegerwhen it was carried out
resulttextJSON: what the provider returned
errortextwhy it failed, when it did
verdicttextuseful or not_useful, as the owner judged the proposal
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

agent_actions

Every tool an agent called through the CLIs or MCP: who, which tool, at what access tier, on what. The audit trail of what agents did.

ColumnTypeMeaning
idinteger, PKPrimary key.
actor_typetext, not nullperson or bot
actor_idinteger, not nullthe row in the actor_type table
tooltext, not nullthe command or MCP tool
tiertext, not nullread, draft, write-private, write-public, destructive, admin
target_typetextthe kind of thing target_id points at: a singular table name
target_idintegerthe row in the target_type table
statustext, not nullok, refused, failed
errortextwhy the call failed or was refused
started_atinteger, not nullwhen the call began
finished_atintegerwhen it ended; null while it runs
created_atinteger, not nullwhen this row was saved here

Tags and topics, the links between any two things, and saved searches.

tags

A tag's name, stored once. Which things carry it is taggings.

ColumnTypeMeaning
idinteger, PKPrimary key.
nametext, not null, uniqueunique across tags and topics, so one name is never both
kindtext, not nulltag: a free label; topic: what a thing is about, from a short list only the owner creates (agents propose)
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

auto_tag_claims

Tags that rules put on a chat by matching its title, username and description.

ColumnTypeMeaning
chat_idinteger, not null → chats.idthe chat it belongs to
tag_idinteger, not null → tags.idthe tag it belongs to
algorithmtext, not nullName and version of the rules that assigned the automatic tag, currently keywords-v1.
scorereal, not nullShare of the chat's metadata fields (title, username, description) that matched the tag's keywords: 1/3, 2/3 or 1; not a probability.
fieldstext, not nullJSON list of which metadata fields matched, from title, username, description.
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

Every connection between two things that no column holds. Kinds: links-to (a document or note links to something, as written in it), about (a note or document is about a person, project, …), member-of (a chat or person belongs to a project or organization), labelled (a folder account → a tag, the subfolder's path in anchor, so every document under it carries the tag), answered-by (a question task → the message that answered it), evidence (a decision or memory → its source), created-from (a thing → what it was made from). Kinds the store does not write yet: duplicate-of (the same question asked again), related-to, assigned-to.

ColumnTypeMeaning
idinteger, PKPrimary key.
from_typetext, not nullthe kind of thing from_id points at: a singular table name
from_idinteger, not nullthe row in the from_type table
to_typetextthe kind of thing to_id points at: a singular table name
to_idintegernull while the link names nobody yet
kindtext, not nullthe connection; the kinds are listed in the table's description
anchortextThe heading or block fragment the link points at inside its target, like #Heading or #^block; NULL for the whole target.
sourcetext, not nullWhere the link came from: file (written in a note file), owner (added by the owner) or suggested (proposed, not yet confirmed).
target_texttextThe target as written (e.g. a name in a note), kept while the link resolves to nobody; up to 500 characters.
target_foldedtexttarget_text case- and accent-folded with any leading @ removed, matched against new person names and aliases to resolve the link.
roletextThe role the relation states, such as a job title in a member-of link; free text up to 200 characters.
evidencetextFree text saying why the relation holds (up to 2000 characters).
metadatatextJSON: what the source sends that has no column of its own and is not searched
confirmedinteger, not null1 if the owner stands behind the link, 0 if it is only a weak, unconfirmed proposal.
created_atinteger, not nullwhen this row was saved here
authortextowner, agent, rule
updated_atinteger, not nullwhen this row last changed here

searches

Every run of search messages and stats messages show, with the parameters as the caller gave them — never a message or a result. A row with a name is a saved search; an identical unnamed run counts on its row.

ColumnTypeMeaning
idinteger, PKPrimary key.
nametext, uniqueThe saved search's unique name; NULL for an unnamed run kept only as history.
commandtext, not nullsearch or stats.
paramstext, not nullJSON, keys sorted, so the same run is the same text.
languagetext, not nulllucene-v1 or legacy.
versioninteger, not nullVersion of the search query language used (currently 1).
fields_versioninteger, not nullVersion of the list of searchable field names the query was written against (currently 2).
created_atinteger, not nullwhen this row was saved here
last_run_atintegerWhen the search was last run (epoch ms); NULL for a saved search never run.
runsinteger, not nullHow many times this search has been run.
updated_atinteger, not nullwhen this row last changed here

taggings

One tag on one thing.

ColumnTypeMeaning
idinteger, PKPrimary key.
tag_idinteger, not null → tags.idthe tag it belongs to
taggable_typetext, not nullchat, identity, message, email, document, note, person, organization, project, task, event, …
taggable_idinteger, not nullthe row in the taggable_type table
maininteger, not null1 on the thing's main topic; at most one per thing
sourcetext, not nullwhere it came from: owner, agent, auto, an import
author_typetextperson or bot
author_idintegerthe row in the author_type table
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

Chunks, embeddings and search state

Derived: can be rebuilt from the tables above. Every long text is cut into chunks; each chunk's text has one row in embeddings.

conversations

A group of one chat's messages that belong to the same discussion, from one build.

ColumnTypeMeaning
idinteger, PKPrimary key.
chat_idinteger, not null → chats.idthe chat it belongs to
first_message_idinteger, not null → messages.idthe message it belongs to
buildinteger, not nullWritten in batches under a new number, then made current at once: readers never see half a build.
first_atinteger, not nullSend time of the conversation's first message (epoch ms).
last_atinteger, not nullSend time of the conversation's last message (epoch ms).
message_countinteger, not nullHow many messages the conversation holds.
built_atinteger, not nullWhen the build that produced this conversation ran (epoch ms).
algorithm_versioninteger, not nullVersion of the linking rules that grouped the messages (currently 5).
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

conversation_messages

Which messages each conversation holds.

ColumnTypeMeaning
conversation_idinteger, not null → conversations.idthe conversation it belongs to
message_idinteger, not null → messages.idthe message it belongs to

conversation_state

Which chats the user enabled, and how fresh their conversations are.

ColumnTypeMeaning
chat_idinteger, PK → chats.idthe chat; one row per chat with grouping turned on
enabled_atinteger, not nullWhen the owner turned on conversation grouping for this chat (epoch ms).
built_atintegerWhen the current conversations of this chat were built (epoch ms); NULL before the first build.
algorithm_versionintegerLinking-rules version of the current build; NULL before the first build.
current_buildintegerThe build readers see; a higher one is being written, or failed.

embeddings

One embedding vector per model and text: any chunk whose content_hash matches uses it.

ColumnTypeMeaning
modeltext, not null&lt;provider>:&lt;model>:&lt;dims> — vectors of different models never mix.
content_hashtext, not nullHex SHA-256 of the chunk text the embedding model was given; the text itself is never stored.
dimsinteger, not nullNumber of dimensions in the vector (its float32 length).
vectorblob, not nullFloat32, little-endian, length one.
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

search_index_state

How far a derived search index is built, one row per index. watermark is the highest message pk when the index was created: rows above it are indexed by triggers, rows up to it by batches that have reached filled_through.

ColumnTypeMeaning
nametext, PKwhich index the row describes, e.g. message_words
watermarkinteger, not nullThe highest message pk when the index was created; rows above it are indexed by triggers, rows up to it by batch fill.
filled_throughinteger, not nullThe highest message pk the batch fill has indexed so far; the index is complete once it reaches the watermark.
terms_throughinteger, not nullThe highest message pk whose words are in the typo-correction vocabulary; 0 means the vocabulary is not built.
normalizer_versioninteger, not nullVersion of the text-normalisation rules the index was built with.
built_atintegerWhen the index finished filling (epoch ms); NULL while it is still being filled.
analyzertextThe stemmer choices and Snowball version that built the stems row (analyzerIdentity); NULL until a fill claims it.

search_terms

Every word in the message search index, for typo correction.

ColumnTypeMeaning
termtext, PKthe word as indexed
lengthinteger, not nullNumber of characters in the word, used to find typo candidates of a similar length.

search_term_trigrams

Three-letter pieces of each search word, to find words that look like a mistyped one.

ColumnTypeMeaning
trigramtext, not nullA three-character piece of a known word, used to find words that look like a mistyped one.
lengthinteger, not nullCharacter length of the word the trigram comes from.
termtext, not nullThe known search word the trigram belongs to (numbers get no trigrams).

involvements

Who took part in what, and when: one row per person or identity per message, email, meeting, task or document. Derived from senders, recipients, participants, assignees and links, and rebuilt at will; it turns "everything about Alex" into one indexed range.

ColumnTypeMeaning
idinteger, PKPrimary key.
person_idinteger → persons.idnull while the identity is linked to nobody
identity_idinteger → identities.idthe identity it belongs to
subject_typetext, not nullmessage, email, meeting, task, document, note, …
subject_idinteger, not nullthe row in the subject_type table
roletext, not nullsender, recipient, participant, assignee, author, mentioned, linked
occurred_atinteger, not nullwhen the subject happened
scopetext, not nullpersonal or work, from the subject's account, chat or project
account_idinteger → accounts.idthe integration account it came through
project_idinteger → projects.idthe project it belongs to
created_atinteger, not nullwhen this row was saved here

involvement_pending

Things whose involvements rows are stale: a message, chat, meeting, email, task or anything a link starts from. Triggers enqueue; the drain recomputes that thing's rows.

ColumnTypeMeaning
idinteger, not nullThe row waiting to be indexed.
indexable_typetext, not nullwhich table the waiting row belongs to

chunks

Pieces of a longer text, the unit that gets an embedding: ranges of a document, an email, an attachment's text, a note, a meeting transcript or summary; a short task or event is one chunk. A conversation of messages is cut into pieces here too, one conversation row per piece, and chunk_messages says which messages each piece spans; so one search can read every kind of text, filtered by scope, account, project and time.

ColumnTypeMeaning
idinteger, PKPrimary key.
chunkable_typetext, not nulldocument, email, attachment, note, memory, conversation, meeting_transcript, meeting_summary, event, task
chunkable_idinteger, not nullthe row in the chunkable_type table
positioninteger, not nullorder within its parent, from 0
start_offsetinteger, not nullwhere the piece starts in the parent's text, in characters
end_offsetinteger, not nullwhere it ends, exclusive
content_hashtext, not nullhash of the piece's text; its embedding is the embeddings row with this hash, so equal text is embedded once
scopetextcopied from the parent's account, chat or project, so a vector search filters before it compares; triggers follow a chat's or account's change
account_idinteger → accounts.idcopied from the parent, a filter for vector search
project_idinteger → projects.idcopied from the parent when it belongs to one
occurred_atintegerwhen the parent happened (sent, held, written), a filter for vector search
created_atinteger, not nullwhen this row was saved here
updated_atinteger, not nullwhen this row last changed here

chunk_messages

Which messages a conversation's chunk is cut from. One row per conversation row of chunks; deleting either message deletes the chunk.

ColumnTypeMeaning
chunk_idinteger, PK → chunks.idthe chunk, of type conversation
first_message_idinteger, not null → messages.idthe message it belongs to
last_message_idinteger, not null → messages.idthe message it belongs to
text_startintegerwhere the piece starts inside the first message's text, when it begins mid-message
text_endintegerwhere it ends inside the last message's text

Store

Bookkeeping for the file itself.

schema_migrations

Which migrations this file has applied.

ColumnTypeMeaning
versioninteger, PKthe migration's number
min_compatibleinteger, not nullThe oldest schema version whose code can still safely write a file at this version; a build older than it refuses the file.
applied_atinteger, not nullWhen this migration was applied to the file (epoch ms).

store_settings

Settings of the store file itself, shared by every profile, tg and MAX — unlike a profile's config file.

ColumnTypeMeaning
keytext, PKthe setting's name
valuetext, not nullThe setting's value as text, shared by every profile and messenger using this store file.
atinteger, not nullWhen the setting was last written (epoch ms).

To see these tables at work, read how search works; to see how the store fits into the tools, read architecture.