Skip to main content
A source container is one place where a conversation happens in one of your tools: a chat channel, a support ticket, a tracked issue, an email thread. Every message you send belongs to exactly one source container, and Modem processes messages source container by source container. Source containers are how Modem aggregates across tools. A customer complaint in a support ticket, the issue your team filed for it, and the channel where engineers discussed the fix are three source containers from three sources. Modem reads each one on its own terms (a ticket has a status, a channel does not) and then connects what it finds into a single topic.

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.
external_id sets the scope. Whatever you compute as external_id decides what counts as one conversation. One source container per help-desk ticket means each ticket is read on its own. One source container per help-desk queue would read every ticket in the queue as one endless conversation. Pick the tool’s natural unit (the thing a person would open in a browser and say “this conversation”), and be consistent. You can change the scope later by changing what you compute, but existing messages stay on the source containers they were sent to.
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.
The examples below use fictitious products (ChatterBox, HelpHub, BugTrack, MailRoom, VoiceBox) so the shape is clear without implying that a specific real product must be modeled a particular way. If your tool looks like one of these, model it the same way.

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.
description is rendered as context when Modem extracts concerns from the channel’s messages. A real sentence about what the channel is for (“escalations from support to engineering”) measurably improves extraction on alert feeds and other channels whose messages do not explain themselves.

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.
Use 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.
The submission’s own text is a message. Send the post body as the first 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 in external_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 open or in_progress, depending on whether the agent is still assigned.
  • Reopened items go back to whatever state fits; Modem records every transition, so a doneopendone history is fine.
  • Closed vs. resolved. If your tool distinguishes them and both mean “the work is finished”, map both to done. canceled is for items that ended without resolution.
  • If you cannot tell, prefer open over inventing precision. A wrong done is worse than a vague open.

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 to done, 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

When state 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 recent created_at, so:
  • A retry of the same event is a no-op.
  • Two events with different created_at resolve 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_at resolve by state_observation_source rank, then by event ID.
This is why 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 openin_progressdone 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’s source_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, and external_id on 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_container reference are stored but not processed today. Once the requirement is enforced they will be rejected with a 400. 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.
  • kind is validated. A kind outside the available set returns 400.
  • 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.

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.