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..1c115b674
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/example-pattern.json
@@ -0,0 +1,62 @@
+{
+ "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"
+ }
+ ]
+}
\ No newline at end of file
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..9f611f9b9
--- /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_24_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..42b0891ed
--- /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/node24": "^24.0.3",
+ "@types/node": "24.10.1",
+ "aws-cdk": "2.1033.0",
+ "esbuild": "^0.27.1",
+ "ts-node": "^10.9.2",
+ "typescript": "~5.9.3"
+ },
+ "dependencies": {
+ "@aws-sdk/client-sfn": "^3.946.0",
+ "@types/aws-lambda": "^8.10.159",
+ "aws-cdk-lib": "2.232.1",
+ "constructs": "^10.4.3"
+ }
+}
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..5f70dc8f7
--- /dev/null
+++ b/cdk-custom-resource-with-wait-condition/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@tsconfig/node24",
+ "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