Message Schema
Felder
Message Schema
Schema for validating message entities (email, comment, internal note)
ticketId
Associated ticket ID
entityId
Generic entity ID this message belongs to (for non-ticket entities)
entityType
Entity type this message belongs to (e.g. sales_order, purchase_order)
isInternal
Whether this is an internal note
isSystemGenerated
Die Notiz stammt von der Anwendung, nicht von einem Menschen: Statuswechsel-Begründung, Merge-Vermerk, Urlaubsübergabe, Eskalation, Regel-Aktion. Ohne dieses Feld ist eine System-Notiz von einem echten Kommentar NICHT unterscheidbar — beide sind INTERNAL_NOTE und beide tragen einen menschlichen `createdBy` (der Auslöser der Aktion). Der Aktivitäts-Zähler des Boards (`GET /tickets/activity`) blendet sie damit aus; ohne das meldete jeder Statuswechsel „neuer Kommentar". Additiv und nullable: fehlend = wie bisher behandelt (also als Kommentar). KEIN Backfill — der Zähler rechnet ohnehin nur ab dem Quittungsstempel, Altbestand fällt von selbst heraus.
messageType
Type of message
EMAILCOMMENTINTERNAL_NOTEstatus
Current message status
DRAFTSENTSEND_FAILEDRECEIVEDSCHEDULEDsendAt
For SCHEDULED outbound messages (WP-U6, ADR 0324): the ISO timestamp at which the delayed send job should run. Null/absent for all other messages.
direction
Message direction
inboundoutboundfrom
Sender email address
emailAccountId
Sender mailbox (`email_accounts._id`) for outbound EMAIL messages. Set by composers that offer an account selection; the DRAFT→SENT transition hook sends through exactly this account (and fails visibly when it is unavailable) instead of falling back to an arbitrary tenant mailbox. On send the hook stamps the account that was ACTUALLY used, so older messages / comment-path messages without a selection carry the resolved one. Additive field — sanctioned by the OCTOJA contract note above; absent on old documents.
bookedAsTimeRecordId
Time record booked off this sent mail (ADR 0779) — the counterpart of `phone_calls.bookedAsTimeRecordId`. Written by the generic CRUD hook when a `time_record` with `sourceType: MESSAGE` / `sourceId: <this _id>` is created; a second click on the mail then opens that booking instead of creating a duplicate. Additive field — sanctioned by the OCTOJA contract note above; absent on old documents (read as "not booked", which is correct: no booking was ever made off a mail before).
to
Recipient email address(es)
cc
CC email address(es)
bcc
BCC email address(es)
attachments
File attachments referenced by file ID (resolved on send). Inbound EMAIL copies additionally carry `size`/`contentId`/`inline` (ADR 0306): `contentId` maps an inline image's `cid:` reference to its persisted email attachment for render-time resolution.
attachmentsSkipped
Attachments the mail provider REPORTED but the ingest could not take over (ADR 0519). They appear in no `files` row and in no `attachments[]` entry above — until 2026-08-17 they vanished without a trace and the message looked exactly like a mail that never had an attachment. The frontend renders an explicit hint from this list instead. `reason` is deliberately coarse and stable, never a raw provider error: 'no-bytes' — the provider delivered metadata but no bytes (Graph omits `contentBytes` from `$expand=attachments` from roughly 3 MB up, and the single-attachment refetch did not obtain them either); 'unsupported-type' — not a `#microsoft.graph.fileAttachment`, i.e. an `itemAttachment` (a mail forwarded as an attachment) or a `referenceAttachment` (OneDrive/SharePoint link); 'not-persisted' — the bytes WERE available but were deliberately (not yet) stored, currently the Outlook add-in path (`attachM365MessageToEntity`), which discards attachments by V1 decision. Omitted entirely when nothing was skipped; there is NO backfill, so an absent field means "nothing recorded", never "nothing was skipped".
subject
Message subject
body
Plain text body
bodyI18nKey
i18n key the client resolves INSTEAD of `body` for system-generated notes. `body` remains the English fallback.
bodyI18nParams
Interpolation values for `bodyI18nKey` (e.g. { ticketNumber: "T-2026-00042" }).
htmlBody
HTML body
aiSummary
AI-generated 2-3 sentence summary of an inbound EMAIL (signatures/quotes stripped). Populated best-effort when AI is active for the tenant; null otherwise. Used as the ticket/activity timeline preview instead of the raw body.
aiSummaryAttemptedAt
Set by the worker when a lazy on-open summarization ran for an imported message (TANSS migration) — even when the LLM returned nothing (AI off, body too short). Prevents the feed trigger from re-enqueuing the same message on every load. Live-ingested emails do not carry this field.
systemMailKind
Marks a message written by the platform itself, e.g. 'ticket_created_confirmation'. Absent/null on every human-authored message. Server-managed (protected field).
systemMailSkipReason
For a system mail that was deliberately NOT sent although the tenant enabled it (e.g. 'no_recipient', 'rate_limited', 'sender_own_mailbox', 'not_enqueued'): the typed reason. Such rows carry status SEND_FAILED so the failure surfaces; the UI labels them "nicht gesendet" instead of "fehlgeschlagen".
isAutoReply
Automated acknowledgement (Eingangsbestätigung, dispatch auto-reply). Excluded from SLA first-response detection (worker ticket-first-response.ts) and from ticket-exchange outbound (a Jira peer never sees an auto-ack as a comment). Written by the worker/system paths only — until 2026-08-17 the dispatch responder set it without a schema entry.
statusChangedAt
When `status` last changed. Written by the transition path and the system-mail record; fallback timestamp for `resolveMessageSendFailure()`.
sentAt
When the message was sent
sendError
Why the last outbound send attempt failed. Written when an async send hook throws; cleared when a later attempt succeeds. Absent on messages that failed before EM-012 — treat as "cause unknown", never as "no error".
sendAttempts
How many send attempts this message has had (initial send + resends). Absent on pre-EM-012 documents — read as 1, since reaching SEND_FAILED implies one attempt.
lastSendAttemptAt
Start of the most recent send attempt, successful or not.
sendInProgress
Time-boxed in-flight lock for the resend endpoint, mirroring the inbound conversion lock (ADR 0231): set before the provider call, cleared after it. A stale lock (older than the lock window) is ignored, so a crashed API process cannot strand a message as permanently un-resendable.
sendStartedAt
When the in-flight lock was taken — basis for the lock expiry.
receivedAt
When the message was received
emailMessageId
Email Message-ID from the provider
emailDocId
For inbound EMAIL copies: the `email_messages._id` this message was created from (ADR 0306). The web app uses it together with the attachments' contentId to resolve inline `cid:` images via GET /email-inbox/:id/attachments/:fileId/content.
extMessageId
External message ID for sync deduplication (e.g., TANSS history item ID)
authorName
Denormalized display name of the original author, written by the TANSS mirror (ADR 0443). Mirrored rows carry the sync actor as createdBy (never the source author) and many source authors have no Codemeta user at all (ex-employees, system entries) — feeds and the timeline enrichment use a present authorName as-is instead of resolving createdBy. Locally created messages leave it unset and resolve via createdBy.
source
Origin system marker (e.g. 'TANSS'). Set by the migration import and the live mirror (ADR 0443); together with entity-config externalMaster it makes the message a read-only mirror. Locally created messages carry no source.
inReplyTo
In-Reply-To header for threading
references
References header for threading
skipSignature
Set by composers (e.g. the inbox EmailComposeDialog) that already embed the author's signature into htmlBody client-side, so the DRAFT→SENT transition hook (transition.service.ts sendEmailHook) does not append a second one.
skipDefaultCc
Set by composers that already resolved the Standard-CC contacts of the customer (party_relationship.isDefaultCc) and showed them as editable chips, so the DRAFT→SENT transition hook does not re-add an address the sender deliberately removed. Messages created outside a composer (ticket comment mail, closing mail, automations) leave it unset and get the Standard-CC injected server-side.
reactions
Emoji reactions (Teams-style). Managed exclusively via POST /:resource/:id/reactions — never set directly via create/update (the field is protected). Absent/missing = no reactions.
pinnedAt
Angeheftet im Verlauf des Elternobjekts (ADR 0991): wichtige Notizen großer Tickets stehen in einer Leiste über dem Verlauf, statt zwischen hunderten Einträgen unterzugehen. Gesetzt/gelöscht ausschließlich über POST /messages/:id/pin (mit History-Zeile), nie per generischem PATCH (protectedFields). Fehlend oder null = nicht angeheftet — kein Backfill.
pinnedBy
`users._id` der Person, die die Nachricht angeheftet hat (ADR 0991). Der Ticket-Zeitstrahl löst daraus `pinnedByName` auf; null, solange nicht angeheftet.
Keine Felder passen zum Filter.
Standard-Endpoints
Diese Resource folgt dem generischen CRUD-Vertrag der Plattform. Lesen Sie die Konventionen für Pagination, Idempotenz, Optimistic Locking und Audit. Die wichtigsten Endpoints:
GET /api/v1/messages— Liste, paginiert + filterbarGET /api/v1/messages/<id>— Einzelne EntityPOST /api/v1/messages— AnlegenPATCH /api/v1/messages/<id>— Teil-UpdateDELETE /api/v1/messages/<id>— Soft-DeleteGET /api/v1/messages/<id>/timeline— Audit + Aktivitäten