Skip to content

Use Amazon Bedrock to Generate Content for Amazon Connect Outbound Campaigns

19 minute read
Content level: Advanced
1

Use AWS Lambda and Amazon Bedrock to dynamically transform Message Template content during Amazon Connect outbound campaign execution - enabling multilingual translation, personalization, and AI-powered content generation at scale.

Overview

This solution integrates Amazon Bedrock with Amazon Connect outbound campaigns through a Journey Flow, AWS Lambda, and the GetMessageTemplate API. A campaign operator writes content in a source Message Template, and at execution time a Lambda function reads that template, processes it through a large language model based on each customer's attributes, and delivers personalized results.

This pattern supports multiple use cases:

  • Multilingual campaigns — translate a single template into each customer's preferred language
  • Tone adaptation — adjust formality based on customer segment or region
  • Content personalization — tailor messaging based on profile attributes or purchase history
  • Summarization — condense detailed content into shorter formats for specific channels
  • Appointment reminders — generate context-aware messages with customer-specific details

Architecture

The solution follows this flow:

  1. Campaign operator creates a source Message Template with content in the default language.
  2. Journey starts and invokes the Lambda via the Custom action block with a batch of customer profiles.
  3. Lambda reads the source template content via the GetMessageTemplate API (cached for 5 minutes).
  4. For each customer profile, Lambda extracts attributes (locale, name, segment) and invokes Bedrock to transform the content.
  5. Lambda writes the transformed subject and body to the Customer Profile.
  6. The Send communication block delivers the email using a delivery Message Template that resolves the transformed content from the profile.

Prerequisites

  • An Amazon Connect instance (see setup details in the Prerequisites section below)
  • Amazon Bedrock model access enabled
  • Customer profiles with FirstName, LastName, emailAddress, and a Locale custom attribute

Time to implement: approximately 30-45 minutes.

Costs: This solution incurs Amazon Bedrock charges for each content transformation. See Amazon Bedrock pricing. Amazon Connect outbound campaign and email delivery charges also apply.


Prerequisites: Configure your Amazon Connect instance

Before building the solution, ensure your Connect instance has the required features enabled.

A. Enable outbound campaigns

  1. Open the Amazon Connect console → select your instance.
  2. In the navigation pane, choose Channels and communicationsOutbound campaigns.
  3. If not already enabled, follow the prompts to enable outbound campaigns.
  4. This enables the Journey builder, Custom actions, and Send communication blocks.

B. Enable the email channel

  1. In the Connect console, go to your instance → Channels and messagingEmail.
  2. If email is not enabled, choose Enable email.
  3. Configure an S3 bucket for email attachments storage (the console guides you through this).
  4. Verify your email sending domain or email address:
    • Go to Email addressesAdd email address
    • Enter your outbound email address (e.g., campaigns@yourdomain.com)
    • Complete domain verification (DKIM/SPF) — Connect will provide DNS records to add to your domain
    • Wait for verification status to show Verified
  5. Note your verified outbound email address — you'll use it in the Send communication block.

C. Enable Customer Profiles

  1. In the Connect console, go to your instance → Customer Profiles.
  2. If not enabled, choose Enable Customer Profiles.
  3. Note your Customer Profiles domain name (format: amazon-connect-<instance-alias>).
  4. Import or create customer profiles with the required fields: FirstName, LastName, emailAddress, and Locale (in the attributes map).

D. Enable AI Agents (Amazon Q in Connect)

The Lambda uses the GetMessageTemplate API to read source template content. This API is part of the Amazon Q in Connect service (labeled AI Agents in the Connect console). Message Templates are stored in a knowledge base that is automatically created when you enable outbound campaigns on your instance.

Note on naming: The boto3 client uses qconnect as the service name, but the IAM action uses the legacy wisdom: prefix (e.g., wisdom:GetMessageTemplate). Both refer to the same service — Amazon Q in Connect.

How to verify it's enabled:

If you can already create Message Templates in your Connect admin site (under Message Templates), then AI Agents is already active and a knowledge base exists.

How to find your knowledge base ID:

Run this CLI command:

aws qconnect list-knowledge-bases --region <your-region>

Look for the entry with your instance name (e.g., amazon-connect-<your-instance-alias>) and knowledgeBaseType: MESSAGE_TEMPLATES. Note the knowledgeBaseId value.

How to find a message template ID:

After creating your source template (Step 5), run:

aws qconnect list-message-templates \
  --knowledge-base-id <your-knowledge-base-id> \
  --region <your-region>

Find your template by name and note the messageTemplateId value.

How to verify the template content is readable:

aws qconnect get-message-template \
  --knowledge-base-id <your-knowledge-base-id> \
  --message-template-id <your-template-id> \
  --region <your-region> \
  --query "messageTemplate.content"

This returns the template's subject and body content — which is exactly what the Lambda reads at runtime.

E. Register a Lambda function for outbound campaigns

This step connects your Lambda to the Connect instance so Journey Flows can invoke it.

  1. In the Connect console, go to your instance → Channels and communicationsOutbound campaigns.
  2. Under Set up custom actionsLambda Functions, you will add your function here after creating it in Step 4.

F. Understand how email delivery works in Journey Flows

The Journey Flow email delivery process works as follows:

  1. Journey starts — selects customers from a segment based on configured audience criteria.
  2. Custom action block — invokes your Lambda function with batches of customer profiles (up to 10 per invocation). The Lambda must respond within 30 seconds.
  3. Send communication block — delivers the email using:
    • A Message Template for content (resolves placeholders from Customer Profile attributes)
    • The From address you configured (must be verified)
    • The recipient's email from their Customer Profile's emailAddress field
  4. Email delivery — Connect routes the email through Amazon SES for actual delivery.

Key constraints:

  • The Lambda response must complete within 30 seconds or the Journey treats it as a failure
  • Message Template placeholders that cannot be resolved cause the email to be silently dropped
  • Custom attributes in Customer Profiles are limited to 255 characters per value
  • The AdditionalInformation standard profile field supports up to 1000 characters
  • All profiles in the segment must have a valid emailAddress or they will be skipped

G. Understand Journey Flow blocks

The Journey Flow builder provides these blocks for our solution:

BlockPurposeConfiguration
Custom actionInvokes the Lambda functionSelect your registered Lambda ARN
Send communicationSends the email via templateConfigure From address, template, version
End flowTerminates the journey for a recipientNo configuration needed

The flow connects as: Custom action → Send communication → End flow. Both success and error paths from Send communication lead to End flow.


Step 1: Enable Bedrock model access

  1. Open the Amazon Bedrock console.
  2. Go to Model accessManage model access.
  3. Request access to Anthropic Claude Sonnet if not already enabled.
  4. Wait for access to be granted.

Step 2: Create the source Message Template

This template contains your email content in the default language (e.g., English). The Lambda reads this template and translates it.

  1. Open Amazon Connect consoleMessage TemplatesCreate template.
  2. Configure:
SettingValue
NameSource-Campaign-Email
ChannelEmail
SubjectYour appointment is tomorrow, {{CustomerName}}
  1. In the Body, write your email content in your default language:
Dear {{CustomerName}},

This is a friendly reminder that your appointment is scheduled for tomorrow. Please arrive 15 minutes early and bring a valid ID. If you need to reschedule, please contact us at least 24 hours in advance.

Regards,
Customer Support Team
  1. Save and publish the template.
  2. Note the template ID and knowledge base ID from the URL (format: https://<instance>.my.connect.aws/message-templates/<knowledge-base-id>/<template-id>).

Step 3: Create SSM parameters

The Lambda reads configuration from SSM Parameter Store. Create these parameters using the template ID and knowledge base ID from Step 2.

  1. Open AWS Systems Manager consoleParameter StoreCreate parameter
  2. Create each parameter:
NameTypeValue
/connect-campaigns/outbound-email/default-localeStringen
/connect-campaigns/outbound-email/max-retry-attemptsString3
/connect-campaigns/outbound-email/llm-model-idString(see note below)
/connect-campaigns/outbound-email/source-template-idString(template ID from Step 2)
/connect-campaigns/outbound-email/knowledge-base-idString(knowledge base ID from Step 2)
/connect-campaigns/outbound-email/connect-instance-idString(your Connect instance UUID)

Note: For llm-model-id, use the current inference profile ID available in your region (e.g., us.anthropic.claude-sonnet-4-20250514-v1:0). Check Amazon Bedrock model IDs for the latest.


Step 4: Create the Lambda function

  1. Open AWS Lambda consoleCreate functionAuthor from scratch
  2. Configure:
SettingValue
Function nameconnect-outbound-email-generator
RuntimePython 3.12
Architecturearm64
Timeout120 seconds
Memory512 MB
  1. Attach the following inline IAM policy to the Lambda execution role:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "BedrockInvoke",
      "Effect": "Allow",
      "Action": "bedrock:InvokeModel",
      "Resource": [
        "arn:aws:bedrock:*::foundation-model/anthropic.*",
        "arn:aws:bedrock:*:ACCOUNT_ID:inference-profile/us.anthropic.*"
      ]
    },
    {
      "Sid": "SSMRead",
      "Effect": "Allow",
      "Action": ["ssm:GetParameter", "ssm:GetParametersByPath"],
      "Resource": "arn:aws:ssm:*:ACCOUNT_ID:parameter/connect-campaigns/outbound-email/*"
    },
    {
      "Sid": "CustomerProfiles",
      "Effect": "Allow",
      "Action": "profile:UpdateProfile",
      "Resource": "arn:aws:profile:*:ACCOUNT_ID:domains/*"
    },
    {
      "Sid": "QConnectReadTemplate",
      "Effect": "Allow",
      "Action": "wisdom:GetMessageTemplate",
      "Resource": "arn:aws:wisdom:*:ACCOUNT_ID:message-template/*/*"
    },
    {
      "Sid": "CloudWatch",
      "Effect": "Allow",
      "Action": "cloudwatch:PutMetricData",
      "Resource": "*"
    }
  ]
}

Note: The CloudWatch PutMetricData action requires Resource: "*" because CloudWatch does not support resource-level permissions for this action.

⚠️ Important: Replace all instances of ACCOUNT_ID in the policy above with your actual 12-digit AWS account ID. You can find it by running aws sts get-caller-identity.

  1. Set environment variables:
KeyValue
SSM_PARAMETER_PATH/connect-campaigns/outbound-email/
CUSTOMER_PROFILES_DOMAINamazon-connect-<your-instance-alias>
AWS_REGION_NAMEYour region (e.g., us-east-1)
  1. Paste the following function code:
import json
import logging
import os
import random
import re
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

import boto3
from botocore.exceptions import ClientError

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# --- Configuration ---
SSM_PATH = os.environ.get("SSM_PARAMETER_PATH", "/connect-campaigns/outbound-email/")
CP_DOMAIN = os.environ.get("CUSTOMER_PROFILES_DOMAIN", "")
REGION = os.environ.get("AWS_REGION_NAME", "us-east-1")

# BCP-47 locale validation
BCP47_PATTERN = re.compile(r"^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$")

# Locale to language name mapping
LOCALE_TO_LANGUAGE = {
    "en": "English", "fr": "French", "es": "Spanish", "de": "German",
    "it": "Italian", "pt": "Portuguese", "ja": "Japanese", "ko": "Korean",
    "zh": "Chinese", "ar": "Arabic", "hi": "Hindi", "nl": "Dutch",
    "ru": "Russian", "sv": "Swedish", "tr": "Turkish", "th": "Thai",
}

# --- Config loader with caching ---
_config_cache = None
_config_loaded_at = 0
CONFIG_TTL = 45

# --- Source template cache ---
_template_cache = None
_template_loaded_at = 0
TEMPLATE_TTL = 300  # Cache template for 5 minutes


def load_config():
    """Load config from SSM with caching."""
    global _config_cache, _config_loaded_at
    if _config_cache and (time.time() - _config_loaded_at) < CONFIG_TTL:
        return _config_cache

    defaults = {
        "default-locale": "en",
        "max-retry-attempts": "3",
        "llm-model-id": "us.anthropic.claude-sonnet-4-20250514-v1:0",
        "source-template-id": "",
        "knowledge-base-id": "",
        "connect-instance-id": "",
    }

    try:
        ssm = boto3.client("ssm", region_name=REGION)
        response = ssm.get_parameters_by_path(Path=SSM_PATH, Recursive=False)
        for param in response.get("Parameters", []):
            name = param["Name"].rsplit("/", 1)[-1]
            defaults[name] = param["Value"]
    except Exception as e:
        logger.error(f"SSM load failed, using defaults: {e}")

    _config_cache = defaults
    _config_loaded_at = time.time()
    return defaults


def load_source_template(config):
    """Read the source Message Template content via GetMessageTemplate API."""
    global _template_cache, _template_loaded_at
    if _template_cache and (time.time() - _template_loaded_at) < TEMPLATE_TTL:
        return _template_cache

    template_id = config.get("source-template-id", "")
    knowledge_base_id = config.get("knowledge-base-id", "")

    if not template_id or not knowledge_base_id:
        logger.error("source-template-id or knowledge-base-id not configured")
        return None

    try:
        qconnect = boto3.client("qconnect", region_name=REGION)
        response = qconnect.get_message_template(
            knowledgeBaseId=knowledge_base_id,
            messageTemplateId=template_id,
        )

        template_data = response.get("messageTemplate", {})
        content = template_data.get("content", {})
        email_content = content.get("email", {})

        subject = email_content.get("subject", "")
        body_html = email_content.get("body", {}).get("html", {}).get("content", "")
        body_plain = email_content.get("body", {}).get("plainText", {}).get("content", "")

        _template_cache = {
            "subject": subject,
            "body": body_plain or body_html,  # Prefer plain text; fall back to HTML
        }
        _template_loaded_at = time.time()
        logger.info(f"Loaded source template: subject='{subject[:50]}...'")
        return _template_cache

    except Exception as e:
        logger.error(f"Failed to load source template: {e}")
        return None


# --- Locale helpers ---
def validate_bcp47(locale):
    if not locale:
        return False
    return BCP47_PATTERN.match(locale) is not None


def resolve_locale(profile_locale, default_locale):
    if not profile_locale:
        return default_locale
    if not validate_bcp47(profile_locale):
        return default_locale
    return profile_locale


def get_language(locale):
    prefix = locale.split("-")[0].lower()
    return LOCALE_TO_LANGUAGE.get(prefix, locale)


# --- Bedrock invocation ---
def invoke_bedrock(prompt, model_id, max_retries=3, bedrock_client=None):
    """Invoke Bedrock with retry and exponential backoff."""
    bedrock = bedrock_client or boto3.client("bedrock-runtime", region_name=REGION)

    for attempt in range(1, max_retries + 1):
        try:
            body = json.dumps({
                "anthropic_version": "bedrock-2023-05-31",
                "max_tokens": 2048,
                "temperature": 0.3,  # Lower temperature for translation accuracy
                "top_p": 0.9,
                "messages": [{"role": "user", "content": prompt}],
            })

            response = bedrock.invoke_model(
                modelId=model_id,
                contentType="application/json",
                accept="application/json",
                body=body,
            )

            result = json.loads(response["body"].read())
            content = result.get("content", [])
            text = "".join(b.get("text", "") for b in content if b.get("type") == "text")
            return text, None

        except ClientError as e:
            code = e.response.get("Error", {}).get("Code", "")
            if code == "ThrottlingException" and attempt < max_retries:
                delay = min(1 * (2 ** (attempt - 1)), 60)
                jitter = delay * 0.1 * (2 * random.random() - 1)
                time.sleep(delay + jitter)
                continue
            if attempt == max_retries:
                return None, f"Bedrock failed after {max_retries} attempts: {e}"
            time.sleep(1)
        except Exception as e:
            if attempt == max_retries:
                return None, f"Bedrock error: {e}"
            time.sleep(1)

    return None, "Bedrock invocation failed"


# --- Response parsing ---
def parse_response(raw):
    """Parse JSON response from LLM."""
    stripped = raw.strip()
    match = re.match(r"^```(?:json)?\s*\n?(.*?)\n?\s*```$", stripped, re.DOTALL)
    if match:
        stripped = match.group(1).strip()

    try:
        parsed = json.loads(stripped)
    except (json.JSONDecodeError, TypeError) as e:
        return None, f"JSON parse error: {e}"

    if "subject" not in parsed:
        return None, "Missing field: subject"
    if "body" not in parsed:
        return None, "Missing field: body"

    return {"subject": parsed["subject"], "body": parsed["body"]}, None


# --- Main handler ---
def lambda_handler(event, context):
    """Handle Journey Flow batch invocations."""
    config = load_config()
    campaign_context = event.get("InvocationMetadata", {}).get("CampaignContext", {})  # Used for campaign name in prompt
    profiles = event["Items"]["CustomerProfiles"]
    model_id = config["llm-model-id"]
    max_retries = int(config["max-retry-attempts"])
    default_locale = config["default-locale"]
    results = []

    # Load the source template content
    source_template = load_source_template(config)
    if not source_template:
        logger.error("Cannot proceed without source template")
        # Return error for all profiles
        for profile in profiles:
            results.append({
                "Id": profile.get("ProfileId", "unknown"),
                "ResultData": {"errorCode": "TEMPLATE_NOT_FOUND"}
            })
        return {"Items": {"CustomerProfiles": results}}

    source_subject = source_template["subject"]
    source_body = source_template["body"]

    # Create boto3 clients once (reused across all profiles in the batch)
    cp_client = boto3.client("customer-profiles", region_name=REGION)
    bedrock_client = boto3.client("bedrock-runtime", region_name=REGION)

    def process_profile(profile):
        profile_id = profile.get("ProfileId", "unknown")
        try:
            customer_data = json.loads(profile.get("CustomerData", "{}"))
        except (json.JSONDecodeError, TypeError):
            customer_data = {}

        # Extract customer info
        first_name = customer_data.get("firstName", customer_data.get("FirstName", ""))
        last_name = customer_data.get("lastName", customer_data.get("LastName", ""))
        customer_name = f"{first_name} {last_name}".strip() or "Customer"

        # Resolve locale
        profile_locale = (
            customer_data.get("attributes", {}).get("Locale")
            or customer_data.get("attributes", {}).get("locale")
            or customer_data.get("Locale")
            or customer_data.get("locale")
            or ""
        )
        locale = resolve_locale(profile_locale or None, default_locale)
        language = get_language(locale)

        # If customer's locale matches the source template language, skip translation
        source_language = get_language(default_locale)
        if language == source_language:
            # No translation needed — use source content directly
            subject = source_subject.replace("{{CustomerName}}", customer_name)[:255]
            body = source_body.replace("{{CustomerName}}", customer_name)[:1000]
        else:
            # Build translation prompt
            prompt = (
                f"Translate the following email into {language}. "
                f"Maintain the same tone, meaning, and formatting. "
                f"Replace any placeholder like '{{{{CustomerName}}}}' with '{customer_name}'. "
                f"Keep the email concise — body must not exceed 900 characters.\n\n"
                f"SOURCE EMAIL:\n"
                f"Subject: {source_subject}\n"
                f"Body: {source_body}\n\n"
                f"Respond in JSON format ONLY:\n"
                f'{{\n  "subject": "<translated subject>",\n  "body": "<translated body>"\n}}'
            )

            # Invoke Bedrock for translation
            raw_response, error = invoke_bedrock(prompt, model_id, max_retries, bedrock_client)
            if error:
                logger.error(f"Translation failed for {profile_id}: {error}")
                return {"Id": profile_id, "ResultData": {"errorCode": "TRANSLATION_FAILED", "errorMessage": error}}

            # Parse response
            parsed, parse_error = parse_response(raw_response)
            if parse_error:
                logger.error(f"Parse failed for {profile_id}: {parse_error}")
                return {"Id": profile_id, "ResultData": {"errorCode": "PARSE_FAILED", "errorMessage": parse_error}}

            subject = parsed["subject"][:255]
            body = parsed["body"][:1000]

        # Write translated content to Customer Profile
        try:
            cp_client.update_profile(
                DomainName=CP_DOMAIN,
                ProfileId=profile_id,
                AdditionalInformation=body,
                Attributes={"emailSubject": subject},
            )
            logger.info(f"Updated profile {profile_id} with {language} content")
        except Exception as e:
            logger.error(f"Profile update failed for {profile_id}: {e}")

        return {"Id": profile_id, "ResultData": {"subject": subject, "body": body, "locale": locale}}

    # Process profiles concurrently (must complete within 30s)
    with ThreadPoolExecutor(max_workers=min(len(profiles), 10)) as executor:
        futures = {executor.submit(process_profile, p): p for p in profiles}
        for future in as_completed(futures):
            results.append(future.result())

    return {"Items": {"CustomerProfiles": results}}
  1. Click Deploy.

Step 5: Register the Lambda with your Connect instance

  1. Open Amazon Connect console → select your instance.
  2. Navigate to Channels and communicationsOutbound campaigns.
  3. Under Set up custom actionsLambda Functions, select connect-outbound-email-generator.
  4. Choose Add Lambda Function.

Step 6: Create the delivery Message Template

This template is used by the Send communication block. It resolves the translated content from the Customer Profile.

  1. Open Amazon Connect consoleMessage TemplatesCreate template.
  2. Configure:
SettingValue
NameDelivery-Localized-Email
ChannelEmail
Subject{{Attributes.Customer.Attributes.emailSubject}}
  1. Body (HTML):
<html>
<body>
  {{Attributes.Customer.AdditionalInformation}}
</body>
</html>
  1. Save and publish.

Tip: You can add CSS styling, branding elements, headers, and footers to the delivery template HTML. The {{Attributes.Customer.AdditionalInformation}} placeholder will be replaced with the translated body content within your branded layout.


Step 7: Create the Journey Flow

  1. Open Amazon Connect consoleFlowsCreate flow → select Journey flow.
  2. Name: AI-Translated-Outbound-Email
  3. Add blocks:
[Custom action] → [Send communication] → [End flow]
                        ↓ (error)
                   [End flow]
  1. Configure Custom action block:
PropertyValue
ActionInvoke Lambda action
Function ARNSelect connect-outbound-email-generator
  1. Configure Send communication block:
PropertyValue
ChannelEmail
FromYour verified outbound email address
Display nameYour brand name
Message templateDelivery-Localized-Email
Template alias or versionSelect your published version
  1. Connect both Success and Error from Send communication to End flow.
  2. Save and publish.

Step 8: Set up Customer Profiles

Ensure your customer profiles have:

FieldLocationRequiredExample
FirstNameStandard fieldYesMarie
LastNameStandard fieldNoDupont
emailAddressStandard fieldYesmarie@example.com
LocaleCustom attributesNofr (defaults to en)

Step 9: Create a segment and run the Journey

  1. Create a Customer Segment targeting your audience.
  2. Go to Outbound CampaignsCreate Journey.
  3. Select your segment and the AI-Translated-Outbound-Email flow.
  4. Configure communication time and guardrails.
  5. Schedule and publish.

Step 10: Verify

  1. Check CloudWatch Logs for the Lambda:
    • Look for "Updated profile" messages (success)
    • Look for "Loaded source template" (template read successful)
  2. Confirm emails arrive in the customer's language.
  3. Test by changing a profile's Locale attribute and re-running.

How it works

  1. Operator writes content once — in the source template (English or any default language)
  2. Journey invokes Lambda — sends a batch of customer profiles
  3. Lambda reads the source template — via GetMessageTemplate API (cached for 5 minutes)
  4. For each customer:
    • If customer's locale matches the source language → use content as-is (skip Bedrock call)
    • If different locale → send content to Bedrock for translation
  5. Lambda writes translated content — subject to Attributes.emailSubject, body to AdditionalInformation
  6. Journey sends email — delivery template resolves the translated content from the profile

Benefits of this approach:

  • Campaign operators write content once in one language
  • No need to maintain separate templates per locale
  • Content changes only require updating the source template — translations happen automatically
  • Customers always receive content in their preferred language

Important notes

  • 30-second Journey timeout: The Lambda processes profiles concurrently to respond within 30 seconds.
  • Template caching: Source template is cached for 5 minutes. Update SSM's source-template-id to point to a different template for immediate change.
  • Skip optimization: Profiles matching the source locale don't invoke Bedrock — reducing cost and latency.
  • Silent delivery failures: If the delivery template can't resolve a placeholder, Connect drops the email silently.
  • Character limits: Body is capped at 1000 characters. The prompt instructs Bedrock to stay under 900 as a safety margin.
  • AdditionalInformation field usage: This solution uses the Customer Profile's AdditionalInformation field as temporary storage for the generated email body. If your profiles use this field for other purposes, consider clearing it after campaign completion or using a dedicated custom attribute (255-char limit) instead.

Troubleshooting

SymptomCauseFix
Lambda logs show "Cannot proceed without source template"source-template-id or knowledge-base-id SSM parameter is missing or emptyVerify both parameters exist and have correct values (see Step 3)
Lambda times outTimeout set too low (default is 3s)Set Lambda timeout to 120 seconds in Configuration → General configuration
Lambda logs show "get_message_template FAILED: AccessDeniedException"Lambda role missing wisdom:GetMessageTemplate permissionAdd the permission to the Lambda's IAM role (see Step 4)
Lambda logs show "NameError: name 'qconnect' is not defined"Missing boto3.client("qconnect") initialization in the codeEnsure qconnect = boto3.client("qconnect", region_name=REGION) exists in load_source_template()
Campaign shows send attempts but no emails receivedMessage Template placeholder cannot be resolved — Connect drops email silentlyVerify delivery template uses {{Attributes.Customer.Attributes.emailSubject}} and {{Attributes.Customer.AdditionalInformation}} exactly
Email body is truncatedContent exceeds AdditionalInformation 1000-character limitShorten source template content or adjust the Bedrock prompt to produce shorter output
Lambda logs show "Profile update failed" with ResourceNotFoundExceptionWrong CUSTOMER_PROFILES_DOMAIN environment variableSet it to amazon-connect-<your-instance-alias> (check aws customer-profiles list-domains)
Emails arrive but not translated (same language as source)Customer profile's Locale attribute matches default-locale — Lambda skips translationVerify the profile's Locale value differs from the SSM default-locale parameter
Duplicate emails receivedJourney retried or ran multiple timesSet communication limits in Journey guardrails (e.g., max 1 per recipient per day)

Cleaning up

  1. Delete the Lambda function.
  2. Remove it from your Connect instance (Outbound campaigns → Lambda Functions).
  3. Delete SSM parameters.
  4. Delete both Message Templates (source and delivery) from Connect.
  5. Delete the Journey Flow and Journey.

Related information