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)