diff --git a/cdk-custom-resource-with-wait-condition/.gitignore b/cdk-custom-resource-with-wait-condition/.gitignore
new file mode 100644
index 000000000..28c650ddc
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/.gitignore
@@ -0,0 +1,10 @@
+*.js
+!jest.config.js
+*.d.ts
+node_modules
+
+# CDK asset staging directory
+.cdk.staging
+cdk.out
+
+dist/
diff --git a/cdk-custom-resource-with-wait-condition/.npmignore b/cdk-custom-resource-with-wait-condition/.npmignore
new file mode 100644
index 000000000..c1d6d45dc
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/.npmignore
@@ -0,0 +1,6 @@
+*.ts
+!*.d.ts
+
+# CDK asset staging directory
+.cdk.staging
+cdk.out
diff --git a/cdk-custom-resource-with-wait-condition/README.md b/cdk-custom-resource-with-wait-condition/README.md
new file mode 100644
index 000000000..b8fc4a191
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/README.md
@@ -0,0 +1,86 @@
+# Use AWS CloudFormation Wait Conditions for long-running custom resources
+
+This project demonstrates how to implement [AWS CloudFormation](https://aws.amazon.com/cloudformation/) custom resources that can run for up to 12 hours using [Wait Conditions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-waitcondition.html).
+
+AWS Lambda functions have a 15-minute execution timeout, limiting CloudFormation custom resources to short-running operations. This pattern extends custom resource execution time to 12 hours by decoupling lifecycle management from process execution.
+
+
+
+## How it works
+
+The architecture uses four components:
+
+1. **Custom Resource Handler Lambda** - Receives CloudFormation lifecycle events and starts a Step Function execution, then returns success immediately to prevent timeouts
+2. **Step Function** - Orchestrates the long-running process with built-in retry mechanisms and error handling
+3. **Completion Signal Handler Lambda** - Sends success/failure signals to the Wait Condition Handle when the process completes
+4. **Wait Condition** - Blocks CloudFormation stack completion until receiving the completion signal
+
+This approach enables asynchronous processing with proper CloudFormation integration, supporting use cases like database migrations, complex infrastructure provisioning, and third-party system integrations that exceed Lambda's 15-minute limit.
+
+Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.
+
+## Requirements
+
+* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
+* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
+* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
+* [Node and NPM](https://nodejs.org/en/download/) installed
+* [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) (AWS CDK) installed
+
+## Deploy
+
+1. Clone the project to your local working directory
+
+ ```sh
+ git clone https://github.com/aws-samples/serverless-patterns
+ ```
+
+1. Change the working directory to this pattern's directory
+
+ ```sh
+ cd cdk-custom-resource-with-wait-condition
+ ```
+
+1. Install the project dependencies
+
+ ```sh
+ npm install
+ ```
+
+1. Deploy the stack to your default AWS account and region
+
+ ```sh
+ cdk deploy --require-approval never
+ ```
+
+## Test
+
+You can review Amazon CloudWatch logs for the Lambda functions and Step Function execution to confirm that the long-running process completed successfully and the wait condition was signaled.
+
+## Cleanup
+
+Run the given command to delete the resources that were created. It might take some time for the CloudFormation stack to get deleted.
+
+```sh
+cdk destroy -f
+```
+
+## Useful commands
+
+* `npm run build` compile typescript to js
+* `npm run watch` watch for changes and compile
+* `npm run test` perform the jest unit tests
+* `npx cdk deploy` deploy this stack to your default AWS account/region
+* `npx cdk diff` compare deployed stack with current state
+* `npx cdk synth` emits the synthesized CloudFormation template
+
+## References
+
+1. [Custom resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html)
+2. [Using wait conditions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-waitcondition.html)
+3. [Implementing long running deployments with AWS CloudFormation Custom Resources using AWS Step Functions](https://aws.amazon.com/blogs/devops/implementing-long-running-deployments-with-aws-cloudformation-custom-resources-using-aws-step-functions/)
+
+----
+Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+SPDX-License-Identifier: MIT-0
diff --git a/cdk-custom-resource-with-wait-condition/bin/app.ts b/cdk-custom-resource-with-wait-condition/bin/app.ts
new file mode 100644
index 000000000..542024c0c
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/bin/app.ts
@@ -0,0 +1,15 @@
+#!/usr/bin/env node
+import { App } from 'aws-cdk-lib';
+import { DemoStack } from '../lib/demo-stack';
+
+const app = new App();
+
+// Deploy demo stack showing custom resource with wait condition pattern
+new DemoStack(app, 'CdkCustomResourceWithWaitConditionStack', {
+ stackName: 'Custom-Resource-With-Wait-Condition-Demo',
+ description: 'Demo of a custom resource with a wait condition',
+ env: {
+ region: process.env.CDK_DEFAULT_REGION,
+ account: process.env.CDK_DEFAULT_ACCOUNT,
+ },
+});
\ No newline at end of file
diff --git a/cdk-custom-resource-with-wait-condition/cdk.json b/cdk-custom-resource-with-wait-condition/cdk.json
new file mode 100644
index 000000000..47d584419
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/cdk.json
@@ -0,0 +1,86 @@
+{
+ "app": "npx ts-node --prefer-ts-exts bin/app.ts",
+ "watch": {
+ "include": [
+ "**"
+ ],
+ "exclude": [
+ "README.md",
+ "cdk*.json",
+ "**/*.d.ts",
+ "**/*.js",
+ "tsconfig.json",
+ "package*.json",
+ "yarn.lock",
+ "node_modules",
+ "test"
+ ]
+ },
+ "context": {
+ "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
+ "@aws-cdk/core:checkSecretUsage": true,
+ "@aws-cdk/core:target-partitions": [
+ "aws",
+ "aws-cn"
+ ],
+ "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
+ "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
+ "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
+ "@aws-cdk/aws-iam:minimizePolicies": true,
+ "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
+ "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
+ "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
+ "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
+ "@aws-cdk/core:enablePartitionLiterals": true,
+ "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
+ "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
+ "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
+ "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
+ "@aws-cdk/aws-route53-patters:useCertificate": true,
+ "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
+ "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
+ "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
+ "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
+ "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
+ "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
+ "@aws-cdk/aws-redshift:columnId": true,
+ "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
+ "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
+ "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
+ "@aws-cdk/aws-kms:aliasNameRef": true,
+ "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
+ "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
+ "@aws-cdk/aws-efs:denyAnonymousAccess": true,
+ "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
+ "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
+ "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
+ "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
+ "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
+ "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
+ "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
+ "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
+ "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
+ "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
+ "@aws-cdk/aws-eks:nodegroupNameAttribute": true,
+ "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
+ "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
+ "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
+ "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
+ "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false,
+ "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
+ "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
+ "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
+ "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
+ "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
+ "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
+ "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
+ "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
+ "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
+ "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
+ "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
+ "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true,
+ "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true
+ }
+}
diff --git a/cdk-custom-resource-with-wait-condition/example-pattern.json b/cdk-custom-resource-with-wait-condition/example-pattern.json
new file mode 100644
index 000000000..7f97dce65
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/example-pattern.json
@@ -0,0 +1,72 @@
+{
+ "title": "Custom resource with wait condition",
+ "description": "Use AWS CloudFormation Wait Conditions for long-running custom resources up to 12 hours",
+ "language": "TypeScript",
+ "level": "200",
+ "framework": "AWS CDK",
+ "introBox": {
+ "headline": "How it works",
+ "text": [
+ "AWS Lambda functions have a 15-minute execution timeout, limiting CloudFormation custom resources to short-running",
+ "operations. This pattern extends custom resource execution time to 12 hours using CloudFormation Wait Conditions.",
+ "The architecture uses four components working together: A Custom Resource Handler Lambda receives CloudFormation",
+ "lifecycle events and immediately starts a Step Function execution, then returns success to prevent timeouts.",
+ "The Step Function orchestrates the long-running process with built-in retry mechanisms and error handling.",
+ "When the process completes, a Completion Signal Handler Lambda sends success or failure signals to a Wait",
+ "Condition Handle URL. The Wait Condition blocks CloudFormation stack completion until receiving the signal.",
+ "This decouples custom resource lifecycle management from process execution, enabling asynchronous processing",
+ "with proper CloudFormation integration. The Step Function provides visual workflow monitoring and state management",
+ "while the Wait Condition ensures stack operations complete only after the long-running process finishes.",
+ "The pattern deploys two Lambda functions, one Step Function, a Wait Condition Handle, and a Wait Condition.",
+ "Use cases include database migrations, complex infrastructure provisioning, and third-party system integrations",
+ "that exceed Lambda's 15-minute limit."
+ ]
+ },
+ "gitHub": {
+ "template": {
+ "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/cdk-custom-resource-with-wait-condition",
+ "templateURL": "serverless-patterns/cdk-custom-resource-with-wait-condition",
+ "projectFolder": "cdk-custom-resource-with-wait-condition",
+ "templateFile": "lib/demo-stack.ts"
+ }
+ },
+ "resources": {
+ "bullets": [
+ {
+ "text": "Custom resources",
+ "link": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html"
+ },
+ {
+ "text": "Using wait conditions",
+ "link": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-waitcondition.html"
+ },
+ {
+ "text": "Implementing long running deployments with AWS CloudFormation Custom Resources using AWS Step Functions",
+ "link": "https://aws.amazon.com/blogs/devops/implementing-long-running-deployments-with-aws-cloudformation-custom-resources-using-aws-step-functions/"
+ }
+ ]
+ },
+ "deploy": {
+ "text": [
+ "cdk deploy --require-approval never"
+ ]
+ },
+ "testing": {
+ "text": [
+ "Review Amazon CloudWatch logs for the Lambda functions and Step Function execution to confirm that the",
+ "long-running process completed successfully and the wait condition was signaled."
+ ]
+ },
+ "cleanup": {
+ "text": [
+ "Delete the stack: cdk destroy -f."
+ ]
+ },
+ "authors": [
+ {
+ "name": "Dmitry Gulin",
+ "bio": "Senior Delivery Consultant, AWS.",
+ "linkedin": "dmitry-gulin"
+ }
+ ]
+}
diff --git a/cdk-custom-resource-with-wait-condition/image/architecture.png b/cdk-custom-resource-with-wait-condition/image/architecture.png
new file mode 100644
index 000000000..8994c876f
Binary files /dev/null and b/cdk-custom-resource-with-wait-condition/image/architecture.png differ
diff --git a/cdk-custom-resource-with-wait-condition/lib/demo-stack.ts b/cdk-custom-resource-with-wait-condition/lib/demo-stack.ts
new file mode 100644
index 000000000..4a720d241
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/lib/demo-stack.ts
@@ -0,0 +1,98 @@
+import { CfnWaitConditionHandle, RemovalPolicy, Stack, StackProps, CustomResource, CfnWaitCondition } from 'aws-cdk-lib';
+import { Architecture, LoggingFormat, Runtime } from 'aws-cdk-lib/aws-lambda';
+import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
+import { LogGroup, LogGroupProps, RetentionDays } from 'aws-cdk-lib/aws-logs';
+import { StateMachine, DefinitionBody } from 'aws-cdk-lib/aws-stepfunctions';
+import { Construct } from 'constructs';
+
+/**
+ * Demo stack showing custom resource with wait condition pattern.
+ * Uses Step Functions for long-running processes and wait conditions for synchronization.
+ */
+export class DemoStack extends Stack {
+ constructor(scope: Construct, id: string, props?: StackProps) {
+ super(scope, id, props);
+
+ // Create unique wait condition handle for each deployment
+ // Note: WaitCondition resources don't support updates, requiring new handles per deployment
+ const resourceName: string = `WaitConditionHandle-${Date.now()}`;
+ const cfnWaitConditionHandle = new CfnWaitConditionHandle(this, resourceName);
+
+ // Common configuration for Lambda functions
+ const commonLambdaProps: Partial = {
+ architecture: Architecture.ARM_64,
+ loggingFormat: LoggingFormat.JSON,
+ runtime: Runtime.NODEJS_22_X,
+ memorySize: 256,
+ };
+
+ // Common configuration for CloudWatch log groups
+ const commonLogGroupProps: Partial = {
+ removalPolicy: RemovalPolicy.DESTROY,
+ retention: RetentionDays.ONE_WEEK,
+ };
+
+ // Lambda function that handles custom resource lifecycle events
+ // Starts Step Function execution and returns immediately
+ const customResourceHandler = new NodejsFunction(this, 'CustomResourceHandler', {
+ ...commonLambdaProps,
+ functionName: 'CustomResourceHandler',
+ entry: 'lib/lambda/custom-resource-handler.mts',
+ logGroup: new LogGroup(this, 'CustomResourceHandlerLogGroup', {
+ ...commonLogGroupProps,
+ logGroupName: `/demo/CustomResourceHandler`,
+ }),
+ });
+
+ // Lambda function that sends completion signals to wait condition handles
+ const sendCompletionSignalHandler = new NodejsFunction(this, 'SendCompletionSignalHandler', {
+ ...commonLambdaProps,
+ functionName: 'SendCompletionSignalHandler',
+ entry: 'lib/lambda/send-completion-signal.mts',
+ logGroup: new LogGroup(this, 'SendCompletionSignalHandlerLogGroup', {
+ ...commonLogGroupProps,
+ logGroupName: `/demo/SendCompletionSignalHandler`,
+ }),
+ });
+
+ // Step Function that simulates a long-running process
+ // Invokes completion signal Lambda when process finishes
+ const longRunningProcessStateMachine = new StateMachine(this, 'LongRunningProcessStateMachine', {
+ definitionBody: DefinitionBody.fromFile('lib/sfn/long-running-process.asl.json'),
+ stateMachineName: 'LongRunningProcessStateMachine',
+ definitionSubstitutions: {
+ SendCompletionSignalLambdaArn: sendCompletionSignalHandler.functionArn,
+ },
+ logs: {
+ destination: new LogGroup(this, 'LongRunningProcessStateMachineLogGroup', {
+ ...commonLogGroupProps,
+ logGroupName: `/demo/LongRunningProcessStateMachine`,
+ }),
+ },
+ });
+
+ // Grant permissions for Lambda functions to interact with Step Function
+ longRunningProcessStateMachine.grantStartExecution(customResourceHandler);
+ sendCompletionSignalHandler.grantInvoke(longRunningProcessStateMachine);
+
+ // Custom resource that triggers the long-running process
+ const customResource = new CustomResource(this, 'CustomResource', {
+ serviceToken: customResourceHandler.functionArn,
+ properties: {
+ WaitConditionHandle: cfnWaitConditionHandle.ref,
+ StateMachineArn: longRunningProcessStateMachine.stateMachineArn,
+ }
+ });
+
+ // Wait condition that blocks stack completion until process finishes
+ const waitCondition = new CfnWaitCondition(this, 'WaitCondition', {
+ count: 1,
+ handle: cfnWaitConditionHandle.ref,
+ timeout: '60', // 60 seconds timeout
+ });
+
+ // Ensure wait condition depends on custom resource and state machine
+ waitCondition.node.addDependency(customResource);
+ waitCondition.node.addDependency(longRunningProcessStateMachine);
+ }
+}
diff --git a/cdk-custom-resource-with-wait-condition/lib/lambda/custom-resource-handler.mts b/cdk-custom-resource-with-wait-condition/lib/lambda/custom-resource-handler.mts
new file mode 100644
index 000000000..7551e57cf
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/lib/lambda/custom-resource-handler.mts
@@ -0,0 +1,84 @@
+import { CloudFormationCustomResourceEvent, CloudFormationCustomResourceHandler, CloudFormationCustomResourceResponse, Context } from "aws-lambda";
+import { SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn";
+import { randomUUID } from "node:crypto";
+
+const sfnClient = new SFNClient({});
+
+//#region Local TypeScript version of the cfn-response package
+type Status = 'SUCCESS' | 'FAILED';
+
+/**
+ * Creates a CloudFormation custom resource response template
+ */
+function responseTemplate(event: CloudFormationCustomResourceEvent, context: Context, physicalResourceId: string, reason?: string) {
+ return {
+ PhysicalResourceId: physicalResourceId,
+ LogicalResourceId: event.LogicalResourceId,
+ RequestId: event.RequestId,
+ StackId: event.StackId,
+ NoEcho: false,
+ Reason: reason || (`See the details in CloudWatch Log Stream: ${context.logStreamName}`),
+ };
+};
+
+/**
+ * Sends response back to CloudFormation
+ */
+async function send(event: CloudFormationCustomResourceEvent, context: Context, status: Status, physicalResourceId: string, reason?: string): Promise {
+ const response: CloudFormationCustomResourceResponse = {
+ ...responseTemplate(event, context, physicalResourceId, reason),
+ Status: status,
+ };
+ const responseBody = JSON.stringify(response);
+ const sendResult = await fetch(event.ResponseURL, {
+ method: 'PUT',
+ body: responseBody,
+ });
+ console.log(`${sendResult.status}: ${sendResult.statusText}`);
+};
+//#endregion
+
+/**
+ * Custom resource handler that starts a Step Function for long-running processes.
+ * Returns immediately after starting the execution - the Step Function handles completion signaling.
+ */
+export const handler: CloudFormationCustomResourceHandler = async (event, context) => {
+ console.log(event);
+
+ // Generate unique physical resource ID for new resources, reuse for updates/deletes
+ const physicalResourceId = (event.RequestType === 'Update' || event.RequestType === 'Delete')
+ ? event.PhysicalResourceId
+ : randomUUID();
+
+ try {
+ // Extract required properties from custom resource
+ const stateMachineArn = event.ResourceProperties.StateMachineArn;
+ const waitConditionHandle = event.ResourceProperties.WaitConditionHandle;
+
+ // Prepare payload for Step Function execution
+ const stepFunctionInput = {
+ RequestType: event.RequestType,
+ WaitConditionHandle: waitConditionHandle,
+ PhysicalResourceId: physicalResourceId,
+ LogicalResourceId: event.LogicalResourceId,
+ RequestId: event.RequestId,
+ StackId: event.StackId
+ };
+
+ // Start Step Function execution asynchronously
+ const startExecutionCommand = new StartExecutionCommand({
+ stateMachineArn: stateMachineArn,
+ input: JSON.stringify(stepFunctionInput)
+ });
+
+ const executionResult = await sfnClient.send(startExecutionCommand);
+ console.log('Step Function execution started:', executionResult.executionArn);
+
+ // Return success immediately - Step Function will signal completion via wait condition
+ await send(event, context, 'SUCCESS', physicalResourceId, 'Step Function execution started successfully');
+ }
+ catch (e: any) {
+ console.error(e.message);
+ await send(event, context, 'FAILED', physicalResourceId, e.message);
+ }
+}
\ No newline at end of file
diff --git a/cdk-custom-resource-with-wait-condition/lib/lambda/send-completion-signal.mts b/cdk-custom-resource-with-wait-condition/lib/lambda/send-completion-signal.mts
new file mode 100644
index 000000000..68a047eeb
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/lib/lambda/send-completion-signal.mts
@@ -0,0 +1,48 @@
+import { Handler } from "aws-lambda";
+
+/**
+ * Status values for wait condition signals
+ */
+enum Status {
+ SUCCESS = 'SUCCESS',
+ FAILURE = 'FAILURE',
+}
+
+/**
+ * Event payload for task completion signaling
+ */
+interface TaskCompletionEvent {
+ readonly waitConditionHandle: string;
+ readonly status: Status;
+ readonly uniqueId: string;
+ readonly data: string;
+ readonly reason: string;
+}
+
+/**
+ * Sends completion signal to CloudFormation wait condition handle
+ */
+async function send(responseURL: string, status: Status, reason: string, uniqueId: string, data: string): Promise {
+ const response = {
+ Status: status,
+ Reason: reason,
+ UniqueId: uniqueId,
+ Data: data,
+ };
+ const responseBody = JSON.stringify(response);
+ const sendResult = await fetch(responseURL, {
+ method: 'PUT',
+ body: responseBody,
+ });
+ console.log(`${sendResult.status}: ${sendResult.statusText}`);
+};
+
+/**
+ * Lambda handler that sends completion signals to wait condition handles.
+ * Called by Step Functions when long-running processes complete.
+ */
+export const handler: Handler = async (event) => {
+ console.log(event);
+ const { waitConditionHandle, status, uniqueId, data, reason } = event;
+ await send(waitConditionHandle, status, reason, uniqueId, data);
+}
\ No newline at end of file
diff --git a/cdk-custom-resource-with-wait-condition/lib/sfn/long-running-process.asl.json b/cdk-custom-resource-with-wait-condition/lib/sfn/long-running-process.asl.json
new file mode 100644
index 000000000..1462424b2
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/lib/sfn/long-running-process.asl.json
@@ -0,0 +1,29 @@
+{
+ "Comment": "This is a demo Step Function immitating a long running process for managing a custom resource.",
+ "StartAt": "Custom Resource Management Placeholder",
+ "States": {
+ "Custom Resource Management Placeholder": {
+ "Type": "Wait",
+ "Seconds": 10,
+ "Next": "Send Completion Signal",
+ "Comment": "This wait task simulates some business logic applying changes to the environment."
+ },
+ "Send Completion Signal": {
+ "Type": "Task",
+ "Resource": "arn:aws:states:::lambda:invoke",
+ "Output": "{% $states.result.Payload %}",
+ "Arguments": {
+ "FunctionName": "${SendCompletionSignalLambdaArn}",
+ "Payload": {
+ "waitConditionHandle": "{% $states.input.WaitConditionHandle %}",
+ "status": "SUCCESS",
+ "reason": "Configuration Complete",
+ "uniqueId": "ID1234",
+ "data": "Application has completed configuration."
+ }
+ },
+ "End": true
+ }
+ },
+ "QueryLanguage": "JSONata"
+}
\ No newline at end of file
diff --git a/cdk-custom-resource-with-wait-condition/package.json b/cdk-custom-resource-with-wait-condition/package.json
new file mode 100644
index 000000000..8a36081e6
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "cdk-custom-resource-with-wait-condition",
+ "version": "0.1.0",
+ "bin": {
+ "cdk-custom-resource-with-wait-condition": "bin/cdk-custom-resource-with-wait-condition.js"
+ },
+ "scripts": {
+ "build": "tsc",
+ "watch": "tsc -w",
+ "cdk": "cdk"
+ },
+ "devDependencies": {
+ "@tsconfig/node22": "^22.0.2",
+ "@types/node": "24.10.0",
+ "aws-cdk": "2.1031.1",
+ "esbuild": "^0.25.12",
+ "ts-node": "^10.9.2",
+ "typescript": "~5.9.3"
+ },
+ "dependencies": {
+ "@aws-sdk/client-sfn": "^3.922.0",
+ "@types/aws-lambda": "^8.10.157",
+ "aws-cdk-lib": "2.222.0",
+ "constructs": "^10.4.2"
+ }
+}
diff --git a/cdk-custom-resource-with-wait-condition/tsconfig.json b/cdk-custom-resource-with-wait-condition/tsconfig.json
new file mode 100644
index 000000000..4a95f7d3b
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@tsconfig/node22",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "inlineSourceMap": true,
+ "inlineSources": true,
+ "experimentalDecorators": true,
+ "isolatedModules": true,
+ "rootDir": "."
+ },
+ "exclude": ["cdk.out", "build", "node_modules", "dist", "**/__tests__/**", "**/*.test.ts", "**/*.spec.ts"]
+}
\ No newline at end of file