Why source containers matter
Before you send a single message, Modem needs to know what the source container is. The source container does four jobs:Identity
A source container is identified by three strings:
Modem derives the source container’s internal ID from these three values plus your organization. The same triple always resolves to the same source container, which is what makes re-sending a source container snapshot safe and what lets a message find its source container.
Make
external_id byte-for-byte identical everywhere you use it. "48213" and "ticket-48213" are two different source containers.
Source container kinds
kind is a policy key, not a label. It decides how Modem extracts content from the source container’s messages, how that content participates in topics, and which lens the source container appears under in the Sources view.
Two kinds are gated when it comes to starting a topic.
issue and pull_request are tracking artifacts: your team’s restatement of
a concern rather than the concern itself. Content from these kinds can always join an existing topic, but it only creates a new
topic on its own when someone outside your organization took part in it. A pull_request additionally can never anchor a topic by
itself. Every other kind seeds topics freely. If you are choosing between issue and support_thread for something in between (a
customer-visible bug portal, say), this is the difference that matters.chat_channel
An ongoing stream of messages with no beginning or end: a channel in your team chat, a shared room with a customer, an alerts feed a bot posts into. The conversation is the signal. People raise concerns directly, and Modem reads the messages as the concern itself rather than as a report about one.
A chat channel has no lifecycle. Send state: null. If your tool can archive channels, report that with archived_at.
Scope: one source container per channel. If your tool has workspaces or servers, put both IDs in external_id so channels with the same ID in different workspaces stay distinct.
support_thread
A two-way conversation between one customer and your team about one problem: a help-desk ticket, an in-app chat conversation, a support email exchange. The customer states the concern in their own words, so Modem reads the thread as the concern itself.
A support thread has a lifecycle. The tool’s status maps onto Modem’s states, and the transition to done is strong evidence that the concern was resolved.
Scope: one source container per ticket or conversation.
author is the person who opened the ticket. human_key is the short label people use to refer to it.
issue
A tracked work item with workflow state: a bug, a task, a card on a project board. Issues are usually your team’s restatement of a concern that was first raised somewhere else (a support thread, a chat message), so Modem treats them as tracking artifacts: an issue joins a topic readily, but it only seeds a new topic on its own when someone outside your organization took part in it. This keeps internal task churn from minting topics that no customer ever raised.
An issue has a lifecycle, and its lifecycle is the most informative of any kind. When the issue moves to done, Modem has direct evidence that the work behind the topic is complete.
Scope: one source container per issue.
external_id for the tool’s immutable ID and human_key for the human-readable key. Keys like ACME-1187 can change when an issue moves between projects; internal IDs do not.
email_thread
A thread in a mailbox, keyed on the root message. Replies often quote earlier messages verbatim, and Modem accounts for that when it reads the thread. Like a chat channel, a mail thread has no lifecycle: send state: null.
Scope: one source container per thread. Use the root message’s Message-ID header (or your mail provider’s thread ID) as external_id, and send the same source container snapshot from every message in the thread. Modem keeps whichever snapshot it observed most recently, so a snapshot derived from a reply is fine as long as the fields you fill are ones every message in the thread agrees on (the subject with Re:/Fwd: stripped, for example).
author and external_created_at belong to the thread’s root message. If the message you are looking at is a reply, you do not know them; send null rather than the reply’s values.
feedback_submission
Something a customer wrote unprompted that stands on its own: a post on a public feedback board, a feature request submitted through a form, an in-app “send feedback” entry. Nobody is obliged to reply, which is what distinguishes it from a support thread. Where the tool supports voting or comments, that activity reads as demand for the request rather than as a back-and-forth about it.
Lifecycle is optional. Many feedback boards have a status (under review, planned, shipped); if yours does, map it. If it does not, send state: null.
Scope: one source container per submission.
message.created in the source container, then each comment as a further message.
Built-in-only kinds
Three kinds are produced by Modem’s built-in integrations and are not yet accepted through the ingest API. They are listed so the full model is visible.Lifecycle state
Kinds that track work (support_thread, issue, feedback_submission, pull_request) carry a state. Your tool has its own vocabulary; Modem normalizes it to five values.
Mapping your tool’s statuses
Map each native status to the closest Modem state. The mapping does not need to be lossless; put the native status inexternal_state if you want it preserved.
A few judgment calls:
- A ticket the customer has replied to after an agent response usually goes back to
openorin_progress, depending on whether the agent is still assigned. - Reopened items go back to whatever state fits; Modem records every transition, so a
done→open→donehistory is fine. - Closed vs. resolved. If your tool distinguishes them and both mean “the work is finished”, map both to
done.canceledis for items that ended without resolution. - If you cannot tell, prefer
openover inventing precision. A wrongdoneis worse than a vagueopen.
Why state matters
Source container state is the most direct lifecycle evidence Modem has. When Modem assesses whether a topic is open, in progress, completed, or dismissed, it reads the current state and transition history of every source container that contributes to the topic. A ticket moving todone, or an issue moving to canceled, changes that assessment. Conversation content alone (“we shipped it yesterday”) is also evidence, but a status change is unambiguous.
This is why state is unimportant for a chat channel and very important for a ticket. Nobody “closes” a channel when a problem is fixed, so a channel’s messages carry the resolution signal in their content. A ticket’s status is the resolution signal, and if you send state: null for every ticket, Modem loses it.
State fields
Whenstate is non-null, describe how you learned it.
state_observation_source values, from most to least authoritative:
When two observations of the same source container arrive with the same
created_at, the more authoritative source wins. Otherwise the most recently observed snapshot wins. See Idempotency.
Events
Three event types manage source containers. All use the standard envelope.source_container.created
Stands up a source container. body.data carries a complete snapshot; every field is present, with null for unknown values.
source_container.updated
Identical body to source_container.created. Send it whenever the source container changes in your tool: a status transition, a retitle, an archive. Always send the full snapshot, not a diff.
The two event types differ in exactly one way: a source_container.created observed after a deletion revives the source container, and a source_container.updated never does (see below). Otherwise they are interchangeable, and if you are not sure whether Modem has seen a source container, sending source_container.created again is safe.
source_container.deleted
Tombstones a source container. Once deleted, the source container’s messages are no longer processed.
deleted_at is when the source container was removed in your tool. Send a deletion only for an actual removal. A ticket that was closed or merged is a state change (done or canceled), not a deletion.
A deletion is a tombstone, not a purge. The row and its messages remain, and a source_container.created event observed after the deletion revives it: deleted_at is cleared, the new snapshot is stored, and processing resumes. source_container.updated does not revive; it updates the snapshot but leaves the source container deleted.
If the first event Modem ever sees for an identity is a deletion, it records a bare tombstone (identity and deleted_at, nothing else). Messages referencing it wait as they would for any missing source container, and a later source_container.created fills it in and starts processing.
Idempotency
Source container events are upserts keyed on the source container’s identity. Re-sending a snapshot is always safe. Modem keeps the snapshot with the most recentcreated_at, so:
- A retry of the same event is a no-op.
- Two events with different
created_atresolve to the later one, regardless of arrival order. An out-of-order delivery of an older snapshot cannot overwrite a newer one. - Two events with the same
created_atresolve bystate_observation_sourcerank, then by event ID.
created_at must be your observation time and never later than the moment you looked. A bulk import stamped with the import time would outrank every live observation that came before it.
State transitions are additionally recorded as history. A source container that moved open → in_progress → done keeps all three facts even though only done is current, and that history is part of the lifecycle evidence.
Ordering and failure modes
The source container gates the start of processing, not the acceptance of messages. A message that references a source container Modem has not seen yet is stored with its reference intact and waits. Once the source container’ssource_container.created event lands, the queued messages are processed in order. Sending the source container first is the natural order, but a message arriving before it is not an error.
Other rules:
- The reference must match exactly.
source_name,kind, andexternal_idon a message must equal the source container’s byte for byte. A mismatch is a reference to a different (probably nonexistent) source container, so the message waits for a source container that never arrives. - The reference will soon be required. Messages without a
source_containerreference are stored but not processed today. Once the requirement is enforced they will be rejected with a400. See The source container reference. - A message’s source container is fixed once it is set. Sending the same message again with a different reference does not move it.
kindis validated. Akindoutside the available set returns400.- One source container event per source container, plus one per lifecycle change. Do not send a source container snapshot alongside every message. Re-sending is safe, but it is wasted work and counts toward usage.
Related
Messages
The message payload and how a message references its source container.
Ingest API overview
Authentication, the event envelope, and the end-to-end protocol.
Topics
What Modem builds from the source containers and messages you send.
Integrations
Built-in integrations, which model their sources the same way.