diff --git a/products/messaging/backend/presentation/views/message_templates.py b/products/messaging/backend/presentation/views/message_templates.py
index a4c0d5785e39..3573f6ffa6e1 100644
--- a/products/messaging/backend/presentation/views/message_templates.py
+++ b/products/messaging/backend/presentation/views/message_templates.py
@@ -791,7 +791,8 @@ def _require_transactional_email_flag(self) -> None:
summary="Make a template transactional",
description=(
"Make this template sendable over the transactional email API. This creates the workflow that sends it, "
- "with its own metrics, run history and versions. Calling it again on a transactional template changes "
+ "with its own metrics, run history and versions. This can't be undone: the template stays transactional. "
+ "Transactional sends ignore marketing opt-outs. Calling it again on a transactional template changes "
"nothing."
),
parameters=[TEMPLATE_ID_PARAMETER],
diff --git a/products/messaging/frontend/generated/api.ts b/products/messaging/frontend/generated/api.ts
index 17f050bec0ab..37b659d0838b 100644
--- a/products/messaging/frontend/generated/api.ts
+++ b/products/messaging/frontend/generated/api.ts
@@ -730,7 +730,7 @@ export const getMessagingTemplatesMakeTransactionalCreateUrl = (projectId: strin
}
/**
- * Make this template sendable over the transactional email API. This creates the workflow that sends it, with its own metrics, run history and versions. Calling it again on a transactional template changes nothing.
+ * Make this template sendable over the transactional email API. This creates the workflow that sends it, with its own metrics, run history and versions. This can't be undone: the template stays transactional. Transactional sends ignore marketing opt-outs. Calling it again on a transactional template changes nothing.
* @summary Make a template transactional
*/
export const messagingTemplatesMakeTransactionalCreate = async (
diff --git a/products/messaging/frontend/generated/api.zod.ts b/products/messaging/frontend/generated/api.zod.ts
index bcbdb2664678..3816dda855be 100644
--- a/products/messaging/frontend/generated/api.zod.ts
+++ b/products/messaging/frontend/generated/api.zod.ts
@@ -1083,7 +1083,7 @@ export const MessagingTemplatesDesignPartialUpdateBody = /* @__PURE__ */ zod.obj
})
/**
- * Make this template sendable over the transactional email API. This creates the workflow that sends it, with its own metrics, run history and versions. Calling it again on a transactional template changes nothing.
+ * Make this template sendable over the transactional email API. This creates the workflow that sends it, with its own metrics, run history and versions. This can't be undone: the template stays transactional. Transactional sends ignore marketing opt-outs. Calling it again on a transactional template changes nothing.
* @summary Make a template transactional
*/
export const MessagingTemplatesMakeTransactionalCreateBody = /* @__PURE__ */ zod.object({
diff --git a/products/workflows/frontend/TemplateLibrary/MakeTransactionalPrompt.tsx b/products/workflows/frontend/TemplateLibrary/MakeTransactionalPrompt.tsx
index 94c014a42813..a8ace8d3333f 100644
--- a/products/workflows/frontend/TemplateLibrary/MakeTransactionalPrompt.tsx
+++ b/products/workflows/frontend/TemplateLibrary/MakeTransactionalPrompt.tsx
@@ -34,6 +34,7 @@ export function MakeTransactionalPrompt(props: MessageTemplateSceneLogicProps):
Your edits save as a draft. Publishing updates the live email, and each publish keeps a version.
Transactional emails ignore marketing opt-outs, so don't use them for marketing.
+ You can't undo this. The template stays transactional.
+ Make an email template sendable from code with one API call, for emails a person triggers such as password
+ resets, receipts and sign-in codes. This is one-way: the template can't be made non-transactional again.
+ Transactional sends ignore marketing opt-outs, so never use this for marketing email. The template needs a
+ subject and a body, and the project needs a verified email sender (pass from_integration_id to pick one,
+ otherwise the project's default sender is used). Requires human typed confirmation. The response has
+ transactional_hog_flow_id, the workflow that sends the email. Share the template's _posthogUrl so the user
+ can review and publish it.
+ feature_flag: workflows-transactional-email-api
+ response:
+ exclude:
+ - content
+ - draft_content
+ - created_by
+ confirmed_action:
+ message: >
+ About to make email template '{id}' transactional. This can't be undone, and its sends will ignore
+ marketing opt-outs. Reply 'confirm' to proceed.
+ action_label: make email template transactional
workflows-patch-email-template:
operation: messaging_templates_design_partial_update
enabled: true
diff --git a/services/mcp/schema/generated-tool-definitions.json b/services/mcp/schema/generated-tool-definitions.json
index 470830dfb205..0e437e8d6f1f 100644
--- a/services/mcp/schema/generated-tool-definitions.json
+++ b/services/mcp/schema/generated-tool-definitions.json
@@ -16403,6 +16403,37 @@
"readOnlyHint": true
}
},
+ "workflows-make-email-template-transactional-execute": {
+ "description": "Step 2 of 2 for make email template transactional. Verifies the confirmation_hash from -prepare and the literal \"confirm\" string typed by the user, then performs the action. ONLY call this after the user has explicitly typed \"confirm\" in chat. Original action: Make an email template sendable from code with one API call, for emails a person triggers such as password resets, receipts and sign-in codes. This is one-way: the template can't be made non-transactional again. Transactional sends ignore marketing opt-outs, so never use this for marketing email. The template needs a subject and a body, and the project needs a verified email sender (pass from_integration_id to pick one, otherwise the project's default sender is used). Requires human typed confirmation. The response has transactional_hog_flow_id, the workflow that sends the email. Share the template's _posthogUrl so the user can review and publish it.",
+ "category": "Workflows",
+ "feature": "workflows",
+ "summary": "Make email template transactional (execute)",
+ "title": "Make email template transactional (execute)",
+ "required_scopes": ["hog_flow:write"],
+ "api_operations": ["POST /api/projects/{project_id}/messaging_templates/{id}/make_transactional/"],
+ "annotations": {
+ "destructiveHint": true,
+ "idempotentHint": true,
+ "openWorldHint": true,
+ "readOnlyHint": false
+ },
+ "feature_flag": "workflows-transactional-email-api"
+ },
+ "workflows-make-email-template-transactional-prepare": {
+ "description": "Step 1 of 2 for make email template transactional. Validates the arguments and returns a signed confirmation_hash plus a message to surface to the user. The user must reply with the literal word \"confirm\" before you call the matching -execute tool with the hash. Original action: Make an email template sendable from code with one API call, for emails a person triggers such as password resets, receipts and sign-in codes. This is one-way: the template can't be made non-transactional again. Transactional sends ignore marketing opt-outs, so never use this for marketing email. The template needs a subject and a body, and the project needs a verified email sender (pass from_integration_id to pick one, otherwise the project's default sender is used). Requires human typed confirmation. The response has transactional_hog_flow_id, the workflow that sends the email. Share the template's _posthogUrl so the user can review and publish it.",
+ "category": "Workflows",
+ "feature": "workflows",
+ "summary": "Make email template transactional (prepare)",
+ "title": "Make email template transactional (prepare)",
+ "required_scopes": ["hog_flow:write"],
+ "annotations": {
+ "destructiveHint": false,
+ "idempotentHint": true,
+ "openWorldHint": true,
+ "readOnlyHint": true
+ },
+ "feature_flag": "workflows-transactional-email-api"
+ },
"workflows-patch-action-email": {
"description": "Surgically edit the email inside one function_email step - the fastest path for copy tweaks and design changes on a workflow email. Use operations (the same design ops as workflows-patch-email-template, addressing blocks by id from the step's config.inputs.email.value.design) to edit the design, and email_patch to change subject, preheader, text, or recipients. After design ops the HTML is re-rendered server-side, so the sent email always matches the design. Edits apply atomically: an invalid batch changes nothing. On an active workflow, edits stage a draft (composing with any staged graph edits) and nothing reaches real people until workflows-publish. The full updated workflow is returned. Prefer this over workflows-patch-graph update_action for email content - it's smaller to send and keeps design and HTML in sync. For library templates use workflows-patch-email-template instead.",
"category": "Workflows",
diff --git a/services/mcp/schema/tool-definitions-all.json b/services/mcp/schema/tool-definitions-all.json
index 4cd55998d085..a4cd456aa0b2 100644
--- a/services/mcp/schema/tool-definitions-all.json
+++ b/services/mcp/schema/tool-definitions-all.json
@@ -17192,6 +17192,37 @@
"readOnlyHint": true
}
},
+ "workflows-make-email-template-transactional-execute": {
+ "description": "Step 2 of 2 for make email template transactional. Verifies the confirmation_hash from -prepare and the literal \"confirm\" string typed by the user, then performs the action. ONLY call this after the user has explicitly typed \"confirm\" in chat. Original action: Make an email template sendable from code with one API call, for emails a person triggers such as password resets, receipts and sign-in codes. This is one-way: the template can't be made non-transactional again. Transactional sends ignore marketing opt-outs, so never use this for marketing email. The template needs a subject and a body, and the project needs a verified email sender (pass from_integration_id to pick one, otherwise the project's default sender is used). Requires human typed confirmation. The response has transactional_hog_flow_id, the workflow that sends the email. Share the template's _posthogUrl so the user can review and publish it.",
+ "category": "Workflows",
+ "feature": "workflows",
+ "summary": "Make email template transactional (execute)",
+ "title": "Make email template transactional (execute)",
+ "required_scopes": ["hog_flow:write"],
+ "api_operations": ["POST /api/projects/{project_id}/messaging_templates/{id}/make_transactional/"],
+ "annotations": {
+ "destructiveHint": true,
+ "idempotentHint": true,
+ "openWorldHint": true,
+ "readOnlyHint": false
+ },
+ "feature_flag": "workflows-transactional-email-api"
+ },
+ "workflows-make-email-template-transactional-prepare": {
+ "description": "Step 1 of 2 for make email template transactional. Validates the arguments and returns a signed confirmation_hash plus a message to surface to the user. The user must reply with the literal word \"confirm\" before you call the matching -execute tool with the hash. Original action: Make an email template sendable from code with one API call, for emails a person triggers such as password resets, receipts and sign-in codes. This is one-way: the template can't be made non-transactional again. Transactional sends ignore marketing opt-outs, so never use this for marketing email. The template needs a subject and a body, and the project needs a verified email sender (pass from_integration_id to pick one, otherwise the project's default sender is used). Requires human typed confirmation. The response has transactional_hog_flow_id, the workflow that sends the email. Share the template's _posthogUrl so the user can review and publish it.",
+ "category": "Workflows",
+ "feature": "workflows",
+ "summary": "Make email template transactional (prepare)",
+ "title": "Make email template transactional (prepare)",
+ "required_scopes": ["hog_flow:write"],
+ "annotations": {
+ "destructiveHint": false,
+ "idempotentHint": true,
+ "openWorldHint": true,
+ "readOnlyHint": true
+ },
+ "feature_flag": "workflows-transactional-email-api"
+ },
"workflows-patch-action-email": {
"description": "Surgically edit the email inside one function_email step - the fastest path for copy tweaks and design changes on a workflow email. Use operations (the same design ops as workflows-patch-email-template, addressing blocks by id from the step's config.inputs.email.value.design) to edit the design, and email_patch to change subject, preheader, text, or recipients. After design ops the HTML is re-rendered server-side, so the sent email always matches the design. Edits apply atomically: an invalid batch changes nothing. On an active workflow, edits stage a draft (composing with any staged graph edits) and nothing reaches real people until workflows-publish. The full updated workflow is returned. Prefer this over workflows-patch-graph update_action for email content - it's smaller to send and keeps design and HTML in sync. For library templates use workflows-patch-email-template instead.",
"category": "Workflows",
diff --git a/services/mcp/src/generated/email_templates/api.ts b/services/mcp/src/generated/email_templates/api.ts
index 74397267a1a2..7c992efdbae0 100644
--- a/services/mcp/src/generated/email_templates/api.ts
+++ b/services/mcp/src/generated/email_templates/api.ts
@@ -3,7 +3,7 @@
* MCP service uses these Zod schemas for generated tool handlers.
* To regenerate: hogli build:openapi
*
- * PostHog API - MCP 5 enabled ops
+ * PostHog API - MCP 6 enabled ops
* OpenAPI spec version: 1.0.0
*/
import * as zod from 'zod'
@@ -396,3 +396,25 @@ export const MessagingTemplatesDesignPartialUpdateBody = () => zod.object({
"Ordered edits applied atomically to a template's Unlayer design: the stored design is read, the ops are applied in order, the result is validated and re-rendered to HTML, and it's saved only if valid — otherwise the template is unchanged. Reference blocks by id so you never resend the whole design."
),
})
+
+/**
+ * Make this template sendable over the transactional email API. This creates the workflow that sends it, with its own metrics, run history and versions. This can't be undone: the template stays transactional. Transactional sends ignore marketing opt-outs. Calling it again on a transactional template changes nothing.
+ * @summary Make a template transactional
+ */
+export const MessagingTemplatesMakeTransactionalCreateParams = () => zod.object({
+ id: zod.string().describe('A UUID string identifying this message template.'),
+ project_id: zod
+ .string()
+ .describe(
+ "Project ID of the project you're trying to access. To find the ID of the project, make a call to \/api\/projects\/."
+ ),
+})
+
+export const MessagingTemplatesMakeTransactionalCreateBody = () => zod.object({
+ from_integration_id: zod
+ .number()
+ .nullish()
+ .describe(
+ "ID of the verified email sender integration the email is sent from. Defaults to the project's default sender."
+ ),
+})
diff --git a/services/mcp/src/tools/generated/email_templates.ts b/services/mcp/src/tools/generated/email_templates.ts
index 4771f42e4c62..b102d2dd0916 100644
--- a/services/mcp/src/tools/generated/email_templates.ts
+++ b/services/mcp/src/tools/generated/email_templates.ts
@@ -5,6 +5,12 @@ import type { Schemas } from '@/api/generated'
import * as orvalSchemas from '@/generated/email_templates/api'
import { withUiApp } from '@/resources/ui-apps'
import { EmailTemplateDesignPatchSchema } from '@/schema/tool-inputs'
+import { getConfirmedActionRuntime } from '@/tools/confirmed-action-registry'
+import {
+ executeConfirmedAction,
+ prepareConfirmedAction,
+ type PrepareConfirmedActionResult,
+} from '@/tools/confirmed-action-runtime'
import { withPostHogUrl, omitResponseFields, type WithPostHogUrl } from '@/tools/tool-utils'
import type { Context, ToolBase, ZodObjectAny } from '@/tools/types'
@@ -124,6 +130,88 @@ const workflowsListEmailTemplates = (): ToolBase<
},
})
+const WorkflowsMakeEmailTemplateTransactionalSchema = () => {
+ const MessagingTemplatesMakeTransactionalCreateBody = orvalSchemas.MessagingTemplatesMakeTransactionalCreateBody()
+ const MessagingTemplatesMakeTransactionalCreateParams =
+ orvalSchemas.MessagingTemplatesMakeTransactionalCreateParams()
+ return MessagingTemplatesMakeTransactionalCreateParams.omit({ project_id: true }).extend(
+ MessagingTemplatesMakeTransactionalCreateBody.shape
+ )
+}
+
+const WorkflowsMakeEmailTemplateTransactionalSchemaExecute = z.strictObject({
+ confirmation_hash: z
+ .string()
+ .describe('The confirmation_hash returned by the matching -prepare tool. Pass it back verbatim.'),
+ confirmation: z.string().describe('The literal string "confirm", typed by the user in chat. Required to proceed.'),
+})
+
+const workflowsMakeEmailTemplateTransactionalPrepare = (): ToolBase<
+ ReturnType,
+ PrepareConfirmedActionResult
+> => ({
+ name: 'workflows-make-email-template-transactional-prepare',
+ schema: WorkflowsMakeEmailTemplateTransactionalSchema(),
+ handler: async (
+ context: Context,
+ params: z.infer>
+ ) => {
+ const __runtime = getConfirmedActionRuntime()
+ const __scopeProjectId = await context.stateManager.getProjectId()
+ return await prepareConfirmedAction(context, {
+ args: params,
+ purpose: 'workflows-make-email-template-transactional',
+ actionLabel: 'make email template transactional',
+ messageTemplate:
+ "About to make email template '{id}' transactional. This can't be undone, and its sends will ignore marketing opt-outs. Reply 'confirm' to proceed.\n",
+ codec: __runtime.codec,
+ stash: __runtime.stash,
+ boundScope: { projectId: String(__scopeProjectId) },
+ })
+ },
+})
+
+const workflowsMakeEmailTemplateTransactionalExecute = (): ToolBase<
+ typeof WorkflowsMakeEmailTemplateTransactionalSchemaExecute,
+ WithPostHogUrl
+> => ({
+ name: 'workflows-make-email-template-transactional-execute',
+ schema: WorkflowsMakeEmailTemplateTransactionalSchemaExecute,
+ handler: async (
+ context: Context,
+ confirmationParams: z.infer
+ ) => {
+ const __runtime = getConfirmedActionRuntime()
+ const __scopeProjectId = await context.stateManager.getProjectId()
+ const __guard = await executeConfirmedAction<
+ z.infer>
+ >(context, {
+ incomingArgs: confirmationParams,
+ purpose: 'workflows-make-email-template-transactional',
+ codec: __runtime.codec,
+ ledger: __runtime.ledger,
+ stash: __runtime.stash,
+ expectedScope: { projectId: String(__scopeProjectId) },
+ })
+ if (!__guard.ok) {
+ return __guard.result as never
+ }
+ const params = __guard.verifiedArgs
+ const projectId = __scopeProjectId
+ const body: Record = {}
+ if (params.from_integration_id !== undefined) {
+ body['from_integration_id'] = params.from_integration_id
+ }
+ const result = await context.api.request({
+ method: 'POST',
+ path: `/api/projects/${encodeURIComponent(String(projectId))}/messaging_templates/${encodeURIComponent(String(params.id))}/make_transactional/`,
+ body,
+ })
+ const filtered = omitResponseFields(result, ['content', 'draft_content', 'created_by']) as typeof result
+ return await withPostHogUrl(context, filtered, `/workflows/library/templates/${filtered.id}`)
+ },
+})
+
const WorkflowsPatchEmailTemplateSchema = () => EmailTemplateDesignPatchSchema
const workflowsPatchEmailTemplate = (): ToolBase<
@@ -228,6 +316,8 @@ export const GENERATED_TOOLS: Record ToolBase> = {
'workflows-create-email-template': workflowsCreateEmailTemplate,
'workflows-get-email-template': workflowsGetEmailTemplate,
'workflows-list-email-templates': workflowsListEmailTemplates,
+ 'workflows-make-email-template-transactional-prepare': workflowsMakeEmailTemplateTransactionalPrepare,
+ 'workflows-make-email-template-transactional-execute': workflowsMakeEmailTemplateTransactionalExecute,
'workflows-patch-email-template': workflowsPatchEmailTemplate,
'workflows-show-email-template': workflowsShowEmailTemplate,
'workflows-update-email-template': workflowsUpdateEmailTemplate,
diff --git a/services/mcp/tests/unit/__snapshots__/tool-schemas/workflows-make-email-template-transactional-execute.json b/services/mcp/tests/unit/__snapshots__/tool-schemas/workflows-make-email-template-transactional-execute.json
new file mode 100644
index 000000000000..f9e863b2ae53
--- /dev/null
+++ b/services/mcp/tests/unit/__snapshots__/tool-schemas/workflows-make-email-template-transactional-execute.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "additionalProperties": false,
+ "properties": {
+ "confirmation": {
+ "description": "The literal string \"confirm\", typed by the user in chat. Required to proceed.",
+ "type": "string"
+ },
+ "confirmation_hash": {
+ "description": "The confirmation_hash returned by the matching -prepare tool. Pass it back verbatim.",
+ "type": "string"
+ }
+ },
+ "required": ["confirmation_hash", "confirmation"],
+ "type": "object"
+}
diff --git a/services/mcp/tests/unit/__snapshots__/tool-schemas/workflows-make-email-template-transactional-prepare.json b/services/mcp/tests/unit/__snapshots__/tool-schemas/workflows-make-email-template-transactional-prepare.json
new file mode 100644
index 000000000000..778419a42fd7
--- /dev/null
+++ b/services/mcp/tests/unit/__snapshots__/tool-schemas/workflows-make-email-template-transactional-prepare.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "from_integration_id": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "ID of the verified email sender integration the email is sent from. Defaults to the project's default sender."
+ },
+ "id": {
+ "description": "A UUID string identifying this message template.",
+ "type": "string"
+ }
+ },
+ "required": ["id"],
+ "type": "object"
+}
diff --git a/services/mcp/tests/unit/tool-filtering.test.ts b/services/mcp/tests/unit/tool-filtering.test.ts
index ba3f52d9fabb..a173c99cc070 100644
--- a/services/mcp/tests/unit/tool-filtering.test.ts
+++ b/services/mcp/tests/unit/tool-filtering.test.ts
@@ -1110,6 +1110,7 @@ describe('Tool Filtering - Feature Flags', () => {
'marketing-analytics-conversion-setup-ai',
'insight-data-checks',
'logs-agent-tools',
+ 'workflows-transactional-email-api',
]
expect(allFlags).toEqual(expect.arrayContaining(branchFlags))
// The flags branches add are asserted on the line above and held out of the list and
diff --git a/services/mcp/tests/unit/tool-schema-snapshots.test.ts b/services/mcp/tests/unit/tool-schema-snapshots.test.ts
index de6cbedff36c..cb4bbac08589 100644
--- a/services/mcp/tests/unit/tool-schema-snapshots.test.ts
+++ b/services/mcp/tests/unit/tool-schema-snapshots.test.ts
@@ -107,6 +107,7 @@ async function loadSnapshotTools(): Promise[]> {
'marketing-analytics-conversion-setup-ai': true,
'workflows-mcp-onboarding': true,
'insight-data-checks': true,
+ 'workflows-transactional-email-api': true,
}
return [...(await getToolsFromContext(createMockContext(), { featureFlags }))].sort((a, b) =>
a.name.localeCompare(b.name)