Skip to content

fix(stepfunctions): wrap optimized lambda:invoke result in the Invoke envelope - #2165

Open
soreavis wants to merge 1 commit into
floci-io:mainfrom
soreavis:fix/sfn-lambda-invoke-envelope
Open

fix(stepfunctions): wrap optimized lambda:invoke result in the Invoke envelope#2165
soreavis wants to merge 1 commit into
floci-io:mainfrom
soreavis:fix/sfn-lambda-invoke-envelope

Conversation

@soreavis

@soreavis soreavis commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #2110.

A Task using the optimized arn:aws:states:::lambda:invoke integration returned the Lambda function's return value directly as the task result, with no ExecutedVersion / Payload / StatusCode keys. Real Step Functions nests the function output inside the Invoke response metadata for this integration, which is why the AWS examples read a Lambda result through $.Payload (JSONPath) or $states.result.Payload (JSONata). Against the unwrapped result those references resolve to nothing, the state still succeeds, and the execution ends SUCCEEDED carrying empty data.

In invokeResource() the direct-ARN branch and the optimized branch shared one if (functionName != null) block, so the output was returned raw for both. This gates the wrap to the optimized branch. A directly specified function ARN still returns only the function output — the documented behavior for that form, and now covered by a regression test.

Old task result for Resource: "arn:aws:states:::lambda:invoke", then new:

{"marker":"RET","echo":{"in":1}}
{"ExecutedVersion":"$LATEST","Payload":{"marker":"RET","echo":{"in":1}},"StatusCode":200}

Note this changes the task result shape for existing users. State machines that adapted to the unwrapped form — reading $.marker where AWS needs $.Payload.marker, or dropping the OutputPath: "$.Payload" that Workflow Studio sets by default — were already incompatible with real AWS and will now behave as they do there. Direct-ARN tasks, .waitForTaskToken tasks and every non-Lambda integration are unchanged.

Type of change

  • Bug fix (fix:)
  • New feature (feat:)
  • Breaking change (feat!: or fix!:)
  • Docs / chore

AWS Compatibility

Invoke Lambda with Step Functions: "When the Task result is returned, the function output is nested inside a dictionary of metadata", with an example carrying ExecutedVersion, Payload, SdkHttpMetadata, SdkResponseMetadata and StatusCode: 200. Same page: "The Payload field of the response is parsed from escaped Json to Json" (so Payload stays a parsed node, matching the existing readTree), and for the direct-ARN form, "the task result contains only the function output". The Task state reference reads the optimized integration's result as "Output": "{% $states.result.Payload %}", and Workflow Studio's docs state the default OutputPath for Lambda Invoke states is $.Payload, which "removes the additional metadata" — both only work if the envelope is there. The new tests cover both shapes.

Values come from real emulator state: ExecutedVersion from the resolved function's version (the same fn.getVersion() that LambdaService.invoke() reports since #2026), StatusCode from InvokeResult, Payload from the existing parse.

SdkHttpMetadata and SdkResponseMetadata are left out on purpose. Their contents are artifacts of a real HTTPS call — Date, X-Amzn-Trace-Id, x-amzn-RequestId, Content-Length — and floci invokes in process, so anything it put there would be invented. The documented read paths only touch the three keys emitted here, and a test pins the key set at exactly those three, so adding the blocks later is a deliberate change rather than a drift. Say the word if you want them and I'll add them.

Two pre-existing gaps this surfaces rather than fixes: extractLambdaFunctionName drops the qualifier (#1660), so a task using Qualifier or a qualified FunctionName still invokes $LATEST and will now report ExecutedVersion: "$LATEST" for it; and InvocationType is hardcoded to RequestResponse, so an InvocationType: "Event" task reports 200 where AWS reports 202. Both want their own issue — I can file them if you'd like them tracked.

Checklist

  • ./mvnw test passes locally (with pre-existing Docker-dependent failures unrelated to this change — see the test note)
  • New or updated integration test added
  • Commit messages follow Conventional Commits

New Docker-free AslExecutorLambdaInvokeResultTest, modeled on AslExecutorCatchTest (mocked LambdaExecutorService + LambdaFunctionStore, driven through executeSync): the envelope and its exact key set, the direct-ARN raw-output guard, ResultSelector over $.Payload.marker, OutputPath: "$.Payload", and JSONata $states.result.Payload. Reverting the AslExecutor change with the tests in place fails 4 of the 5, with the direct-ARN guard green on both sides. make docs-check passes; no handler action changed.

Test note: in the full ./mvnw test (8393 tests) all 27 services/stepfunctions classes are green. The failures on this machine sit in container-backed subsystems (DocumentDB, Neptune, WebSockets, ECR, API Gateway authorizers) — there is no Docker daemon here, and the same classes fail identically on a clean main tree.

… envelope

A Task using the arn:aws:states:::lambda:invoke integration returned the
Lambda function's return value directly as the task result, with no
ExecutedVersion, Payload or StatusCode key. Real Step Functions nests the
function output inside the Invoke response metadata for this integration,
which is why $.Payload and the JSONata $states.result.Payload are the
documented ways to read a Lambda result.

Against the unwrapped result those references resolved to nothing, the
state still succeeded, and the execution finished SUCCEEDED carrying
silently-empty data — a pipeline accumulating results across stages looked
healthy while producing an empty result.

Both the direct-ARN branch and the optimized branch shared one block in
invokeResource(), so the parsed function output was returned raw for both.
Gate the wrap to the optimized branch: it now returns ExecutedVersion (from
the resolved function's version, matching what LambdaService.invoke already
reports), Payload (the parsed function output) and StatusCode (from the
invoke result). A directly specified function ARN keeps returning only the
function output, which is the documented behavior for that form.

SdkHttpMetadata and SdkResponseMetadata are deliberately not emitted. Their
contents are artifacts of a real HTTPS call to the Lambda service — Date,
X-Amzn-Trace-Id, RequestId, Content-Length — and floci invokes in process,
so any value it put there would be invented.

The .waitForTaskToken flow is unaffected: executeTaskState overwrites the
task result with the callback payload, so the envelope is discarded for
that pattern, as it should be.

Closes floci-io#2110
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

🎉 Thanks for your first pull request to Floci!

Your CI checks need a maintainer to approve them before they run. That is GitHub's standard gate on first-time contributors, not a problem with your PR — so if the checks look like they are doing nothing, that is why. Once a maintainer approves, CI and the compatibility suite start automatically. Nothing is needed from you in the meantime.

While you wait, a couple of things that make review faster:

  • Link the issue this fixes with Closes #N in the description
  • Commits follow Conventional Commits (feat(s3): ..., fix(dynamodb): ...)
  • Behaviour changes come with a test — see CONTRIBUTING.md

Come join us in Slack — it is the fastest way to reach maintainers if you get stuck, or want feedback on an approach before investing more time in it.

@greptile-apps

greptile-apps Bot commented Aug 7, 2026

Copy link
Copy Markdown

Greptile Summary

This PR makes optimized Step Functions Lambda invocations return an AWS-compatible Invoke envelope while preserving raw function output for direct Lambda ARNs.

  • Adds ExecutedVersion, parsed Payload, and StatusCode to optimized invocation results.
  • Adds regression coverage for envelope shape, direct-ARN behavior, JSONPath processing, and JSONata processing.

Confidence Score: 5/5

The PR appears safe to merge because the result-shape correction is narrowly gated and covered across the affected Step Functions output-processing paths.

The optimized integration now receives the documented envelope, direct Lambda ARNs retain their existing raw output, and no new blocking or non-blocking defect remains.

Important Files Changed

Filename Overview
src/main/java/io/github/hectorvent/floci/services/stepfunctions/AslExecutor.java Correctly distinguishes optimized Lambda invocation results from direct-ARN results and constructs the expected response envelope.
src/test/java/io/github/hectorvent/floci/services/stepfunctions/AslExecutorLambdaInvokeResultTest.java Provides focused coverage of both result shapes and their JSONPath and JSONata consumers.

Reviews (1): Last reviewed commit: "fix(stepfunctions): wrap optimized lambd..." | Re-trigger Greptile

@hectorvent hectorvent added bug Something isn't working sfn AWS Step Functions lambda AWS Lambda labels Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working lambda AWS Lambda sfn AWS Step Functions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] SFN Lambda task result missing Payload / StatusCode wrapper

2 participants