Loading…

Star4.1kBook a meeting

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). An integrationType-scoped list without withSurvey excludes them instead, so the inbox's Messenger row and Surveys row are disjoint.
  • automationStatus maps standby → handoff_requested and handoff → human_active on automatedReplyControl.status; responded matches any conversation automation touched.
  • awaitingResponse: "true" filters to isCustomerRespondedLast: true.
  • searchValue matches customers via Core (name/email/phone; ≥4 digits triggers phone search, which also scans CallCdrs by src/dst) plus conversation content.

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/afterProcess handlers maintain unread counters and channel counts after conversation changes.

Operate on one system.

Talk to our team about consolidating your stack onto erxes. 30-minute walkthrough, custom to your org.