AWS Contact Center

Managing Amazon Connect flows as Code with AWS CDK

Every day, Amazon Customer Service handles millions of customer contacts across Amazon and its subsidiaries, spanning multiple regions including North America, Europe, South Africa, and Asia Pacific. Managing contact flows at this scale across multiple Amazon Connect instances to accommodate Retail and Amazon subsidiaries required a scalable, programmatic approach. The team set out to maintain consistency and support rapid innovation while maintaining high-quality customer experiences.

Amazon Connect provides L1 (low-level) AWS CloudFormation constructs for programmatic flow deployment. While these constructs support infrastructure as code (IaC) practices, developers work directly with JSON structures that mirror the Amazon Connect Flow language specification. For enterprise contact centers managing hundreds of flows across multiple instances, Amazon Customer Service built higher-level abstractions that bring software engineering best practices, including type safety, build-time testing, version control, and CI/CD, to contact flow management.

This post covers how the team built L2 AWS Cloud Development Kit (AWS CDK) constructs to manage contact flows as code at scale, migrating hundreds of flows and modules while reducing deployment and maintenance time from days to minutes.

The opportunity with L1 constructs

Amazon Connect’s L1 AWS CloudFormation constructs provide programmatic access to flow deployment, which is essential for IaC practices. The following example shows a basic flow deployment using L1 constructs:

import * as connect from 'aws-cdk-lib/aws-connect';
new connect.CfnContactFlow(this, 'MyFlow', {
  instanceArn: 'arn:aws:connect:us-east-1:123456789012:instance/abc-123',
  name: 'CustomerServiceFlow',
  type: 'CONTACT_FLOW',
  content: JSON.stringify({
    "Version": "2019-10-30",
    "StartAction": "12345678-1234-1234-1234-123456789012",
    "Actions": [
      {
        "Identifier": "87654321-1234-1234-1234-123456789012",
        "Parameters": {
          "Text": "Hello, welcome to Customer Service"
        },
        "Transitions": {
          "NextAction": "12345678-1234-1234-1234-123456789012"
        },
        "Type": "MessageParticipant"
      }
    ]
  })
});

At enterprise scale with hundreds of flows across all Connect instances, the team identified opportunities to build further on this foundation:

  • Type safety: Raw JSON strings don’t provide compile-time validation or IDE support.
  • Maintainability: Flows with dozens of actions become difficult to read and modify in JSON.
  • Consistency: Deploying the same flow across multiple instances requires careful manual duplication.
  • Testing: While Amazon Connect now offers native testing and simulation, the team needed additional compile-time structural validation, resource dependency checks, and organizational rule enforcement that go beyond runtime testing.
  • Standards enforcement: No mechanism to verify organizational best practices are followed across flows.

These observations led to building L2 constructs that provide higher-level abstractions while maintaining full compatibility with Amazon Connect’s capabilities.

Solution overview: L2 constructs for contact flows

The solution introduces a TypeScript library that provides L2 AWS CDK constructs for defining Amazon Connect flows programmatically. The architecture consists of four key components:

  • TypeScript library: High-level, type-safe builders for flow action blocks, flows, modules, and transitions.
  • Validation framework: Build-time checks for flow structure, resource dependencies, and organizational rules.
  • Transformation engine: Bidirectional conversion between TypeScript code and Amazon Connect Flow JSON.
  • AWS CDK integration: Automated deployment to multiple Amazon Connect instances through CI/CD pipelines.

The library allows developers to write flows using intuitive builder patterns instead of manipulating raw JSON. The following example shows the same flow from earlier, reimagined with L2 constructs:

import { FlowBuilder, MessageParticipantActionBuilder, DisconnectActionBuilder }
  from '@amzn/connect-flows-typescript';
const disconnectAction = new DisconnectActionBuilder().build();
const messageAction = new MessageParticipantActionBuilder()
  .withText('Hello, welcome to Customer Service')
  .nextAction(disconnectAction)
  .build();
const flow = new FlowBuilder('CustomerServiceFlow')
  .startingWith(messageAction)
  .build();
new ConnectFlowConstruct(this, 'MyFlow', {
  instanceArn: connectInstance.instanceArn,
  flowDefinition: flow
});

This approach provides immediate benefits: type safety catches errors at compile time, the code is self-documenting and easier to review, and the same flow definition can be deployed consistently across instances.

Building flows with L2 constructs

Consider a flow that checks if a queue is within operating hours and routes customers accordingly, a common pattern in high-volume contact centers. This pattern directly impacts the customer journey by directing contacts to the right destination at the right time, whether that’s a live agent, self-service automation, or an after-hours message.

Using L2 constructs (TypeScript):

const checkHoursOfOperation = new CheckHoursOfOperationActionBuilder()
  .withHoursOfOperation(hoursOfOperationId)
  .whenInHours(transferToQueueAction)
  .whenOutOfHours(disconnectAction)
  .build();

const flow = new FlowBuilder('QueueRoutingFlow')
  .startingWith(checkHoursOfOperation)
  .build();

Equivalent L1 approach (raw JSON):

{
  "Version": "2019-10-30",
  "StartAction": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "Actions": [
    {
      "Parameters": {},
      "Identifier": "verifyQueueOperatingHours",
      "Type": "CheckHoursOfOperation",
      "Transitions": {
        "NextAction": "b2c3d4e5-f678-90ab-cdef-123456789012",
        "Conditions": [
          {
            "NextAction": "b2c3d4e5-f678-90ab-cdef-123456789012",
            "Condition": {
              "Operator": "Equals",
              "Operands": [
                "True"
              ]
            }
          }
        ],
        "Errors": [
          {
            "NextAction": "c3d4e5f6-7890-abcd-ef12-34567890abcd",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Identifier": "b2c3d4e5-f678-90ab-cdef-123456789012",
      "Type": "SetWorkingQueue",
      "Parameters": {
        "QueueArn": "arn:aws:connect:us-east-1:123456789012:instance/abc-123/queue/primary-queue"
      }
    },
    {
      "Parameters": {},
      "Identifier": "c3d4e5f6-7890-abcd-ef12-34567890abcd",
      "Type": "DisconnectParticipant",
      "Transitions": {}
    }
  ]
}

The difference is clear: 10 lines of readable, type-safe TypeScript versus 30+ lines of manually managed JSON with hardcoded UUIDs and nested transition mappings.

With the L2 construct approach, you can build safer flows with compile-time type mismatch detection, automatic reference updates during refactoring, extractable common patterns for reuse, and clear intent without needing to understand JSON structure details.

Composite action blocks

One of the most powerful capabilities of L2 constructs is creating composite action blocks, reusable components that encapsulate common patterns of actions frequently used together. These composites help maintain consistent customer journey experiences by standardizing how common scenarios are handled across flows.

Snippet showing a potential repeating pattern of action blocks which can be designed to be a composite action block for implementation to increase reusable patterns.

As shown in the image above, instead of repeating a “transfer to queue and tell customer if there’s no capacity before disconnecting” pattern throughout flows, you can create a composite action. These blocks create a library of reusable patterns that reduce code duplication, encapsulate proven patterns, simplify flow authoring, and provide building blocks for rapid development.

L2 construct architecture

The library prioritizes type safety, bidirectional transformation, and stable IDs for analytics.

The public API consists of three main components: ActionDeclaration (base interface for all flow actions), FlowBuilder (orchestrates flow construction and validation), and action-specific builders, such as SetWorkingQueue, CheckHoursOfOperation, and MessageParticipant. Each builder follows a consistent pattern with fluent APIs:

export class SetWorkingQueueActionBuilder {
  private queueArn?: string;
  private id?: string;

  withQueueArn(arn: string): this {
    this.queueArn = arn;
    return this;
  }

  withId(id: string): this {
    this.id = id;
    return this;
  }

  build(): ActionDeclaration {
    if (!this.queueArn) {
      throw new Error('Queue ARN is required');
    }
    return {
      id: this.id ?? generateStableId(),
      type: 'SetWorkingQueue',
      parameters: { QueueArn: this.queueArn }
    };
  }
}

The transformation engine handles three critical conversions: JSON to in-memory model, in-memory model to TypeScript code, and TypeScript code back to JSON. This bidirectional capability is essential for migration. Existing flows authored in the Amazon Connect UI can be exported as JSON, transformed into TypeScript code, and then maintained as code going forward. The transformation maintains high fidelity with the original flow definition, supporting behavioral consistency during migration.

Validation and rule engine

Build-time validation is a key differentiator of the L2 construct approach. The validation framework operates at three levels:

Type-level validation: TypeScript’s type system enforces correct parameter types at compile time:

// Compile error: wrong parameter type
const action = new CheckContactAttributesActionBuilder()
  .whenNumberGreaterThan("not a number", nextAction); // Type error

// Correct usage
const action = new CheckContactAttributesActionBuilder()
  .whenNumberGreaterThan(5, nextAction); // Type-safe

Structural validation: The FlowBuilder validates flow structure before generating JSON:

const flow = new FlowBuilder('MyFlow')
  .build(); // Error: No start action defined

const validFlow = new FlowBuilder('MyFlow')
  .startingWith(action1)
  .build(); // Valid structure

Custom rule engine: Organizational standards are enforced through a pluggable rule engine:

class EnableLoggingRule implements ValidationRule {
  validate(flow: FlowDefinition): ValidationResult {
    const firstAction = flow.actions[0];
    if (firstAction.type !== 'enableLogging') {
      return { valid: false, message: 'First action must enable cloudwatch logging' };
    }
    return { valid: true };
  }
}

Before deployment, the validation framework also verifies that referenced AWS resources exist in the target account. Since each contact flow and module is deployed as an AWS CloudFormation template following the CRUD pattern, CloudFormation validates resource dependencies during stack operations, preventing runtime failures caused by missing dependencies.

Deployment pipeline at scale

Deploying flows to all Amazon Connect instances across multiple regions, subsidiaries, and deployment stages requires multi-stage automated orchestration.

The AWS CDK construct handles deploying identical flow logic across instances while managing environment-specific configurations. In line with how Amazon performs safe, hands-off deployments, Amazon Connect flows progress through four stages with automated testing gates: Beta (limited test traffic with monitoring), Gamma (significant test traffic with monitoring and approval gate), and Production (production traffic with real-time monitoring and automated rollback).

Architecture diagram showing TypeScript flow definitions passing through the L2 construct library for validation, then deploying via AWS CDK and CloudFormation through Beta, Gamma, and Production

Different environments require different resource ARNs (AWS Lambda functions, queues, prompts). The library supports environment-specific configuration through a centralized mapping system:

function getLambdaArnForEnvironment(stage: string): string {
  return stage === 'Prod'
  ? 'arn:aws:lambda:us-east-1:123456789012:function:prod-handler'
  : 'arn:aws:lambda:us-east-1:123456789012:function:dev-handler';
}

Deployments include automated rollback on failure with service level indicator (SLI) based monitoring:

class FlowDeploymentMonitor {
  async monitorDeployment(flowId: string): Promise<void> {
    const metrics = await this.getFlowMetrics(flowId);
    if (metrics.errorRate > THRESHOLD) {
      await this.rollbackDeployment(flowId);
      throw new Error('Deployment rolled back due to high error rate');
    }
  }
}

What previously required careful coordination across instances now completes in minutes through automated CI/CD pipelines.

Best practices and lessons learned

Through implementing and operating this system at scale, the team identified several best practices. For L2 construct design: use single-parameter methods for clarity, validate parameters in builder methods (fail fast), and provide sensible defaults.

When refactoring flows, preserve action IDs to maintain analytics continuity. For testing: validate individual action builders with unit tests, verify resource dependencies with integration tests, and deploy to Beta for basic functionality verification.

When to use each approach: L2 constructs for complex flows, multi-instance deployments, and frequently updated flows. L1 constructs for simple flows and one-off deployments. The Amazon Connect UI for rapid prototyping, visual flow design, and training new team members. Consider prototyping new flows in the Amazon Connect UI first, then using the transformation engine to convert them to TypeScript for ongoing maintenance.

For scaling: extract common patterns into shared libraries, use semantic versioning, maintain examples and migration guides, establish code review processes, and assign clear ownership to development teams.

Conclusion

Building L2 AWS CDK constructs for Amazon Connect flows has changed how Amazon Customer Service manages contact flows. The results include:

  • Type safety with compile-time validation, eliminating classes of runtime errors that previously caused customer-impacting incidents.
  • Deployment and maintenance time reduced from days to minutes across all instances.
  • Hundreds of contact flows and modules migrated; handling contact volume across subsidiaries and stages.
  • Zero configuration drift with consistent flow logic deployed across subsidiaries, monitored by AWS CloudFormation’s drift detection feature on templates.
  • Significant reduction in production incidents, as manual deployment errors that previously caused high-impact incidents are now caught before deployment.
  • Full audit history with every flow change tracked in Git with code review, approval, and rollback capability.

By treating contact flows as IaC, Amazon Customer Service created a foundation that supports rapid experimentation and continuous improvement of customer journeys. Flows get tested before deployment, deploy consistently across touchpoints, and are monitored in production.

This approach is applicable to enterprise Amazon Connect customers managing multiple instances. Whether you’re managing flows across subsidiaries, regions, or stages, the patterns described here can help scale your contact flow operations while maintaining quality and consistency.

Next steps

Ready to implement Contact Flows as Code? Start by auditing your current flows to identify migration candidates, then pick your most complex flow as a proof of concept. Set up a test environment and try implementing your first L2 construct.

Resources to get started:

We’re exploring making the L2 construct library available as an open-source repository. If you’re interested in using or contributing to the library, or ready to transform your customer service experience with Amazon Connect, contact us.

About the Authors

Manish Tulzapur Manish Tulzapur (he/him) is a Senior Software Development Engineer at Amazon based in Seattle, WA. He is passionate about building developer-friendly tooling that empowers teams to deliver consistent, high-quality customer experiences at scale, making it easier for builders to do the right thing. When he’s not writing code, you’ll find Manish competing on the ultimate frisbee field or logging miles on Seattle’s trail networks as he trains for his next ultramarathon.
Lindsey Pogue Lindsey Pogue (she/her) is a Software Development Manager at Amazon based in Seattle, WA. With a background as a Software Development Engineer, she leads a team of engineers working on Amazon Customer Service technology. Outside of work, you’ll find Lindsey outside, whether it’s backpacking or boating in the summer, or snowboarding in the winter.