Building Contact Flows
Looking for API Reference?
For detailed documentation of each flow block (PlayPrompt, GetCustomerInput, InvokeLambda, etc.), see the Flow Building Blocks Reference.
What is a Contact Flow?
A contact flow is like a roadmap for customer phone calls. It tells Amazon Connect what to do when someone calls your contact center - should they hear a greeting? Get routed to a queue? Talk to an agent? Press buttons to navigate a menu?
Think of it as a flowchart that handles incoming calls automatically. Just like a choose-your-own-adventure book, a contact flow guides callers through different paths based on their choices and your business logic.
Real-World Examples
- Simple Greeting Flow: "Thank you for calling. Transferring you to an agent..." → Sends caller to a queue
- IVR Menu Flow: "Press 1 for Sales, 2 for Support..." → Routes based on input
- Business Hours Flow: Checks if you're open → Routes to agents or plays after-hours message
- Callback Flow: Offers to call the customer back instead of waiting on hold
When You Need Contact Flows
You'll create contact flows whenever you want to:
- Greet callers with a custom message
- Build phone menus (IVR systems)
- Route calls to different departments
- Check business hours before connecting to agents
- Collect information from callers (account numbers, choices, etc.)
- Integrate with external systems (customer databases, CRMs)
Types of Contact Flows
Amazon Connect supports different types of flows for specific purposes:
| Flow Type | Purpose | Example Use |
|---|---|---|
| Contact Flow | Main inbound call flow | Primary phone menu, greeting, routing |
| Customer Queue Flow | Plays while caller waits | "Your call is important to us..." |
| Customer Hold Flow | Plays when agent puts caller on hold | Hold music |
| Customer Whisper Flow | Plays to caller before connecting to agent | "Connecting you now..." |
| Agent Whisper Flow | Plays to agent before connecting to caller | "Incoming call from Sales queue" |
| Agent Hold Flow | Plays to agent while waiting | Agent hold music |
| Outbound Whisper Flow | For outbound calls | "This is a call from Acme Corp" |
| Agent Transfer Flow | Agent-to-agent transfers | Transfer between agents |
| Queue Transfer Flow | Queue-to-queue transfers | Move caller between queues |
Two Ways to Build Flows
Switchboard gives you two approaches to build contact flows in C#. Both create the same Amazon Connect flows - just pick the style you prefer!
Approach 1: Fluent API (Recommended for Beginners)
The Fluent API approach uses method chaining to build flows step-by-step. It's great for:
- Learning contact flow concepts
- Building simple flows quickly
- Seeing the flow structure clearly in code
- Debugging flow logic
Example:
var flow = Flow.Create("SimpleSalesFlow")
.PlayPrompt("Welcome to sales!")
.TransferToQueue("Sales")
.Disconnect()
.Build();Pros:
- Easy to read and understand
- IntelliSense guides you through available actions
- No special syntax to learn
- Great for debugging
Cons:
- More verbose for complex flows
- No compile-time validation of flow structure
Approach 2: Attributes (Advanced, Declarative)
The Attribute-Based approach uses C# attributes to define flows declaratively. It's great for:
- Complex multi-step flows
- Teams familiar with declarative patterns
- Compile-time validation (via source generators)
- Clean separation of flow definition and logic
Example:
[ContactFlow("SimpleSalesFlow")]
public partial class SalesFlow : FlowDefinitionBase
{
[Action(Order = 1)]
[Message("Welcome to sales!")]
public partial void Welcome();
[Action(Order = 2)]
[TransferToQueue("Sales")]
public partial void TransferToSales();
[Action(Order = 3)]
[Disconnect]
public partial void End();
}Pros:
- Clean, declarative syntax
- Compile-time validation (catches errors early)
- Auto-generated boilerplate code
- Great for complex flows
Cons:
- Requires understanding of attributes and source generators
- Steeper learning curve
- Less intuitive for beginners
For the rest of this guide, we'll focus on the Fluent API approach. Once you're comfortable with flow concepts, check out the Attribute-Based Flows guide.
Your First Flow
Let's build your first contact flow step-by-step. We'll create a simple greeting flow that welcomes callers and transfers them to a queue.
Step 1: Set Up Your CDK Stack
First, create your CDK stack with a Connect instance:
using Amazon.CDK;
using Switchboard;
var app = new App();
var stack = new SwitchboardStack(app, "MyFirstFlow", "my-contact-center");Step 2: Create the Flow
Build a simple flow using FlowBuilder:
var flow = Flow.Create("Flow")
.SetName("WelcomeFlow")
.PlayPrompt("Thank you for calling. Please hold while we connect you to an agent.")
.TransferToQueue("GeneralSupport")
.Build();What this does:
- Creates a flow named "WelcomeFlow"
- Plays a greeting message to the caller
- Transfers the caller to the "GeneralSupport" queue
Step 3: Create the Queue
The flow references a queue, so we need to create it:
var queue = new QueueBuilder()
.SetName("GeneralSupport")
.SetMaxContacts(50)
.Build();Step 4: Add Resources to Stack
Add the queue and flow to your stack:
stack.AddQueue(queue);
stack.AddFlow(flow); // Must add queue before flow!Important: Add the queue before the flow because the flow references the queue.
Step 5: Deploy
Synthesize and deploy your stack:
app.Synth();Then deploy using CDK CLI:
cdk deployComplete Working Example
Here's the full code in one place:
using Amazon.CDK;
using Switchboard;
var app = new App();
var stack = new SwitchboardStack(app, "MyFirstFlow", "my-contact-center");
// Create queue
var queue = new QueueBuilder()
.SetName("GeneralSupport")
.SetMaxContacts(50)
.Build();
// Create flow
var flow = Flow.Create("Flow")
.SetName("WelcomeFlow")
.PlayPrompt("Thank you for calling. Please hold while we connect you to an agent.")
.TransferToQueue("GeneralSupport")
.Build();
// Add to stack
stack.AddQueue(queue);
stack.AddFlow(flow);
// Deploy
app.Synth();Congratulations! You've built your first contact flow! When someone calls your contact center, they'll hear your greeting and be transferred to the GeneralSupport queue.
Flow Actions Guide
Now let's explore the different actions you can add to your flows. Each action does something specific in the customer's call journey.
PlayPrompt - Playing Messages
Speaks text to the caller using Amazon Polly text-to-speech.
When to use:
- Greeting callers
- Providing information
- Explaining menu options
- Confirming actions
Basic Example:
var flow = Flow.Create("Flow")
.SetName("GreetingFlow")
.PlayPrompt("Welcome to Acme Corporation!")
.Build();With Custom Identifier:
var flow = Flow.Create("Flow")
.SetName("GreetingFlow")
.PlayPrompt("Welcome to Acme Corporation!", "welcome-message")
.Build();Tips:
- Keep messages short and clear
- Speak naturally (Amazon Polly sounds best with natural language)
- Avoid spelling out words unless necessary
GetCustomerInput - Getting Caller Input
Collects DTMF input (button presses) from the caller.
When to use:
- Building IVR menus ("Press 1 for Sales...")
- Collecting account numbers
- Getting confirmation (Press 1 to confirm)
- Navigating options
Basic Example:
var flow = Flow.Create("Flow")
.SetName("MenuFlow")
.GetCustomerInput("Press 1 for sales, 2 for support, or 3 for billing")
.Build();Advanced Configuration:
var flow = Flow.Create("Flow")
.SetName("AccountNumberFlow")
.GetCustomerInput("Please enter your 10-digit account number", input =>
{
input.MaxDigits = 10; // Collect up to 10 digits
input.TimeoutSeconds = 15; // Wait 15 seconds for input
input.EncryptInput = true; // Encrypt sensitive data
})
.Build();Configuration Options:
MaxDigits- Maximum number of digits to collect (default: 1)TimeoutSeconds- How long to wait for input (default: 5)EncryptInput- Whether to encrypt the input for security (default: false)
Tips:
- Use short timeout (5-10 seconds) for single-digit input
- Use longer timeout (15-30 seconds) for account numbers
- Always encrypt sensitive data like account numbers or PINs
TransferToQueue - Sending to Queue
Transfers the caller to a queue where they'll wait for an available agent.
When to use:
- Connecting callers to agents
- Routing to specific departments
- After IVR menu selection
Basic Example:
var flow = Flow.Create("Flow")
.SetName("TransferFlow")
.PlayPrompt("Transferring you to our sales team")
.TransferToQueue("Sales")
.Build();With Custom Identifier:
var flow = Flow.Create("Flow")
.SetName("TransferFlow")
.PlayPrompt("Transferring you to our sales team")
.TransferToQueue("Sales", "transfer-to-sales")
.Build();What happens:
- The flow automatically adds a "SetQueue" action before the transfer
- The caller enters the specified queue
- Queue music/announcements play while waiting
- First available agent receives the call
Important: The queue must exist in your stack:
var salesQueue = new QueueBuilder()
.SetName("Sales")
.Build();
stack.AddQueue(salesQueue);
stack.AddFlow(flow); // Now the flow can reference "Sales" queueBranch - Conditional Logic
Routes the call to different actions based on conditions or customer input.
When to use:
- IVR menu routing ("If they pressed 1, go here...")
- VIP customer routing
- Conditional business logic
- Different paths based on data
Simple IVR Menu:
var flow = Flow.Create("Flow")
.SetName("MainMenu")
.GetCustomerInput("Press 1 for sales, 2 for support, or 3 for billing")
.Branch(branch =>
{
branch.Case("1", "sales-action");
branch.Case("2", "support-action");
branch.Case("3", "billing-action");
branch.Otherwise("invalid-input");
})
.Build();Advanced Conditional Routing:
var flow = Flow.Create("Flow")
.SetName("VIPRouter")
.InvokeLambda("CustomerLookup")
.Branch(branch =>
{
branch.When(
"$.External.CustomerType == \"VIP\"",
"vip-queue",
ComparisonOperator.Equals
);
branch.When(
"$.External.AccountBalance > 10000",
"premium-queue",
ComparisonOperator.GreaterThan
);
branch.Otherwise("standard-queue");
})
.Build();Comparison Operators:
Equals- Exact match (==)NotEquals- Not equal (!=)GreaterThan- Numeric comparison (>)LessThan- Numeric comparison (<)GreaterThanOrEqual- Numeric comparison (>=)LessThanOrEqual- Numeric comparison (<=)Contains- String contains substringStartsWith- String starts withEndsWith- String ends with
InvokeLambda - External Integrations
Calls an AWS Lambda function to fetch data or perform logic outside the contact flow.
When to use:
- Looking up customer information
- Checking account balances
- Validating input
- Integrating with CRMs or databases
- Custom business logic
Basic Example:
var flow = Flow.Create("Flow")
.SetName("CustomerLookup")
.InvokeLambda("GetCustomerInfo")
.Build();With Timeout Configuration:
var flow = Flow.Create("Flow")
.SetName("CustomerLookup")
.InvokeLambda("GetCustomerInfo", lambda =>
{
lambda.TimeoutSeconds = 8; // Wait up to 8 seconds for response
})
.Build();What happens:
- Flow pauses and calls your Lambda function
- Lambda receives contact attributes and custom parameters
- Lambda returns data (customer info, account status, etc.)
- Flow stores returned data in contact attributes
- Flow continues with the Lambda response data
Tips:
- Keep Lambda functions fast (under 3 seconds if possible)
- Set appropriate timeouts (3-8 seconds)
- Return data in contact attributes for use later in the flow
- Handle Lambda failures gracefully (use Branch to check for errors)
SetContactAttributes - Storing Data
Sets custom attributes on the contact that persist throughout the call lifecycle.
When to use:
- Storing customer information for later use
- Passing data between flow actions
- Tracking call metadata
- Personalizing agent experience
Basic Example:
var flow = Flow.Create("Flow")
.SetName("SetCustomerInfo")
.SetContactAttributes(attrs =>
{
attrs["CustomerType"] = "VIP";
attrs["Priority"] = "High";
attrs["Source"] = "MobileApp";
})
.Build();After Lambda Lookup:
var flow = Flow.Create("Flow")
.SetName("EnrichCustomerData")
.InvokeLambda("CustomerLookup")
.SetContactAttributes(attrs =>
{
attrs["AccountBalance"] = "$.External.balance";
attrs["CustomerTier"] = "$.External.tier";
attrs["LastPurchaseDate"] = "$.External.lastPurchase";
})
.Build();What happens:
- Attributes are stored on the contact record
- Available to all subsequent actions in the flow
- Visible to agents in the Contact Control Panel (CCP)
- Can be used in routing decisions
- Included in contact trace records (CTRs)
Tips:
- Use clear, descriptive attribute names
- Set attributes early in the flow for routing decisions
- Limit to essential data (attributes have size limits)
CheckHoursOfOperation - Business Hours
Checks if the current time falls within your configured business hours.
When to use:
- Routing calls differently during/after business hours
- Playing after-hours messages
- Directing to voicemail when closed
- Different queues for off-hours support
Basic Example:
var flow = Flow.Create("Flow")
.SetName("BusinessHoursCheck")
.CheckHoursOfOperation("MainOfficeHours")
.Build();With Conditional Routing:
var flow = Flow.Create("Flow")
.SetName("HoursRouter")
.CheckHoursOfOperation("BusinessHours")
.Branch(branch =>
{
branch.When(
"$.HoursOfOperation.IsOpen == true",
"transfer-to-agents"
);
branch.Otherwise("after-hours-message");
})
.Build();Complete Example:
// Create business hours
var hours = new HoursOfOperation
{
Name = "BusinessHours",
TimeZone = "America/New_York"
};
hours.AddDayConfig(new HoursOfOperationConfig
{
Day = DayOfWeek.Monday,
StartTime = new TimeRange { Hours = 9, Minutes = 0 },
EndTime = new TimeRange { Hours = 17, Minutes = 0 }
});
stack.AddHoursOfOperation(hours);
// Use in flow
var flow = Flow.Create("Flow")
.SetName("HoursCheck")
.CheckHoursOfOperation("BusinessHours")
.PlayPrompt("We are currently open")
.TransferToQueue("Support")
.Build();What happens:
- Flow checks current time against the hours schedule
- Sets
$.HoursOfOperation.IsOpenattribute (true/false) - You can branch based on this attribute
Important: The hours of operation must be added to the stack before the flow references it.
Disconnect - Ending Calls
Ends the contact (hangs up the call).
When to use:
- After playing a final message
- At the end of every flow path
- After completing actions that don't transfer to an agent
Basic Example:
var flow = Flow.Create("Flow")
.SetName("AfterHoursFlow")
.PlayPrompt("We are currently closed. Please call back during business hours.")
.Disconnect()
.Build();With Custom Identifier:
var flow = Flow.Create("Flow")
.SetName("AfterHoursFlow")
.PlayPrompt("We are currently closed. Please call back during business hours.")
.Disconnect("end-call")
.Build();Important: Every flow path must end with either:
Disconnect()- Hangs up the callTransferToQueue()- Sends to a queueTransferToFlow()- Transfers to another flow
Common Mistake:
// WRONG - Flow has no ending
var flow = Flow.Create("Flow")
.SetName("IncompleteFlow")
.PlayPrompt("Hello")
.Build(); // Missing Disconnect() or Transfer!Correct:
// CORRECT - Flow ends properly
var flow = Flow.Create("Flow")
.SetName("CompleteFlow")
.PlayPrompt("Hello")
.Disconnect()
.Build();Advanced Flow Patterns
Now that you know the basic actions, let's build some real-world contact flows.
Multi-Level IVR
A phone menu with multiple levels of options:
var mainMenu = Flow.Create("Flow")
.SetName("MainMenu")
.PlayPrompt("Welcome to Acme Corporation")
.GetCustomerInput("Press 1 for sales, 2 for support, or 3 for billing")
.Branch(branch =>
{
branch.Case("1", "sales-submenu");
branch.Case("2", "support-submenu");
branch.Case("3", "billing-queue");
branch.Otherwise("invalid-retry");
})
.Build();
var salesSubmenu = Flow.Create("Flow")
.SetName("SalesSubmenu")
.GetCustomerInput("Press 1 for new customers, 2 for existing customers")
.Branch(branch =>
{
branch.Case("1", "new-customer-queue");
branch.Case("2", "existing-customer-queue");
branch.Otherwise("sales-general-queue");
})
.Build();Tips for Multi-Level IVRs:
- Keep menus shallow (2-3 levels maximum)
- Limit options to 3-5 per menu
- Always provide an "other" or "operator" option
- Allow callers to go back to previous menu
Callback Flows
Offer callers the option to receive a callback instead of waiting:
var callbackFlow = Flow.Create("Flow")
.SetName("CallbackOffer")
.GetCustomerInput("Press 1 to hold for the next agent, or press 2 to receive a callback")
.Branch(branch =>
{
branch.Case("1", "hold-in-queue");
branch.Case("2", "schedule-callback");
branch.Otherwise("hold-in-queue");
})
.Build();Conditional Routing Based on Customer Data
Route VIP customers differently:
var vipRouter = Flow.Create("Flow")
.SetName("VIPRouter")
.PlayPrompt("Please hold while we look up your account")
.InvokeLambda("CustomerLookup", lambda =>
{
lambda.TimeoutSeconds = 5;
})
.Branch(branch =>
{
// VIP customers go to priority queue
branch.When(
"$.External.CustomerTier == \"VIP\"",
"vip-queue-action",
ComparisonOperator.Equals
);
// High account balance customers
branch.When(
"$.External.AccountBalance",
"premium-queue-action",
ComparisonOperator.GreaterThan
);
// Everyone else
branch.Otherwise("standard-queue-action");
})
.TransferToQueue("VIPSupport")
.Build();Integration with Lambda for Personalization
Greet customers by name using Lambda lookup:
var personalizedGreeting = Flow.Create("Flow")
.SetName("PersonalizedGreeting")
.InvokeLambda("GetCustomerName", lambda =>
{
lambda.TimeoutSeconds = 3;
})
.PlayPrompt("Hello $.External.CustomerName, thank you for calling")
.TransferToQueue("Support")
.Build();Lambda function response:
{
"CustomerName": "John Smith",
"AccountStatus": "Active",
"LastContact": "2024-01-15"
}Common Mistakes & Troubleshooting
Mistake 1: Missing Disconnect Action
Problem:
var flow = Flow.Create("Flow")
.SetName("IncompleteFlow")
.PlayPrompt("Thank you")
.Build(); // No ending!Error: Flow validation fails or caller hears silence.
Solution: Always end flows with Disconnect() or TransferToQueue():
var flow = Flow.Create("Flow")
.SetName("CompleteFlow")
.PlayPrompt("Thank you")
.Disconnect()
.Build();Mistake 2: Invalid Action Transitions
Problem:
var flow = Flow.Create("Flow")
.SetName("BadFlow")
.TransferToQueue("Sales")
.PlayPrompt("This will never play") // Dead code after transfer!
.Build();Error: Actions after transfer are unreachable.
Solution: Don't add actions after TransferToQueue() or Disconnect() - they end the flow.
Mistake 3: Referencing Non-Existent Queues
Problem:
var flow = Flow.Create("Flow")
.SetName("BadFlow")
.TransferToQueue("NonExistentQueue") // Queue doesn't exist!
.Build();
stack.AddFlow(flow); // Fails during deploymentError: CloudFormation deployment fails - queue not found.
Solution: Create the queue before referencing it:
var queue = new QueueBuilder()
.SetName("NonExistentQueue")
.Build();
stack.AddQueue(queue); // Add queue first
stack.AddFlow(flow); // Then add flowMistake 4: Circular Flow References
Problem:
// Flow A transfers to Flow B
var flowA = Flow.Create("Flow")
.SetName("FlowA")
.TransferToFlow("FlowB")
.Build();
// Flow B transfers back to Flow A
var flowB = Flow.Create("Flow")
.SetName("FlowB")
.TransferToFlow("FlowA") // Circular reference!
.Build();Error: CloudFormation detects circular dependency.
Solution: Redesign flows to avoid circular transfers. Use a main menu flow as the hub.
Mistake 5: Branch Without Default Case
Problem:
var flow = Flow.Create("Flow")
.SetName("IncompleteMenu")
.GetCustomerInput("Press 1 or 2")
.Branch(branch =>
{
branch.Case("1", "action-1");
branch.Case("2", "action-2");
// Missing Otherwise()!
})
.Build();Error: If caller presses anything other than 1 or 2, flow has no path.
Solution: Always include Otherwise() for unexpected input:
var flow = Flow.Create("Flow")
.SetName("CompleteMenu")
.GetCustomerInput("Press 1 or 2")
.Branch(branch =>
{
branch.Case("1", "action-1");
branch.Case("2", "action-2");
branch.Otherwise("invalid-input"); // Handle unexpected input
})
.Build();Next Steps
Now that you understand contact flows, explore these related topics:
- Building Queues - Where callers wait for agents
- Building Routing Profiles - How agents handle calls
- Business Hours - Configure open/closed schedules
- Complete Example - Put it all together
Quick Reference
Basic Flow Template
var flow = Flow.Create("Flow")
.SetName("FlowName")
.SetDescription("Optional description")
.SetType(FlowType.ContactFlow)
.PlayPrompt("Welcome message")
.GetCustomerInput("Menu prompt")
.Branch(branch =>
{
branch.Case("1", "option-1");
branch.Otherwise("default");
})
.TransferToQueue("QueueName")
.Build();
stack.AddFlow(flow);Essential Actions Checklist
SetName(name)- Required, unique flow namePlayPrompt(text)- Speak to callerGetCustomerInput(prompt)- Collect DTMF inputTransferToQueue(queueName)- Send to queueBranch(configure)- Conditional routingInvokeLambda(functionName)- Call LambdaSetContactAttributes(attrs)- Store dataCheckHoursOfOperation(hoursName)- Check business hoursDisconnect()- End call
Flow Deployment Checklist
- [ ] Create all referenced queues first
- [ ] Create hours of operation if needed
- [ ] Add queues to stack before flows
- [ ] Every flow path ends with Disconnect or Transfer
- [ ] Test flow logic before deploying
- [ ] Use descriptive names for debugging
- [ ] Add tags for organization
Getting Help
If you run into issues:
- Check the Flow Blocks Reference for detailed action documentation
- Review Complete Example for a working contact center
- Explore the Examples for common patterns
- File an issue on GitHub