From 5d38bb0d944c1dd5f7533597201619d461940a94 Mon Sep 17 00:00:00 2001 From: Adnene KHALFA Date: Thu, 22 Jan 2026 19:23:29 +0200 Subject: [PATCH 1/5] =?UTF-8?q?feat(AWS=20Lambda):=20=E2=9C=A8=20Add=20sup?= =?UTF-8?q?port=20for=20durable=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Lambda Durable Functions support with: - durableConfig property in serverless.yml (executionTimeout, retentionPeriodInDays) - Auto-enable versioning when durableConfig is present (required for qualified ARNs) - CloudFormation DurableConfig property generation - Deploy function support for durable configuration Durable functions enable long-running workflows that can span multiple Lambda invocations with automatic state management and checkpointing. --- lib/plugins/aws/deploy-function.js | 9 ++++ lib/plugins/aws/package/compile/functions.js | 16 +++++- lib/plugins/aws/provider.js | 9 ++++ .../aws/package/compile/functions.test.js | 53 +++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/lib/plugins/aws/deploy-function.js b/lib/plugins/aws/deploy-function.js index 821ac8b8b..4a78f1190 100644 --- a/lib/plugins/aws/deploy-function.js +++ b/lib/plugins/aws/deploy-function.js @@ -233,6 +233,15 @@ class AwsDeployFunction { }; } + if (functionObj.durableConfig) { + params.DurableConfig = { + ExecutionTimeout: functionObj.durableConfig.executionTimeout, + }; + if (functionObj.durableConfig.retentionPeriodInDays !== undefined) { + params.DurableConfig.RetentionPeriodInDays = functionObj.durableConfig.retentionPeriodInDays; + } + } + if ( functionObj.description && functionObj.description !== remoteFunctionConfiguration.Description diff --git a/lib/plugins/aws/package/compile/functions.js b/lib/plugins/aws/package/compile/functions.js index 728b9ab72..0075116ab 100644 --- a/lib/plugins/aws/package/compile/functions.js +++ b/lib/plugins/aws/package/compile/functions.js @@ -466,7 +466,9 @@ class AwsCompileFunctions { const shouldVersionFunction = functionObject.versionFunction != null ? functionObject.versionFunction - : this.serverless.service.provider.versionFunctions; + : this.serverless.service.provider.versionFunctions || + // Durable Functions require versioning (qualified ARNs), so we enable it automatically + !!functionObject.durableConfig; if ( shouldVersionFunction || @@ -632,6 +634,18 @@ class AwsCompileFunctions { } } + if (functionObject.durableConfig) { + const durableConfig = { + ExecutionTimeout: functionObject.durableConfig.executionTimeout, + }; + + if (functionObject.durableConfig.retentionPeriodInDays !== undefined) { + durableConfig.RetentionPeriodInDays = functionObject.durableConfig.retentionPeriodInDays; + } + + functionResource.Properties.DurableConfig = durableConfig; + } + const logs = functionObject.logs || (this.serverless.service.provider.logs && this.serverless.service.provider.logs.lambda); diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index 2bb29e701..ce1f62a22 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -1489,6 +1489,15 @@ class AwsProvider { }, kmsKeyArn: { $ref: '#/definitions/awsKmsArn' }, snapStart: { type: 'boolean' }, + durableConfig: { + type: 'object', + properties: { + executionTimeout: { type: 'integer', minimum: 1, maximum: 31536000 }, + retentionPeriodInDays: { type: 'integer', minimum: 1, maximum: 90 }, + }, + required: ['executionTimeout'], + additionalProperties: false, + }, layers: { $ref: '#/definitions/awsLambdaLayers' }, logRetentionInDays: { $ref: '#/definitions/awsLogRetentionInDays', diff --git a/test/unit/lib/plugins/aws/package/compile/functions.test.js b/test/unit/lib/plugins/aws/package/compile/functions.test.js index fc99258f7..6345994a3 100644 --- a/test/unit/lib/plugins/aws/package/compile/functions.test.js +++ b/test/unit/lib/plugins/aws/package/compile/functions.test.js @@ -964,6 +964,59 @@ describe('AwsCompileFunctions', () => { expect(cfTemplate.Resources.BasicLambdaFunction.Properties).to.not.have.property('SnapStart'); }); + + it('should set function DurableConfig when enabled with all properties', async () => { + const { cfTemplate } = await runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + durableConfig: { + executionTimeout: 3600, + retentionPeriodInDays: 30, + }, + }, + }, + }, + command: 'package', + }); + + expect(cfTemplate.Resources.BasicLambdaFunction.Properties.DurableConfig).to.deep.equal({ + ExecutionTimeout: 3600, + RetentionPeriodInDays: 30, + }); + }); + + it('should set function DurableConfig with only executionTimeout', async () => { + const { cfTemplate } = await runServerless({ + fixture: 'function', + configExt: { + functions: { + basic: { + durableConfig: { + executionTimeout: 7200, + }, + }, + }, + }, + command: 'package', + }); + + expect(cfTemplate.Resources.BasicLambdaFunction.Properties.DurableConfig).to.deep.equal({ + ExecutionTimeout: 7200, + }); + }); + + it('should not set function DurableConfig when not specified', async () => { + const { cfTemplate } = await runServerless({ + fixture: 'function', + command: 'package', + }); + + expect(cfTemplate.Resources.BasicLambdaFunction.Properties).to.not.have.property( + 'DurableConfig' + ); + }); }); describe('#compileRole()', () => { From 9b0e7878dc433608afe38dfae927e2872c62fbee Mon Sep 17 00:00:00 2001 From: Adnene KHALFA Date: Thu, 22 Jan 2026 19:23:42 +0200 Subject: [PATCH 2/5] =?UTF-8?q?feat(AWS=20Lambda):=20=E2=9C=A8=20Add=20IAM?= =?UTF-8?q?=20policy=20for=20durable=20function=20execution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automatically attach AWSLambdaBasicDurableExecutionRolePolicy managed policy to Lambda execution roles when durableConfig is present. This grants the necessary permissions for durable function operations including checkpoint management and execution state access. --- .../aws/package/lib/merge-iam-templates.js | 22 ++++++++ .../package/lib/merge-iam-templates.test.js | 50 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/lib/plugins/aws/package/lib/merge-iam-templates.js b/lib/plugins/aws/package/lib/merge-iam-templates.js index 3b2fffcfe..575317b67 100644 --- a/lib/plugins/aws/package/lib/merge-iam-templates.js +++ b/lib/plugins/aws/package/lib/merge-iam-templates.js @@ -216,6 +216,28 @@ module.exports = { ]); } + // check if one of the functions contains durable configuration + const durableConfigProvided = this.serverless.service.getAllFunctions().some((functionName) => { + const functionObject = this.serverless.service.getFunction(functionName); + return 'durableConfig' in functionObject; + }); + + if (durableConfigProvided) { + // add managed iam policy for durable execution + this.mergeManagedPolicies([ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy', + ], + ], + }, + ]); + } + return; }, diff --git a/test/unit/lib/plugins/aws/package/lib/merge-iam-templates.test.js b/test/unit/lib/plugins/aws/package/lib/merge-iam-templates.test.js index a2675400e..481812e33 100644 --- a/test/unit/lib/plugins/aws/package/lib/merge-iam-templates.test.js +++ b/test/unit/lib/plugins/aws/package/lib/merge-iam-templates.test.js @@ -601,6 +601,56 @@ describe('lib/plugins/aws/package/lib/mergeIamTemplates.test.js', () => { ], }); }); + + it('should ensure needed IAM configuration when `functions[].durableConfig` is configured', async () => { + const { cfTemplate, awsNaming } = await runServerless({ + fixture: 'function', + command: 'package', + configExt: { + functions: { + basic: { + durableConfig: { + executionTimeout: 3600, + }, + }, + }, + }, + }); + + const IamRoleLambdaExecution = awsNaming.getRoleLogicalId(); + const { Properties } = cfTemplate.Resources[IamRoleLambdaExecution]; + expect(Properties.ManagedPolicyArns).to.deep.includes({ + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy', + ], + ], + }); + }); + + it('should not add durable IAM policy when `functions[].durableConfig` is not configured', async () => { + const { cfTemplate, awsNaming } = await runServerless({ + fixture: 'function', + command: 'package', + }); + + const IamRoleLambdaExecution = awsNaming.getRoleLogicalId(); + const { Properties } = cfTemplate.Resources[IamRoleLambdaExecution]; + const durablePolicyArn = { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy', + ], + ], + }; + expect(Properties.ManagedPolicyArns || []).to.not.deep.includes(durablePolicyArn); + }); }); }); }); From 96871697cac0b068ab9bd24b5f0540c1ff6fa889 Mon Sep 17 00:00:00 2001 From: Adnene KHALFA Date: Thu, 22 Jan 2026 19:23:54 +0200 Subject: [PATCH 3/5] =?UTF-8?q?feat(AWS=20Lambda):=20=E2=9C=A8=20Add=20inv?= =?UTF-8?q?oke=20command=20support=20for=20durable=20executions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add --durable-execution-name option to serverless invoke command: - Enables idempotent invocations with unique execution names - Supports both synchronous and asynchronous durable invocations - Execution names must be 1-64 chars (alphanumeric, hyphens, underscores) - Repeated invocations with same name return cached results Usage: serverless invoke -f myFunc --durable-execution-name order-12345 --- lib/cli/commands-schema/aws-service.js | 3 ++ lib/plugins/aws/invoke.js | 4 ++ test/unit/lib/plugins/aws/invoke.test.js | 53 ++++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/lib/cli/commands-schema/aws-service.js b/lib/cli/commands-schema/aws-service.js index 1f24d6d7f..f14215e5b 100644 --- a/lib/cli/commands-schema/aws-service.js +++ b/lib/cli/commands-schema/aws-service.js @@ -123,6 +123,9 @@ commands.set('invoke', { contextPath: { usage: 'Path to JSON or YAML file holding context data', }, + 'durable-execution-name': { + usage: 'Unique name for durable function execution (enables idempotency)', + }, }, lifecycleEvents: ['invoke'], }); diff --git a/lib/plugins/aws/invoke.js b/lib/plugins/aws/invoke.js index 9b5d14c9f..1b2446f83 100644 --- a/lib/plugins/aws/invoke.js +++ b/lib/plugins/aws/invoke.js @@ -97,6 +97,10 @@ class AwsInvoke { params.Qualifier = this.options.qualifier; } + if (this.options['durable-execution-name']) { + params.DurableExecutionName = this.options['durable-execution-name']; + } + return this.provider.request('Lambda', 'invoke', params); } diff --git a/test/unit/lib/plugins/aws/invoke.test.js b/test/unit/lib/plugins/aws/invoke.test.js index e7d92a27f..15df0e48d 100644 --- a/test/unit/lib/plugins/aws/invoke.test.js +++ b/test/unit/lib/plugins/aws/invoke.test.js @@ -481,4 +481,57 @@ describe('test/unit/lib/plugins/aws/invoke.test.js', () => { }) ).to.be.eventually.fulfilled; }); + + it('should support --durable-execution-name option', async () => { + const lambdaInvokeStub = sinon.stub(); + const result = await runServerless({ + fixture: 'invocation', + command: 'invoke', + options: { + function: 'callback', + 'durable-execution-name': 'order-123', + }, + awsRequestStubMap: { + Lambda: { + invoke: (args) => { + lambdaInvokeStub.returns('payload'); + return lambdaInvokeStub(args); + }, + }, + }, + }); + expect(lambdaInvokeStub.args[0][0]).to.deep.equal({ + FunctionName: result.serverless.service.getFunction('callback').name, + InvocationType: 'RequestResponse', + LogType: 'None', + DurableExecutionName: 'order-123', + Payload: Buffer.from('{}'), + }); + }); + + it('should not include DurableExecutionName when option is not provided', async () => { + const lambdaInvokeStub = sinon.stub(); + const result = await runServerless({ + fixture: 'invocation', + command: 'invoke', + options: { + function: 'callback', + }, + awsRequestStubMap: { + Lambda: { + invoke: (args) => { + lambdaInvokeStub.returns('payload'); + return lambdaInvokeStub(args); + }, + }, + }, + }); + expect(lambdaInvokeStub.args[0][0]).to.not.have.property('DurableExecutionName'); + expect(lambdaInvokeStub.args[0][0]).to.deep.equal({ + FunctionName: result.serverless.service.getFunction('callback').name, + InvocationType: 'RequestResponse', + LogType: 'None', + Payload: Buffer.from('{}'), + }); + }); }); From fd460eaf39cac12cd494534cbb20d5dd817b0c66 Mon Sep 17 00:00:00 2001 From: Adnene KHALFA Date: Thu, 22 Jan 2026 19:24:07 +0200 Subject: [PATCH 4/5] =?UTF-8?q?docs:=20=F0=9F=93=96=20Add=20Lambda=20Durab?= =?UTF-8?q?le=20Functions=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document durable functions feature including: - Configuration properties (executionTimeout, retentionPeriodInDays) - Supported runtimes (Node.js 22+, Python 3.13+) - IAM permissions and managed policy - Invoke command --durable-execution-name option - Usage examples and limitations --- docs/cli-reference/invoke.md | 11 +++++++ docs/guides/functions.md | 59 +++++++++++++++++++++++++++++++++++ docs/guides/serverless.yml.md | 4 +++ 3 files changed, 74 insertions(+) diff --git a/docs/cli-reference/invoke.md b/docs/cli-reference/invoke.md index a555f3ea5..59c4b2506 100644 --- a/docs/cli-reference/invoke.md +++ b/docs/cli-reference/invoke.md @@ -21,6 +21,7 @@ serverless invoke [local] --function functionName - `--context` String data to be passed as a context to your function. Same like with `--data`, context included in `--contextPath` will overwrite the context you passed with `--context` flag. - `--type` or `-t` The type of invocation. Either `RequestResponse`, `Event` or `DryRun`. Default is `RequestResponse`. - `--log` or `-l` If set to `true` and invocation type is `RequestResponse`, it will output logging data of the invocation. Default is `false`. +- `--durable-execution-name` Unique name for the durable function execution (enables idempotency). Execution names must be 1-64 characters: alphanumeric, hyphens, or underscores. ## Provided lifecycle events @@ -98,6 +99,16 @@ serverless invoke --function functionName --path lib/data.json This example will pass the json data in the `lib/data.json` file (relative to the root of the service) while invoking the specified/deployed function. +#### Function invocation with durable execution name + +```bash +serverless invoke --function orderProcessor --durable-execution-name order-12345 +``` + +This example invokes a durable function with a unique execution name. If you invoke the same function with the same execution name again, Lambda returns the cached result from the previous execution instead of re-executing the function. This enables idempotent invocations for reliable workflow orchestration. + +**Note:** Durable execution names must be 1-64 characters and contain only alphanumeric characters, hyphens, or underscores. See the [Functions guide](../guides/functions.md#aws-lambda-durable-functions) for more information on configuring durable functions. + #### Example `data.json` ```json diff --git a/docs/guides/functions.md b/docs/guides/functions.md index e02a3923f..b3eda7977 100644 --- a/docs/guides/functions.md +++ b/docs/guides/functions.md @@ -448,6 +448,65 @@ functions: **Note:** Lambda SnapStart only supports the Java 11, Java 17 and Java 21 runtimes and does not support provisioned concurrency, the arm64 architecture, the Lambda Extensions API, Amazon Elastic File System (Amazon EFS), AWS X-Ray, or ephemeral storage greater than 512 MB. +## AWS Lambda Durable Functions + +[AWS Lambda Durable Functions](https://docs.aws.amazon.com/lambda/latest/dg/) enable long-running, fault-tolerant workflows without requiring custom state management. Durable functions use checkpoint-and-replay mechanisms to reliably execute workflows that can run for up to 1 year. + +Durable functions are ideal for: +- Multi-step order processing workflows +- Long-running data processing jobs +- Reliable task orchestration +- Workflows requiring idempotent execution guarantees + +### Configuration + +To enable durable function configuration for your Lambda function, add the `durableConfig` object property in the function configuration: + +```yaml +functions: + orderProcessor: + handler: handler.processOrder + runtime: nodejs22.x + durableConfig: + executionTimeout: 3600 # Required: 1-31536000 seconds (1 sec to 1 year) + retentionPeriodInDays: 30 # Optional: 1-90 days +``` + +### Configuration Properties + +- **`executionTimeout`** (required, integer): Maximum execution time for the durable function in seconds. Range: 1 to 31,536,000 (1 year). +- **`retentionPeriodInDays`** (optional, integer): Number of days to retain execution history and state. Range: 1 to 90 days. This determines how long AWS maintains execution metadata after completion. + +### Invoking Durable Functions + +When invoking a durable function, you can provide a unique execution name to enable idempotent invocations: + +```bash +serverless invoke --function orderProcessor --durable-execution-name order-12345 +``` + +If you invoke the same function with the same execution name again, Lambda returns the cached result instead of re-executing. Execution names must be 1-64 characters long and contain only alphanumeric characters, hyphens, or underscores. + +See the [invoke command documentation](../cli-reference/invoke.md) for more details on the `--durable-execution-name` option. + +### IAM Permissions + +When you configure a function with `durableConfig`, the Serverless Framework automatically adds the required IAM managed policy (`AWSLambdaBasicDurableExecutionRolePolicy`) to the Lambda execution role. + +**Note:** If you use a custom IAM role for your function (via `functions[].role` or `provider.iam.role`), you must manually add the `AWSLambdaBasicDurableExecutionRolePolicy` to your custom role. The framework cannot automatically modify custom roles. + +### Supported Runtimes and Limitations + +**Supported Runtimes:** +- Node.js: `nodejs22.x`, `nodejs24.x` +- Python: `python3.13`, `python3.14` +- Container images with compatible runtimes + +**Notes:** +- Durable functions require versioning, which is automatically enabled when `durableConfig` is present +- Functions must be invoked with a specific version or alias for durability guarantees +- Review the [AWS Lambda Durable Functions documentation](https://docs.aws.amazon.com/lambda/latest/dg/) for complete compatibility information and limitations + ## VPC Configuration You can add VPC configuration to a specific function in `serverless.yml` by adding a `vpc` object property in the function configuration. This object should contain the `securityGroupIds` and `subnetIds` array properties needed to construct VPC for this function. Here's an example configuration: diff --git a/docs/guides/serverless.yml.md b/docs/guides/serverless.yml.md index a4b9f01b3..0c141e43d 100644 --- a/docs/guides/serverless.yml.md +++ b/docs/guides/serverless.yml.md @@ -658,6 +658,10 @@ functions: kmsKeyArn: arn:aws:kms:us-east-1:XXXXXX:key/some-hash # Defines if you want to make use of SnapStart, this feature can only be used in combination with a Java runtime. Configuring this property will result in either None or PublishedVersions for the Lambda function snapStart: true + # Configure AWS Lambda Durable Functions for long-running workflows (auto-enables versioning) + durableConfig: + executionTimeout: 3600 # Required: 1-31536000 seconds + retentionPeriodInDays: 30 # Optional: 1-90 days # Disable the creation of the CloudWatch log group disableLogs: false # Duration for CloudWatch log retention (default: forever). Overrides provider setting. From ce47758c54f23dbefb652cf240114c49867fd2ba Mon Sep 17 00:00:00 2001 From: Adnene KHALFA Date: Thu, 22 Jan 2026 20:08:27 +0200 Subject: [PATCH 5/5] =?UTF-8?q?docs(cli-reference):=20=F0=9F=93=96=20Reorg?= =?UTF-8?q?anize=20durable=20execution=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move durable execution example to a more appropriate location in the invoke.md documentation and update example to include contextPath parameter. --- docs/cli-reference/invoke.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/cli-reference/invoke.md b/docs/cli-reference/invoke.md index 59c4b2506..c7974b262 100644 --- a/docs/cli-reference/invoke.md +++ b/docs/cli-reference/invoke.md @@ -99,16 +99,6 @@ serverless invoke --function functionName --path lib/data.json This example will pass the json data in the `lib/data.json` file (relative to the root of the service) while invoking the specified/deployed function. -#### Function invocation with durable execution name - -```bash -serverless invoke --function orderProcessor --durable-execution-name order-12345 -``` - -This example invokes a durable function with a unique execution name. If you invoke the same function with the same execution name again, Lambda returns the cached result from the previous execution instead of re-executing the function. This enables idempotent invocations for reliable workflow orchestration. - -**Note:** Durable execution names must be 1-64 characters and contain only alphanumeric characters, hyphens, or underscores. See the [Functions guide](../guides/functions.md#aws-lambda-durable-functions) for more information on configuring durable functions. - #### Example `data.json` ```json @@ -135,6 +125,16 @@ serverless invoke local --function functionName \ This example will pass the json context in the `lib/context.json` file (relative to the root of the service) while invoking the specified/deployed function. +### Function invocation with durable execution name + +```bash +serverless invoke --function functionName --contextPath lib/context.json --durable-execution-name order-12345 +``` + +This example invokes a durable function with a unique execution name. If you invoke the same function with the same execution name again, Lambda returns the cached result from the previous execution instead of re-executing the function. This enables idempotent invocations for reliable workflow orchestration. + +**Note:** Durable execution names must be 1-64 characters and contain only alphanumeric characters, hyphens, or underscores. See the [Functions guide](../guides/functions.md#aws-lambda-durable-functions) for more information on configuring durable functions. + ### Limitations Currently, `invoke local` only supports the Node.js, Python, Java and Ruby runtimes.