Inbox & Channels
Work the unified inbox: conversations and messages scoped by channel membership, team and personal channels, response templates, and conversation-to-card conversion.
Backend modules: backend/plugins/frontline_api/src/modules/{inbox,channel,response}. Frontend: frontend/plugins/frontline_ui/src/modules/{inbox,channels,responseTemplate} and the /frontline/inbox route.
Concepts
Conversations and messages
A Conversation belongs to one Integration and one customer, carries status (new, open, closed, engageVisitorAuto), assignedUserId, participatedUserIds, readUserIds, tagIds, hasSurvey, and automatedReplyControl (status, reason, pausedUntil). A ConversationMessage carries content, attachments, internal (agent-only note), fromBot, botData, mailData for the mail channel, and extraData (survey snapshots, Discord polls).
Customer messages land through receiveInboxMessage (modules/inbox/receiveMessage.ts), which creates the conversation and message, then publishes conversationClientMessageInserted to every member of the conversation's channel. One GraphQL subscription covers all of a user's channels.
Membership-scoped visibility
An agent only sees conversations whose integration sits on a channel they belong to. Builder in src/conversationQueryBuilder.ts maps the caller's ChannelMembers rows to integration ids and intersects them with channelId, brandId, and integrationId filters. A system-role user bypasses the membership filter. Channels the user does not belong to return an empty integrationId.$in; there is no error, just no rows.
Team vs personal channels
| Team channel | Personal channel | |
|---|---|---|
scope | team (default; missing scope on legacy records means team) | personal |
| Members | Many, via channelAddMembers | Exactly one: the owner, as admin |
| Provisioning | channelAdd | Lazy: created the first time getPersonalChannel runs or an integration is created without a channelId |
| Integrations | Any kind | Any kind; integrationsCreateExternalIntegration without channelId attaches to the caller's personal channel |
Members carry a role of admin, lead, or member (ChannelMembers, unique on (channelId, memberId)). A partial unique index on Channels allows at most one personal channel per createdBy, and removing or demoting the last admin of a channel is refused.
Response templates
Per-channel canned replies (ResponseTemplates: name, content, channelId, files). conversationMessageAdd accepts a responseTemplateId to send one.
Conversation filters and counts
Every conversation list/count query accepts the same filter params: channelId, integrationId, integrationType, status, brandId, tag, customerId, segment, searchValue, startDate/endDate, and the string flags unassigned, participating, mentioned, unread, starred, awaitingResponse, withSurvey, automationStatus.
withSurvey: "true"keeps only conversations carrying a survey (hasSurvey). AnintegrationType-scoped list withoutwithSurveyexcludes them instead, so the inbox'sMessengerrow andSurveysrow are disjoint.automationStatusmapsstandby→handoff_requestedandhandoff→human_activeonautomatedReplyControl.status;respondedmatches any conversation automation touched.awaitingResponse: "true"filters toisCustomerRespondedLast: true.searchValuematches customers via Core (name/email/phone; ≥4 digits triggers phone search, which also scansCallCdrsbysrc/dst) plus conversationcontent.
Two builders exist. The Mongo Builder (src/conversationQueryBuilder.ts) backs conversations, conversationsTotalCount, conversationsGetLast, conversationsTotalUnreadCount, and the scalar buckets of conversationCounts. The Elasticsearch-backed CommonBuilder (modules/inbox/conversationUtils.ts) serves the conversationCounts(only: "byChannels"|"byIntegrationTypes"|"byTags"|"byIntegrations") buckets. CommonBuilder.runQueries currently returns 0 (the conversations Elasticsearch index is not populated), so the sidebar reads per-kind counts from integrationsGetUsedTypesByChannel instead.
Queries and mutations
Schema: modules/inbox/graphql/schemas/conversation.ts.
| Operation | Kind | Purpose |
|---|---|---|
conversations(filter…) | query | Cursor-paginated inbox list |
conversationDetail(_id) / conversationMessages(conversationId, …) | query | Detail and message history |
conversationCounts(only, …) / conversationsTotalCount / conversationsTotalUnreadCount | query | Sidebar counts |
conversationMessageAdd(conversationId, content, internal, attachments, poll, replyToMessageId, …) | mutation | Agent reply, internal note, or native poll |
conversationMessageEdit, conversationMarkAsRead | mutation | Edit / mark read |
conversationsAssign / conversationsUnassign / conversationsChangeStatus / conversationsResolve | mutation | Triage actions |
conversationSetAutomatedReplyControl(_id, status, reason, pausedUntil) | mutation | Pause/hand off automation on a conversation |
conversationConvertToCard(_id, type, itemName, stageId, …) | mutation | Convert to ticket, deal, or task |
conversationConvertedItems(_id) | query | What a conversation was already converted into |
getMyChannels / getChannels / getChannel(_id) / getPersonalChannel / getChannelMembers | query | Channel reads |
channelAdd / channelUpdate / channelRemove / channelAddMembers / channelRemoveMember(s) / channelUpdateMember | mutation | Channel management |
responseTemplates(filter) / responseTemplatesAdd / responseTemplatesEdit / responseTemplatesRemove | query/mutation | Response templates |
integrations / integrationsGetUsedTypes / integrationsGetUsedTypesByChannel / integrationDetail | query | Integration list and sidebar kinds |
conversationClientMessageInserted(userId) | subscription | Live customer messages for the whole inbox |
Convert a conversation to a ticket, deal, or task
conversationConvertToCard(_id, type, …) dispatches to a per-kind handler in modules/inbox/services/conversationConvertTargets.ts:
type | Created by | Permission checked | URL returned by conversationConvertedItems |
|---|---|---|---|
ticket | Ticket.addTicket in this plugin | createTicket + pipeline access | /frontline/tickets?ticketId=<id> |
deal | sales deal.createItem over tRPC | dealsAdd | /sales/deals?boardId=…&pipelineId=…&salesItemId=<id> |
task | operation task.createFromSource over tRPC | taskCreate | /operation/tasks/<id> |
The new item is related to the conversation (and its customer for tickets/tasks) through Core relations. A second item of the same kind for one conversation is refused; stageId is required (a status id for tickets/tasks, a stage id for deals).
mutation {
conversationConvertToCard(
_id: "conv_abc123"
type: "ticket"
itemName: "Billing issue from messenger"
stageId: "status_xyz"
assignedUserIds: ["user_1"]
tagIds: ["tag_9"]
)
}
Widget and client portal resolvers
The unauthenticated messenger widget talks to the widgets* operations (widgetsMessengerConnect, widgetsInsertMessage, widgetsConversations, widgetsConversationDetail, widgetsMessages, widgetsTotalUnreadCount, widgetsMessengerSupporters, widgetsGetEngageMessage, widgetsSendTypingInfo, widgetsReadConversationMessages) and the ticket-widget operations (widgetTicketCreated, widgetTicketCheckProgress, widgetTicketComments, widgetTicketActivityLogs, widgetTicketsByCustomer). Signed-in portal users use the cp* conversation operations (cpConnect, cpConversations, cpInsertMessage, cpReadConversationMessages). For installation, see Messenger Widget.
tRPC procedures
inbox.* router highlights (src/modules/inbox/trpc/inbox.ts):
| Procedure | Kind | Purpose |
|---|---|---|
inbox.createConversationAndMessage | mutation | Create a conversation and its first message (used by survey submit and other services) |
inbox.createOnlyMessage | mutation | Append a message to an existing conversation |
inbox.integrations.receive | mutation | Inbound webhook entrypoint → receiveInboxMessage |
inbox.integrations.remove | mutation | Remove an integration plus its conversations and messages |
inbox.conversationClientMessageInserted | mutation | Publish the live-message subscription event |
inbox.getConversationsList | query | Cursor-paginated conversation list |
inbox.conversations.find / findOne / count | query | Conversation reads |
inbox.conversations.changeStatus | query | Set status (new/open/closed/resolved) |
inbox.conversationMessages.find / findOne | query | Message reads |
inbox.integrations.find / findOne / count | query | Integration reads |
inbox.getIntegrationKinds | query | Kind → label map (messenger, lead, webhook, mail, facebook-messenger, …) |
inbox.channels.find, inbox.getConversations, inbox.removeCustomersConversations, inbox.changeCustomer, inbox.updateUserChannels, inbox.sendNotifications, inbox.widgetsGetUnreadMessagesCount | query/mutation | Service-to-service helpers |
Also merged into the root router: conversation.find/conversation.tag, relation.onRelationAdded (logs a conversation's form submissions onto a related ticket or other entity), and integration.find.
Permissions
Permission modules: inbox (showConversations always granted, conversationMessageAdd, conversationMessageEdit, conversationsAssign, conversationsUnassign, conversationsChangeStatus, conversationsResolve, conversationConvertToCard, conversationEditCustomFields), channel (showChannels, showAllChannels, channelAdd, channelUpdate, channelRemove, channelManageMembers), integration (showIntegrations, integrationsAdd, integrationsEdit, integrationsRemove), and responseTemplate (showResponseTemplates, responseTemplatesAdd, responseTemplatesEdit, responseTemplatesRemove). Default groups: frontline:admin, frontline:user, frontline:viewer.
Automations, segments, notifications
- Automation triggers:
frontline:inbox.conversations"Conversation event" (assignee/status/tag changes) and "Erxes Messenger Message" (direct message, Get Started, quick reply, request-create-ticket, ticket-form-submission conditions); action "Send Erxes Messenger Message". - Segment content types and field definitions are contributed for conversations and messages (
modules/inbox/meta/segments/). - Notification events include
conversationAddMessage,conversationAssigneeChange,conversationCreated,conversationParticipantAdded,conversationStateChange,conversationTagged. meta/afterProcesshandlers maintain unread counters and channel counts after conversation changes.