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:
Félix Malfait
2026-07-03 18:01:40 +02:00
committed by GitHub
parent 3cf04bea28
commit 9f4efa57ff
11 changed files with 314 additions and 29 deletions
@@ -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,
@@ -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: {