Expose sent message identifiers in workflow send-email step output (#22520)
## Context
First step toward thread-continuity / follow-up email steps in workflows
(email sequences). The outbound send pipeline already knows the sent
email's RFC-822 Message-ID, the provider thread id, and the persisted
message/thread records — but none of it was surfaced in the send-email
step output, so a later step had no way to reference the email that was
sent.
## What changed
- `saveMessagesWithinTransaction` also returns a `messageExternalId →
messageThreadId` map, and `saveMessagesAndEnqueueContactCreation`
returns the message/thread id maps (both other call sites ignore the
return value)
- `SentMessagePersistenceService.persistSentMessage` and
`SendEmailService.persistSentMessage` return the persisted `{ messageId,
messageThreadId }` (`undefined` when persistence is skipped or fails —
sending still succeeds)
- `SendEmailTool` result now includes `headerMessageId`,
`threadExternalId`, `messageId` and `messageThreadId`
- SEND_EMAIL step output schema (server + frontend) declares
`headerMessageId`/`messageId`/`messageThreadId` so they show up in the
variable picker; DRAFT_EMAIL keeps its success-only schema since draft
creation returns no identifiers yet
This already enables manual thread continuity today: wire
`{{sendEmailStep.headerMessageId}}` into a later email step's
In-Reply-To advanced field — the composer resolves the References chain
and provider thread from it.
## Tests
- New `send-email-tool.spec.ts` covering identifiers in the result,
persistence disabled, and persistence failure
- Extended save-messages spec with the new map, updated frontend
`computeStepOutputSchema` tests
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22520?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
+37
-1
@@ -425,12 +425,48 @@ describe('computeStepOutputSchema', () => {
|
||||
});
|
||||
|
||||
describe('SEND_EMAIL step', () => {
|
||||
it('should return success boolean schema', () => {
|
||||
it('should return success and sent message identifier schema', () => {
|
||||
const result = computeStepOutputSchema({
|
||||
step: { type: 'SEND_EMAIL', settings: {} } as any,
|
||||
objectMetadataItems: [],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'Success',
|
||||
value: true,
|
||||
},
|
||||
headerMessageId: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Message-ID header',
|
||||
value: '',
|
||||
},
|
||||
messageId: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Message record ID',
|
||||
value: '',
|
||||
},
|
||||
messageThreadId: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Message thread ID',
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DRAFT_EMAIL step', () => {
|
||||
it('should return success boolean schema', () => {
|
||||
const result = computeStepOutputSchema({
|
||||
step: { type: 'DRAFT_EMAIL', settings: {} } as any,
|
||||
objectMetadataItems: [],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: {
|
||||
isLeaf: true,
|
||||
|
||||
+29
-1
@@ -218,7 +218,35 @@ export const computeStepOutputSchema = ({
|
||||
return generateFormOutputSchema(formFields, objectMetadataItems);
|
||||
}
|
||||
|
||||
case 'SEND_EMAIL':
|
||||
case 'SEND_EMAIL': {
|
||||
return {
|
||||
success: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'Success',
|
||||
value: true,
|
||||
},
|
||||
headerMessageId: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Message-ID header',
|
||||
value: '',
|
||||
},
|
||||
messageId: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Message record ID',
|
||||
value: '',
|
||||
},
|
||||
messageThreadId: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Message thread ID',
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'DRAFT_EMAIL': {
|
||||
return {
|
||||
success: {
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { EmailComposerService } from 'src/engine/core-modules/tool/tools/email-tool/email-composer.service';
|
||||
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/send-email-tool';
|
||||
import { type EmailToolInput } from 'src/engine/core-modules/tool/tools/email-tool/types/email-tool-input.type';
|
||||
import { SendEmailService } from 'src/modules/messaging/message-outbound-manager/services/send-email.service';
|
||||
|
||||
const buildComposedEmail = (shouldPersistMessage: boolean) => ({
|
||||
recipients: { to: ['test@example.com'], cc: [], bcc: [] },
|
||||
toRecipientsDisplay: 'test@example.com',
|
||||
sanitizedSubject: 'Subject',
|
||||
plainTextBody: 'body',
|
||||
sanitizedHtmlBody: '<p>body</p>',
|
||||
attachments: [],
|
||||
connectedAccount: { id: 'account-1' },
|
||||
messageChannelId: 'channel-1',
|
||||
shouldPersistMessage,
|
||||
});
|
||||
|
||||
const sendResult = {
|
||||
headerMessageId: '<sent-message@mail.example.com>',
|
||||
messageExternalId: 'provider-message-id',
|
||||
threadExternalId: 'provider-thread-id',
|
||||
};
|
||||
|
||||
const baseInput: EmailToolInput = {
|
||||
recipients: { to: 'test@example.com', cc: '', bcc: '' },
|
||||
subject: 'Subject',
|
||||
body: '<p>body</p>',
|
||||
files: [],
|
||||
};
|
||||
|
||||
describe('SendEmailTool', () => {
|
||||
let tool: SendEmailTool;
|
||||
let mockComposeEmail: jest.Mock;
|
||||
let mockSendComposedEmail: jest.Mock;
|
||||
let mockPersistSentMessage: jest.Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockComposeEmail = jest.fn();
|
||||
mockSendComposedEmail = jest.fn().mockResolvedValue(sendResult);
|
||||
mockPersistSentMessage = jest.fn().mockResolvedValue({
|
||||
messageId: 'message-record-id',
|
||||
messageThreadId: 'message-thread-record-id',
|
||||
});
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SendEmailTool,
|
||||
{
|
||||
provide: EmailComposerService,
|
||||
useValue: { composeEmail: mockComposeEmail },
|
||||
},
|
||||
{
|
||||
provide: SendEmailService,
|
||||
useValue: {
|
||||
sendComposedEmail: mockSendComposedEmail,
|
||||
persistSentMessage: mockPersistSentMessage,
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
tool = module.get(SendEmailTool);
|
||||
});
|
||||
|
||||
it('returns the sent message identifiers when the message is persisted', async () => {
|
||||
mockComposeEmail.mockResolvedValue({
|
||||
success: true,
|
||||
data: buildComposedEmail(true),
|
||||
});
|
||||
|
||||
const result = await tool.execute(baseInput, {
|
||||
workspaceId: 'workspace-1',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.result).toMatchObject({
|
||||
headerMessageId: '<sent-message@mail.example.com>',
|
||||
threadExternalId: 'provider-thread-id',
|
||||
messageId: 'message-record-id',
|
||||
messageThreadId: 'message-thread-record-id',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the send identifiers without record ids when persistence is disabled', async () => {
|
||||
mockComposeEmail.mockResolvedValue({
|
||||
success: true,
|
||||
data: buildComposedEmail(false),
|
||||
});
|
||||
|
||||
const result = await tool.execute(baseInput, {
|
||||
workspaceId: 'workspace-1',
|
||||
});
|
||||
|
||||
expect(mockPersistSentMessage).not.toHaveBeenCalled();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.result).toMatchObject({
|
||||
headerMessageId: '<sent-message@mail.example.com>',
|
||||
threadExternalId: 'provider-thread-id',
|
||||
messageId: undefined,
|
||||
messageThreadId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('still succeeds without record ids when persistence fails', async () => {
|
||||
mockComposeEmail.mockResolvedValue({
|
||||
success: true,
|
||||
data: buildComposedEmail(true),
|
||||
});
|
||||
mockPersistSentMessage.mockResolvedValue(undefined);
|
||||
|
||||
const result = await tool.execute(baseInput, {
|
||||
workspaceId: 'workspace-1',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.result).toMatchObject({
|
||||
headerMessageId: '<sent-message@mail.example.com>',
|
||||
messageId: undefined,
|
||||
messageThreadId: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
+11
-7
@@ -41,13 +41,13 @@ export class SendEmailTool implements Tool {
|
||||
|
||||
const sendResult = await this.sendEmailService.sendComposedEmail(data);
|
||||
|
||||
if (data.shouldPersistMessage) {
|
||||
await this.sendEmailService.persistSentMessage(
|
||||
sendResult,
|
||||
data,
|
||||
context.workspaceId,
|
||||
);
|
||||
}
|
||||
const persistedMessage = data.shouldPersistMessage
|
||||
? await this.sendEmailService.persistSentMessage(
|
||||
sendResult,
|
||||
data,
|
||||
context.workspaceId,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
this.logger.log(
|
||||
`Email sent successfully to ${data.toRecipientsDisplay}${data.attachments.length > 0 ? ` with ${data.attachments.length} attachments` : ''}`,
|
||||
@@ -65,6 +65,10 @@ export class SendEmailTool implements Tool {
|
||||
plainTextBody: data.plainTextBody,
|
||||
connectedAccountId: data.connectedAccount.id,
|
||||
attachmentCount: data.attachments.length,
|
||||
headerMessageId: sendResult.headerMessageId,
|
||||
threadExternalId: sendResult.threadExternalId,
|
||||
messageId: persistedMessage?.messageId,
|
||||
messageThreadId: persistedMessage?.messageThreadId,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
+14
@@ -57,6 +57,7 @@ export class MessagingMessageService {
|
||||
string,
|
||||
string
|
||||
>;
|
||||
messageExternalIdToMessageThreadIdMap: Map<string, string>;
|
||||
}> {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
@@ -273,6 +274,7 @@ export class MessagingMessageService {
|
||||
const messageExternalIdsAndIdsMap = new Map<string, string>();
|
||||
const messageExternalIdToMessageChannelMessageAssociationIdMap =
|
||||
new Map<string, string>();
|
||||
const messageExternalIdToMessageThreadIdMap = new Map<string, string>();
|
||||
|
||||
for (const [
|
||||
externalId,
|
||||
@@ -292,6 +294,17 @@ export class MessagingMessageService {
|
||||
);
|
||||
}
|
||||
|
||||
const messageThreadId =
|
||||
accumulator.messageToCreate?.messageThreadId ??
|
||||
accumulator.existingMessageInDB?.messageThreadId;
|
||||
|
||||
if (isDefined(messageThreadId)) {
|
||||
messageExternalIdToMessageThreadIdMap.set(
|
||||
externalId,
|
||||
messageThreadId,
|
||||
);
|
||||
}
|
||||
|
||||
const createdAssociationId =
|
||||
accumulator.messageChannelMessageAssociationToCreate?.id;
|
||||
const existingAssociationId =
|
||||
@@ -310,6 +323,7 @@ export class MessagingMessageService {
|
||||
createdMessages: messagesToCreate,
|
||||
messageExternalIdsAndIdsMap,
|
||||
messageExternalIdToMessageChannelMessageAssociationIdMap,
|
||||
messageExternalIdToMessageThreadIdMap,
|
||||
};
|
||||
},
|
||||
authContext,
|
||||
|
||||
+12
-1
@@ -142,6 +142,10 @@ describe('MessagingSaveMessagesAndEnqueueContactCreationService', () => {
|
||||
['message-1', 'db-message-id-1'],
|
||||
['message-2', 'db-message-id-2'],
|
||||
]),
|
||||
messageExternalIdToMessageThreadIdMap: new Map([
|
||||
['message-1', 'db-thread-id-1'],
|
||||
['message-2', 'db-thread-id-1'],
|
||||
]),
|
||||
createdMessages: [
|
||||
{ id: 'db-message-id-1' },
|
||||
{ id: 'db-message-id-2' },
|
||||
@@ -192,7 +196,7 @@ describe('MessagingSaveMessagesAndEnqueueContactCreationService', () => {
|
||||
});
|
||||
|
||||
it('should save messages and enqueue contact creation', async () => {
|
||||
await service.saveMessagesAndEnqueueContactCreation(
|
||||
const result = await service.saveMessagesAndEnqueueContactCreation(
|
||||
mockMessages,
|
||||
mockMessageChannel,
|
||||
mockConnectedAccount,
|
||||
@@ -210,6 +214,13 @@ describe('MessagingSaveMessagesAndEnqueueContactCreationService', () => {
|
||||
messageParticipantService.saveMessageParticipants,
|
||||
).toHaveBeenCalled();
|
||||
expect(messageQueueService.add).toHaveBeenCalled();
|
||||
|
||||
expect(result?.messageExternalIdsAndIdsMap.get('message-1')).toBe(
|
||||
'db-message-id-1',
|
||||
);
|
||||
expect(result?.messageExternalIdToMessageThreadIdMap.get('message-1')).toBe(
|
||||
'db-thread-id-1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not enqueue contact creation when it is disabled', async () => {
|
||||
|
||||
+30
-10
@@ -48,11 +48,17 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
||||
messageChannel: MessageChannelEntity,
|
||||
connectedAccount: ConnectedAccountEntity,
|
||||
workspaceId: string,
|
||||
) {
|
||||
): Promise<
|
||||
| {
|
||||
messageExternalIdsAndIdsMap: Map<string, string>;
|
||||
messageExternalIdToMessageThreadIdMap: Map<string, string>;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
const handleAliases = connectedAccount.handleAliases || [];
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
const participantsWithMessageId =
|
||||
const savedMessagesResult =
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const workspaceDataSource =
|
||||
@@ -63,6 +69,7 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
||||
const {
|
||||
messageExternalIdsAndIdsMap,
|
||||
messageExternalIdToMessageChannelMessageAssociationIdMap,
|
||||
messageExternalIdToMessageThreadIdMap,
|
||||
} = await this.messageService.saveMessagesWithinTransaction(
|
||||
messagesToSave,
|
||||
messageChannel.id,
|
||||
@@ -155,7 +162,11 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return participantsWithMessageId;
|
||||
return {
|
||||
participantsWithMessageId,
|
||||
messageExternalIdsAndIdsMap,
|
||||
messageExternalIdToMessageThreadIdMap,
|
||||
};
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -163,13 +174,11 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
||||
{ lite: true },
|
||||
);
|
||||
|
||||
if (
|
||||
messageChannel.isContactAutoCreationEnabled &&
|
||||
participantsWithMessageId
|
||||
) {
|
||||
const contactsToCreate = participantsWithMessageId.filter(
|
||||
(participant) => participant.shouldCreateContact,
|
||||
);
|
||||
if (messageChannel.isContactAutoCreationEnabled && savedMessagesResult) {
|
||||
const contactsToCreate =
|
||||
savedMessagesResult.participantsWithMessageId.filter(
|
||||
(participant) => participant.shouldCreateContact,
|
||||
);
|
||||
|
||||
await this.messageQueueService.add<CreateCompanyAndContactJobData>(
|
||||
CreateCompanyAndContactJob.name,
|
||||
@@ -181,5 +190,16 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(savedMessagesResult)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
messageExternalIdsAndIdsMap:
|
||||
savedMessagesResult.messageExternalIdsAndIdsMap,
|
||||
messageExternalIdToMessageThreadIdMap:
|
||||
savedMessagesResult.messageExternalIdToMessageThreadIdMap,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -4,6 +4,7 @@ import { type ComposedEmail } from 'src/engine/core-modules/tool/tools/email-too
|
||||
import { MessagingDraftSendService } from 'src/modules/messaging/message-outbound-manager/services/messaging-draft-send.service';
|
||||
import { MessagingMessageOutboundService } from 'src/modules/messaging/message-outbound-manager/services/messaging-message-outbound.service';
|
||||
import { SentMessagePersistenceService } from 'src/modules/messaging/message-outbound-manager/services/sent-message-persistence.service';
|
||||
import { type PersistedSentMessage } from 'src/modules/messaging/message-outbound-manager/types/persisted-sent-message.type';
|
||||
import { type SendMessageInput } from 'src/modules/messaging/message-outbound-manager/types/send-message-input.type';
|
||||
import { type SendMessageResult } from 'src/modules/messaging/message-outbound-manager/types/send-message-result.type';
|
||||
|
||||
@@ -78,9 +79,9 @@ export class SendEmailService {
|
||||
sendResult: SendMessageResult,
|
||||
data: ComposedEmail,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
): Promise<PersistedSentMessage | undefined> {
|
||||
try {
|
||||
await this.sentMessagePersistenceService.persistSentMessage({
|
||||
return await this.sentMessagePersistenceService.persistSentMessage({
|
||||
sendResult,
|
||||
subject: data.sanitizedSubject,
|
||||
body: data.plainTextBody,
|
||||
@@ -95,6 +96,8 @@ export class SendEmailService {
|
||||
this.logger.warn(
|
||||
`Failed to persist sent message (sync will recover): ${persistenceError}`,
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-6
@@ -1,11 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service';
|
||||
import { type PersistSentMessageInput } from 'src/modules/messaging/message-outbound-manager/types/persist-sent-message-input.type';
|
||||
import { type PersistedSentMessage } from 'src/modules/messaging/message-outbound-manager/types/persisted-sent-message.type';
|
||||
import { formatSentMessage } from 'src/modules/messaging/message-outbound-manager/utils/format-sent-message.util';
|
||||
|
||||
@Injectable()
|
||||
@@ -16,7 +18,9 @@ export class SentMessagePersistenceService {
|
||||
private readonly saveMessagesAndEnqueueContactCreationService: MessagingSaveMessagesAndEnqueueContactCreationService,
|
||||
) {}
|
||||
|
||||
async persistSentMessage(input: PersistSentMessageInput): Promise<void> {
|
||||
async persistSentMessage(
|
||||
input: PersistSentMessageInput,
|
||||
): Promise<PersistedSentMessage | undefined> {
|
||||
const messageChannel = await this.messageChannelRepository.findOneOrFail({
|
||||
where: {
|
||||
id: input.messageChannelId,
|
||||
@@ -27,11 +31,26 @@ export class SentMessagePersistenceService {
|
||||
|
||||
const messageToSave = formatSentMessage(input);
|
||||
|
||||
await this.saveMessagesAndEnqueueContactCreationService.saveMessagesAndEnqueueContactCreation(
|
||||
[messageToSave],
|
||||
messageChannel,
|
||||
messageChannel.connectedAccount,
|
||||
input.workspaceId,
|
||||
const savedMessagesResult =
|
||||
await this.saveMessagesAndEnqueueContactCreationService.saveMessagesAndEnqueueContactCreation(
|
||||
[messageToSave],
|
||||
messageChannel,
|
||||
messageChannel.connectedAccount,
|
||||
input.workspaceId,
|
||||
);
|
||||
|
||||
const messageId = savedMessagesResult?.messageExternalIdsAndIdsMap.get(
|
||||
messageToSave.externalId,
|
||||
);
|
||||
const messageThreadId =
|
||||
savedMessagesResult?.messageExternalIdToMessageThreadIdMap.get(
|
||||
messageToSave.externalId,
|
||||
);
|
||||
|
||||
if (!isDefined(messageId) || !isDefined(messageThreadId)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { messageId, messageThreadId };
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export type PersistedSentMessage = {
|
||||
messageId: string;
|
||||
messageThreadId: string;
|
||||
};
|
||||
+21
-1
@@ -380,7 +380,27 @@ export class WorkflowSchemaWorkspaceService {
|
||||
}
|
||||
|
||||
private computeSendEmailActionOutputSchema(): OutputSchema {
|
||||
return { success: { isLeaf: true, type: 'boolean', value: true } };
|
||||
return {
|
||||
success: { isLeaf: true, type: 'boolean', value: true },
|
||||
headerMessageId: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
label: 'Message-ID header',
|
||||
value: '<message-id@mail.example.com>',
|
||||
},
|
||||
messageId: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
label: 'Message record ID',
|
||||
value: '',
|
||||
},
|
||||
messageThreadId: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
label: 'Message thread ID',
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async computeAiAgentActionOutputSchema({
|
||||
|
||||
Reference in New Issue
Block a user