Schemas
AIProfileCapabilities
Every registry row, per kind, disabled rows included with
enabled=false: a picker offers the enabled ones by display name, and a
component already on a disabled one can still say what it is on and that
it is no longer offered. The client filters; the server does not guess
which rows a caller already has written.
What the reader reads at most, every value filled in: the platform's own
(PLATFORM_READER_CEILINGS) or a profile's lower one, as ReaderLimits.ceilings
resolves them. What the text channel holds an attachment to.
AbsoluteWindow
sinceuntilAccessLevel
How much of one channel a member reads (authentication.md rule 32): none; every conversation; those a chosen set of people have or had; or only their own. Every level but none also reads what no person has or had and what the member has or had themselves (rule 33, owner ruling 2026-10-06).
ActRefusalOut
detailrefusalWhy a finding's action is not offered to this person, or was refused (rule 28): it is offered only to someone who could do the act themselves.
ActionRefusal
Why a finding's action is not offered to this person, or was refused (rule 28): it is offered only to someone who could do the act themselves.
AgentCopy
nameAgentMapOut
agent_idOne card. key is the map's own handle for it (<kind>:<id>), what every
line and lane names; id is the part's own id, for opening its screen (rule 23)
-- for a documents or capability card, the agent's.
state is None for a card that carries no dot (rule 35): a collection,
documents, a capability, a number. failure -- why it failed -- is filled only
for a reader who may read where the failure was recorded (rule 30). missing
marks a part something points at that no longer exists, or a part an incomplete
recipe lacks; name is then the last name known, or None (rule 37).
One opened card's inside (rule 11). parts are the cards inside it, each with
frame set to key; lines every line drawn inside it, including those from a
part inside to an outer card it reads or writes. reads and writes name the
outer cards drawn above and below it; one both read and written is in writes
alone (rule 17).
hiddennamepublishedread_atversion_idAgentOut
idlanguageA language the platform can speak — which is to say, one it has default phrases written in. The agent states one of these and every phrase on it is written in that one; a language with no phrases behind it would be a label.
nameowner_idplatformrecord_audioupdated_atAgentVersionOut
agent_idanswering_machine_enabledanswering_machine_instructioncourtesy_window_secondscreated_atduration_ceiling_secondsfailure_thresholdfirst_line_delay_msgraph_idgreeting_interruptibleidlanguageA language the platform can speak — which is to say, one it has default phrases written in. The agent states one of these and every phrase on it is written in that one; a language with no phrases behind it would be a label.
search_fragment_limitsilence_timeout_secondsstatusA version's place in the draft → published → archived lifecycle.
takes_documentstakes_imagestakes_videostakes_voice_notesdocument_idsglobal_tool_idsnoticespost_hook_tool_idspre_hook_tool_idsAggregation
How a measure reduces the matching rows to one number per group. Owned
here because analytics is the one caller that has to speak it identically
over three different slices' data (interactions, billable_metrics,
custom_data) — none of which may import this module to borrow it, so each
one's own aggregate surface takes it as a plain string instead.
AgreedVersionOut
documentThe six legal documents the platform keeps (rule 1). Each value is also the last segment of the document's address, so it is spelled as a URL is.
fingerprintlanguageThe languages a version may be written in (values table): Spanish required, English optional, and the Spanish prevails (rule 4).
nameAllTimeWindow
all_timeAnalystCostOut
sinceAnswerField
keyquestionAnsweredBy
Who picked up an outbound call.
VOICEMAIL and MENU are separate members on purpose: "a number behind a menu
is screened, not unattended, and a client deciding what to do with a list needs
to tell them apart".
AnsweredFolderOut
AnsweringRecipe
install_idrecipe_keyApiDescriptionIn
ApiImportIn
operationsserverApiImportOut
ApiMissingOut
nameoperationtool_idApiOperationOut
descriptionkeymethodnamepathsummaryApiPreviewOut
kindmax_operations_per_importtitleApiServerOut
The connection a description suggests (api-import.md rule 9). provider
http_key comes with the placement (header, query or basic) and the
header / parameter / prefix to put in that connection; google suggests
the client's Google account; None suggests nothing, and note says why.
descriptionApiSuggestionOut
noteApiUnimportableOut
keymethodpathreasonsummaryApplyTo
Where an edit of a component used by more than one profile lands (ai-profiles.md rule 3: "change it for every profile on it, or make a copy for this profile only. It never guesses between them.").
ALL edits the component itself, so every profile on it changes. COPY
edits a copy of it and repoints only the one profile named beside it.
ArrangementAttempt
kindpositionwhypassed_overArrangementIn
kindArrangementOut
kindpositionAssignIn
person_idAttachment
datafilenamemimeAttachmentIn
kindWhat one outbound attachment is (channels.md rule 76): an image, a video,
an audio file or a document. Not channels' AttachmentKind, which names
what arrived -- a sticker, a contact card, a location among them -- and
which no road is asked to send.
urlAudience-Input
Who writes on a connection (channels.md rule 50): the organization's own
members, or its contacts -- people outside it. A road declares which it can
carry (rule 51); a connection names one of those. Not notifications'
Audience, which is who a notice is for: same word, another question.
AutomationCreate
flow_idnametrigger_kindWhat sets an automation off. Closed, and each member names exactly one
place in this deployment where it is detected — see tasks.py and the
webhook route.
Four, and four is all the spec asks for. automations.md used to list a
fifth, "when a message or call arrives — including on channels we do not
answer conversationally", and withdrew it on 2026-09-13 when channels.md
was specified: "a channel nobody answers conversationally is simply a flow
with no branch that hands to an agent", so an arriving message runs the
channel's own flow and there is nothing left for a trigger to do. What an
automation does with a channel is send on one — channels'
send_on_channel built-in inside a scheduled flow, which is SCHEDULE.
#395 closed.
min_interval_secondsThe small typed shape a trigger is configured by. Every field is optional
here and required by service for the kind that needs it, so one column
stores four vocabularies without four columns of nulls.
AutomationCreated
consecutive_failuresenabledflow_idhas_webhook_secretidmin_interval_secondsnameorg_idThe small typed shape a trigger is configured by. Every field is optional
here and required by service for the kind that needs it, so one column
stores four vocabularies without four columns of nulls.
trigger_kindWhat sets an automation off. Closed, and each member names exactly one
place in this deployment where it is detected — see tasks.py and the
webhook route.
Four, and four is all the spec asks for. automations.md used to list a
fifth, "when a message or call arrives — including on channels we do not
answer conversationally", and withdrew it on 2026-09-13 when channels.md
was specified: "a channel nobody answers conversationally is simply a flow
with no branch that hands to an agent", so an arriving message runs the
channel's own flow and there is nothing left for a trigger to do. What an
automation does with a channel is send on one — channels'
send_on_channel built-in inside a scheduled flow, which is SCHEDULE.
#395 closed.
AutomationMapOut
automation_idOne card. key is the map's own handle for it (<kind>:<id>), what every
line and lane names; id is the part's own id, for opening its screen (rule 23)
-- for a documents or capability card, the agent's.
state is None for a card that carries no dot (rule 35): a collection,
documents, a capability, a number. failure -- why it failed -- is filled only
for a reader who may read where the failure was recorded (rule 30). missing
marks a part something points at that no longer exists, or a part an incomplete
recipe lacks; name is then the last name known, or None (rule 37).
One opened card's inside (rule 11). parts are the cards inside it, each with
frame set to key; lines every line drawn inside it, including those from a
part inside to an outer card it reads or writes. reads and writes name the
outer cards drawn above and below it; one both read and written is in writes
alone (rule 17).
hiddennameread_atAutomationOut
consecutive_failuresenabledflow_idhas_webhook_secretidmin_interval_secondsnameorg_idThe small typed shape a trigger is configured by. Every field is optional
here and required by service for the kind that needs it, so one column
stores four vocabularies without four columns of nulls.
trigger_kindWhat sets an automation off. Closed, and each member names exactly one
place in this deployment where it is detected — see tasks.py and the
webhook route.
Four, and four is all the spec asks for. automations.md used to list a
fifth, "when a message or call arrives — including on channels we do not
answer conversationally", and withdrew it on 2026-09-13 when channels.md
was specified: "a channel nobody answers conversationally is simply a flow
with no branch that hands to an agent", so an arriving message runs the
channel's own flow and there is nothing left for a trigger to do. What an
automation does with a channel is send on one — channels'
send_on_channel built-in inside a scheduled flow, which is SCHEDULE.
#395 closed.
AutomationRunOut
attemptsautomation_ididorg_idoutcomeWhere one run got to.
trigger_keyAutomationScope
For an automation: what sets it off (rules 7, 15).
AutomationUpdate
BlockedReason
Which bound stopped a hop — dynamic-context.md's two.
BoardChartPlacement
hwxykindBoardIn
nameA board's arrangement, and its own global filters
(docs/features/dashboard.md:7) — merged into every chart's spec at run
time by run_chart/get_board and never written back into the chart
itself (§3.5 of the design study).
BoardLayout
BoardOut
created_atidA board's arrangement, and its own global filters
(docs/features/dashboard.md:7) — merged into every chart's spec at run
time by run_chart/get_board and never written back into the chart
itself (§3.5 of the design study).
nameorg_idupdated_atBoardUpdate
BoxesOut
BridgeCeilingIn
minutesBridgeCeilingOut
is_defaultminutesmax_minutesmin_minutesBrowserCall
expires_atinteraction_idroomtokenurlBrowserCallRefusalOut
detailBucket
A time-bucket width. Same authorship reason as Aggregation.
BuiltinOut
nameconfig_keysfixed_parametersprovidersBulkDeleteIn
BulkResult
matchedBulkSetFieldIn
keyvalueCallBackIn
max_attemptsretry_delay_minutesCallIn
agent_iddestinationtrunk_idWhen to place the call. Absent means it takes its turn in the organization's rotation, on the next free slot; a value is an instant somebody named, which jumps the rotation and is never brought forward into a free slot. A call that belongs to no campaign has no calling hours, so any instant is kept. Must carry a UTC offset (2026-09-10T11:00:00Z or 2026-09-10T13:00:00+02:00); without one the request is refused with 422 naming the field, so the call is never placed at a guessed-at moment.
told_fieldsCampaignIn
agent_idnametimezonetrunk_idcollection_told_fieldscontact_sourceWhere a campaign's contacts come from.
COLLECTION names docs/features/custom-data.md's stored collection. This
slice holds the id and nothing else: the read is an injected ContactSource,
bound to nothing today.
CampaignOut
agent_idcollection_told_fieldscontact_sourceWhere a campaign's contacts come from.
COLLECTION names docs/features/custom-data.md's stored collection. This
slice holds the id and nothing else: the read is an injected ContactSource,
bound to nothing today.
created_atidnameorg_idprovider_failure_streakstatusWhere a campaign is in its life.
PAUSED is one state whoever caused it — see PauseOrigin. There is no
separate "stopped by the provider" status, because resuming is the same
operation either way and a second status would double every read of "is this
campaign dialling".
timezonetrunk_idupdated_atwarningsCampaignStatus
Where a campaign is in its life.
PAUSED is one state whoever caused it — see PauseOrigin. There is no
separate "stopped by the provider" status, because resuming is the same
operation either way and a second status would double every read of "is this
campaign dialling".
CampaignSummaryOut
as_ofattempts_spentcampaign_idcontacts_without_numberdue_laterdue_nowstatusWhere a campaign is in its life.
PAUSED is one state whoever caused it — see PauseOrigin. There is no
separate "stopped by the provider" status, because resuming is the same
operation either way and a second status would double every read of "is this
campaign dialling".
unclassified_pickupsCampaignUpdate
CapabilityField
kindCatalogueVoice
idnameChangeKind
What changed since the version before, in the vocabulary of the product.
Not a diff of text: each member is a sentence a client would recognise about their own agent. Rendering one into their language is the dashboard's job, which is why these are named states and not English strings.
Channel
Voice and text are two channels over one graph, not two kinds of record.
ChannelAccessBody
levelHow much of one channel a member reads (authentication.md rule 32): none; every conversation; those a chosen set of people have or had; or only their own. Every level but none also reads what no person has or had and what the member has or had themselves (rule 33, owner ruling 2026-10-06).
peopleChannelIn
connection_idnameChannelKind
What an inbox channel is, as the person granting access would name it (inbox.md rules 6-7).
ChannelOut
audienceWho writes on a connection (channels.md rule 50): the organization's own
members, or its contacts -- people outside it. A road declares which it can
carry (rule 51); a connection names one of those. Not notifications'
Audience, which is who a notice is for: same word, another question.
burst_window_secondsconnection_idcreated_atenabledflow_idhands_to_agentidinbox_offnameorg_idowns_flowupdated_atagent_cannot_writeChannelScope
keykindWhat an inbox channel is, as the person granting access would name it (inbox.md rules 6-7).
labelopenremovedChannelUpdate
ChartIn
nameA chart's whole definition — what analytics_charts.spec stores, and
what POST /analytics/charts/preview runs unsaved. dataset is one of the
catalogue's keys (GET /analytics/datasets lists them): "interactions",
"tool_executions", "node_traversal", "usage", the Analyst's
"analyst_readings" and "analyst_answers", or "collection:<uuid>" for one
of the organization's own custom_data collections.
There is no tz here, and its absence is the design.
docs/features/analytics.md: "a chart's days belong to whoever is looking
at it — the zone is resolved every time a chart is read, and is never saved
with it" (owner, 2026-09-15). This is the same rule window already obeys
one field up: what is stored is the question that was asked, not what that
question happened to mean at the moment of saving. The zone arrives instead
as a per-request argument to service.run_spec/run_chart, resolved at the
edge by backend.api.deps.get_person_timezone from the person doing the
reading.
A spec read back out of JSONB may still carry a tz key, and it is
ignored. Pydantic's default extra="ignore" does that with no branch to
maintain and nothing to remove later; DECISIONS.md records why this shape
is tolerated where the pre-series shape was rewritten by a migration
instead.
descriptionChartOption
countoptionChartOut
created_atdescriptionidnameorg_idA chart's whole definition — what analytics_charts.spec stores, and
what POST /analytics/charts/preview runs unsaved. dataset is one of the
catalogue's keys (GET /analytics/datasets lists them): "interactions",
"tool_executions", "node_traversal", "usage", the Analyst's
"analyst_readings" and "analyst_answers", or "collection:<uuid>" for one
of the organization's own custom_data collections.
There is no tz here, and its absence is the design.
docs/features/analytics.md: "a chart's days belong to whoever is looking
at it — the zone is resolved every time a chart is read, and is never saved
with it" (owner, 2026-09-15). This is the same rule window already obeys
one field up: what is stored is the question that was asked, not what that
question happened to mean at the moment of saving. The zone arrives instead
as a per-request argument to service.run_spec/run_chart, resolved at the
edge by backend.api.deps.get_person_timezone from the person doing the
reading.
A spec read back out of JSONB may still carry a tz key, and it is
ignored. Pydantic's default extra="ignore" does that with no branch to
maintain and nothing to remove later; DECISIONS.md records why this shape
is tolerated where the pre-series shape was rewritten by a migration
instead.
updated_atChartResult
ChartResultRow
value^(?!^[-+.]*$)[+-]?0*… · requiredChartRunIn
ChartSeries
agg(column), scaled if scale is given. column is None only for
Aggregation.COUNT; every other aggregation names a column the dataset's
catalogue declares as measurable. Validated against that catalogue by
service.run_spec/service.create_chart — never trusted on its own.
nameChartSeriesResult
nameChartSpec
chart_typedatasetChartWeek
answeredweekChoiceIn
labelClassCapabilities
carries_languagecarries_speedends_own_turnkindproviderreads_imagesreads_temperaturesends_language_hintClosedConversation
farewellinteraction_idCollectionLink
collection_idnumber_fieldCollectionNameOut
idnameCollectionOut
created_atdescriptionidnameorg_idslugupdated_atrecords_deleted_wholeCollectionQuestion
Condition
fieldopThe DSL's comparison operators, evaluated against a step output field.
ConditionOp
The DSL's comparison operators, evaluated against a step output field.
ConfirmationAnswer
decisionHow a person answered the question a consequential tool was put behind.
Two members and no third. There is no "expired" or "timed out": a
conversation that ends with a question outstanding answers it DECLINED,
because the thing the record has to be able to say is whether the action
happened, and it did not. Inventing a member for why nobody said yes would
put two answers in the one field a client reads to find out.
ConfirmationDecision
How a person answered the question a consequential tool was put behind.
Two members and no third. There is no "expired" or "timed out": a
conversation that ends with a question outstanding answers it DECLINED,
because the thing the record has to be able to say is whether the action
happened, and it did not. Inventing a member for why nobody said yes would
put two answers in the one field a client reads to find out.
ConfirmationMode
Whether one conversation is still asking before consequential actions.
architect.md rule 20: "a person may tell one conversation to stop asking…
It is a property of that conversation only — not of the person or the
organization — and asking is always where a conversation starts."
It is a column vocabulary here and an input to the run-loop there, which
is the same two-names split Channel has and for the same reason: this one is
what the record says a conversation was set to, and that one is what decides
whether the loop stops. Member for member, so a writer maps with
interactions.ConfirmationMode(mode) and needs no table.
ConnectionIn
account_labelprovidergranted_viaWhich door the grant arrived by.
The spec's "a grant can arrive by more than one door, and the connection is the same whichever it came through": this is the record of the door, and nothing in this slice or above it branches on it.
per_personscopesConnectionOut
account_labelcapabilitiesWhat a google connection may do, in the words the person picked
from, and empty for every other provider.
scopes is on this shape too and stays: it is what the grant literally
holds, including anything Google volunteered that this platform never
asked about. But a scope URL is this module's vocabulary and not a
person's, and connections.md is explicit that what a connection may do is
offered "never as the provider's own vocabulary for the same thing" -- so
the reading in the client's words travels with the row rather than
leaving a dashboard to spell https://www.googleapis.com/auth/... back
at them.
created_atgranted_viaWhich door the grant arrived by.
The spec's "a grant can arrive by more than one door, and the connection is the same whichever it came through": this is the record of the door, and nothing in this slice or above it branches on it.
idproductsWhich Google products this connection is authorized for, and empty for every other provider -- which has no such division, rather than one this would have to invent a word for.
A reading of scopes and never a column, for the reason
GoogleProduct gives: what a connection is for is what its grant
reaches, so a stored copy would be a second place for one fact and the
first of the two to go stale. Computed here rather than filled in by the
service because every path that hands a connection out goes through this
class, and one of them would eventually forget.
providerscopesstatusWhether the grant is currently usable.
updated_atper_personConnectionUpdate
ContactFileOut
message_idsize_bytesurlContactIn
numberWhen to call this contact. An instant somebody named: it jumps the rotation, outranks the retry delay and spends nobody's turn. An instant outside the campaign's calling hours is kept: the row is added and also returned in warnings, naming the hours. Omit it to let the contact take its turn in the rotation. Must carry a UTC offset (2026-09-10T11:00:00Z or 2026-09-10T13:00:00+02:00); without one the request is refused with 422 naming the field, so the call is never placed at a guessed-at moment.
ContactOut
attempts_spentcreated_atdue_nowidstatusOne contact's place in the state machine outbound-calls.md calls
single-writer and idempotent.
EXHAUSTED is we are finished with this number — the attempt budget is
gone, the provider confirmed the destination will never work, or the
organization's own retention window destroyed the number while the contact
was still waiting for a turn and there is nothing left to dial.
EXCLUDED is the client said not to call this person, and only a client
ever writes it. Keeping them apart is what lets "excluding a contact cancels
its pending calls" be re-openable while a confirmed-invalid number is not.
There is no pending. A contact is dialable the moment it exists —
"activating a campaign does not queue its contacts" — so it is written
SCHEDULED and there is no earlier state for it to sit in. A member nothing
writes is a state a reader has to ask about and nobody can reach.
updated_atContactSourceKind
Where a campaign's contacts come from.
COLLECTION names docs/features/custom-data.md's stored collection. This
slice holds the id and nothing else: the read is an injected ContactSource,
bound to nothing today.
ContactStatus
One contact's place in the state machine outbound-calls.md calls
single-writer and idempotent.
EXHAUSTED is we are finished with this number — the attempt budget is
gone, the provider confirmed the destination will never work, or the
organization's own retention window destroyed the number while the contact
was still waiting for a turn and there is nothing left to dial.
EXCLUDED is the client said not to call this person, and only a client
ever writes it. Keeping them apart is what lets "excluding a contact cancels
its pending calls" be re-openable while a confirmed-invalid number is not.
There is no pending. A contact is dialable the moment it exists —
"activating a campaign does not queue its contacts" — so it is written
SCHEDULED and there is no earlier state for it to sit in. A member nothing
writes is a state a reader has to ask about and nobody can reach.
ContactsIn
ConversationAccessOut
One level on a channel (authentication.md rule 32), and the people it
names when it is people -- empty for every other level.
everythingConversationAccessUpdate
One level on a channel (authentication.md rule 32), and the people it
names when it is people -- empty for every other level.
ConversationLabels
interaction_idlabel_idsConversationMemoryOut
interaction_idkeptnoted_goneConversationMode
interaction_idmodeWhether one conversation is still asking before consequential actions.
architect.md rule 20: "a person may tell one conversation to stop asking…
It is a property of that conversation only — not of the person or the
organization — and asking is always where a conversation starts."
It is a column vocabulary here and an input to the run-loop there, which
is the same two-names split Channel has and for the same reason: this one is
what the record says a conversation was set to, and that one is what decides
whether the loop stops. Member for member, so a writer maps with
interactions.ConfirmationMode(mode) and needs no table.
ConversationState
The inbox's state filter (inbox.md rule 11): still going, or over. Not
stored -- ended_at is the one place that fact lives.
CustomToolAuthor
kindWho a published version came from.
The same distinction agents records for a published agent version, spelled
in this slice's own words because a slice does not import another's enum for
a fact of its own.
CustomToolAuthorKind
Who a published version came from.
The same distinction agents records for a published agent version, spelled
in this slice's own words because a slice does not import another's enum for
a fact of its own.
CustomToolCreate
namepurposedeclared_reachdescriptionsource_codeCustomToolOut
created_atdeclared_reachdescriptionidnameorg_idpurposestatusWhether this custom tool has ever gone live.
DRAFT is not "being edited" — there is always a draft. It is "no version of
this has ever been published", which is what decides whether the registered
row has anything to run.
updated_atCustomToolStatus
Whether this custom tool has ever gone live.
DRAFT is not "being edited" — there is always a draft. It is "no version of
this has ever been published", which is what decides whether the registered
row has anything to run.
CustomToolVersionOut
author_kindWho a published version came from.
The same distinction agents records for a published agent version, spelled
in this slice's own words because a slice does not import another's enum for
a fact of its own.
change_summarycreated_atcustom_tool_ididsource_codeupdated_atCutCounts
backchannelbefore_lineconfirmed_by_durationconfirmed_by_wordscutsemptyovertakenphantomproposedresumedwithdrawnwithdrawn_by_budgetDatasetFilterOut
columnkindWhat a filterable column holds, in the only terms a filter cares about.
Coarser than any one owner's own type vocabulary on purpose: it exists so a
control can be chosen for a column, and a control is chosen by "is this
picked from a list, typed as text, compared as a number, or picked from a
calendar". custom_data.FieldType maps onto it one-to-one today; an
analytics dataset column maps onto it from whatever it actually is —
an enum column, a uuid a client picks by name, a timestamp.
labelopsoptionsDatasetMeasureOut
aggHow a measure reduces the matching rows to one number per group. Owned
here because analytics is the one caller that has to speak it identically
over three different slices' data (interactions, billable_metrics,
custom_data) — none of which may import this module to borrow it, so each
one's own aggregate surface takes it as a plain string instead.
labelscalesDatasetOut
datasetlabelDecisionAct
tooldeliveredheldkeptlabelsokDecisionOut
decided_atfailedidinteraction_idmessage_idDecisionOutcome
The five outcomes a thread flow's run may declare (mail-handling.md rule 1): the counterpart was answered, a reply was kept for a person to send, something was done and nothing sent, the thread went to a person, or nothing was done beyond reading. A run declares it; the platform never infers it.
DecisionReading
questionanswerDeletedKey
idnameDeletionRefusalOut
detailDimension
columnDirectoryEntryIn
kindWhat a directory entry reaches (rule 55): a phone number, normalised to E.164 like every number; or an extension, or an address the client's phone system gives a person, a queue or a group -- both held as text exactly as the system names them (rule 56) and reached only through the entry's trunk.
nametargetring_even_if_divertedDirectoryEntryOut
closedcreated_atidkindWhat a directory entry reaches (rule 55): a phone number, normalised to E.164 like every number; or an extension, or an address the client's phone system gives a person, a queue or a group -- both held as text exactly as the system names them (rule 56) and reached only through the entry's trunk.
namereaches_nothingring_even_if_divertedtargetupdated_atDirectoryEntryUpdate
DirectoryHours
ends_atstarts_atweekdayDirectoryTargetKind
What a directory entry reaches (rule 55): a phone number, normalised to E.164 like every number; or an extension, or an address the client's phone system gives a person, a queue or a group -- both held as text exactly as the system names them (rule 56) and reached only through the entry's trunk.
DnsRecordOut
namerecord_typevalueDocumentAttachmentOut
agent_iddocument_idin_draftin_livenameDocumentOut
byte_sizechunk_countcreated_aterrorfilenameidmedia_typeorg_idstatusWhere a document is between "uploaded" and "searchable".
failed is a state, not an exception (DECISIONS.md D7): a PDF that will
not parse stays in the library with a reason the client can read, because
they uploaded a file and a file that vanishes is worse than one that says
what went wrong.
updated_atDomainState
What the platform's mail service last said of the address's domain.
unchecked until anybody asked; not_registered while the mail service
holds no sending domain for it -- every client domain today, with no
records to show, until per-client sending domains land (#1555);
unverified while the records the client must add are not all found;
verified once they are. Shown, and holding nothing back: no state lets or
stops a send, since nothing leaves as the address -- answers leave from the
address the platform gave the connection (rules 53-54, email-threads.md
rule 28).
DraftUpdate
EdgeIn
source_node_idtarget_node_idconditionEdgeOut
conditionidsource_node_idtarget_node_idEnabledModelOut
configuration_classdisplay_nameenabledenabled_atidkindmodelproviderEndReason
Why the conversation ended.
Deliberately extendable: what ends a text conversation is an open product question, so this is stored as text rather than as a database enum. Adding a way to end is one line here and no migration.
EventType
Everything a conversation can record about the path it walked.
ExampleSuggestionIn
namedescriptionexample_requestsExampleSuggestionOut
example_requestsFallbackCondition
Whether the trunk is currently forwarding calls to its fallback number.
Binary on purpose: "A trunk can sit in fallback indefinitely... entering fallback raises a notification... and again when the condition clears" — the spec only ever names two states, not a severity scale.
FanOut
collection_idlimitFeatureFlag
The capability vocabulary. Grows with a one-line edit, never a migration: flags are stored as string arrays and a JSONB map, never as an enum column.
FieldDefault
kindoffset_daysvalueFieldIn
key^[a-z][a-z0-9_]*$ · requiredlabeltypeWhat one declared field holds.
Five, and each is a parse rule rather than a column type. The values live
in JSONB and the database enforces nothing about them, so what a type means
is service's to keep — which is where it had to live anyway: "a 0 is not a
no" is a product rule, not a column.
Still deliberately no email and no phone, even now that DATOS-02's
default country is ruled (the organization's own — docs/features/ authentication.md). A PHONE type would impose a parse obligation on the
whole column — one bad number and the record stops being writable — where
what #378 needed was a normaliser on the identity, which changes only how
two rows compare. So an identity value is canonicalised by shape, not by
a declared type: service._canonical_identity reads the value itself,
and if it parses as a phone number under the organization's default
country it is stored E.164 (shared.phone.to_e164, ADR 0023); otherwise
strip() + casefold(), as before. A TEXT field holding a phone number
is still the ordinary case this platform's collections use. A field
holding an email address is a TEXT field the client marked personal,
which is what the retention rule actually keys on.
optionspersonalrequiredFieldOut
collection_idfilter_kindWhat this field holds, in the terms a control is chosen by.
filter_opsThe questions this field admits — "the type travels with the
question" (custom-data.md). Computed rather than stored: it is a
function of type, and a column holding a copy of it is a column that
can disagree with it. Answered on the field itself so a control is
chosen from what the platform says the field is, never from what its
name looks like — which is the rule apps/web/CLAUDE.md refuses to let
the browser hold and the same gap #521 names for a
chart's own filter editor.
idkeylabeloptionspersonalpositionrequiredtypeWhat one declared field holds.
Five, and each is a parse rule rather than a column type. The values live
in JSONB and the database enforces nothing about them, so what a type means
is service's to keep — which is where it had to live anyway: "a 0 is not a
no" is a product rule, not a column.
Still deliberately no email and no phone, even now that DATOS-02's
default country is ruled (the organization's own — docs/features/ authentication.md). A PHONE type would impose a parse obligation on the
whole column — one bad number and the record stops being writable — where
what #378 needed was a normaliser on the identity, which changes only how
two rows compare. So an identity value is canonicalised by shape, not by
a declared type: service._canonical_identity reads the value itself,
and if it parses as a phone number under the organization's default
country it is stored E.164 (shared.phone.to_e164, ADR 0023); otherwise
strip() + casefold(), as before. A TEXT field holding a phone number
is still the ordinary case this platform's collections use. A field
holding an email address is a TEXT field the client marked personal,
which is what the retention rule actually keys on.
FieldType
What one declared field holds.
Five, and each is a parse rule rather than a column type. The values live
in JSONB and the database enforces nothing about them, so what a type means
is service's to keep — which is where it had to live anyway: "a 0 is not a
no" is a product rule, not a column.
Still deliberately no email and no phone, even now that DATOS-02's
default country is ruled (the organization's own — docs/features/ authentication.md). A PHONE type would impose a parse obligation on the
whole column — one bad number and the record stops being writable — where
what #378 needed was a normaliser on the identity, which changes only how
two rows compare. So an identity value is canonicalised by shape, not by
a declared type: service._canonical_identity reads the value itself,
and if it parses as a phone number under the organization's default
country it is stored E.164 (shared.phone.to_e164, ADR 0023); otherwise
strip() + casefold(), as before. A TEXT field holding a phone number
is still the ordinary case this platform's collections use. A field
holding an email address is a TEXT field the client marked personal,
which is what the retention rule actually keys on.
FieldUpdate
Filter
columnopEvery question that may be asked of a column. A closed set: a word that is not here cannot be asked anywhere, and a word that is here is asked with the same meaning wherever the column's owner admits it.
IS_EMPTY / IS_NOT_EMPTY are the pair with no value, and they are the
reason this set is not merely the comparison operators. A state that exists
as an absence — a cell never filled in, one emptied by the retention window,
a column the last file said nothing about — is unaskable as an equality, and
a client who cannot ask "which of these has no phone number" cannot find the
work they have to do. Legacy asked the same question as isnull on its own
engine (src/lib/dashboard/widgetConfig.ts), and for the same reason.
CONTAINS is LIKE renamed, not a new word. It was spelled like while
nothing accepted it; contains is what it means to the person choosing it,
and the SQL it becomes is the owner's business rather than the name's.
valuegeneratedFilterKind
What a filterable column holds, in the only terms a filter cares about.
Coarser than any one owner's own type vocabulary on purpose: it exists so a
control can be chosen for a column, and a control is chosen by "is this
picked from a list, typed as text, compared as a number, or picked from a
calendar". custom_data.FieldType maps onto it one-to-one today; an
analytics dataset column maps onto it from whatever it actually is —
an enum column, a uuid a client picks by name, a timestamp.
FilterOp
Every question that may be asked of a column. A closed set: a word that is not here cannot be asked anywhere, and a word that is here is asked with the same meaning wherever the column's owner admits it.
IS_EMPTY / IS_NOT_EMPTY are the pair with no value, and they are the
reason this set is not merely the comparison operators. A state that exists
as an absence — a cell never filled in, one emptied by the retention window,
a column the last file said nothing about — is unaskable as an equality, and
a client who cannot ask "which of these has no phone number" cannot find the
work they have to do. Legacy asked the same question as isnull on its own
engine (src/lib/dashboard/widgetConfig.ts), and for the same reason.
CONTAINS is LIKE renamed, not a new word. It was spelled like while
nothing accepted it; contains is what it means to the person choosing it,
and the SQL it becomes is the owner's business rather than the name's.
FindingActionOut
kindThe one act a finding offers (rule 28).
offeredFindingKind
The kinds of finding rule 25 names; their labels are the interface's.
FindingMomentOut
interaction_idFindingOut
A finding's one action, shown first as what will happen (rule 28).
agent_idconversationsexpires_atidkindThe kinds of finding rule 25 names; their labels are the interface's.
listedraised_atsentenceFindingsOut
agent_idfreelookingreadyFlagsUpdate
flagsFlowEdgeOut
idsource_step_idtarget_step_idFlowEntryStepIn
step_idFlowGraphOut
idnameorg_idFlowStepStatus
What became of one step a test reached. REUSED is an output the flow's
latest test recorded, handed on without running the step (rule 22) -- never
reported as run.
FlowStepTrace
orderstatusWhat became of one step a test reached. REUSED is an output the flow's
latest test recorded, handed on without running the step (rule 22) -- never
reported as run.
step_idtool_idtool_namedetailduration_msfingerprintstaletruncatedFlowTestIn
FlowTestOut
created_atduration_msflow_ididoutcomeHow a test ended (rule 20). STOPPED is the person's choice, not a failure
(rule 21); DID_NOT_TERMINATE is the graph passing the step bound, not a step
failing (rule 10).
inputs_inheritedFlowTestOutcome
How a test ended (rule 20). STOPPED is the person's choice, not a failure
(rule 21); DID_NOT_TERMINATE is the graph passing the step bound, not a step
failing (rule 10).
ForwardHealth
A forwarded address's health is its forward (rule 54): waiting for the
forward, then working once it is proved, whatever its domain's state, which
is DomainState and shown beside it -- or broken, when the connection is
marked so.
ForwardOrigin
Which of the two places forwarded a given call — "Two guarantees, and they are not the same size" (docs/features/trunks.md). Kept apart because they carry different evidentiary weight: one is first-hand, the other is hearsay we may never receive.
ForwardOutcome
What became of a call the trunk could not put through to its agent. A
trunk with a fallback number FORWARDED it there; one with none left it
UNSERVED -- it reached nobody (trunks.md rule 34, #361). Its own column and
not an inference from forwarded_to IS NULL, which a carrier-edge record
may also carry.
ForwardRecordOut
created_atidoriginWhich of the two places forwarded a given call — "Two guarantees, and they are not the same size" (docs/features/trunks.md). Kept apart because they carry different evidentiary weight: one is first-hand, the other is hearsay we may never receive.
reasontrunk_idoutcomeWhat became of a call the trunk could not put through to its agent. A
trunk with a fallback number FORWARDED it there; one with none left it
UNSERVED -- it reached nobody (trunks.md rule 34, #361). Its own column and
not an inference from forwarded_to IS NULL, which a carrier-edge record
may also carry.
ForwardedAddressIn
addressForwardedAddressOut
account_labeladdressconnection_iddomaindomain_stateWhat the platform's mail service last said of the address's domain.
unchecked until anybody asked; not_registered while the mail service
holds no sending domain for it -- every client domain today, with no
records to show, until per-client sending domains land (#1555);
unverified while the records the client must add are not all found;
verified once they are. Shown, and holding nothing back: no state lets or
stops a send, since nothing leaves as the address -- answers leave from the
address the platform gave the connection (rules 53-54, email-threads.md
rule 28).
forwarded_tohealthA forwarded address's health is its forward (rule 54): waiting for the
forward, then working once it is proved, whatever its domain's state, which
is DomainState and shown beside it -- or broken, when the connection is
marked so.
on_disconnectGoogleCapability
What a person can let this platform do with their Google account, named the way they would say it rather than the way Google spells it.
The gallery offers these and the person picks; what is then asked of Google is exactly the scopes behind what was picked (connections.md, "Choosing what it may do"). The provider's own vocabulary stays inside this module: a dashboard that had to name a scope URL would be a dashboard deciding what a permission means.
They come in seeing/changing pairs, one pair per Google product, because that is the line a person actually draws: a client who wants a sheet read is not thereby a client who wants it written into (spreadsheets.md, "What the client allowed"). A product whose provider offers no narrower read is a pair whose halves map to the same scope, and that is a fact to state on the entry rather than a pair to collapse -- the words are what the person consents to.
A product with nothing that writes is a single entry, not a pair with an
empty half (contacts.md, "Nothing here writes"). see_contacts has no
manage_contacts beside it because this platform builds no tool that changes
an address book: a capability a client can tick that backs no tool is a
permission asked for nothing, on the gallery and at Google's review alike.
The day such a tool exists, the capability is added with it.
And a product may be three entries rather than a pair, when the provider
draws its line somewhere other than between seeing and changing
(files.md, "What the client allowed"). Drive's are manage_own_files — the
files this platform itself created, both read and written, and the only
non-sensitive one Google publishes — plus see_all_files and
manage_all_files over the whole drive. manage_own_files is deliberately
not half of a pair: within the app's own files it is both halves at once,
and a see_own_files beside it would back nothing a client could not already
do. Which of these the connection holds decides not only which file tools
work but how much of the drive they can see, which is the first time a
capability has done that and is why the spec says so out loud.
And a product's three may not be a ladder. Mail's are send_mail,
see_mail and manage_mail (mail.md, "What the client allowed"), and the
first is beside the other two rather than under them: it puts a message into
the world and reads, searches and lists nothing, so a client can grant a
sending agent without opening their correspondence at all. That is Google's
own line -- gmail.send is the only Gmail scope it classifies as sensitive
rather than restricted -- and it is the one shape in this enum where the
narrowest entry is also the one that acts most consequentially.
GoogleProduct
Which of Google's products an account is authorized for.
A connection is an account authorized for a product, not an account
(connections.md, "One account and two products are two connections",
2026-09-23). This is that product, and it is named by the domain rather than
by Google's brand for it -- the same words the slices that consume it carry,
mail and not Gmail -- because the brand is a word on a screen and this is
what the grant is about. Which brand name a person reads is the dashboard's
to say, the way the capabilities' own wording already is.
There is no google_product column behind this and there must not be: a
connection is a provider, an account, a health state and one opaque secret,
and a field shaped by one provider is what connections.md refuses by name. So
a row's product is read from its grant -- google_products_for -- which
also makes it impossible for the two to disagree.
GrantedVia
Which door the grant arrived by.
The spec's "a grant can arrive by more than one door, and the connection is the same whichever it came through": this is the record of the door, and nothing in this slice or above it branches on it.
GraphEdgeSpec
sourcetargetconditionGraphNodeSpec
keyGraphReplace
GraphReplaced
A whole graph: its nodes, its edges, and which node is the root.
HandToAgentIn
agent_idtextdeliverHandedOverOut
agent_iddeliveredinteraction_idsaidheldkeptHandoffAct
What a PersonHandoff event records -- the five acts interactions.md rule 45
names, and the four of a reply prepared for review (handoff.md rules 32, 37
and 41). A close from the inbox is not one: it is an ending, and the record
already has one of those (finish).
A native PostgreSQL enum (interaction_handoff_act).
HandoffReasonCode
Why the platform itself left a thread waiting, in a word the reader's
language can say (handoff.md rules 2, 4): beside a hand-over's reason, which
is English, names the conversation's facts and is purged with its content.
The code names nobody and survives the purge.
A native PostgreSQL enum (interaction_handoff_reason_code); None on a
hand-over the built-in or a person made, whose reason is their own words.
HeaderCollision
headerskeyHoursChange
movesHow many waiting calls that keep to the calling hours -- a timed retry, or a callback the client asked to keep to them -- these hours would not hold. Saving the hours moves each to the first later day whose hours hold the same local time, or, when no day's hours hold it, to the next opening.
stays_outsideHow many waiting calls set for a time a person, an integration or a caller named these hours would not hold. Saving the hours leaves each at its time: this is a warning, not a move.
HoursIn
HoursSlot
ends_atstarts_atweekdayHungUpBy
Which side ended a call's line (interactions.md rule 16, #690).
The side that left first (rule 54). CONTACT is the other party leaving on
their own -- a remote BYE, the room's client_initiated -- and also when the
agent's own hang-up ran after they had already gone (#1818). PLATFORM is
this side putting the line down: an ending the channel decided, the agent's
own hang-up while the contact was still there, or the room removing the
participant. LINE is neither: the telephony bridge closed the leg because
its audio stopped, a drop the room reports as it reports a hang-up (#1817).
MACHINE is CONTACT's case where what answered was a machine -- a
voicemail or a menu (AnsweredBy) that left first, during the machine
ending: a machine is not a contact, so a count of contacts who hung up
must not include it (owner ruling 2026-10-26, #1974).
Text on the row, like end_reason, so the room growing a reason -- or this
growing a value -- never needs a migration.
ImportPlanOut
createsrejectedtotalupdatesImportProposalOut
totalImportResult
addedImportRunOut
acceptedcollection_idcreated_atidrejectedstatustotalInboxItemOut
audienceevent_codeidoccurred_atInboxViewUpdate
IngestionStatus
Where a document is between "uploaded" and "searchable".
failed is a state, not an exception (DECISIONS.md D7): a PDF that will
not parse stays in the library with a reason the client can read, because
they uploaded a file and a file that vanishes is worse than one that says
what went wrong.
InstallIn
InstallState
Where an installed recipe stands (rules 8, 16, 18).
InstalledRecipeOut
enabledhanded_overidinstalled_atkeymissingnamesetupstateWhere an installed recipe stands (rules 8, 16, 18).
versionunseenworst_stateInteractionCount
countInteractionEventOut
idoccurred_atseqtypeEverything a conversation can record about the path it walked.
colleagueflow_actheldoverlappedprepared_changedreturned_to_flowunvoicedInteractionOut
channelVoice and text are two channels over one graph, not two kinds of record.
directionidis_openorg_idThe settings a conversation actually ran with, copied in as values.
An AI profile is not frozen into an agent's published version, so the version
alone cannot say how a conversation sounded. The profile id is kept for
provenance; the provider, model and voice are kept because they are the answer.
used_standby records that a standby served the conversation instead of some
component's first choice — the same fields still describe what was actually
served — and standby_component names which one switched.
Source attribution, as recorded. kind says which of the rest is meaningful;
the others stay unset.
connection_id and address are CHANNEL's pair, and they are two fields for
the reason the thread itself is identified by two: "Threads are per connection;
contacts are per organization" (channels.md). The connection says which of the
organization's accounts the message arrived on — "reply on this thread has to
know which account it leaves by" — and the address says who wrote, in whatever
shape the provider's own namespace has: a phone number in E.164 on WhatsApp, an
opaque user id elsewhere, "decided when each is built".
address is personal data and connection_id is not. The address is the
contact — "on a channel, the contact is their address" — so the retention purge
clears it exactly as it clears number, which is the same datum arriving by a
different road; the connection names one of the organization's own accounts and
survives, like trunk_id beside it.
provider is CHANNEL's third, and it is stored as a value rather than read
off the connection. interactions.md: "a conversation that arrived on a channel
names the provider it arrived on, beside the contact's address", because an
address alone does not say whether the person was reached on WhatsApp, by mail or
somewhere else. It is written by the channel at open, from the provider it is
already speaking to, for the reason the run settings beside it are values too: a
connection is what it is now, and this answers what the conversation arrived on
then. It also keeps connection_id the opaque reference it is — nothing here
dereferences a connection to render a row. Not personal data: it names a kind of
account of the organization's own and nobody outside it, so it survives the purge
exactly as connection_id does.
started_atchannel_keycontact_spokedraft_testlabel_idsrangunrepliedInteractionTraceOut
One conversation, as the log shows it.
InteractionTranscriptOut
One conversation, as the log shows it.
InvitationCreate
emailroleA person's role inside one organization. Per-membership, not per-person.
InvitationOut
emailexpiredexpires_atflagsidorg_idroleA person's role inside one organization. Per-membership, not per-person.
sent_atKnewNoteOut
contentscopeWhich layer a memory lives in. The retrieval side pulls the relevant layer(s) into context; the write side must respect the PII boundary.
LLMSettingsIn
modelnameprovidertemperatureLLMSettingsOut
idmodelnameprovidertemperatureLabelChoice
descriptionLabelIn
namedescriptionLabelOut
descriptionidnameconversation_countLabelUpdate
LabelsChange
addremoveLanguage
A language the platform can speak — which is to say, one it has default phrases written in. The agent states one of these and every phrase on it is written in that one; a language with no phrases behind it would be a label.
LastLine
atspeakerWho said something. Three, and the third only happens on a channel -- and,
on an email thread, a fourth: a reply sent from the mailbox itself (MAILBOX).
OPERATOR is "a person answering through the platform, recorded as a third
speaker" (interactions.md) — the hand-over to a human that channels.md
describes, "reply on this thread, the same built-in, from outside a flow".
It is a speaker and not a flag on AGENT because the transcript has to say
who the words belong to: an agent shown a person's reply as its own would read
its history as things it had said and had not.
What a branch of the client's flow sent is not a fourth member. A template
a branch fired is "recorded on the thread as the flow's act, as a tool call
is, not as something an agent said" — and a tool call is a ToolCall, which
this vocabulary already has. A speaker for it would claim somebody spoke.
A native PostgreSQL enum (interaction_speaker), so a member added here ships
an ALTER TYPE ... ADD VALUE migration — b7e2c4d9a1f3's precedent.
textLayout
How an entry lays out the list and a conversation (inbox.md rule 37): side by side, or one at a time across the whole width.
LegalDocument
The six legal documents the platform keeps (rule 1). Each value is also the last segment of the document's address, so it is spelled as a URL is.
LegalDocumentOut
addressdocumentThe six legal documents the platform keeps (rule 1). Each value is also the last segment of the document's address, so it is spelled as a URL is.
LegalLanguage
The languages a version may be written in (values table): Spanish required, English optional, and the Spanish prevails (rule 4).
LegalRecordOut
idkindWhat one entry of the proof records.
originWhere an entry came from (rule 26): the dashboard or a key -- and, for a mark,
staff from the directory (rule 23). before_legal is an agreement kept before
this proof existed, carried over with what was kept then.
person_namerecorded_atLineAbout
Whom a line asks the agent to notice something about (rule 19): the person in each conversation, or the business. It decides where the note is filed under rule 2 -- a line about a person produces notes about that person, one about the business produces business notes.
LineKind
Why two cards are connected, in the client's words (rule 18).
LineType
What a line's number is (trunks.md rule 7): a phone number, normalised to
E.164, or an extension of the client's phone system, held as text exactly as the
system names it and never read as a number.
LiveProvider
Who runs a live (speech-to-speech) model. ai-profiles.md, "The providers are an enumerated list, and the list grows by adding to it": Google first, OpenAI next (R11) -- a further provider is one more member here and the classes its models were measured into, never a new kind of component.
LiveSettingsIn
languagemodelnameproviderWho runs a live (speech-to-speech) model. ai-profiles.md, "The providers are an enumerated list, and the list grows by adding to it": Google first, OpenAI next (R11) -- a further provider is one more member here and the classes its models were measured into, never a new kind of component.
voiceLiveSettingsOut
idlanguagemodelnameproviderWho runs a live (speech-to-speech) model. ai-profiles.md, "The providers are an enumerated list, and the list grows by adding to it": Google first, OpenAI next (R11) -- a further provider is one more member here and the classes its models were measured into, never a new kind of component.
voiceMailFolderOut
idnamepathMailFoldersOut
MailLineOut
copied_tomessage_idreceived_atMailServerDiscoveryOut
addresssourceWhere the proposed servers were found (rule 6): the domain's own autoconfig, Thunderbird's database of providers (by the domain, or by the provider its mail goes to), the domain's SRV records, or nowhere -- the member enters them.
usernameMailServerEndpoint
hostportsecurityHow the connection to a server is encrypted (rule 2): tls from the first
byte (993, 465), or starttls on a plain port that must upgrade before the
password is sent (143, 587, 2525). There is no plain value: a server that
offers no encryption is refused (rule 7).
MailServerFault
Why a test refused it or a use broke it (rules 7, 10, 13), for the screen
to say in the member's language; fault_host names the server where one is
meant. platform_cannot_open is the platform's own fault and never the
server's (rule 13): the password is asked for again.
MailServerHealth
What the connection shows (rules 10, 13, 17): being tested, working,
refused by its last test (the values kept for the member to correct), or
broken in use. Only working reads or sends.
MailServerIn
MailServerOut
account_labelacts_in_mailboxaddressThe folder the mail a thread answered is filed in (email-threads.md rule
36): id as MailFolderOut gives it and its name, both None while
nothing is filed.
connection_idhealthWhat the connection shows (rules 10, 13, 17): being tested, working,
refused by its last test (the values kept for the member to correct), or
broken in use. Only working reads or sends.
mark_answered_readon_disconnectThe folder a mailbox is read in (email-threads.md rule 35): id and name
of the one chosen instead of the inbox, both None while the inbox is read;
since when the choice last changed -- what reached the folder before it is
never read -- None while it never did.
One server: its name, its port and how the connection to it is encrypted
(rule 2). Checked by the service, not here (MailServerIn's reason): the
name is a host name or an IP literal (mail_host), the port one of the
road's (READING_PORTS, SENDING_PORTS).
One server: its name, its port and how the connection to it is encrypted
(rule 2). Checked by the service, not here (MailServerIn's reason): the
name is a host name or an IP literal (mail_host), the port one of the
road's (READING_PORTS, SENDING_PORTS).
usernameMailServerPasswordIn
MailServerSecurity
How the connection to a server is encrypted (rule 2): tls from the first
byte (993, 465), or starttls on a plain port that must upgrade before the
password is sent (143, 587, 2525). There is no plain value: a server that
offers no encryption is refused (rule 7).
MailServerServersIn
MailServerSource
Where the proposed servers were found (rule 6): the domain's own autoconfig, Thunderbird's database of providers (by the domain, or by the provider its mail goes to), the domain's SRV records, or nowhere -- the member enters them.
MailServerUpdate
acts_in_mailboxmark_answered_readMailboxAuthorizeIn
addressMailboxAuthorizeOut
admin_approval_urladmin_noteurlMailboxDraftState
Where a thread's prepared reply stands in the mailbox's drafts, on a
signed-in mailbox whose client opted in (mail-handling.md rules 12-18):
mailbox, it waits there too, as the platform wrote it; taken_over, the
draft was changed or removed in the mail app, so it is the person's and the
platform no longer follows it (rule 13). A prepared reply with neither waits
in the platform alone. writing and removing are the draft's own row while
the platform asks the mailbox to write or remove it -- recorded before it
asks, so an answer that never came is known on the next run -- and never
shown on a prepared reply.
A native PostgreSQL enum (channel_mailbox_draft_state).
MailboxHealth
A signed-in mailbox works from the moment of consent and breaks only on the provider's word (rule 64).
MailboxOut
account_labelacts_in_mailboxaddressThe folder the mail a thread answered is filed in (email-threads.md rule
36): id as MailFolderOut gives it and its name, both None while
nothing is filed.
connection_idconsented_athealthA signed-in mailbox works from the moment of consent and breaks only on the provider's word (rule 64).
mark_answered_readon_disconnecton_opt_outThe folder a mailbox is read in (email-threads.md rule 35): id and name
of the one chosen instead of the inbox, both None while the inbox is read;
since when the choice last changed -- what reached the folder before it is
never read -- None while it never did.
scopesworking_sincewrite_consentMicrosoft's second consent, to change mail (connections.md rule 60): never asked, given, or refused by Microsoft since -- which stops the acting and nothing else (email-threads.md rule 37).
mirrors_draftsMailboxUpdate
acts_in_mailboxmark_answered_readmirrors_draftsMapFrameOut
alonehiddenkeyA frame left to right (rules 12, 13), by key. inbound names outer cards;
during starts with the frame's own card -- the agent, or the automation.
readswritesdraftMapGroupOut
A part's owner: an installed recipe (id the install's, key and name the
recipe's), the organization's own, or the platform's (a capability).
partsMapLanesOut
afterbeforeduringinboundMapLineOut
kindWhy two cards are connected, in the client's words (rule 18).
sourcetargetMapOwner
kindWho a part belongs to, as the organization's inventory marks it (rule 25).
MapPartOut
keykindWhat a card is. The first six sit on the outer level (rule 4: agents, automations no conversation sets off, what brings conversations, collections, an agent's documents); the rest only inside an open frame. Each opens its own ordinary screen (rule 23).
A part's owner: an installed recipe (id the install's, key and name the
recipe's), the organization's own, or the platform's (a capability).
missingnever_runnodeson_every_nodeopenablereaches_no_agentruns_flow_as_stepseen_byMapStepToolOut
idmissingMcpImportIn
server_urlMeasure
aggHow a measure reduces the matching rows to one number per group. Owned
here because analytics is the one caller that has to speak it identically
over three different slices' data (interactions, billable_metrics,
custom_data) — none of which may import this module to borrow it, so each
one's own aggregate surface takes it as a plain string instead.
MediaKind
What one outbound attachment is (channels.md rule 76): an image, a video,
an audio file or a document. Not channels' AttachmentKind, which names
what arrived -- a sticker, a contact card, a location among them -- and
which no road is asked to send.
MemberRemoved
MembershipOut
What a member reads. everything is an owner or an admin, who read every
conversation and whose access is never set (rule 35); default and
channels are then every conversation, and nothing set on its own.
effective_flagsemailflagsgrantable_flagsidis_activenameorg_idperson_idroleA person's role inside one organization. Per-membership, not per-person.
MemoryBoundsOut
max_line_lengthmax_linesremember_always_maxreread_per_scopeMemoryLineIn
aboutWhom a line asks the agent to notice something about (rule 19): the person in each conversation, or the business. It decides where the note is filed under rule 2 -- a line about a person produces notes about that person, one about the business produces business notes.
wordsMemoryLineOut
aboutWhom a line asks the agent to notice something about (rule 19): the person in each conversation, or the business. It decides where the note is filed under rule 2 -- a line about a person produces notes about that person, one about the business produces business notes.
agent_idcreated_atidnotesupdated_atwordsMemoryLinePatch
MemoryLinesOut
The notebook's bounds as the interface prints them (rule 27): lines, a line's length, the notes reread from each layer and the business notes that may be marked "remember always". The same numbers recall and the writes enforce.
MemoryOut
agent_idcontentcreated_atidlooks_personalorg_idpinnedredactedscopeWhich layer a memory lives in. The retrieval side pulls the relevant layer(s) into context; the write side must respect the PII boundary.
support_countupdated_atMemoryPatch
MemoryPersonNotesOut
MemoryPersonOut
contact_keylast_learned_atnotesredactedMemoryRefusal
Which refusal a 409 from the lines or from marking a note is, as a word the screen can name rather than a sentence to parse.
MemoryRefusalOut
detailrefusalWhich refusal a 409 from the lines or from marking a note is, as a word the screen can name rather than a sentence to parse.
MemorySwitchIn
onMemorySwitchOut
agent_idonMessageMark
The mark a road gives an arriving message (channels.md rule 88): written by a machine, unwanted by the provider's own verdict, or failing the proof of who sent it that the provider checked (owner ruling 2026-10-16, #1550). A burst holding only marked messages runs no flow and no agent turn.
MetricCategory
The five families every metric belongs to, and the whole of the list.
docs/features/billable-metrics.md fixes them there and says why they are
not staff-editable: "a category is a sentence about what the platform sells,
and the list of those sentences is this document's to hold". They are the top
level of every consumption picture and of every price list, so a sixth is a
decision written into the spec before it is a row anywhere. Ruled by the
owner (2026-09-15).
STORAGE was added 2026-09-18 (ADR 0040) for what it costs to keep something,
as distinct from something that happened: a gigabyte held for a month costs
the same whether it came from a call recording, a RAG document or an upload.
MetricOut
activecategoryThe five families every metric belongs to, and the whole of the list.
docs/features/billable-metrics.md fixes them there and says why they are
not staff-editable: "a category is a sentence about what the platform sells,
and the list of those sentences is this document's to hold". They are the top
level of every consumption picture and of every price list, so a sixth is a
decision written into the spec before it is a row anywhere. Ruled by the
owner (2026-09-15).
STORAGE was added 2026-09-18 (ADR 0040) for what it costs to keep something,
as distinct from something that happened: a gigabyte held for a month costs
the same whether it came from a call recording, a RAG document or an upload.
codecreated_atidnameupdated_atModeUpdate
modeWhether one conversation is still asking before consequential actions.
architect.md rule 20: "a person may tell one conversation to stop asking…
It is a property of that conversation only — not of the person or the
organization — and asking is always where a conversation starts."
It is a column vocabulary here and an input to the run-loop there, which
is the same two-names split Channel has and for the same reason: this one is
what the record says a conversation was set to, and that one is what decides
whether the loop stops. Member for member, so a writer maps with
interactions.ConfirmationMode(mode) and needs no table.
ModelsByKind
Moment
MomentLine
atseqspeakertextNoRunRule
Why a burst ran no flow, by the platform's own rules (mail-handling.md rule
8): the reason its decision record carries. not_working is a connection not
proved or broken (connections.md rule 55) and not_a_member a sender nobody
vouches for on a members connection (rule 52), beside rule 8's six.
NodeOut
idinstructionslabelorigin_idWhere a node sits in the visual editor.
tool_idsNodeUpdate
NotAnswered
Why a question is not being answered now (rules 39, 41). Each question says so on its own, so a list read while reading is off says it of every one.
ObjectionIn
languageThe languages a version may be written in (values table): Spanish required, English optional, and the Spanish prevails (rule 4).
reasonsubprocessorsversionObjectionPeriodOut
ends_atversionOpenCountOut
countOpenRequest
agent_idmodeWhether one conversation is still asking before consequential actions.
architect.md rule 20: "a person may tell one conversation to stop asking…
It is a property of that conversation only — not of the person or the
organization — and asking is always where a conversation starts."
It is a column vocabulary here and an input to the run-loop there, which
is the same two-names split Channel has and for the same reason: this one is
what the record says a conversation was set to, and that one is what decides
whether the loop stops. Member for member, so a writer maps with
interactions.ConfirmationMode(mode) and needs no table.
OpenedConversation
agent_idgreetinginteraction_idmodeWhether one conversation is still asking before consequential actions.
architect.md rule 20: "a person may tell one conversation to stop asking…
It is a property of that conversation only — not of the person or the
organization — and asking is always where a conversation starts."
It is a column vocabulary here and an input to the run-loop there, which
is the same two-names split Channel has and for the same reason: this one is
what the record says a conversation was set to, and that one is what decides
whether the loop stops. Member for member, so a writer maps with
interactions.ConfirmationMode(mode) and needs no table.
OrgMapOut
alonehiddenread_atOrganizationAgreementIn
languageThe languages a version may be written in (values table): Spanish required, English optional, and the Spanish prevails (rule 4).
tickedversionOrganizationAskOut
heldmay_agreeversionOrganizationOut
default_countryentitled_flagsgrantable_flagsidis_activelanguagenametimezoneOrganizationUpdate
Origin
Where an entry came from (rule 26): the dashboard or a key -- and, for a mark,
staff from the directory (rule 23). before_legal is an agreement kept before
this proof existed, carried over with what was kept then.
OurSide
source_addressestransportsOutboundIn
Owner
idkindOwnerKind
Who a part belongs to, as the organization's inventory marks it (rule 25).
Parameter
kindrecipes.md rule 2: what a parameter's value is.
labelname^[a-z][a-z0-9_]*$ · requireddefaultdescriptionrequiredParameterKind
recipes.md rule 2: what a parameter's value is.
ParametersIn
PartKind
What a card is. The first six sit on the outer level (rule 4: agents, automations no conversation sets off, what brings conversations, collections, an agent's documents); the rest only inside an open frame. Each opens its own ordinary screen (rule 23).
PartOut
idkeykindwhereeditablepositionPartState
Rule 29's four states. Declared worst first: the worst of several is the first of these (rule 31).
PauseOrigin
Who stopped a campaign: a person, the provider's breaker, or the cap on failures of our own.
OUR_FAILURE is docs/features/outbound-calls.md rule 31: a contact returned
for failures of ours OUR_FAILURE_RETURN_CAP times, or this deployment unable
to reach the carrier run after run. It is kept apart from PROVIDER because
the two are fixed by different people -- the provider's account is the
client's to settle at the carrier, and ours is ours.
PersonAgreementIn
languageThe languages a version may be written in (values table): Spanish required, English optional, and the Spanish prevails (rule 4).
privacytermstickedPersonAskOut
blockingdocumentThe six legal documents the platform keeps (rule 1). Each value is also the last segment of the document's address, so it is spelled as a URL is.
versionPersonGrantIn
grant_secretPersonGrantOut
created_atdisplay_namegranted_viaWhich door the grant arrived by.
The spec's "a grant can arrive by more than one door, and the connection is the same whichever it came through": this is the record of the door, and nothing in this slice or above it branches on it.
roster_entry_idstatusWhether the grant is currently usable.
updated_atPersonState
Where a thread is when it is not with its flow (handoff.md rule 1).
Two members and no third: "with a person, it is either waiting -- nobody has
taken it -- or held by exactly one person". With its flow is the absence of
either -- None on the record -- because every thread starts there and is
there whenever it is not with a person; a member for it would be a value every
row but a few carries to say nothing happened. One enum rather than two
columns, so "waiting and held at once" cannot be written.
A native PostgreSQL enum (interaction_person_state).
PickupDecider
Who decided who picked up: the classifier's verdict, or the platform without one (interactions.md rule 4). The platform decides when nobody spoke (a person), when the ceiling produced no verdict (a voicemail), and when the classifier was asked and reached nothing (a person).
PickupDecision
decided_byWho decided who picked up: the classifier's verdict, or the platform without one (interactions.md rule 4). The platform decides when nobody spoke (a person), when the ceiling produced no verdict (a voicemail), and when the classifier was asked and reached nothing (a person).
duration_mspathBy which path who picked up an outbound call was decided (voice-agent.md, "Who picked up", rule 61): "the other side spoke, nobody spoke, the ceiling". The values are the voice channel's own labels for the three, so the record, the metric and the log line say the same word.
PickupPath
By which path who picked up an outbound call was decided (voice-agent.md, "Who picked up", rule 61): "the other side spoke, nobody spoke, the ceiling". The values are the voice channel's own labels for the three, so the record, the metric and the log line say the same word.
PlacementOut
agent_idenabledidWhat another feature says when it owns a row: what kind of thing it is, and which one.
A value and not two loose columns, because the pair is meaningless apart:
an id with no kind names nothing, and a kind with no id names everything.
Both halves are opaque to this slice on purpose — tools is tier 0 and the
things that own rows sit above it, so resolving "campaign 3f2a" into a name
a person reads is the screen's job and never this one's. What is asked of it
here is only ever equality: is the caller the owner.
slotThe lifecycle slot a tool is placed in. Which tool is attached to which
slot belongs to agents; whether a tool can be in a slot belongs here,
because it is a fact about the tool (service.available_in_slot).
tool_idPreferencesOut
collapsedhidden_entriesPreferencesUpdate
PreparedOutdatedBy
What turned a prepared reply outdated (handoff.md rule 33): the
counterpart writing again unmarked, or a reply sent from the mailbox itself
(email-threads.md rule 33). Set only while the reply is outdated; None
otherwise.
A native PostgreSQL enum (interaction_prepared_outdated_by).
PreparedReplyOut
handed_overidinteraction_idprepared_atstateWhere a conversation's prepared reply stands (handoff.md rule 33): ready
to send as it is, or outdated because the counterpart wrote again unmarked
after the message it answers, or a reply was sent from the mailbox itself
(PreparedOutdatedBy says which) -- sent then only with the text a person gives
(rule 34). It never expires. None on the record when it holds none.
A native PostgreSQL enum (interaction_prepared_state).
burstPreparedSendIn
PreparedSentOut
changedinteraction_idreturned_to_flowheldkeptPreparedState
Where a conversation's prepared reply stands (handoff.md rule 33): ready
to send as it is, or outdated because the counterpart wrote again unmarked
after the message it answers, or a reply was sent from the mailbox itself
(PreparedOutdatedBy says which) -- sent then only with the text a person gives
(rule 34). It never expires. None on the record when it holds none.
A native PostgreSQL enum (interaction_prepared_state).
ProfileCopy
nameProfileIn
nameProfileOut
descriptionidkindnameProfileUpdate
ProposedColumn
headerkeylabeltypeWhat one declared field holds.
Five, and each is a parse rule rather than a column type. The values live
in JSONB and the database enforces nothing about them, so what a type means
is service's to keep — which is where it had to live anyway: "a 0 is not a
no" is a product rule, not a column.
Still deliberately no email and no phone, even now that DATOS-02's
default country is ruled (the organization's own — docs/features/ authentication.md). A PHONE type would impose a parse obligation on the
whole column — one bad number and the record stops being writable — where
what #378 needed was a normaliser on the identity, which changes only how
two rows compare. So an identity value is canonicalised by shape, not by
a declared type: service._canonical_identity reads the value itself,
and if it parses as a phone number under the organization's default
country it is stored E.164 (shared.phone.to_e164, ADR 0023); otherwise
strip() + casefold(), as before. A TEXT field holding a phone number
is still the ordinary case this platform's collections use. A field
holding an email address is a TEXT field the client marked personal,
which is what the retention rule actually keys on.
matches_existingoptionspersonalsampleProposedLabel
key^[a-z][a-z0-9_]*$ · requirednamedescriptionjoinsProposedLabels
parameterProviderOut
per_personproviderProviderPresence
staleProvidersByKind
PublishRefusalOut
detailreasonsQuestionChartOut
agent_idconversationskindThe kind of answer a question takes (rule 35), which is also what its chart is (rule 42): nobody configures one.
question_idshort_namesincestatetimezoneuntilQuestionKind
The kind of answer a question takes (rule 35), which is also what its chart is (rule 42): nobody configures one.
QuestionMatch
QuestionProposalOut
from_verdictskindThe kind of answer a question takes (rule 35), which is also what its chart is (rule 42): nobody configures one.
meaningsearch_idshort_nameoptionsQuestionRefusal
Why a question cannot be added now, or its count cannot be confirmed (rules 44, 59): a word the screen can name, to invite or to explain.
QuestionRefusalOut
detailrefusalWhy a question cannot be added now, or its count cannot be confirmed (rules 44, 59): a word the screen can name, to invite or to explain.
QuestionSearchIn
sentenceQuestionSearchOut
foundidsentenceverdicts_askedRailEntry
The inbox's filters (inbox.md rule 17), by the names its address gives
them. A name the inbox does not recognise is refused, never dropped
(extra="forbid"); every one is optional, and an unset one is absent.
The values are the address's: holder, channelKey, agentId,
endReason, counterparty, answeredBy, label and preparedReply hold
several values joined by commas; since/until are calendar days in the reader's own zone; the
either-way filters are yes or no; unreplied is on or absent. An
answer sends an unset filter as null, which the address reads as absent;
what is stored leaves it out.
keykindWhat a rail entry is (rules 12-13).
layoutHow an entry lays out the list and a conversation (inbox.md rule 37): side by side, or one at a time across the whole width.
RailEntryKind
What a rail entry is (rules 12-13).
RailOut
can_sharemax_viewsA person's inbox preferences (rules 15 and 39), kept with their
membership. collapsed is whether the rail opens collapsed;
hidden_entries the keys of the predefined views they hid.
shownReachMode
Who reaches agents on a connection (rule 90), on a road that declares it
-- the three email roads (email-threads.md rules 21, 34): everyone, the
default; only the senders its entries list; or everyone except them. A
sender it keeps for people runs no flow and waits for a person.
Reachability
Whether the phone network can reach a trunk, as far as the reconciler knows
-- four readings, not two (docs/features/trunks.md: "a trunk nobody has
checked is not shown as reachable either"; #513). UNREACHABLE is a fact a
run found; REACHABLE is a fact a recent run found; UNKNOWN is the
absence of a recent run, which is not health; INCOMPLETE is the one reading
that is not about a run at all -- the trunk is missing what its carrier
authenticates by, and no check could make it work (#110). A StrEnum and not
a bare Literal for the reason FallbackCondition is one: a client switches
on it, and the contract renders an enum's members where an alias renders a
name.
ReadFolderOut
ReaderCeilings
document_bytesdocument_charsimage_bytesimage_side_pximages_per_burstquestions_per_turnscan_pagessheet_rowsReaderLimits
ReadingRefusal
Why reading cannot be turned on (analyst.md rule 12). One today: an organization whose window is zero keeps nothing of a conversation to quote.
ReadingRefusalOut
detailrefusalWhy reading cannot be turned on (analyst.md rule 12). One today: an organization whose window is zero keeps nothing of a conversation to quote.
ReadingSwitchOut
agent_idWhat the Analyst's work for this agent cost since since (rule 15).
amount is None for a person who may not see consumption: they are told
the Analyst is charged by what its model reads and writes, and no amount --
not even zero, which would be one. There is deliberately no price per
conversation here: none is fixed.
onreadingRecallIn
agent_idscopeWhich layer a memory lives in. The retrieval side pulls the relevant layer(s) into context; the write side must respect the PII boundary.
limitqueryRecipeAccount
enabledkeystateWhere an installed recipe stands (rules 8, 16, 18).
RecipeMark
The glyph a recipe is drawn with beside its name -- a closed set, so a new recipe picks one in its YAML and no screen needs a change to draw it (#1849). Each names what it depicts, never an icon library's name.
RecipeOut
adaptdescriptionintentkeynameneedssetupversionheadlinemarkThe glyph a recipe is drawn with beside its name -- a closed set, so a new recipe picks one in its YAML and no screen needs a change to draw it (#1849). Each names what it depicts, never an icon library's name.
RecordCount
countRecordFilter
fieldopEvery question that may be asked of a column. A closed set: a word that is not here cannot be asked anywhere, and a word that is here is asked with the same meaning wherever the column's owner admits it.
IS_EMPTY / IS_NOT_EMPTY are the pair with no value, and they are the
reason this set is not merely the comparison operators. A state that exists
as an absence — a cell never filled in, one emptied by the retention window,
a column the last file said nothing about — is unaskable as an equality, and
a client who cannot ask "which of these has no phone number" cannot find the
work they have to do. Legacy asked the same question as isnull on its own
engine (src/lib/dashboard/widgetConfig.ts), and for the same reason.
CONTAINS is LIKE renamed, not a new word. It was spelled like while
nothing accepted it; contains is what it means to the person choosing it,
and the SQL it becomes is the owner's business rather than the name's.
valuesRecordIn
without_defaultsRecordKind
What one entry of the proof records.
RecordOut
collection_idcreated_atidupdated_atRejection
reasonrowvalueRelativeWindow
lastunitWhat a relative window counts back in. The same five widths Bucket
offers, and deliberately the same words: a chart bucketed by week over
the last six weeks should not need two vocabularies to say "week".
RemovedFromCampaign
removedReplyMode
How a connection replies (rule 92), on a road that declares it -- the
three email roads (email-threads.md rule 21): send delivers the reply of
"hand to the agent" (rule 14), prepare keeps it on the thread as its
prepared reply, for a person to send (handoff.md rules 32-41). A road that
declares none sends, and its channel shows no mode.
ReplyOut
interaction_idtook_threadheldkeptResourceSummary
keykindnamesaysRetentionIn
retention_daysRetentionOut
retention_daysRole
A person's role inside one organization. Per-membership, not per-person.
RoleFlagsOut
grantable_flagsRoleFlagsUpdate
flagsroleA person's role inside one organization. Per-membership, not per-person.
RoleUpdate
roleA person's role inside one organization. Per-membership, not per-person.
RosterAddressIn
kindA kind of address a road knows a sender by (rule 70). The three WhatsApp roads share one. The app's address is the membership itself and is never written by hand (rule 72), so it is not a kind anyone writes.
valueRosterAddressKind
A kind of address a road knows a sender by (rule 70). The three WhatsApp roads share one. The app's address is the membership itself and is never written by hand (rule 72), so it is not a kind anyone writes.
RosterAddressOut
idkindA kind of address a road knows a sender by (rule 70). The three WhatsApp roads share one. The app's address is the membership itself and is never written by hand (rule 72), so it is not a kind anyone writes.
valueRosterEntryIn
display_nameRosterEntryOut
display_nameidresolvesRoute
idkindWhat kinds of thing send conversations to an agent. A trunk and a campaign "are the same case" per the spec, so they are one enum: the refusal reads the same either way. A channel -- WhatsApp, web chat -- is the third: it hands every message it receives to its agent (#774).
labelRouteKind
What kinds of thing send conversations to an agent. A trunk and a campaign "are the same case" per the spec, so they are one enum: the refusal reads the same either way. A channel -- WhatsApp, web chat -- is the third: it hands every message it receives to its agent (#774).
RunOutcome
Where one run got to.
RunSettings
STTSettingsIn
modelnameprovidereot_thresholdeot_timeout_msfiller_wordskeytermsnumeralsprofanity_filterpunctuateredactSTTSettingsOut
eot_thresholdeot_timeout_msfiller_wordsidkeytermslanguagemodelnamenumeralsprofanity_filterproviderpunctuateredactScale
A closed set of named transforms applied to a measure's value after it comes back — never a caller-supplied divisor (v1's own stated rule, carried forward: every duration this platform stores is seconds, every question about one is asked in minutes).
Scope
Which layer a memory lives in. The retrieval side pulls the relevant layer(s) into context; the write side must respect the PII boundary.
SearchCostOut
daysreadingsSearchHit
contentdocument_idfilenameseqSearchIn
agent_idquerylimitSelectionOfIds
idsmodeSelectionOfMatching
exclude_idsmodeSendOnChannelIn
addressSentFileOut
file_idmimesize_bytesurlSentOut
interaction_idheldkeptSlot
The lifecycle slot a tool is placed in. Which tool is attached to which
slot belongs to agents; whether a tool can be in a slot belongs here,
because it is a fact about the tool (service.available_in_slot).
Source-Input
Where a tool comes from. Provenance matters to us (config/auth/billing), never to the LLM — which sees every source identically.
CUSTOM is retired and unreachable, and it is still here. It used to mean
"code written for one organization", and runtime.execution._route refused it
by name because nothing could run one. Code written for one organization now
runs the way every other first-party tool does — BUILTIN, with config
naming which built-in and the composition root binding it (ADR 0014, widened
by ADR 0017) — so there is one dispatch path and this member names none of
them. It is kept rather than removed because tool_source is a native
Postgres enum: dropping a value means recreating the type, rewriting every
dependent column and taking a lock on tools to delete a name no row has any
reason to carry. backend/features/custom_tools/DECISIONS.md D10 records
the trade. A row that somehow carries it is dispatched like any other non-MCP
row and answers execute_http's "this tool has no destination configured",
which is one answer rather than two.
SourceAddress
addressopenSourceFilter
fieldopEvery question that may be asked of a column. A closed set: a word that is not here cannot be asked anywhere, and a word that is here is asked with the same meaning wherever the column's owner admits it.
IS_EMPTY / IS_NOT_EMPTY are the pair with no value, and they are the
reason this set is not merely the comparison operators. A state that exists
as an absence — a cell never filled in, one emptied by the retention window,
a column the last file said nothing about — is unaskable as an equality, and
a client who cannot ask "which of these has no phone number" cannot find the
work they have to do. Legacy asked the same question as isnull on its own
engine (src/lib/dashboard/widgetConfig.ts), and for the same reason.
CONTAINS is LIKE renamed, not a new word. It was spelled like while
nothing accepted it; contains is what it means to the person choosing it,
and the SQL it becomes is the owner's business rather than the name's.
valuesSourceKind
Where the conversation came from.
BROWSER is the platform's own interface — a person trying their agent by
voice or by text from the dashboard, or the agent builder doing it for them.
WIDGET is the embeddable widget on the client's own site. They are two
origins and not one because the one thing source attribution exists to answer
is who was on the other end, and "the client trying their own agent" and "a
visitor on the client's site" are different answers (interactions.md).
IMPERSONATION is not a sixth entry surface — it is staff reaching a
customer's own dashboard, which arrives on the same routes as BROWSER and is
admitted, recorded and counted identically. It is recorded apart for the same
reason: "the client testing their own agent" and "we were looking at it for
them" are different answers too. impersonation.md records the session and
not what was done inside it (#527); a conversation held under one is the
single exception that sentence names, and this is where it is written down.
CHANNEL is a messaging channel — WhatsApp and the rest — where the origin
is "which connection, and the contact's address on it", as an inbound
call's origin is the number that rang (channels.md, "The record"). It is a
source and not a fourth Channel: a thread is recorded as Channel.TEXT
in the log beside every other written conversation, and what makes it a
thread is where it came from.
CONNECTED_APP is an outside AI app a member connected by signing in
(connected-apps.md), held to every rule a key is and still its own origin
(interactions.md rule 2): "which integration placed this" has two answers
once an organization has both, and a cell that read "API key" for an app
would name the wrong one. The record keeps the app connection's identifier
beside it as it keeps a key's (rule 3), never the token.
A native PostgreSQL enum (interaction_source_kind): a member added here
ships an ALTER TYPE ... ADD VALUE migration, b7e2c4d9a1f3's precedent.
Speaker
Who said something. Three, and the third only happens on a channel -- and,
on an email thread, a fourth: a reply sent from the mailbox itself (MAILBOX).
OPERATOR is "a person answering through the platform, recorded as a third
speaker" (interactions.md) — the hand-over to a human that channels.md
describes, "reply on this thread, the same built-in, from outside a flow".
It is a speaker and not a flag on AGENT because the transcript has to say
who the words belong to: an agent shown a person's reply as its own would read
its history as things it had said and had not.
What a branch of the client's flow sent is not a fourth member. A template
a branch fired is "recorded on the thread as the flow's act, as a tool call
is, not as something an agent said" — and a tool call is a ToolCall, which
this vocabulary already has. A speaker for it would claim somebody spoke.
A native PostgreSQL enum (interaction_speaker), so a member added here ships
an ALTER TYPE ... ADD VALUE migration — b7e2c4d9a1f3's precedent.
StandbyComponent
Which component of a profile switched to its standby.
ai-profiles.md: a standby is named per component, "because an outage
belongs to one provider and not to the bundle" — so recording that one
switched is only half the fact; recording which one is the other half.
StateReason
Why a part is failing, paused or unknown. The dot carries none (rule 30); the reason is here for a reader of the API, who has no screen to open.
Status
A version's place in the draft → published → archived lifecycle.
SummaryOut
agent_idopen_findingsreadySummaryScope
What a summary speaks of: the organization's day, or on a quiet day its week so far (rules 20, 21).
TTSSettingsIn
modelnameprovidervoiceemotionemotion_intensityspeedvolumeTTSSettingsOut
emotionemotion_intensityidlanguagemodelnameproviderspeedvoicevolumeTemplateIn
languagenamebutton_payloadsvariablesTestLink
howurlTextSectionIn
llm_settings_idThe reader's ceilings as a profile writes them (ai-profiles.md rule 77): each
one the platform's, or lower -- None is "the platform's", which is what every
profile starts with. The smallest image runs the other way: a profile may ask
for a larger one, never a smaller. Unknown fields are refused, so a ceiling
misspelled is a 422 rather than the platform's silently.
TextSectionOut
The reader's ceilings as a profile writes them (ai-profiles.md rule 77): each
one the platform's, or lower -- None is "the platform's", which is what every
profile starts with. The smallest image runs the other way: a profile may ask
for a larger one, never a smaller. Unknown fields are refused, so a ceiling
misspelled is a 422 rather than the platform's silently.
TextStreamEnd
interaction_idTextStreamError
detailinteraction_idTextStreamFrame
Decision Table
| Variant | Matching Criteria |
|---|---|
| type = object · requires: id, seq, type +1 more | |
| type = object · requires: interaction_id | |
| type = object · requires: interaction_id, detail |
idoccurred_atseqtypeEverything a conversation can record about the path it walked.
colleagueflow_actheldoverlappedprepared_changedreturned_to_flowunvoicedThreadOut
addresschannel_idcreated_atidinteraction_idmay_speak_freelyThreadSubjectOut
atinteraction_idsubjectToolIn
namesourceWhere a tool comes from. Provenance matters to us (config/auth/billing), never to the LLM — which sees every source identically.
CUSTOM is retired and unreachable, and it is still here. It used to mean
"code written for one organization", and runtime.execution._route refused it
by name because nothing could run one. Code written for one organization now
runs the way every other first-party tool does — BUILTIN, with config
naming which built-in and the composition root binding it (ADR 0014, widened
by ADR 0017) — so there is one dispatch path and this member names none of
them. It is kept rather than removed because tool_source is a native
Postgres enum: dropping a value means recreating the type, rewriting every
dependent column and taking a lock on tools to delete a name no row has any
reason to carry. backend/features/custom_tools/DECISIONS.md D10 records
the trade. A row that somehow carries it is dispatched like any other non-MCP
row and answers execute_http's "this tool has no destination configured",
which is one answer rather than two.
conversation_onlydeadline_secondsdescriptionexample_requestsrequires_confirmationtagsToolOutcome
How one attempt at a tool call ended. One event is one attempt: RETRIED means
this attempt failed and another was made, FAILED that it failed and was not,
ABANDONED that it was given up without completing.
ToolPhase
tools.md's pre/in/post lifecycle. A PRE or POST failure happens where
nobody in the conversation can hear it, which is exactly why it is recorded.
ToolTestIn
ToolTestOut
attemptscreated_atdetailduration_msidoutcomeHow one execution of a tool ended.
response_bodytool_idToolUpdate
ToolView
conversation_onlydeadline_secondsdescriptionidnamerequires_confirmationsourceWhere a tool comes from. Provenance matters to us (config/auth/billing), never to the LLM — which sees every source identically.
CUSTOM is retired and unreachable, and it is still here. It used to mean
"code written for one organization", and runtime.execution._route refused it
by name because nothing could run one. Code written for one organization now
runs the way every other first-party tool does — BUILTIN, with config
naming which built-in and the composition root binding it (ADR 0014, widened
by ADR 0017) — so there is one dispatch path and this member names none of
them. It is kept rather than removed because tool_source is a native
Postgres enum: dropping a value means recreating the type, rewriting every
dependent column and taking a lock on tools to delete a name no row has any
reason to carry. backend/features/custom_tools/DECISIONS.md D10 records
the trade. A row that somehow carries it is dispatched like any other non-MCP
row and answers execute_http's "this tool has no destination configured",
which is one answer rather than two.
example_requestsholds_credentialnamespacetagsvisibilityWhether the model is shown a tool whenever it is in reach, or only once the conversation has found it (tools.md rules 87-89). A fact about the tool, the same wherever it is attached; never what an agent may do, which attachment decides.
TrackedQuestionIn
kindThe kind of answer a question takes (rule 35), which is also what its chart is (rule 42): nobody configures one.
search_idshort_nameoptionsTrackedQuestionOut
agent_idanswering_sincecreated_atidkindThe kind of answer a question takes (rule 35), which is also what its chart is (rule 42): nobody configures one.
meaningoptionsquestionshort_namestateTrackedQuestionUpdate
TransferOutcome
How a call's transfer ended (interactions.md rule 57; voice-agent.md rules 103-107): the last attempt's while the conversation is open, then -- on a bridge -- the bridge's own last word, learned after the agent left.
HANDED_OVER is a hand-over the peer took; PUT_THROUGH a bridge whose
colleague was put through to the caller and stayed until one of them left;
NOT_ANSWERED, BUSY and REJECTED a second leg that never answered within
the no-answer wait, found the line busy, or was refused with any other final
word; DECLINED a colleague who declined or left during the consult;
CALLER_LEFT a caller who hung up before being put through;
REFUSED a transfer refused before anything was dialled (voice rule 86);
AGENT_CEILING the agent's ceiling reached while the second leg rang or during
the consult (voice rule 107); BRIDGE_CEILING a bridge the platform ended at
the organization's bridge ceiling. Text on the row, like HungUpBy, so a value
added here needs no migration. It names nobody, so it survives the purge.
TraversalEdgeOut
countfrom_originto_originTraversalNodeOut
dropsentriesexitslabelorigin_iduntitledTraversalOut
captruncatedTriggerConfig
leave_out_testsTriggerKind
What sets an automation off. Closed, and each member names exactly one
place in this deployment where it is detected — see tasks.py and the
webhook route.
Four, and four is all the spec asks for. automations.md used to list a
fifth, "when a message or call arrives — including on channels we do not
answer conversationally", and withdrew it on 2026-09-13 when channels.md
was specified: "a channel nobody answers conversationally is simply a flow
with no branch that hands to an agent", so an arriving message runs the
channel's own flow and there is nothing left for a trigger to do. What an
automation does with a channel is send on one — channels'
send_on_channel built-in inside a scheduled flow, which is SCHEDULE.
#395 closed.
TrunkIn
directionWhich way calls flow over the trunk.
namenumberallowed_ipsline_typeWhat a line's number is (trunks.md rule 7): a phone number, normalised to
E.164, or an extension of the client's phone system, held as text exactly as the
system names it and never read as a number.
TrunkOut
allowed_ipscarrier_failover_possibleThe per-kind guarantee, stated rather than left for the client to infer
from kind (rules 21-22): true only for a number the platform provides; a
line on a trunk of the client's own gets our routing's forward only.
created_atdirectionWhich way calls flow over the trunk.
fallback_conditionWhether the trunk is currently forwarding calls to its fallback number.
Binary on purpose: "A trunk can sit in fallback indefinitely... entering fallback raises a notification... and again when the condition clears" — the spec only ever names two states, not a severity scale.
idkindWhich peer a trunk connects to (trunks.md rule 4).
namenumberupdated_atanswer_bound_secondscarrierWhich carrier this line's calls cross: for a placing line, the carrier its trunk's SIP host is; for a receiving line, the host or else the addresses it accepts calls from; for a platform trunk, the deployment's own connection. telnyx, netelip, or unknown. Read-only; there is no setting for it.
fallback_answered_by_platformline_typeWhat a line's number is (trunks.md rule 7): a phone number, normalised to
E.164, or an extension of the client's phone system, held as text exactly as the
system names it and never read as a number.
reachabilityWhether the phone network can reach this line's trunk: incomplete (it lacks what its peer authenticates by), unreachable (the last run found no carrier-side object), reachable (a run within the last five minutes found it), or unknown (never reconciled, or not recently -- which is not health).
Deprecated: always null, kept for one release.
fallback_wait_secondsDeprecated: answer_bound_seconds, kept for one release.
reachableDeprecated: reachability == "reachable", kept for one release. False for an unknown trunk as well as an unreachable one; read reachability.
TrunkPeerOut
idkindWhich peer a trunk connects to (trunks.md rule 4).
nameallowed_ipscarries_directoryhands_over_inboundhands_over_outboundplacesreachabilityWhether the phone network can reach this trunk (rule 14): incomplete (it lacks what its peer authenticates by -- a receiving trunk with neither the addresses its peer calls from nor, on a carrier we already take calls from, a username), unreachable, reachable (a check in the last five minutes), or unknown.
receivesrecognised_asTrunkUpdate
TryOut
duration_msokdetailresultTurnStages
UnreadCountOut
countUsageSummaryOut
amount^(?!^[-+.]*$)[+-]?0*… · requiredmetric_codetotal^(?!^[-+.]*$)[+-]?0*… · requiredUtterance
The closed set of situations the platform speaks for the agent in.
"A situation the platform can produce is a situation the platform names." The client rewrites every phrase and can add no entry, so this enum — not a free-form dictionary — is what the set is. Adding a member is a change to this platform (a default phrase per language, and a migration), which is the cost that keeps the set a set.
VADCapabilities
VADSettingsIn
nameallow_interruptionsdeactivation_gapmin_silence_msmin_speech_msprefix_padding_msproviderthresholdVADSettingsOut
allow_interruptionsdeactivation_gapidmin_silence_msmin_speech_msnameprefix_padding_msproviderthresholdVerdict
aboutinteraction_idVerdictsIn
VersionChange
kindWhat changed since the version before, in the vocabulary of the product.
Not a diff of text: each member is a sentence a client would recognise about their own agent. Rendering one into their language is the dashboard's job, which is why these are named states and not English strings.
VersionOut
addresscomingdocumentThe six legal documents the platform keeps (rule 1). Each value is also the last segment of the document's address, so it is spelled as a URL is.
effective_atin_forcelanguagesmaterialnamepublished_atVersionTextOut
addresscomingdocumentThe six legal documents the platform keeps (rule 1). Each value is also the last segment of the document's address, so it is spelled as a URL is.
effective_atfell_backfingerprintin_forcelanguageThe languages a version may be written in (values table): Spanish required, English optional, and the Spanish prevails (rule 4).
languagesmaterialnamepublished_attextViewFilters
ViewIn
The inbox's filters (inbox.md rule 17), by the names its address gives
them. A name the inbox does not recognise is refused, never dropped
(extra="forbid"); every one is optional, and an unset one is absent.
The values are the address's: holder, channelKey, agentId,
endReason, counterparty, answeredBy, label and preparedReply hold
several values joined by commas; since/until are calendar days in the reader's own zone; the
either-way filters are yes or no; unreplied is on or absent. An
answer sends an unset filter as null, which the address reads as absent;
what is stored leaves it out.
namesharedshow_countViewOut
editableThe inbox's filters (inbox.md rule 17), by the names its address gives
them. A name the inbox does not recognise is refused, never dropped
(extra="forbid"); every one is optional, and an unset one is absent.
The values are the address's: holder, channelKey, agentId,
endReason, counterparty, answeredBy, label and preparedReply hold
several values joined by commas; since/until are calendar days in the reader's own zone; the
either-way filters are yes or no; unreplied is on or absent. An
answer sends an unset filter as null, which the address reads as absent;
what is stored leaves it out.
idminenamesharedshow_countVisibility
Whether the model is shown a tool whenever it is in reach, or only once the conversation has found it (tools.md rules 87-89). A fact about the tool, the same wherever it is attached; never what an agent may do, which attachment decides.
VoiceSectionIn
WarnedRow
numberwarningWindowUnit
What a relative window counts back in. The same five widths Bucket
offers, and deliberately the same words: a chart bucketed by week over
the last six weeks should not need two vocabularies to say "week".
WithdrawalIn
languageThe languages a version may be written in (values table): Spanish required, English optional, and the Spanish prevails (rule 4).
WriteConsent
Microsoft's second consent, to change mail (connections.md rule 60): never asked, given, or refused by Microsoft since -- which stops the acting and nothing else (email-threads.md rule 37).
WriteResult
createddropped_fieldsbackend__features__interactions__schemas__Source
kindWhere the conversation came from.
BROWSER is the platform's own interface — a person trying their agent by
voice or by text from the dashboard, or the agent builder doing it for them.
WIDGET is the embeddable widget on the client's own site. They are two
origins and not one because the one thing source attribution exists to answer
is who was on the other end, and "the client trying their own agent" and "a
visitor on the client's site" are different answers (interactions.md).
IMPERSONATION is not a sixth entry surface — it is staff reaching a
customer's own dashboard, which arrives on the same routes as BROWSER and is
admitted, recorded and counted identically. It is recorded apart for the same
reason: "the client testing their own agent" and "we were looking at it for
them" are different answers too. impersonation.md records the session and
not what was done inside it (#527); a conversation held under one is the
single exception that sentence names, and this is where it is written down.
CHANNEL is a messaging channel — WhatsApp and the rest — where the origin
is "which connection, and the contact's address on it", as an inbound
call's origin is the number that rang (channels.md, "The record"). It is a
source and not a fourth Channel: a thread is recorded as Channel.TEXT
in the log beside every other written conversation, and what makes it a
thread is where it came from.
CONNECTED_APP is an outside AI app a member connected by signing in
(connected-apps.md), held to every rule a key is and still its own origin
(interactions.md rule 2): "which integration placed this" has two answers
once an organization has both, and a cell that read "API key" for an app
would name the wrong one. The record keeps the app connection's identifier
beside it as it keeps a key's (rule 3), never the token.
A native PostgreSQL enum (interaction_source_kind): a member added here
ships an ALTER TYPE ... ADD VALUE migration, b7e2c4d9a1f3's precedent.
backend__features__messaging__schemas__Audience
Who writes on a connection (channels.md rule 50): the organization's own
members, or its contacts -- people outside it. A road declares which it can
carry (rule 51); a connection names one of those. Not notifications'
Audience, which is who a notice is for: same word, another question.
backend__features__tools__schemas__Source
Where a tool comes from. Provenance matters to us (config/auth/billing), never to the LLM — which sees every source identically.
CUSTOM is retired and unreachable, and it is still here. It used to mean
"code written for one organization", and runtime.execution._route refused it
by name because nothing could run one. Code written for one organization now
runs the way every other first-party tool does — BUILTIN, with config
naming which built-in and the composition root binding it (ADR 0014, widened
by ADR 0017) — so there is one dispatch path and this member names none of
them. It is kept rather than removed because tool_source is a native
Postgres enum: dropping a value means recreating the type, rewriting every
dependent column and taking a lock on tools to delete a name no row has any
reason to carry. backend/features/custom_tools/DECISIONS.md D10 records
the trade. A row that somehow carries it is dispatched like any other non-MCP
row and answers execute_http's "this tool has no destination configured",
which is one answer rather than two.