Add In-Reply-To to Email Workflow Node (#18641)

Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
neo773
2026-03-22 21:19:08 +05:30
committed by GitHub
parent 7bde8a4dfa
commit 8ef32c4781
26 changed files with 259 additions and 27 deletions
+28 -14
View File
@@ -175,7 +175,8 @@
"user-guide/workflows/how-tos/crm-automations/formula-fields",
"user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -621,7 +622,8 @@
"l/fr/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/fr/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/fr/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/fr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/fr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/fr/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -1067,7 +1069,8 @@
"l/ar/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/ar/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/ar/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/ar/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/ar/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/ar/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -1513,7 +1516,8 @@
"l/cs/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/cs/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/cs/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/cs/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/cs/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/cs/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -1959,7 +1963,8 @@
"l/de/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/de/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/de/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/de/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/de/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/de/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -2405,7 +2410,8 @@
"l/es/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/es/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/es/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/es/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/es/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/es/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -2851,7 +2857,8 @@
"l/it/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/it/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/it/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/it/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/it/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/it/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -3297,7 +3304,8 @@
"l/ja/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/ja/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/ja/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/ja/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/ja/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/ja/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -3743,7 +3751,8 @@
"l/ko/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/ko/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/ko/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/ko/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/ko/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/ko/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -4189,7 +4198,8 @@
"l/pt/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/pt/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/pt/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/pt/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/pt/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/pt/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -4635,7 +4645,8 @@
"l/ro/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/ro/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/ro/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/ro/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/ro/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/ro/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -5081,7 +5092,8 @@
"l/ru/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/ru/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/ru/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/ru/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/ru/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/ru/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -5527,7 +5539,8 @@
"l/tr/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/tr/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/tr/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/tr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/tr/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/tr/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -5973,7 +5986,8 @@
"l/zh/user-guide/workflows/how-tos/crm-automations/formula-fields",
"l/zh/user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"l/zh/user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"l/zh/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"l/zh/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"l/zh/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

@@ -157,7 +157,8 @@
"user-guide/workflows/how-tos/crm-automations/formula-fields",
"user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -156,7 +156,8 @@
"user-guide/workflows/how-tos/crm-automations/formula-fields",
"user-guide/workflows/how-tos/crm-automations/display-related-record-data",
"user-guide/workflows/how-tos/crm-automations/closed-won-automations",
"user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities"
"user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities",
"user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails"
]
},
{
@@ -0,0 +1,121 @@
---
title: Auto-Reply to Inbound Emails
description: Build a workflow that uses AI to triage incoming emails and send threaded replies automatically.
---
Respond to inbound emails in seconds — not hours. This workflow uses an AI Agent to filter out noise (newsletters, spam, auto-replies) and draft a personalized response to real messages, then sends it as a threaded reply inside the original conversation.
## How email threading works
Every email carries a hidden `Message-ID` header — a unique fingerprint assigned by the sender's mail server. When you reply to an email, your mail client sets an `In-Reply-To` header referencing that fingerprint. That's how Gmail, Outlook, and every other client groups messages into threads.
In Twenty, that fingerprint is stored as `headerMessageId` on the Message object. Your workflow grabs it and passes it into the Send Email action's In-Reply-To field.
## Building the workflow
### Step 1: Create a new workflow
Head to **Settings -> Workflows** and click **+ New Workflow**.
### Step 2: Trigger on incoming messages
Pick **When a Record is Created** and choose **Messages**.
Every time an email lands in Twenty, this fires.
<img src="/images/user-guide/workflows/auto-reply/workflow-overview.png" style={{width:'100%'}}/>
### Step 3: Look up who sent it
Add a **Search Records** action.
The sender's address isn't on the message itself — it's on the related Message Participant record.
| Field | Value |
|-------|-------|
| **Object** | Message Participants |
| **Filter** | Message **is** `{{trigger.id}}` |
| **Filter** | Role **is** From |
| **Limit** | 1 |
This gives you the sender's email in `handle` and their name in `displayName`.
<img src="/images/user-guide/workflows/auto-reply/find-sender.png" style={{width:'100%'}}/>
### Step 4: AI triage and draft reply
Add an **AI Agent** action. This single step does two things: decides whether the email deserves a reply, and if so, writes one.
Use a prompt like:
```
You are an email triage assistant for a sales team. Read the following
inbound email and decide if it deserves a reply.
Subject: {{trigger.subject}}
Body: {{trigger.text}}
From: {{Find Sender.first.displayName}} ({{Find Sender.first.handle}})
If this email is spam, a newsletter, an automated notification, or
otherwise does not need a human reply, respond with exactly: SKIP
Otherwise, write a short, professional reply (3-4 sentences max) that:
- Acknowledges their specific message
- Lets them know someone from the team will follow up shortly
- Is warm but not overly casual
Respond with only the reply text, no subject line or greeting prefix.
```
The AI Agent outputs its response in a `response` field that the next steps can reference.
<img src="/images/user-guide/workflows/auto-reply/ai-triage.png" style={{width:'100%'}}/>
### Step 5: Branch on the AI decision
Add an **If/Else** action to check whether the AI decided to reply or skip.
| Field | Value |
|-------|-------|
| **Condition** | AI Agent `response` **does not contain** `SKIP` |
| **If true** | Continue to Send Email |
| **Else** | Do nothing (workflow ends) |
Spam, newsletters, and auto-generated messages get dropped. Everything else moves to the next step.
<img src="/images/user-guide/workflows/auto-reply/should-reply.png" style={{width:'100%'}}/>
### Step 6: Send a threaded reply
Add a **Send Email** action on the "if true" branch. Click **Advanced options**, then **Add In-Reply-To**.
| Field | Value |
|-------|-------|
| **To** | `{{Find Sender.first.handle}}` |
| **Subject** | `Re: {{trigger.subject}}` |
| **Body** | `{{AI Triage & Draft Reply.response}}` |
| **In-Reply-To** | `{{trigger.headerMessageId}}` |
The In-Reply-To field is what makes this a reply instead of a new conversation. The recipient sees it threaded under the original email in Gmail, Outlook, or any other client.
<img src="/images/user-guide/workflows/auto-reply/send-email.png" style={{width:'100%'}}/>
<Tip>
**In-Reply-To** expects a `message.headerMessageId` from the trigger — it's the email's unique fingerprint, not a recipient address. If you leave it empty, the email still sends, just as a standalone message.
</Tip>
<Warning>
Gmail uses the subject line to group messages into threads. The subject **must** start with `Re:` (including the colon and space) for Gmail to display the reply inside the original thread. Without it, the reply will appear as a separate conversation — even if the In-Reply-To header is set correctly.
</Warning>
### Step 7: Test and activate
Hit **Test**, then check your email client. The reply should appear nested under the original message.
Activate when you're happy with it.
## Ideas to build on
- **Only reply to VIPs** — add a branch that checks the sender's domain or whether they exist as a Contact in Twenty
- **Route by intent** — use separate AI Agent prompts to handle sales inquiries differently from support requests
- **Enrich before replying** — add a Search Records step to pull the sender's company or deal history into the AI prompt for more personalized replies
@@ -9,4 +9,5 @@ export type EmailFormData = {
subject: string;
body: string;
files: WorkflowAttachment[];
inReplyTo: string;
};
@@ -75,12 +75,14 @@ export const WorkflowEditActionEmailBase = ({
const [visibleAdvancedFields, setVisibleAdvancedFields] = useState<{
cc: boolean;
bcc: boolean;
inReplyTo: boolean;
}>(() => {
const inputRecipients = action.settings.input.recipients;
return {
cc: Boolean(inputRecipients?.cc),
bcc: Boolean(inputRecipients?.bcc),
inReplyTo: Boolean(action.settings.input.inReplyTo),
};
});
@@ -89,7 +91,9 @@ export const WorkflowEditActionEmailBase = ({
const advancedOptionsDropdownId = `${action.id}-email-advanced-options`;
const hasAvailableAdvancedOptions =
!visibleAdvancedFields.cc || !visibleAdvancedFields.bcc;
!visibleAdvancedFields.cc ||
!visibleAdvancedFields.bcc ||
!visibleAdvancedFields.inReplyTo;
const handleReauthorize = async () => {
if (!isDefined(missingScopes)) {
@@ -276,6 +280,18 @@ export const WorkflowEditActionEmailBase = ({
VariablePicker={WorkflowVariablePicker}
/>
)}
{visibleAdvancedFields.inReplyTo && (
<FormTextFieldInput
label={t`In-Reply-To`}
placeholder={t`Enter Message-ID to reply to`}
readonly={actionOptions.readonly}
defaultValue={formData.inReplyTo}
onChange={(value) => {
handleFieldChange('inReplyTo', value);
}}
VariablePicker={WorkflowVariablePicker}
/>
)}
{!actionOptions.readonly && hasAvailableAdvancedOptions && (
<Dropdown
dropdownId={advancedOptionsDropdownId}
@@ -317,6 +333,18 @@ export const WorkflowEditActionEmailBase = ({
}}
/>
)}
{!visibleAdvancedFields.inReplyTo && (
<MenuItem
text={t`Add In-Reply-To`}
onClick={() => {
setVisibleAdvancedFields((prev) => ({
...prev,
inReplyTo: true,
}));
closeDropdown(advancedOptionsDropdownId);
}}
/>
)}
</DropdownMenuItemsContainer>
</DropdownContent>
}
@@ -32,6 +32,7 @@ const DEFAULT_SEND_EMAIL_ACTION: WorkflowSendEmailAction = {
subject: '',
body: '',
files: [],
inReplyTo: '',
},
outputSchema: {},
errorHandlingOptions: {
@@ -62,6 +63,7 @@ const CONFIGURED_SEND_EMAIL_ACTION: WorkflowSendEmailAction = {
subject: 'Welcome to Twenty!',
body: 'Dear Tim,\n\nWelcome to Twenty! We are excited to have you on board.\n\nBest regards,\nThe Team',
files: [],
inReplyTo: '',
},
outputSchema: {},
errorHandlingOptions: {
@@ -91,6 +93,7 @@ const DEFAULT_DRAFT_EMAIL_ACTION: WorkflowDraftEmailAction = {
subject: '',
body: '',
files: [],
inReplyTo: '',
},
outputSchema: {},
errorHandlingOptions: {
@@ -28,6 +28,7 @@ export const useEmailForm = ({
subject: action.settings.input.subject ?? '',
body: action.settings.input.body ?? '',
files: action.settings.input.files ?? [],
inReplyTo: action.settings.input.inReplyTo ?? '',
};
});
@@ -46,6 +47,7 @@ export const useEmailForm = ({
subject: formData.subject,
body: formData.body,
files: formData.files,
inReplyTo: formData.inReplyTo,
},
},
});
@@ -89,6 +89,7 @@ export class DraftEmailTool implements Tool {
body: data.plainTextBody,
html: data.sanitizedHtmlBody,
attachments: data.attachments,
inReplyTo: data.inReplyTo,
},
data.connectedAccount,
);
@@ -214,7 +214,7 @@ export class EmailComposerService {
context: ToolExecutionContext,
): Promise<EmailComposerResult> {
const { workspaceId } = context;
const { subject, body, files } = parameters;
const { subject, body, files, inReplyTo } = parameters;
let { connectedAccountId } = parameters;
let recipients: { to: string[]; cc: string[]; bcc: string[] };
@@ -309,6 +309,7 @@ export class EmailComposerService {
sanitizedHtmlBody,
attachments,
connectedAccount: connectedAccountWithFreshTokens,
inReplyTo,
},
};
}
@@ -37,4 +37,10 @@ export const EmailToolInputZodSchema = z.object({
.describe('Array of file objects to attach to the email')
.optional()
.default([]),
inReplyTo: z
.string()
.describe(
'The RFC 2822 Message-ID of an existing email to reply to. When provided, the email is sent as a reply in the same thread.',
)
.optional(),
});
@@ -88,6 +88,7 @@ export class SendEmailTool implements Tool {
body: data.plainTextBody,
html: data.sanitizedHtmlBody,
attachments: data.attachments,
inReplyTo: data.inReplyTo,
},
data.connectedAccount,
);
@@ -9,4 +9,5 @@ export type ComposedEmail = {
sanitizedHtmlBody: string;
attachments: MessageAttachment[];
connectedAccount: ConnectedAccountWorkspaceEntity;
inReplyTo?: string;
};
@@ -1,13 +1,13 @@
import { Injectable } from '@nestjs/common';
import { z } from 'zod';
import { type MessageOutboundDriver } from 'src/modules/messaging/message-outbound-manager/interfaces/message-outbound-driver.interface';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { toMicrosoftRecipients } from 'src/modules/messaging/message-import-manager/utils/to-microsoft-recipients.util';
import { type SendMessageInput } from 'src/modules/messaging/message-outbound-manager/types/send-message-input.type';
import { type Client as MicrosoftGraphClient } from '@microsoft/microsoft-graph-client';
import { isDefined } from 'twenty-shared/utils';
@Injectable()
export class MicrosoftMessageOutboundService implements MessageOutboundDriver {
@@ -24,13 +24,12 @@ export class MicrosoftMessageOutboundService implements MessageOutboundDriver {
connectedAccount,
);
const message = this.composeMicrosoftMessage(sendMessageInput);
const messageId = await this.createDraftMessage(
microsoftClient,
sendMessageInput,
);
const response = await microsoftClient.api(`/me/messages`).post(message);
z.string().parse(response.id);
await microsoftClient.api(`/me/messages/${response.id}/send`).post({});
await microsoftClient.api(`/me/messages/${messageId}/send`).post({});
}
async createDraft(
@@ -42,9 +41,50 @@ export class MicrosoftMessageOutboundService implements MessageOutboundDriver {
connectedAccount,
);
await this.createDraftMessage(microsoftClient, sendMessageInput);
}
private async createDraftMessage(
microsoftClient: MicrosoftGraphClient,
sendMessageInput: SendMessageInput,
): Promise<string> {
const parentMessageGraphId = sendMessageInput.inReplyTo
? await this.findMessageByInternetMessageId(
microsoftClient,
sendMessageInput.inReplyTo,
)
: undefined;
const message = this.composeMicrosoftMessage(sendMessageInput);
await microsoftClient.api(`/me/messages`).post(message);
if (isDefined(parentMessageGraphId)) {
const reply = await microsoftClient
.api(`/me/messages/${parentMessageGraphId}/createReply`)
.post({});
await microsoftClient.api(`/me/messages/${reply.id}`).patch(message);
return reply.id;
}
const response = await microsoftClient.api('/me/messages').post(message);
return response.id;
}
private async findMessageByInternetMessageId(
microsoftClient: MicrosoftGraphClient,
internetMessageId: string,
): Promise<string | undefined> {
const encodedId = encodeURIComponent(internetMessageId);
const response = await microsoftClient
.api(
`/me/messages?$filter=internetMessageId eq '${encodedId}'&$select=id&$top=1`,
)
.get();
return response?.value?.[0]?.id;
}
private composeMicrosoftMessage(
@@ -12,4 +12,5 @@ export type SendMessageInput = {
content: Buffer;
contentType: string;
}[];
inReplyTo?: string;
};
@@ -21,5 +21,11 @@ export const toMailComposerOptions = (
})),
}
: {}),
...(sendMessageInput.inReplyTo
? {
inReplyTo: sendMessageInput.inReplyTo,
references: sendMessageInput.inReplyTo,
}
: {}),
};
};
@@ -5,4 +5,5 @@ export type WorkflowSendEmailActionInput = {
recipients: EmailRecipients;
subject?: string;
body?: string;
inReplyTo?: string;
};
@@ -272,6 +272,8 @@ export const DOCUMENTATION_PATHS = {
'/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty',
USER_GUIDE_WORKFLOWS_HOW_TOS_CONNECT_TO_OTHER_TOOLS_SET_UP_A_WEBHOOK_TRIGGER:
'/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger',
USER_GUIDE_WORKFLOWS_HOW_TOS_CRM_AUTOMATIONS_AUTO_REPLY_TO_INBOUND_EMAILS:
'/user-guide/workflows/how-tos/crm-automations/auto-reply-to-inbound-emails',
USER_GUIDE_WORKFLOWS_HOW_TOS_CRM_AUTOMATIONS_CLOSED_WON_AUTOMATIONS:
'/user-guide/workflows/how-tos/crm-automations/closed-won-automations',
USER_GUIDE_WORKFLOWS_HOW_TOS_CRM_AUTOMATIONS_DETECT_STALE_OPPORTUNITIES:
@@ -14,5 +14,6 @@ export const workflowSendEmailActionSettingsSchema =
subject: z.string().optional(),
body: z.string().optional(),
files: z.array(workflowFileSchema).optional().default([]),
inReplyTo: z.string().trim().optional(),
}),
});