Add a conversation inbox
Use the conversations surface when an app needs an inbox, message history, or human handoff experience. It reuses the same frontend module and organization-scoped storage as Lead Outreach while allowing each installed app to keep a separate inbox.
Declare the surface
import {defineApp} from '@twl-tool/app-sdk';
export default defineApp({
key: 'ai-receptionist',
name: 'AI Receptionist',
version: '0.2.0',
permissions: [
'conversations.read',
'conversations.write',
],
ui: {
defaultSurface: 'activity',
surfaces: [
{
key: 'activity',
label: 'Activity',
type: 'conversations',
icon: 'message-circle',
inboxKey: 'activity',
allowRecordMessages: true,
},
],
},
});
The host resolves this as an inbox scoped to:
- the current organization;
- the app installation;
- the declared
inboxKey.
Use different keys such as sales, support, and handoffs when one app needs multiple independent inboxes.
Put channel data into the inbox
The conversations package provides a canonical store for inboxes, conversations, participants, messages, and lifecycle events. Channel packages remain adapters:
- Sync or receive a provider event in the channel package.
- Normalize its thread, participants, and messages.
- Upsert them through
ConversationStoreusing stable provider and external IDs. - Let the native surface render the resulting conversation.
use LaundryOS\Conversations\Services\ConversationStore;
$inbox = $store->resolveInbox(
organizationId: $organizationId,
key: 'activity',
scopeType: 'app_installation',
scopeId: $installationId,
);
$conversation = $store->upsertConversation($inbox, [
'provider' => 'twilio',
'external_id' => $providerThreadId,
'channel' => 'sms',
'subject' => 'New enquiry',
]);
$store->upsertMessage($conversation, [
'provider' => 'twilio',
'external_id' => $providerMessageId,
'direction' => 'inbound',
'body' => $body,
'occurred_at' => $receivedAt,
]);
Stable external IDs make projection idempotent, so re-running a sync updates existing records instead of duplicating messages.
Sending versus recording
The generic composer records an outbound message in the shared store. It does not assume which provider should deliver it. For real delivery, connect the app to a workflow or channel adapter that:
- sends through the provider;
- stores the provider message ID;
- projects delivery, failure, and reply events back into the conversation.
This separation lets Lead Outreach, an AI receptionist, and future marketplace apps share the same inbox UI without coupling the conversations package to Gmail, Twilio, LinkedIn, or a particular telephony vendor.