Skip to content

ConnectRouter ​

ConnectRouter routes Amazon Connect contact flow events to handlers, one contact per invocation.

Connect calls your Lambda from an Invoke AWS Lambda function block in a contact flow. Each call carries one contact, with the channel it came in on and the method that started it. The router matches on those and hands your handler the contact to act on and a map of values to return to the flow.

Install ​

bash
npm install @lambda-event-router/base @lambda-event-router/connect

@lambda-event-router/base is a peer dependency, so install it yourself. It exports LambdaRouter, which every router plugs into.

Create the router ​

ts
import { createConnectRouter } from '@lambda-event-router/connect'
import { logInvocation } from './middleware/logInvocation'

const connectRouter = createConnectRouter({
  middleware: [logInvocation],  // Optional
})

createConnectRouter() on its own gives you a router with no shared middleware.

Options ​

OptionTypeRequiredDefaultDescription
middlewareConnectMiddleware[]No[]Runs for every contact this router handles, before any route middleware. See Middleware

Register routes ​

ts
connectRouter.route({
  filters: {
    channel: 'VOICE',
    initiationMethod: 'INBOUND',
  },
  middleware: [withContactContext],  // Optional
  handler: greetInboundCaller,
})

filters and handler are the only required keys, and filters can be an empty object to match every contact.

route() returns the router, so you can chain registrations.

ts
connectRouter.route(inboundVoiceRoute).route(chatRoute)

Routes are ranked by how specific they are. Where two overlap, the one matching only a subset of the other is tried first, whatever order you registered them in. Registration order decides the rest, so give each route filters no other route can match. See match order for how ranking works and what it cannot settle.

A contact that matches no route throws No route matched for Amazon Connect event (channel: ..., initiationMethod: ...). Connect expects a response, so an unmatched contact fails the invocation and the flow takes the Error branch of the block rather than falling through. Register a filter-less catch-all if you would rather handle everything else in one place. It ranks last on its own, so it takes what the other routes turn down. See nothing matched for what the other routers do instead.

A contact carries metadata about a call or chat rather than a payload you control, so there is nothing to validate and no schema validation section on this page.

Convenience methods ​

Each convenience method presets one filter key, so you register with the rest of the filters and the method fills in the channel or the initiation method.

ts
connectRouter.voice({
  filters: { initiationMethod: 'INBOUND' },
  handler: greetInboundCaller,
})

// The same route through route()
connectRouter.route({
  filters: { channel: 'VOICE', initiationMethod: 'INBOUND' },
  handler: greetInboundCaller,
})
MethodPresets
voicechannel: 'VOICE'
chatchannel: 'CHAT'
emailchannel: 'EMAIL'
taskchannel: 'TASK'
inboundinitiationMethod: 'INBOUND'
outboundinitiationMethod: 'OUTBOUND'
transferinitiationMethod: 'TRANSFER'
callbackinitiationMethod: 'CALLBACK'
apiinitiationMethod: 'API'

A channel method still takes an initiationMethod filter and the reverse, so voice({ filters: { initiationMethod: 'INBOUND' } }) matches inbound voice contacts. A channel method's filters omit channel and an initiation-method method's omit initiationMethod, so the key the method presets is set through the method and passing it in filters is a type error. See convenience methods for how the other routers use them.

Filters ​

Every filter key on one route, showing each form a value can take. All of them are optional, so set the ones that pick out the contacts you want and leave the rest off.

ts
connectRouter.route({
  filters: {
    channel: ['VOICE', 'CHAT', 'EMAIL', 'TASK'],
    initiationMethod: ['INBOUND', 'OUTBOUND', 'TRANSFER', 'CALLBACK', 'API'],
    instanceArn: CONNECT_INSTANCE_ARN, // Or a pattern: /:instance\//
    custom: ({ event }) => event.Details.ContactData.Queue?.Name === 'support',
  },
  handler: routeContact,
})
FilterTypeDescription
channelConnectChannel | ConnectChannel[]Exact match against the contact's channel, one of VOICE, CHAT, EMAIL or TASK. Not a pattern, so list every channel you want
initiationMethodConnectInitiationMethod | ConnectInitiationMethod[]Exact match against how the contact started, such as INBOUND, API or WEBRTC_API. See Types for all twelve
instanceArnFilterStringMatcherMatches the ARN of the Connect instance the contact belongs to
custom(input: ConnectFilterInput) => boolean | Promise<boolean>Given the channel, the initiation method and the raw event. Anything the other filters cannot express. Can be async

channel and initiationMethod are exact-match unions, not patterns, so a value has to be one of the ones listed. Only instanceArn is a FilterStringMatcher, which is string | RegExp | Array<string | RegExp>. See filters for how each form matches, including the * wildcard.

Both unions are declared by this package rather than taken from aws-lambda. The aws-lambda types are narrower than the Connect service model: they omit the TASK channel and seven initiation methods, so a contact that arrives on one of those could not be named in a filter. ConnectChannel and ConnectInitiationMethod follow the service model instead. ConnectRequest.contactData and ConnectRequest.event are retyped to match, so a handler reading contactData.Channel sees the same four values a filter can name.

custom is the only filter that reaches the whole event. Its event is the typed ConnectEvent, so use it to match on the queue, customer endpoint or a contact attribute that no built-in key covers. See custom for where it sits in the filter order.

Handler ​

Handlers take one argument and return a map of values for the flow.

ts
import { logger } from '@lambda-event-router/base'
import type { ConnectRequest, ConnectResponse } from '@lambda-event-router/connect'

export async function greetInboundCaller({ contactData }: ConnectRequest): Promise<ConnectResponse> {
  logger.info(`Inbound voice contact ${contactData.ContactId} from ${contactData.CustomerEndpoint?.Address}`)
  return { greeting: 'Welcome back' }
}

Request object ​

FieldTypeDescription
contactDataConnectContactDataThe contact: its channel, initiation method, queue, customer endpoint, saved attributes and the rest
parametersRecord<string, string>The key/value parameters set on the Invoke AWS Lambda function block for this call
eventConnectEventThe event from AWS, with the channel and initiation method widened
contextContextThe Lambda context

Context comes from aws-lambda, not this package. contactData and parameters are the Details.ContactData and Details.Parameters off event.

Response type ​

ConnectResponse is ConnectContactFlowResult, a flat map of string values. See Responses for what Connect does with it and the shape it has to be in.

Inferred handlers ​

defineRoute types the handler from the router, so you get contactData, parameters, event and context without naming ConnectRequest anywhere.

ts
import { logger } from '@lambda-event-router/base'
import { defineRoute } from '@lambda-event-router/connect'

export const greetInboundCaller = defineRoute({
  filters: { channel: 'VOICE', initiationMethod: 'INBOUND' },
}).handle(async ({ contactData }) => {
  logger.info(`Inbound voice contact ${contactData.ContactId}`)
  return { greeting: 'Welcome back' }
})

connectRouter.route(greetInboundCaller)

Not having to name the request shape pays off most in a Lambda taking several event sources, since every router hands its handler something different. See inferred handlers, where the same source is written both ways to compare.

Annotated handlers ​

Annotating the request yourself splits route setup from business logic, using ConnectRequest as the argument type.

ts
// handlers/greetInboundCaller.ts
import { logger } from '@lambda-event-router/base'
import type { ConnectRequest, ConnectResponse } from '@lambda-event-router/connect'

export async function greetInboundCaller({ contactData }: ConnectRequest): Promise<ConnectResponse> {
  logger.info(`Inbound voice contact ${contactData.ContactId}`)
  return { greeting: 'Welcome back' }
}
ts
// connect.ts
import { createConnectRouter } from '@lambda-event-router/connect'
import { greetInboundCaller } from './handlers/greetInboundCaller'

const connectRouter = createConnectRouter()

connectRouter.voice({
  filters: { initiationMethod: 'INBOUND' },
  handler: greetInboundCaller,
})

ConnectRequest is the whole request type. There is no schema and no generic parameter, so nothing to derive with z.infer and nothing to keep in sync. A handler typed ConnectRequest fits every registration form, route() and each convenience method, since they all take the same handler signature. See annotated handlers for the worked version.

Responses ​

Your handler returns a flat object of string key/value pairs and the router hands it straight back to Connect, so ConnectResponse is ConnectContactFlowResult, Record<string, string | null>. The return shape is Connect's contract rather than the router's.

ts
return {
  customerBalance: '1000',
  greeting: 'Welcome back',
}

Connect exposes each pair to the flow. Reference a value directly as $.External.<key>, for example $.External.greeting, or store it with a Set contact attributes block to keep it for later blocks.

Keep the object flat and every value a string when the block validates the response as a STRING_MAP, which is the shape ConnectContactFlowResult describes. Nested objects and arrays only reach the flow when the block is set to JSON validation instead. The returned data has to be under 32 KB, and Connect documents the values as alphanumeric, dash and underscore only.

Throwing from a handler, timing out or returning something Connect cannot read sends the contact down the Error branch of the block. An unmatched contact throws, so it lands there too.

Middleware ​

Router and route middleware are both typed ConnectMiddleware, and the chain runs once per contact.

ts
import { logger } from '@lambda-event-router/base'
import type { ConnectMiddleware } from '@lambda-event-router/connect'

export const logInvocation: ConnectMiddleware = async (request, next) => {
  logger.info(`Handling ${request.contactData.Channel} contact ${request.contactData.ContactId}`)
  return next(request)
}
ts
const connectRouter = createConnectRouter({ middleware: [logInvocation] })

connectRouter.route({
  filters: { channel: 'VOICE' },
  middleware: [withContactContext],
  handler: routeContact,
})

Router middleware runs before route middleware. See middleware for the execution order and the three levels it attaches at.

Types ​

All exported from @lambda-event-router/connect.

TypeDescription
ConnectRequestThe handler argument
ConnectResponseHandler return type, ConnectContactFlowResult
ConnectHandlerThe handler function, (request: ConnectRequest) => Promise<ConnectResponse>
ConnectFiltersThe filters object
ConnectFilterInputWhat custom receives
ConnectChannel'CHAT' | 'EMAIL' | 'TASK' | 'VOICE'
ConnectInitiationMethod'AGENT_REPLY' | 'API' | 'CALLBACK' | 'DISCONNECT' | 'EXTERNAL_OUTBOUND' | 'FLOW' | 'INBOUND' | 'MONITOR' | 'OUTBOUND' | 'QUEUE_TRANSFER' | 'TRANSFER' | 'WEBRTC_API'
ConnectEventThe contact flow event, with Channel and InitiationMethod widened
ConnectContactDataDetails.ContactData off ConnectEvent
ConnectMiddlewareRouter and route middleware
ConnectRouteDefinitionA full route passed to route()
ConnectChannelRouteDefinitionA route passed to a channel method
ConnectInitiationMethodRouteDefinitionA route passed to an initiation-method method
ConnectChannelFiltersThe filters for a channel method
ConnectInitiationMethodFiltersThe filters for an initiation-method method
ConnectRouterOptionsOptions for createConnectRouter

The ConnectRouter class and the createConnectRouter and defineRoute functions come from the same place.

No Connect type takes a generic parameter. The request and response shapes are fixed by the event, so there is no ### Generic parameters table that a reader arriving from another router might expect.

Code example ​

One Lambda fronting a contact centre: greeting inbound voice callers, answering chats and emails, and a catch-all for everything else.

Open a file: index.ts | Connect router | handlers

File viewer
TSconnect.ts

The first three routes match a distinct channel, so no contact reaches two of them and the order they register in makes no difference. defaultContact filters on nothing, so it has to come last, and it catches anything the others miss, such as an outbound voice call.

index.ts hands the router to LambdaRouter, which is what AWS invokes and what every router in the Lambda gets registered on. See routers for how the two levels of matching fit together.